import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:mews/l10n/locales.dart'; import 'package:mews/l10n/strings.dart'; final _message = RegExp(r'^([a-z0-9-]+) = (.*)$', multiLine: true); final _placeholder = RegExp(r'\{ \$([a-z]+) \}'); Map _messagesOf(String tag) { final source = File('locales/$tag/main.ftl').readAsStringSync(); return { for (final m in _message.allMatches(source)) m.group(1)!: m.group(2)!, }; } void main() { final english = _messagesOf(fallbackLocale); test('every shipped language has a file declared as an asset', () { final pubspec = File('pubspec.yaml').readAsStringSync(); for (final locale in appLocales) { expect(File('locales/${locale.tag}/main.ftl').existsSync(), isTrue, reason: '${locale.tag} has no messages'); expect(pubspec, contains('locales/${locale.tag}/main.ftl'), reason: '${locale.tag} is not bundled as an asset'); } }); test('every language says everything English says', () { for (final locale in appLocales) { if (locale.tag == fallbackLocale) continue; final messages = _messagesOf(locale.tag); for (final id in english.keys) { expect(messages, contains(id), reason: '${locale.tag} is missing $id'); expect(messages[id], isNotEmpty, reason: '${locale.tag}: $id is empty'); } // The only messages a language may add are plural forms English has no // use for, such as Russian's few or Hebrew's two. for (final id in messages.keys.where((i) => !english.containsKey(i))) { expect(id, matches(r'-(two|few)$'), reason: '${locale.tag} defines an unknown $id'); expect(english, contains(id.replaceFirst(RegExp(r'-(two|few)$'), '-many')), reason: '$id has no English counterpart to fall back on'); } } }); test('every language keeps the placeholders a message needs', () { for (final locale in appLocales) { if (locale.tag == fallbackLocale) continue; final messages = _messagesOf(locale.tag); for (final entry in english.entries) { final wanted = _placeholder .allMatches(entry.value) .map((m) => m.group(1)) .toSet(); final got = _placeholder .allMatches(messages[entry.key]!) .map((m) => m.group(1)) .toSet(); // A dropped placeholder loses the number or name it carried. expect(got, containsAll(wanted), reason: '${locale.tag}: ${entry.key} drops a placeholder'); // An invented one renders as nothing. The exception is the count on a // singular message: English writes "1 bookmark", but a language whose // singular also covers 21 and 31 has to show the number. final extra = got.difference(wanted); expect( extra.isEmpty || (extra.length == 1 && extra.first == 'count' && entry.key.endsWith('-one')), isTrue, reason: '${locale.tag}: ${entry.key} adds $extra'); } } }); test('a language naming a plural form defines every one it needs', () { // The plural rules decide which suffix a count asks for; a language whose // rules can produce a form it never defines would fall back to English. for (final locale in appLocales) { final messages = _messagesOf(locale.tag); final stems = english.keys .where((id) => id.endsWith('-many')) .map((id) => id.substring(0, id.length - '-many'.length)); for (final stem in stems) { for (var count = 1; count <= 200; count++) { final suffix = pluralSuffix(locale.tag, count); if (suffix == 'one' || suffix == 'many') continue; expect(messages, contains('$stem-$suffix'), reason: '${locale.tag} needs $stem-$suffix for $count'); } } } }); group('choosing a language', () { test('matches on the language alone, so a region still fits', () { expect(matchLocale('de-AT'), 'de'); expect(matchLocale('pt-BR'), 'pt'); expect(matchLocale('en-GB'), 'en-US'); expect(matchLocale('en-US'), 'en-US'); expect(matchLocale('is'), isNull); }); test('takes the first of the device languages it ships', () { expect(resolveLocale(['is', 'fo', 'ru-RU']), 'ru'); expect(resolveLocale(['is', 'fo']), fallbackLocale); expect(resolveLocale([]), fallbackLocale); }); test('knows which languages mirror', () { expect(isRightToLeft('he'), isTrue); expect(isRightToLeft('en-US'), isFalse); }); }); group('plural rules', () { test('Russian separates two to four from the rest', () { expect(pluralSuffix('ru', 1), 'one'); expect(pluralSuffix('ru', 21), 'one'); expect(pluralSuffix('ru', 11), 'many'); expect(pluralSuffix('ru', 2), 'few'); expect(pluralSuffix('ru', 24), 'few'); expect(pluralSuffix('ru', 12), 'many'); expect(pluralSuffix('ru', 5), 'many'); }); test('Hebrew has a form for two', () { expect(pluralSuffix('he', 1), 'one'); expect(pluralSuffix('he', 2), 'two'); expect(pluralSuffix('he', 3), 'many'); }); test('French and Portuguese read one for a single thing', () { expect(pluralSuffix('fr', 1), 'one'); expect(pluralSuffix('fr', 2), 'many'); expect(pluralSuffix('pt', 1), 'one'); }); test('Japanese has one form', () { expect(pluralSuffix('ja', 1), 'many'); expect(pluralSuffix('ja', 7), 'many'); }); }); test('a message missing from a language falls back to English', () { final strings = Strings() ..loadSource(File('locales/en-US/main.ftl').readAsStringSync()) ..loadSource('nav-posts = Beiträge', tag: 'de'); expect(strings.t('nav-posts'), 'Beiträge'); // Not translated in this stub, so English stands in rather than an id. expect(strings.t('nav-bookmarks'), 'Bookmarks'); expect(strings.t('no-such-message'), 'no-such-message'); expect(strings.lookup('no-such-message'), isNull); }); }