Files
farolero/test/l10n_cobertura_test.dart
FreeTLab 56f07364c2
Build & Deploy Farolero / Análisis de código (push) Successful in 13s
Build & Deploy Farolero / Build APK + AAB release (push) Successful in 1m22s
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.
2026-07-25 23:59:57 +02:00

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'));
});
});
}
}