app_strings.dart 1.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. import 'dart:convert';
  2. import 'package:flutter/services.dart' show rootBundle;
  3. import '../theme/locale_controller.dart';
  4. /// Hand-rolled translation dictionary loaded from flat JSON files in
  5. /// `assets/lang/`, one per language — freely hand-editable, no codegen.
  6. /// All dictionaries are preloaded at startup since they're tiny, so [t]
  7. /// is a pure synchronous lookup with no async gap on language switch.
  8. class AppStrings {
  9. AppStrings._();
  10. static const supportedLocales = ['en', 'ms', 'zh'];
  11. static final Map<String, Map<String, String>> _dictionaries = {};
  12. static Future<void> preloadAll() async {
  13. for (final code in supportedLocales) {
  14. final raw = await rootBundle.loadString('assets/lang/$code.json');
  15. _dictionaries[code] = Map<String, String>.from(json.decode(raw));
  16. }
  17. }
  18. /// Looks up [key] in the active locale, falling back to English, then
  19. /// to the raw key itself — a missing translation is loud, not silent.
  20. static String t(String key) {
  21. final code = LocaleController.localeCode.value;
  22. return _dictionaries[code]?[key] ?? _dictionaries['en']?[key] ?? key;
  23. }
  24. /// Like [t], but replaces `{placeholder}` tokens with [params] — for
  25. /// strings with dynamic content (counts, timestamps, error messages).
  26. static String tp(String key, Map<String, String> params) {
  27. var result = t(key);
  28. for (final entry in params.entries) {
  29. result = result.replaceAll('{${entry.key}}', entry.value);
  30. }
  31. return result;
  32. }
  33. /// Display-only translation for an MPOB class name (e.g. "Ripe").
  34. /// The underlying data key (used for DB storage, color lookups, and
  35. /// tally maps) is never touched — this only affects what's shown on
  36. /// screen.
  37. static String className(String englishClassName) => t('class_$englishClassName');
  38. }