import "dart:convert"; import "package:flutter/services.dart" show rootBundle; import "package:intl/intl.dart"; import "main.dart" show appSettings; /// UI translation for the phone app. Every user-visible string lives in a /// per-language JSON file under assets/i18n/; nothing hardcodes English. /// /// The language is the language half of the signed-in user's stored BCP-47 /// locale (appSettings.locale), the same value that already drives dates and /// numbers in format.dart — so the Settings language picker steers both. Because /// MaterialApp is wrapped in a ListenableBuilder on appSettings, changing the /// locale rebuilds the whole tree and every t() re-resolves. /// /// Deliberately hand-rolled rather than gen_l10n/ARB: the app already reads a /// runtime locale from the profile (not the device locale Flutter's own /// localization is built around), and "a JSON file per language the app reads /// from" is exactly the shape asked for. const String _base = "en"; /// Languages that ship a real translation file. The Settings picker offers every /// European language for date/number formatting; only these change the UI text, /// and the rest fall back to English. const List translatedLanguages = ["en", "pl", "da"]; final Map> _messages = {}; /// Loads every bundled language file into memory. Call once in main() before /// runApp, alongside initializeDateFormatting — the files are small assets and /// t() is synchronous thereafter. Future loadTranslations() async { for (final code in translatedLanguages) { final raw = await rootBundle.loadString("assets/i18n/$code.json"); _messages[code] = json.decode(raw) as Map; } } String get _lang { final lang = appSettings.locale.split("-").first; return _messages.containsKey(lang) ? lang : _base; } /// True when the active language has its own file (vs. falling back to English). bool get languageTranslated => _messages.containsKey(appSettings.locale.split("-").first); dynamic _lookup(Map? dict, String key) { dynamic node = dict; for (final part in key.split(".")) { if (node is! Map) return null; node = node[part]; } return node; } String _interpolate(String template, Map? params) { if (params == null) return template; return template.replaceAllMapped(RegExp(r"\{(\w+)\}"), (m) { final v = params[m.group(1)]; return v == null ? m.group(0)! : v.toString(); }); } /// Translate [key] for the active language. /// /// Pass [n] to select a plural form (a JSON object keyed by CLDR category — /// one/few/many/other); Intl.plural picks the right one for the language, which /// is what makes Polish read correctly rather than an English-style n==1 split. /// Any [params] interpolate `{name}` placeholders. A key missing from the active /// language falls back to English; missing from English too, the key is returned /// so the gap is visible rather than blank. String t(String key, {Map? params, int? n}) { final lang = _lang; dynamic entry = _lookup(_messages[lang], key); entry ??= _lookup(_messages[_base], key); if (entry == null) return key; if (entry is Map) { if (n == null) return key; final forms = entry; entry = Intl.plural( n, one: forms["one"] as String?, few: forms["few"] as String?, many: forms["many"] as String?, other: (forms["other"] ?? forms["one"]) as String? ?? key, locale: lang, ); } if (entry is! String) return key; final merged = {if (n != null) "n": n, ...?params}; return _interpolate(entry, merged.isEmpty ? null : merged); }