feat(i18n): add Asturian (asturianu) as a language
Adds Asturianu, translated in full (345 strings), plus the wiring to make a minority language work end to end — the OS language picker is an unreliable way to reach it, so the in-app choice must be explicit and remembered. - Model Asturian as the real `ast` locale (honest, international-by-design), not a fake `es-AST` region — slang rejects a 3-letter region anyway. - Flutter ships no Material/Cupertino/intl localizations for `ast`, so map the framework chrome to Spanish (`materialLocaleFor`); the app's own text stays Asturian via slang. Fixes an "Invalid locale ast" crash when the auto-backup date was formatted with intl. - Persist the picked language in the keystore (LocaleStore) and restore it at startup so it survives restarts and wins over the device locale. - Settings: add the Asturianu tile; compare the full AppLocale so `es` and `ast` aren't both marked selected. Tests: LocaleStore round-trip, locale switch + Spanish-chrome fallback, and a regression for the intl date crash under Asturian.
This commit is contained in:
parent
8d1a66e7b8
commit
c9c764e624
10 changed files with 695 additions and 15 deletions
73
apps/app_seeds/test/ui/asturian_locale_test.dart
Normal file
73
apps/app_seeds/test/ui/asturian_locale_test.dart
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tane/app.dart';
|
||||
import 'package:tane/i18n/strings.g.dart';
|
||||
import 'package:tane/services/locale_store.dart';
|
||||
import 'package:tane/ui/settings_screen.dart';
|
||||
|
||||
import '../support/test_support.dart';
|
||||
|
||||
/// Asturian (`ast`) is a first-class language, but Flutter ships no
|
||||
/// Material/Cupertino localizations for it. The app carries the honest `ast`
|
||||
/// locale for its own strings and borrows Spanish for the framework chrome
|
||||
/// (see [materialLocaleFor]). These tests pin both halves of that contract.
|
||||
void main() {
|
||||
testWidgets('picking Asturian switches app text to Asturian, keeps Material '
|
||||
'chrome resolvable in Spanish, and persists the choice', (tester) async {
|
||||
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||
final store = LocaleStore(InMemorySecretStore());
|
||||
late Locale materialLocale;
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final appLocale = TranslationProvider.of(context).flutterLocale;
|
||||
return MaterialApp(
|
||||
// Mirrors app.dart: the framework localizations follow the mapped
|
||||
// locale, the app's own strings follow slang.
|
||||
locale: materialLocaleFor(appLocale),
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: Builder(
|
||||
builder: (context) {
|
||||
// Resolving these must never throw for the Asturian locale.
|
||||
materialLocale = Localizations.localeOf(context);
|
||||
MaterialLocalizations.of(context);
|
||||
return SettingsScreen(localeStore: store);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Starts in English.
|
||||
expect(find.text('Language'), findsOneWidget);
|
||||
expect(materialLocale.languageCode, 'en');
|
||||
|
||||
await tester.tap(find.text('Asturianu'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// App strings are now Asturian…
|
||||
expect(find.text('Llingua'), findsOneWidget);
|
||||
// …the framework chrome fell back to Spanish (not the unsupported `ast`)…
|
||||
expect(materialLocale.languageCode, 'es');
|
||||
// …and the choice was remembered for next launch.
|
||||
expect(await store.saved(), AppLocale.ast);
|
||||
});
|
||||
|
||||
test('materialLocaleFor maps Asturian to Spanish and passes others through',
|
||||
() {
|
||||
expect(materialLocaleFor(const Locale('ast')), const Locale('es'));
|
||||
expect(materialLocaleFor(const Locale('es')), const Locale('es'));
|
||||
expect(materialLocaleFor(const Locale('en')), const Locale('en'));
|
||||
expect(materialLocaleFor(const Locale('pt')), const Locale('pt'));
|
||||
});
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ import 'package:tane/services/auto_backup_store.dart';
|
|||
import 'package:tane/services/export_import_service.dart';
|
||||
import 'package:tane/services/file_service.dart';
|
||||
import 'package:tane/services/recovery_sheet_service.dart';
|
||||
import 'package:tane/app.dart';
|
||||
import 'package:tane/ui/backup_section.dart';
|
||||
import 'package:tane/ui/settings_screen.dart';
|
||||
|
||||
|
|
@ -254,6 +255,41 @@ void main() {
|
|||
expect(spy.calls, 1);
|
||||
expect(find.text('Copy saved'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the last-backup date renders under Asturian without an '
|
||||
'invalid-locale crash', (tester) async {
|
||||
// Asturian (`ast`) has no `intl` date symbols; formatting the "last copy"
|
||||
// date used to throw "Invalid locale ast". The framework locale is mapped
|
||||
// to Spanish, so the date must format through that instead.
|
||||
LocaleSettings.setLocaleSync(AppLocale.ast);
|
||||
addTearDown(() => LocaleSettings.setLocaleSync(AppLocale.en));
|
||||
|
||||
await tester.pumpWidget(
|
||||
TranslationProvider(
|
||||
child: MaterialApp(
|
||||
locale: materialLocaleFor(const Locale('ast')),
|
||||
supportedLocales: AppLocaleUtils.supportedLocales,
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
home: Scaffold(
|
||||
body: BackupSection(
|
||||
service: service,
|
||||
sheet: sheet,
|
||||
autoBackup: _DatedAutoBackup(db),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(); // resolve the "last copy" future
|
||||
|
||||
// The Asturian tile (and its Spanish-formatted date subtitle) built fine.
|
||||
expect(find.text('Copies automátiques'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
/// Counts explicit "back up now" taps without touching the disk.
|
||||
|
|
@ -277,3 +313,21 @@ class _SpyAutoBackup extends AutoBackupService {
|
|||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Reports a fixed "last copy" date so the auto-backup subtitle formats a real
|
||||
/// date — the path that crashed under Asturian.
|
||||
class _DatedAutoBackup extends AutoBackupService {
|
||||
_DatedAutoBackup(AppDatabase db)
|
||||
: super(
|
||||
exporter: ExportImportService(
|
||||
repository: newTestRepository(db),
|
||||
files: FakeFileService(),
|
||||
keys: SecureKeyStore(store: InMemorySecretStore()),
|
||||
),
|
||||
store: AutoBackupStore(InMemorySecretStore()),
|
||||
directory: () async => throw UnimplementedError(),
|
||||
);
|
||||
|
||||
@override
|
||||
Future<DateTime?> lastBackupAt() async => DateTime.utc(2026, 3, 14);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue