refactor: improve locale handling fixing issues with yoruba, hausa and guarani languages

This commit is contained in:
Blazebrain
2026-09-09 02:35:27 +01:00
parent 3fabbc5c5e
commit 7e0f0fc3cd
9 changed files with 68 additions and 44 deletions
+2 -5
View File
@@ -1,5 +1,5 @@
import "package:cw_core/amount/money.dart";
import "package:intl/intl.dart";
import "package:cw_core/crypto_amount_format.dart";
extension WithLocalSeparator on Money {
String toLocalStringWithSymbol({
@@ -37,10 +37,7 @@ extension WithLocalSeparator on Money {
String _withLocalSeparator(String amount, {String? locale}) {
final isNegative = amount.startsWith("-");
final formater = NumberFormat("#,###", locale);
final parts = (isNegative ? amount.substring(1) : amount).split(".");
final formatted = [formater.format(int.tryParse(parts.first) ?? 0), ...parts.sublist(1)]
.join(formater.symbols.DECIMAL_SEP);
final formatted = (isNegative ? amount.substring(1) : amount).withLocalSeperator(locale);
return isNegative ? "-$formatted" : formatted;
}
+4 -1
View File
@@ -67,7 +67,10 @@ extension MaxDecimals on String {
return [parts.first.withLocalSeperator(locale), ...parts.sublist(1)].join(" ");
}
final formater = NumberFormat("#,###", locale);
final formater = NumberFormat(
"#,###",
Intl.verifiedLocale(locale, NumberFormat.localeExists, onFailure: (_) => "en"),
);
final parts = replaceAll(",", "").split(".");
if (parts.first.contains("< 0")) parts.first = "0";
@@ -23,6 +23,10 @@ void main() {
expect(_btcGrouping.toLocalStringWithPrecision(locale: "de_DE"), "1.234,5");
});
test("formats as English when intl has no number data for the locale (yo)", () {
expect(_btcGrouping.toLocalStringWithPrecision(locale: "yo"), "1,234.5");
});
test("base unit renders integer sats with grouping", () {
expect(
_btc012.toLocalStringWithPrecision(useBaseUnit: true, locale: "en_US"),
+9
View File
@@ -108,6 +108,15 @@ void main() {
});
});
group("Locale without intl number data", () {
test("should format as English instead of throwing (ha)", () {
const input = "1123.4567";
final result = input.withLocalSeperator("ha");
expect(result, equals("1,123.4567"));
});
});
group('With XMR Suffix', () {
const locale = 'de_CH';
+3 -8
View File
@@ -3,16 +3,11 @@ import 'package:devicelocale/devicelocale.dart';
import "package:flutter/widgets.dart";
import 'package:intl/intl.dart';
// This will turn stored language code into Flutter Locale.
// Some of our language codes uses an underscore (like "pt_BR") and this must be split into language
// and country subtags, if we pass the whole string as the languageCode, GlobalMaterialLocalizations
// would reject it as an invalid ISO 639-1 code and crash the app on locale switch
// Flutter's Material delegates match on the language subtag, so "pt_BR" must be split into two
Locale localeFromLanguageCode(String code) {
final parts = code.split("_");
if (parts.length == 2 && parts[0].isNotEmpty && parts[1].isNotEmpty) {
return Locale(parts[0], parts[1]);
}
return Locale(code);
return parts.length == 2 ? Locale(parts[0], parts[1]) : Locale(code);
}
class LanguageService {
+2
View File
@@ -65,6 +65,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_daemon/flutter_daemon.dart';
import "package:flutter_localizations/flutter_localizations.dart";
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:hive/hive.dart';
import 'package:quick_actions/quick_actions.dart';
@@ -329,6 +330,7 @@ Future<void> initialSetup({
required int initialMigrationVersion,
}) async {
LanguageService.loadLocaleList();
await GlobalMaterialLocalizations.delegate.load(const Locale("en"));
await defaultSettingsMigration(
secureStorage: secureStorage,
version: initialMigrationVersion,
@@ -32,30 +32,35 @@ class ListItemSelectorWidget extends StatelessWidget {
onTap: onTap,
isFirstInSection: isFirstInSection,
isLastInSection: isLastInSection,
builder: (context, textStyle, labelStyle) {
return Row(
builder: (context, textStyle, labelStyle) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(child: Text(label, style: textStyle)),
Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
spacing: 8,
children: [
Text(
options[selectedIndex],
style: labelStyle,
),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
colorFilter:
ColorFilter.mode(theme.colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [
Flexible(
child: Text(
options[selectedIndex],
style: labelStyle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
CakeImageWidget(
imageUrl: "assets/new-ui/chooser.svg",
colorFilter:
ColorFilter.mode(theme.colorScheme.onSurfaceVariant, BlendMode.srcIn),
),
],
),
),
),
],
);
});
));
}
}
+14 -3
View File
@@ -1378,8 +1378,8 @@ abstract class SettingsStoreBase with Store {
pinLength = defaultPinLength;
}
final savedLanguageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ??
await LanguageService.localeDetection();
final savedLanguageCode =
_offeredLanguageCode(sharedPreferences) ?? await LanguageService.localeDetection();
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
@@ -1938,7 +1938,12 @@ abstract class SettingsStoreBase with Store {
}
pinCodeLength = pinLength;
languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode;
final restoredLanguageCode = _offeredLanguageCode(sharedPreferences);
if (restoredLanguageCode == null) {
await sharedPreferences.setString(PreferencesKey.currentLanguageCode, languageCode);
} else {
languageCode = restoredLanguageCode;
}
shouldShowYatPopup =
sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup;
shouldShowDEuroDisclaimer =
@@ -2315,6 +2320,12 @@ abstract class SettingsStoreBase with Store {
await _sharedPreferences.setString(key, serializedData);
}
static String? _offeredLanguageCode(SharedPreferences sharedPreferences) {
final code = sharedPreferences.getString(PreferencesKey.currentLanguageCode);
return LanguageService.list.containsKey(code) ? code : null;
}
static Future<String?> _getDeviceName() async {
String? deviceName = '';
final deviceInfoPlugin = DeviceInfoPlugin();
+6 -8
View File
@@ -1,3 +1,4 @@
import "dart:collection";
import 'dart:io';
import 'dart:convert';
@@ -48,7 +49,7 @@ Future<void> main(List<String> args) async {
return;
}
final arbFiles = <String, dynamic>{};
final localePath = SplayTreeMap<String, dynamic>();
await dir.list(recursive: false).forEach((element) {
// Parse the locale from the file name (e.g. strings_pt_br.arb -> pt_BR),
// normalizing the case so keys match LanguageService.supportedLocales.
@@ -62,12 +63,9 @@ Future<void> main(List<String> args) async {
final locale = parts.length > 1
? '${parts.first.toLowerCase()}_${parts.sublist(1).join('_').toUpperCase()}'
: parts.first.toLowerCase();
arbFiles[locale] = element.path;
localePath[locale] = element.path;
});
final sortedLocales = arbFiles.keys.toList()..sort();
final localePath = <String, dynamic>{for (final key in sortedLocales) key: arbFiles[key]};
if (!localePath.keys.contains(defaultLocale)) {
print("Locale list doesn't contain $defaultLocale");
return;
@@ -105,13 +103,13 @@ Future<void> main(List<String> args) async {
output += classDeclaration;
// for handling keys like pt_BR so entry becomes Locale("pt", "BR") and doesn't crash app
// Flutter matches on the language subtag, so pt_BR is emitted as Locale("pt", "BR")
localePath.keys.forEach((key) {
final parts = key.split("_");
if (parts.length == 2) {
output += " Locale(\"${parts[0]}\", \"${parts[1]}\"),\n";
output += ' Locale("${parts[0]}", "${parts[1]}"),\n';
} else {
output += " Locale(\"$key\", \"\"),\n";
output += ' Locale("$key", ""),\n';
}
});