feat: add localisation

This commit is contained in:
2026-07-31 21:46:05 +02:00
parent b58c9a8722
commit f5b2041612
30 changed files with 4099 additions and 326 deletions
+86 -42
View File
@@ -10,6 +10,8 @@ import '../models/settings.dart';
import '../utils/app_update_util.dart';
import '../viewmodels/pin_lock_view_model.dart';
import '../viewmodels/settings_view_model.dart';
import '../l10n/app_localizations.dart';
import '../l10n/app_localizations_helpers.dart';
class SettingsScreenView extends StatefulWidget {
const SettingsScreenView({super.key});
@@ -58,8 +60,9 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
});
}
Future<String?> _promptPin(String title, {String hint = 'Enter PIN (4 digits)'}) {
Future<String?> _promptPin(String title, {String? hint}) {
final controller = TextEditingController();
final l10n = AppLocalizations.of(context);
return showDialog<String>(
context: context,
@@ -81,12 +84,12 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
child: Text(l10n.cancel),
),
const SizedBox(width: 12),
FilledButton(
onPressed: () => Navigator.pop(context, controller.text),
child: const Text('Confirm'),
child: Text(l10n.confirm),
),
],
),
@@ -101,11 +104,12 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
}
Future<void> _enablePin() async {
final pin = await _promptPin('Set PIN Code', hint: '4 digits');
final l10n = AppLocalizations.of(context);
final pin = await _promptPin(l10n.setPinCode, hint: l10n.fourDigits);
if (pin == null) return;
if (pin.length != 4) {
_showError('PIN must be exactly 4 digits');
_showError(l10n.pinExactlyFour);
return;
}
@@ -113,7 +117,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
final success = await pinLockViewModel.setPin(pin);
if (!success) {
_showError(pinLockViewModel.errorMessage ?? 'Could not set PIN.');
_showError(l10n.localizedError(pinLockViewModel.errorMessage));
return;
}
@@ -123,19 +127,20 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
await context.read<SettingsViewModel>().updatePinRequired(true);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
_showError('Could not save PIN setting.');
_showError(l10n.couldNotSavePin);
}
}
Future<void> _changePin() async {
final current = await _promptPin('Enter Current PIN');
final l10n = AppLocalizations.of(context);
final current = await _promptPin(l10n.enterCurrentPin);
if (current == null) return;
final newPin = await _promptPin('Enter New PIN', hint: '4 digits');
final newPin = await _promptPin(l10n.enterNewPin, hint: l10n.fourDigits);
if (newPin == null) return;
if (newPin.length != 4) {
_showError('PIN must be exactly 4 digits');
_showError(l10n.pinExactlyFour);
return;
}
@@ -146,19 +151,20 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
);
if (!success) {
_showError(pinLockViewModel.errorMessage ?? 'Could not change PIN.');
_showError(l10n.localizedError(pinLockViewModel.errorMessage));
}
}
Future<void> _disablePin() async {
final current = await _promptPin('Enter Current PIN to Disable');
final l10n = AppLocalizations.of(context);
final current = await _promptPin(l10n.enterCurrentPin);
if (current == null) return;
final pinLockViewModel = context.read<PinLockViewModel>();
final success = await pinLockViewModel.disablePin(current);
if (!success) {
_showError(pinLockViewModel.errorMessage ?? 'Could not disable PIN.');
_showError(l10n.localizedError(pinLockViewModel.errorMessage));
return;
}
@@ -168,12 +174,13 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
await context.read<SettingsViewModel>().updatePinRequired(false);
} catch (e, stack) {
Sentry.captureException(e, stackTrace: stack);
_showError('Could not save PIN setting.');
_showError(l10n.couldNotSavePin);
}
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final settingsViewModel = context.watch<SettingsViewModel>();
final pinLockViewModel = context.watch<PinLockViewModel>();
@@ -186,7 +193,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
icon: const Icon(Icons.arrow_back),
),
const SizedBox(width: 5),
const Text('Settings'),
Text(l10n.settingsTitle),
],
),
),
@@ -209,11 +216,11 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
padding: const EdgeInsets.symmetric(vertical: 12),
children: [
_SettingsSection(
title: 'Security',
title: l10n.security,
children: [
_SettingsSwitchTile(
icon: Icons.lock_outline_rounded,
title: 'PIN Required',
title: l10n.pinRequired,
value: settings.pinRequired,
onChanged: (value) {
if (value) {
@@ -226,22 +233,58 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
if (settings.pinRequired)
_SettingsTile(
icon: Icons.pin_rounded,
title: 'Change PIN',
title: l10n.changePin,
onTap: _changePin,
),
],
),
_SettingsSection(
title: 'Appearance',
title: l10n.appearance,
children: [
_SettingsTile(
icon: Icons.language_rounded,
title: l10n.language,
subtitle: switch (settings.language) {
AppLanguage.system => l10n.languageSystem,
AppLanguage.english => l10n.languageEnglish,
AppLanguage.dutch => l10n.languageDutch,
},
onTap: () async {
final selected = await showModalBottomSheet<AppLanguage>(
context: context,
builder: (context) => SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: AppLanguage.values.map((language) {
return RadioListTile<AppLanguage>(
value: language,
groupValue: settings.language,
title: Text(switch (language) {
AppLanguage.system => l10n.languageSystem,
AppLanguage.english => l10n.languageEnglish,
AppLanguage.dutch => l10n.languageDutch,
}),
onChanged: (value) =>
Navigator.pop(context, value),
);
}).toList(),
),
),
);
if (selected != null) {
await settingsViewModel.updateLanguage(selected);
}
},
),
_SettingsTile(
icon: Icons.brightness_6_rounded,
title: 'Theme',
title: l10n.theme,
subtitle: switch (settings.themeMode) {
AppThemeMode.system => 'System',
AppThemeMode.light => 'Light',
AppThemeMode.dark => 'Dark',
AppThemeMode.ugly => 'Ugly',
AppThemeMode.system => l10n.system,
AppThemeMode.light => l10n.light,
AppThemeMode.dark => l10n.dark,
AppThemeMode.ugly => l10n.ugly,
},
onTap: () async {
final selected = await showModalBottomSheet<AppThemeMode>(
@@ -254,10 +297,10 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
value: mode,
groupValue: settings.themeMode,
title: Text(switch (mode) {
AppThemeMode.system => 'System',
AppThemeMode.light => 'Light',
AppThemeMode.dark => 'Dark',
AppThemeMode.ugly => 'Ugly',
AppThemeMode.system => l10n.system,
AppThemeMode.light => l10n.light,
AppThemeMode.dark => l10n.dark,
AppThemeMode.ugly => l10n.ugly,
}),
onChanged: (value) =>
Navigator.pop(context, value),
@@ -276,13 +319,13 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
),
if (Platform.isAndroid)
_SettingsSection(
title: 'Updates',
title: l10n.updates,
children: [
_SettingsTile(
icon: Icons.system_update_alt_rounded,
title: settingsViewModel.checkingForUpdates
? 'Checking for updates...'
: 'Check for updates',
? l10n.checkingForUpdates
: l10n.checkForUpdates,
onTap: settingsViewModel.checkingForUpdates
? null
: _checkForUpdates,
@@ -294,7 +337,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
onTap: _onVersionTap,
child: Center(
child: Text(
'Version $_appVersion',
l10n.version(_appVersion),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
@@ -313,6 +356,7 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
Future<void> _checkForUpdates() async {
if (!Platform.isAndroid) return;
final l10n = AppLocalizations.of(context);
final vm = context.read<SettingsViewModel>();
try {
@@ -321,11 +365,9 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
if (!mounted) return;
if (update == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('You are already on the latest version.'),
),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.latestVersion)));
return;
}
@@ -336,21 +378,23 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('Update check failed: $e')));
).showSnackBar(SnackBar(content: Text(l10n.updateCheckFailed)));
}
}
void _showUpdateDialog(UpdateInfo update) {
final l10n = AppLocalizations.of(context);
showDialog(
context: context,
barrierDismissible: !update.mandatory,
builder: (dialogContext) => AlertDialog(
title: const Text("Update available"),
title: Text(l10n.updateAvailable),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Version ${update.version} is available."),
Text(l10n.versionAvailable(update.version)),
const SizedBox(height: 12),
Text(update.notes),
],
@@ -359,14 +403,14 @@ class _SettingsScreenViewState extends State<SettingsScreenView> {
if (!update.mandatory)
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text("Later"),
child: Text(l10n.later),
),
FilledButton(
onPressed: () {
Navigator.pop(dialogContext);
context.push('/update-progress', extra: update);
},
child: const Text("Update"),
child: Text(l10n.update),
),
],
),