mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
refactor: improve locale handling fixing issues with yoruba, hausa and guarani languages
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user