feat: ship the interface in eight languages, with per-language plurals and mirroring
This commit is contained in:
parent
6de6672ae3
commit
a4a49f41d7
28 changed files with 1795 additions and 32 deletions
162
app/test/locales_test.dart
Normal file
162
app/test/locales_test.dart
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
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);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue