the-mews/app/test/locales_test.dart

162 lines
6 KiB
Dart

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<String, String> _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);
});
}