feat: ship the interface in eight languages, with per-language plurals and mirroring

This commit is contained in:
randogoth 2026-10-11 14:54:43 +03:00
parent 6de6672ae3
commit a4a49f41d7
28 changed files with 1795 additions and 32 deletions

View file

@ -2,27 +2,66 @@ import 'package:fluent/fluent.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter/widgets.dart';
import 'locales.dart';
/// Loads and formats the app's Fluent messages. All user-facing strings come
/// from locales/en-US/main.ftl; lookups fall back to the message ID when a
/// message is missing so that missing translations are obvious in the UI.
/// from `locales/<tag>/main.ftl`.
///
/// A message missing from the chosen language falls back to English, and a
/// message missing from English too comes back as its own id, so a gap shows
/// in the interface rather than crashing or rendering as nothing.
class Strings {
final FluentBundle _bundle = FluentBundle('en-US');
FluentBundle _bundle = FluentBundle(fallbackLocale);
FluentBundle _fallback = FluentBundle(fallbackLocale);
/// Loads the messages from the app's asset bundle.
String _tag = fallbackLocale;
/// The language now in use, as a shipped locale tag.
String get tag => _tag;
bool get isRtl => isRightToLeft(_tag);
/// Loads English, which every other language falls back to.
Future<void> load() async {
final source = await rootBundle.loadString('locales/en-US/main.ftl');
loadSource(source);
_fallback = await _bundleFor(fallbackLocale);
_bundle = _fallback;
_tag = fallbackLocale;
}
/// Loads messages from an already-read source, so tests can read the file
/// from disk instead of the asset bundle.
void loadSource(String source) {
_bundle.addMessages(source);
/// Switches to [tag], keeping English underneath it.
Future<void> use(String tag) async {
if (tag == _tag) return;
_bundle = tag == fallbackLocale ? _fallback : await _bundleFor(tag);
_tag = tag;
}
String t(String id, [Map<String, Object?> args = const {}]) {
return _bundle.format(id, args: args) ?? id;
Future<FluentBundle> _bundleFor(String tag) async {
final bundle = FluentBundle(tag);
bundle.addMessages(await rootBundle.loadString('locales/$tag/main.ftl'));
return bundle;
}
/// Loads messages from an already-read source, so tests can read files from
/// disk instead of the asset bundle.
void loadSource(String source, {String tag = fallbackLocale}) {
final bundle = FluentBundle(tag);
bundle.addMessages(source);
_bundle = bundle;
_tag = tag;
if (tag == fallbackLocale) _fallback = bundle;
}
/// The message for [id], or null when neither the chosen language nor
/// English has one. Used where a missing message is expected, such as a
/// plural form a language does not need.
String? lookup(String id, [Map<String, Object?> args = const {}]) =>
_bundle.format(id, args: args) ?? _fallback.format(id, args: args);
String t(String id, [Map<String, Object?> args = const {}]) =>
lookup(id, args) ?? id;
/// Which plural message [count] needs in the language in use.
String pluralSuffixFor(int count) => pluralSuffix(_tag, count);
}
/// Provides [Strings] to the widget tree.