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:
vjrj 2026-07-10 15:43:33 +02:00
parent 8d1a66e7b8
commit c9c764e624
10 changed files with 695 additions and 15 deletions

View file

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