feat: confirm before leaving a game, and fix untranslated strings
Exit confirmation - A dialog in the app's own style now guards every in-game screen. The safe action is the prominent one, so a stray tap keeps you in the game. - Eleven screens covered, against both the system back gesture and the close buttons. The message adapts: a single-device game is lost, a host ends it for everyone, a client can rejoin. - The two screens the host also opens from the manager take a flag so that back there just closes them instead of offering to quit. Translations - 21 keys were still holding the English text in Arabic, Basque, Hindi, Japanese, Korean, Dutch, Polish, Russian, Turkish and both Chinese variants: 231 strings, covering the whole multi-device flow, permissions and the mode picker. Nothing failed, the app just showed English. - The permissions dialog was hardcoded in Spanish; it is localized now. - Portuguese used Spanish gerunds where pt-PT takes "a + infinitive". Checked what looked untranslated but is not: Animals and Notes are correct Catalan, Configuration and Vibration correct French, Version correct German. Those are whitelisted in the new test rather than silently rewritten. l10n_cobertura_test guards all of it: no missing or orphan keys, no empty values, no English left outside the whitelist, and placeholders preserved.
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
/// Guardas sobre los ficheros de traducción.
|
||||
///
|
||||
/// El fallo que motivó estos tests: 21 claves se quedaron con el texto inglés
|
||||
/// dentro de árabe, japonés, coreano, chino, ruso, turco, hindi, neerlandés,
|
||||
/// polaco y euskera. Nada fallaba: la app mostraba inglés y ya está.
|
||||
void main() {
|
||||
const idiomaPlantilla = 'es';
|
||||
|
||||
/// Coincidencias reales entre idiomas: palabras que se escriben igual y por
|
||||
/// tanto no son un descuido. Cada entrada es "idioma:clave".
|
||||
const coincidenciasLegitimas = {
|
||||
// Nombre de la app y rangos numéricos, iguales en todas partes.
|
||||
'de:appTitle', 'en:appTitle', 'fr:appTitle', 'it:appTitle',
|
||||
// "Animals", "Professions", "Impostors", "Notes" son catalán correcto.
|
||||
'ca:categoryAnimals', 'ca:categoryProfessions', 'ca:impostors',
|
||||
'ca:notes', 'ca:notesTitle',
|
||||
// "Vibration", "Version", "Name" son alemán correcto.
|
||||
'de:vibration', 'de:version', 'de:profileName',
|
||||
// "Sports", "Configuration", "Notes", "Vibration", "Version" son francés.
|
||||
'fr:categorySports', 'fr:configuration', 'fr:notes', 'fr:notesTitle',
|
||||
'fr:vibration', 'fr:version',
|
||||
// "Round" se usa tal cual en italiano.
|
||||
'it:roundNumber', 'it:roundElimination',
|
||||
};
|
||||
|
||||
Map<String, String> cargar(String idioma) {
|
||||
final fichero = File('lib/l10n/app_$idioma.arb');
|
||||
final datos = json.decode(fichero.readAsStringSync()) as Map<String, dynamic>;
|
||||
return {
|
||||
for (final e in datos.entries)
|
||||
if (!e.key.startsWith('@') && e.value is String)
|
||||
e.key: e.value as String,
|
||||
};
|
||||
}
|
||||
|
||||
final idiomas = Directory('lib/l10n')
|
||||
.listSync()
|
||||
.whereType<File>()
|
||||
.where((f) => f.path.endsWith('.arb'))
|
||||
.map((f) => f.uri.pathSegments.last.replaceAll(RegExp(r'^app_|\.arb$'), ''))
|
||||
.toList()
|
||||
..sort();
|
||||
|
||||
final plantilla = cargar(idiomaPlantilla);
|
||||
final ingles = cargar('en');
|
||||
|
||||
test('están los 18 idiomas soportados', () {
|
||||
expect(idiomas.length, 18);
|
||||
});
|
||||
|
||||
for (final idioma in idiomas) {
|
||||
group('app_$idioma.arb', () {
|
||||
final valores = cargar(idioma);
|
||||
|
||||
test('no le falta ninguna clave de la plantilla', () {
|
||||
final faltan = plantilla.keys.where((k) => !valores.containsKey(k));
|
||||
expect(faltan, isEmpty, reason: 'sin traducir: ${faltan.join(", ")}');
|
||||
});
|
||||
|
||||
test('no tiene claves que la plantilla no conozca', () {
|
||||
final sobran = valores.keys.where((k) => !plantilla.containsKey(k));
|
||||
expect(sobran, isEmpty, reason: 'huérfanas: ${sobran.join(", ")}');
|
||||
});
|
||||
|
||||
test('ningún valor está vacío', () {
|
||||
final vacios = valores.entries
|
||||
.where((e) => e.value.trim().isEmpty)
|
||||
.map((e) => e.key);
|
||||
expect(vacios, isEmpty);
|
||||
});
|
||||
|
||||
if (idioma != 'en' && idioma != idiomaPlantilla) {
|
||||
test('no se ha quedado el texto en inglés', () {
|
||||
final enIngles = valores.entries
|
||||
.where((e) =>
|
||||
ingles[e.key] == e.value &&
|
||||
plantilla[e.key] != e.value &&
|
||||
!coincidenciasLegitimas.contains('$idioma:${e.key}'))
|
||||
.map((e) => '${e.key} = "${e.value}"')
|
||||
.toList();
|
||||
expect(enIngles, isEmpty, reason: enIngles.join('\n'));
|
||||
});
|
||||
}
|
||||
|
||||
test('conserva los marcadores de posición de la plantilla', () {
|
||||
final patron = RegExp(r'\{(\w+)\}');
|
||||
final fallos = <String>[];
|
||||
for (final entrada in valores.entries) {
|
||||
final esperados = patron
|
||||
.allMatches(plantilla[entrada.key] ?? '')
|
||||
.map((m) => m.group(1)!)
|
||||
.toSet();
|
||||
final presentes =
|
||||
patron.allMatches(entrada.value).map((m) => m.group(1)!).toSet();
|
||||
// Los plurales ICU declaran el contador fuera de las llaves simples.
|
||||
if (entrada.value.contains(', plural,')) continue;
|
||||
if (!presentes.containsAll(esperados)) {
|
||||
fallos.add('${entrada.key}: faltan ${esperados.difference(presentes)}');
|
||||
}
|
||||
}
|
||||
expect(fallos, isEmpty, reason: fallos.join('\n'));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user