Files
authsec_flutter/lib/providers/locale_provider.dart
T

93 lines
3.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Single source of truth for app language (Group-2).
/// Drives BOTH systems together:
/// - ARB static words via MaterialApp.locale (AppLocalizations)
/// - wireframe field words via DynamicStrings.tr(key) backed by
/// assets/translations_dynamic/{lang}.json (free, offline, KBs).
/// Revert: remove provider wiring in main.dart; generated screens fall back
/// to English keys (DynamicStrings.tr returns readable fallback, never crash).
class LocaleProvider with ChangeNotifier {
static const _prefKey = 'app_locale_code';
static const supportedCodes = ['en', 'hi'];
Locale _locale = const Locale('en');
Locale get locale => _locale;
String get code => _locale.languageCode;
/// lang -> (key -> translated value). Embedded assets loaded once at startup.
final Map<String, Map<String, String>> _tables = {};
/// Server-supplied diff cache (future: portal-backend translations API).
/// Key format: "lang::key" -> value, persisted to SharedPreferences.
final Map<String, String> _serverDiff = {};
bool _ready = false;
bool get ready => _ready;
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString(_prefKey);
if (saved != null && supportedCodes.contains(saved)) {
_locale = Locale(saved);
}
_ready = true;
notifyListeners();
}
/// Called once from bootstrap with preloaded asset tables.
void seedTables(Map<String, Map<String, String>> tables) {
_tables.addAll(tables);
}
/// Merge server diff (new wireframe fields added after release).
/// Works offline afterwards because diff is persisted.
Future<void> updateFromServer(Map<String, String> words, String lang) async {
if (words.isEmpty) return;
final table = _tables.putIfAbsent(lang, () => {});
table.addAll(words);
for (final e in words.entries) {
_serverDiff['$lang::${e.key}'] = e.value;
}
final prefs = await SharedPreferences.getInstance();
// Persist each entry individually to survive restarts (small volume).
for (final e in words.entries) {
await prefs.setString('dyn_${lang}_${e.key}', e.value);
}
notifyListeners();
}
Future<void> loadServerCache() async {
final prefs = await SharedPreferences.getInstance();
for (final lang in supportedCodes) {
final table = _tables.putIfAbsent(lang, () => {});
for (final key in prefs.getKeys().where((k) => k.startsWith('dyn_${lang}_'))) {
final field = key.substring('dyn_${lang}_'.length);
final v = prefs.getString(key);
if (v != null) table[field] = v;
}
}
notifyListeners();
}
Future<void> setLocale(String code) async {
if (!supportedCodes.contains(code)) return;
_locale = Locale(code);
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefKey, code);
notifyListeners();
}
/// Runtime lookup for wireframe-driven strings.
/// Never throws: missing key -> English table -> pretty fallback.
String trDynamic(String key) {
final norm = key.trim().toLowerCase().replaceAll(RegExp(r'\s+'), '_');
final cur = _tables[code]?[norm];
if (cur != null && cur.isNotEmpty) return cur;
final en = _tables['en']?[norm];
if (en != null && en.isNotEmpty) return en;
return key.replaceAll('_', ' ');
}
}