feat(localization): Improve locale resolution and add Chinese script variants

This commit is contained in:
cogwheel0
2025-11-24 16:08:55 +05:30
parent aadabf90ae
commit 6e8a19371c
13 changed files with 207 additions and 24 deletions

View File

@@ -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<void> 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,
);
}
}

View File

@@ -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<String?> _showLanguageSelector(BuildContext context, String current) {
final normalizedCurrent = current.replaceAll('_', '-');
return showModalBottomSheet<String>(
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,

View File

@@ -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?",

View File

@@ -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."

View File

@@ -178,6 +178,8 @@
"nederlands": "Nederlands",
"russian": "Русский",
"chinese": "中文",
"chineseSimplified": "Chino (simplificado)",
"chineseTraditional": "Chino (tradicional)",
"korean": "한국어",
"deleteMessagesTitle": "Eliminar mensajes",
"deleteMessagesMessage": "¿Eliminar {count} mensajes?",

View File

@@ -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 ?",

View File

@@ -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?",

View File

@@ -286,6 +286,8 @@
"nederlands": "Nederlands",
"russian": "Русский",
"chinese": "中文",
"chineseSimplified": "중국어(간체)",
"chineseTraditional": "중국어(번체)",
"korean": "한국어",
"deleteMessagesTitle": "메시지 삭제",
"deleteMessagesMessage": "{count}개의 메시지를 삭제하시겠습니까?",

View File

@@ -178,6 +178,8 @@
"nederlands": "Nederlands",
"russian": "Русский",
"chinese": "中文",
"chineseSimplified": "Chinees (vereenvoudigd)",
"chineseTraditional": "Chinees (traditioneel)",
"korean": "한국어",
"deleteMessagesTitle": "Berichten verwijderen",
"deleteMessagesMessage": "{count} berichten verwijderen?",

View File

@@ -178,6 +178,8 @@
"nederlands": "Nederlands",
"russian": "Русский",
"chinese": "中文",
"chineseSimplified": "Китайский (упрощённый)",
"chineseTraditional": "Китайский (традиционный)",
"korean": "한국어",
"deleteMessagesTitle": "Удалить сообщения",
"deleteMessagesMessage": "Удалить {count, plural, one{{count} сообщение} few{{count} сообщения} other{{count} сообщений}}?",

View File

@@ -178,6 +178,8 @@
"nederlands": "Nederlands",
"russian": "Русский",
"chinese": "中文",
"chineseSimplified": "简体中文",
"chineseTraditional": "繁體中文",
"korean": "한국어",
"deleteMessagesTitle": "删除消息",
"deleteMessagesMessage": "删除 {count} 条消息?",

View File

@@ -178,6 +178,8 @@
"nederlands": "Nederlands",
"russian": "Русский",
"chinese": "中文",
"chineseSimplified": "簡體中文",
"chineseTraditional": "繁體中文",
"korean": "한국어",
"deleteMessagesTitle": "刪除消息",
"deleteMessagesMessage": "刪除 {count} 條消息?",

View File

@@ -189,12 +189,8 @@ class _ConduitAppState extends ConsumerState<ConduitApp> {
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<ConduitApp> {
),
);
}
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<Locale>? deviceLocales,
Iterable<Locale> 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.