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> _tables = {}; /// Server-supplied diff cache (future: portal-backend translations API). /// Key format: "lang::key" -> value, persisted to SharedPreferences. final Map _serverDiff = {}; bool _ready = false; bool get ready => _ready; Future 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> tables) { _tables.addAll(tables); } /// Merge server diff (new wireframe fields added after release). /// Works offline afterwards because diff is persisted. Future updateFromServer(Map 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 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 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('_', ' '); } }