diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index ca8765b..484bb31 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -105,14 +105,40 @@ class AppLocale extends _$AppLocale { _storage = ref.watch(optimizedStorageServiceProvider); final code = _storage.getLocaleCode(); if (code != null && code.isNotEmpty) { - return Locale(code); + final parsed = _parseLocaleCode(code); + if (parsed != null) return parsed; } return null; // system default } Future setLocale(Locale? locale) async { state = locale; - await _storage.setLocaleCode(locale?.languageCode); + await _storage.setLocaleCode(locale?.toLanguageTag()); + } + + Locale? _parseLocaleCode(String code) { + final normalized = code.replaceAll('_', '-'); + final parts = normalized.split('-'); + if (parts.isEmpty || parts.first.isEmpty) return null; + + final language = parts.first; + String? script; + String? country; + + for (var i = 1; i < parts.length; i++) { + final part = parts[i]; + if (part.length == 4) { + script = '${part[0].toUpperCase()}${part.substring(1).toLowerCase()}'; + } else if (part.length == 2 || part.length == 3) { + country = part.toUpperCase(); + } + } + + return Locale.fromSubtags( + languageCode: language, + scriptCode: script, + countryCode: country, + ); } } diff --git a/lib/features/profile/views/app_customization_page.dart b/lib/features/profile/views/app_customization_page.dart index 9178f9c..f6ded71 100644 --- a/lib/features/profile/views/app_customization_page.dart +++ b/lib/features/profile/views/app_customization_page.dart @@ -38,7 +38,7 @@ class AppCustomizationPage extends ConsumerWidget { return l10n.currentlyUsingLightTheme; }(); final locale = ref.watch(appLocaleProvider); - final currentLanguageCode = locale?.languageCode ?? 'system'; + final currentLanguageCode = locale?.toLanguageTag() ?? 'system'; final languageLabel = _resolveLanguageLabel(context, currentLanguageCode); final activeTheme = ref.watch(appThemePaletteProvider); @@ -198,7 +198,7 @@ class AppCustomizationPage extends ConsumerWidget { Widget _buildLanguageSection( BuildContext context, WidgetRef ref, - String currentLanguageCode, + String currentLanguageTag, String languageLabel, ) { final theme = context.conduitTheme; @@ -220,15 +220,16 @@ class AppCustomizationPage extends ConsumerWidget { onTap: () async { final selected = await _showLanguageSelector( context, - currentLanguageCode, + currentLanguageTag, ); if (selected == null) return; if (selected == 'system') { await ref.read(appLocaleProvider.notifier).setLocale(null); } else { + final parsed = _parseLocaleTag(selected); await ref .read(appLocaleProvider.notifier) - .setLocale(Locale(selected)); + .setLocale(parsed ?? Locale(selected)); } }, ), @@ -1741,6 +1742,8 @@ class AppCustomizationPage extends ConsumerWidget { } String _resolveLanguageLabel(BuildContext context, String code) { + final normalizedCode = code.replaceAll('_', '-'); + switch (code) { case 'en': return AppLocalizations.of(context)!.english; @@ -1757,10 +1760,21 @@ class AppCustomizationPage extends ConsumerWidget { case 'ru': return AppLocalizations.of(context)!.russian; case 'zh': - return AppLocalizations.of(context)!.chinese; + return AppLocalizations.of(context)!.chineseSimplified; case 'ko': return AppLocalizations.of(context)!.korean; + case 'zh-Hant': + return AppLocalizations.of(context)!.chineseTraditional; default: + if (normalizedCode == 'zh-hant') { + return AppLocalizations.of(context)!.chineseTraditional; + } + if (normalizedCode == 'zh') { + return AppLocalizations.of(context)!.chineseSimplified; + } + if (normalizedCode == 'ko') { + return AppLocalizations.of(context)!.korean; + } return AppLocalizations.of(context)!.system; } } @@ -1929,6 +1943,8 @@ class AppCustomizationPage extends ConsumerWidget { } Future _showLanguageSelector(BuildContext context, String current) { + final normalizedCurrent = current.replaceAll('_', '-'); + return showModalBottomSheet( context: context, backgroundColor: Colors.transparent, @@ -1949,52 +1965,79 @@ class AppCustomizationPage extends ConsumerWidget { const SizedBox(height: Spacing.sm), ListTile( title: Text(AppLocalizations.of(context)!.system), - trailing: current == 'system' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'system' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'system'), ), ListTile( title: Text(AppLocalizations.of(context)!.english), - trailing: current == 'en' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'en' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'en'), ), ListTile( title: Text(AppLocalizations.of(context)!.deutsch), - trailing: current == 'de' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'de' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'de'), ), ListTile( title: Text(AppLocalizations.of(context)!.espanol), - trailing: current == 'es' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'es' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'es'), ), ListTile( title: Text(AppLocalizations.of(context)!.francais), - trailing: current == 'fr' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'fr' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'fr'), ), ListTile( title: Text(AppLocalizations.of(context)!.italiano), - trailing: current == 'it' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'it' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'it'), ), ListTile( title: Text(AppLocalizations.of(context)!.nederlands), - trailing: current == 'nl' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'nl' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'nl'), ), ListTile( title: Text(AppLocalizations.of(context)!.russian), - trailing: current == 'ru' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'ru' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'ru'), ), ListTile( - title: Text(AppLocalizations.of(context)!.chinese), - trailing: current == 'zh' ? const Icon(Icons.check) : null, + title: Text(AppLocalizations.of(context)!.chineseSimplified), + trailing: normalizedCurrent == 'zh' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'zh'), ), + ListTile( + title: Text(AppLocalizations.of(context)!.chineseTraditional), + trailing: normalizedCurrent == 'zh-Hant' + ? const Icon(Icons.check) + : null, + onTap: () => Navigator.pop(context, 'zh-Hant'), + ), ListTile( title: Text(AppLocalizations.of(context)!.korean), - trailing: current == 'ko' ? const Icon(Icons.check) : null, + trailing: normalizedCurrent == 'ko' + ? const Icon(Icons.check) + : null, onTap: () => Navigator.pop(context, 'ko'), ), const SizedBox(height: Spacing.sm), @@ -2006,6 +2049,31 @@ class AppCustomizationPage extends ConsumerWidget { } } +Locale? _parseLocaleTag(String code) { + final normalized = code.replaceAll('_', '-'); + final parts = normalized.split('-'); + if (parts.isEmpty || parts.first.isEmpty) return null; + + final language = parts.first; + String? script; + String? country; + + for (var i = 1; i < parts.length; i++) { + final part = parts[i]; + if (part.length == 4) { + script = '${part[0].toUpperCase()}${part.substring(1).toLowerCase()}'; + } else if (part.length == 2 || part.length == 3) { + country = part.toUpperCase(); + } + } + + return Locale.fromSubtags( + languageCode: language, + scriptCode: script, + countryCode: country, + ); +} + class _PaletteOption extends StatelessWidget { const _PaletteOption({ required this.themeDefinition, diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index ceb2a51..b74147c 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -178,6 +178,8 @@ "nederlands": "Niederländisch", "russian": "Russisch", "chinese": "Chinesisch", + "chineseSimplified": "Chinesisch (Vereinfacht)", + "chineseTraditional": "Chinesisch (Traditionell)", "korean": "Koreanisch", "deleteMessagesTitle": "Nachrichten löschen", "deleteMessagesMessage": "{count} Nachrichten löschen?", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a03f938..0de9784 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -862,6 +862,14 @@ "@chinese": { "description": "Language name: Chinese." }, + "chineseSimplified": "Chinese (Simplified)", + "@chineseSimplified": { + "description": "Language name: Chinese (Simplified)." + }, + "chineseTraditional": "Chinese (Traditional)", + "@chineseTraditional": { + "description": "Language name: Chinese (Traditional)." + }, "korean": "한국어", "@korean": { "description": "Language name: Korean." diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 3f8b0d2..d7f2474 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -178,6 +178,8 @@ "nederlands": "Nederlands", "russian": "Русский", "chinese": "中文", + "chineseSimplified": "Chino (simplificado)", + "chineseTraditional": "Chino (tradicional)", "korean": "한국어", "deleteMessagesTitle": "Eliminar mensajes", "deleteMessagesMessage": "¿Eliminar {count} mensajes?", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bcc5610..d3f8cee 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -178,6 +178,8 @@ "nederlands": "Néerlandais", "russian": "Russe", "chinese": "Chinois", + "chineseSimplified": "Chinois (simplifié)", + "chineseTraditional": "Chinois (traditionnel)", "korean": "Coréen", "deleteMessagesTitle": "Supprimer les messages", "deleteMessagesMessage": "Supprimer {count} messages ?", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index e62a8c9..f8bde04 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -178,6 +178,8 @@ "nederlands": "Olandese", "russian": "Russo", "chinese": "Cinese", + "chineseSimplified": "Cinese (semplificato)", + "chineseTraditional": "Cinese (tradizionale)", "korean": "Coreano", "deleteMessagesTitle": "Elimina messaggi", "deleteMessagesMessage": "Eliminare {count} messaggi?", diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index b4e22e9..93f7199 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -286,6 +286,8 @@ "nederlands": "Nederlands", "russian": "Русский", "chinese": "中文", + "chineseSimplified": "중국어(간체)", + "chineseTraditional": "중국어(번체)", "korean": "한국어", "deleteMessagesTitle": "메시지 삭제", "deleteMessagesMessage": "{count}개의 메시지를 삭제하시겠습니까?", diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 6ea69fe..b706083 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -178,6 +178,8 @@ "nederlands": "Nederlands", "russian": "Русский", "chinese": "中文", + "chineseSimplified": "Chinees (vereenvoudigd)", + "chineseTraditional": "Chinees (traditioneel)", "korean": "한국어", "deleteMessagesTitle": "Berichten verwijderen", "deleteMessagesMessage": "{count} berichten verwijderen?", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 189f8dc..41cd196 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -178,6 +178,8 @@ "nederlands": "Nederlands", "russian": "Русский", "chinese": "中文", + "chineseSimplified": "Китайский (упрощённый)", + "chineseTraditional": "Китайский (традиционный)", "korean": "한국어", "deleteMessagesTitle": "Удалить сообщения", "deleteMessagesMessage": "Удалить {count, plural, one{{count} сообщение} few{{count} сообщения} other{{count} сообщений}}?", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 37ea494..4148d33 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -178,6 +178,8 @@ "nederlands": "Nederlands", "russian": "Русский", "chinese": "中文", + "chineseSimplified": "简体中文", + "chineseTraditional": "繁體中文", "korean": "한국어", "deleteMessagesTitle": "删除消息", "deleteMessagesMessage": "删除 {count} 条消息?", diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 7c69c31..9b8433c 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -178,6 +178,8 @@ "nederlands": "Nederlands", "russian": "Русский", "chinese": "中文", + "chineseSimplified": "簡體中文", + "chineseTraditional": "繁體中文", "korean": "한국어", "deleteMessagesTitle": "刪除消息", "deleteMessagesMessage": "刪除 {count} 條消息?", diff --git a/lib/main.dart b/lib/main.dart index 50a1027..73b310e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -189,12 +189,8 @@ class _ConduitAppState extends ConsumerState { if (deviceLocales == null || deviceLocales.isEmpty) { return supported.first; } - for (final device in deviceLocales) { - for (final loc in supported) { - if (loc.languageCode == device.languageCode) return loc; - } - } - return supported.first; + final resolved = _resolveSupportedLocale(deviceLocales, supported); + return resolved ?? supported.first; }, builder: (context, child) { final brightness = Theme.of(context).brightness; @@ -221,6 +217,73 @@ class _ConduitAppState extends ConsumerState { ), ); } + + bool _prefersTraditionalChinese(Locale deviceLocale) { + final script = deviceLocale.scriptCode?.toLowerCase(); + if (script == 'hant') return true; + + final country = deviceLocale.countryCode?.toUpperCase(); + return country == 'TW' || country == 'HK' || country == 'MO'; + } + + Locale? _resolveSupportedLocale( + List? deviceLocales, + Iterable supported, + ) { + if (deviceLocales == null || deviceLocales.isEmpty) return null; + + for (final device in deviceLocales) { + final prefersTraditional = _prefersTraditionalChinese(device); + final deviceLanguage = device.languageCode.toLowerCase(); + final deviceScript = device.scriptCode?.toLowerCase(); + final deviceCountry = device.countryCode?.toUpperCase(); + + // Pass 1: match language with script (or preferred Traditional) + for (final loc in supported) { + final languageMatches = + loc.languageCode.toLowerCase() == deviceLanguage; + if (!languageMatches) continue; + + final locScript = loc.scriptCode?.toLowerCase(); + final scriptMatches = + locScript != null && + locScript.isNotEmpty && + (locScript == deviceScript || + (loc.languageCode == 'zh' && + locScript == 'hant' && + prefersTraditional)); + if (!scriptMatches) continue; + + final locCountry = loc.countryCode?.toUpperCase(); + final countryMatches = + locCountry == null || + locCountry.isEmpty || + locCountry == deviceCountry; + + if (countryMatches) { + return loc; + } + } + + // Pass 2: prefer Traditional Chinese when applicable + if (prefersTraditional) { + for (final loc in supported) { + if (loc.languageCode == 'zh' && loc.scriptCode == 'Hant') { + return loc; + } + } + } + + // Pass 3: language-only match + for (final loc in supported) { + if (loc.languageCode.toLowerCase() == deviceLanguage) { + return loc; + } + } + } + + return null; + } } /// Dismisses the soft keyboard whenever the user scrolls.