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.
111 lines
4.1 KiB
Dart
111 lines
4.1 KiB
Dart
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'));
|
|
});
|
|
});
|
|
}
|
|
}
|