Externalize strings.

This commit is contained in:
Dain Nilsson 2023-02-28 11:34:29 +01:00
parent 64e2d1bc51
commit 6bf231dc7d
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
18 changed files with 292 additions and 214 deletions

View File

@ -40,8 +40,9 @@ class AboutPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return ResponsiveDialog( return ResponsiveDialog(
title: Text(AppLocalizations.of(context)!.general_about), title: Text(l10n.general_about),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 32), padding: const EdgeInsets.symmetric(vertical: 32),
child: Column( child: Column(
@ -51,7 +52,7 @@ class AboutPage extends ConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.only(top: 24.0), padding: const EdgeInsets.only(top: 24.0),
child: Text( child: Text(
'Yubico Authenticator', l10n.general_app_name,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -62,7 +63,7 @@ class AboutPage extends ConsumerWidget {
children: [ children: [
TextButton( TextButton(
child: Text( child: Text(
AppLocalizations.of(context)!.general_terms_of_use, l10n.general_terms_of_use,
style: style:
const TextStyle(decoration: TextDecoration.underline), const TextStyle(decoration: TextDecoration.underline),
), ),
@ -72,7 +73,7 @@ class AboutPage extends ConsumerWidget {
), ),
TextButton( TextButton(
child: Text( child: Text(
AppLocalizations.of(context)!.general_privacy_policy, l10n.general_privacy_policy,
style: style:
const TextStyle(decoration: TextDecoration.underline), const TextStyle(decoration: TextDecoration.underline),
), ),
@ -84,7 +85,7 @@ class AboutPage extends ConsumerWidget {
), ),
TextButton( TextButton(
child: Text( child: Text(
AppLocalizations.of(context)!.general_open_src_licenses, l10n.general_open_src_licenses,
style: const TextStyle(decoration: TextDecoration.underline), style: const TextStyle(decoration: TextDecoration.underline),
), ),
onPressed: () { onPressed: () {
@ -103,7 +104,7 @@ class AboutPage extends ConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text( child: Text(
AppLocalizations.of(context)!.general_help_and_feedback, l10n.general_help_and_feedback,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -112,7 +113,7 @@ class AboutPage extends ConsumerWidget {
children: [ children: [
TextButton( TextButton(
child: Text( child: Text(
AppLocalizations.of(context)!.general_send_feedback, l10n.general_send_feedback,
style: style:
const TextStyle(decoration: TextDecoration.underline), const TextStyle(decoration: TextDecoration.underline),
), ),
@ -122,7 +123,7 @@ class AboutPage extends ConsumerWidget {
), ),
TextButton( TextButton(
child: Text( child: Text(
AppLocalizations.of(context)!.general_i_need_help, l10n.general_i_need_help,
style: style:
const TextStyle(decoration: TextDecoration.underline), const TextStyle(decoration: TextDecoration.underline),
), ),
@ -139,7 +140,7 @@ class AboutPage extends ConsumerWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Text( child: Text(
AppLocalizations.of(context)!.general_troubleshooting, l10n.general_troubleshooting,
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -150,8 +151,7 @@ class AboutPage extends ConsumerWidget {
const SizedBox(height: 12.0), const SizedBox(height: 12.0),
ActionChip( ActionChip(
avatar: const Icon(Icons.bug_report_outlined), avatar: const Icon(Icons.bug_report_outlined),
label: label: Text(l10n.general_run_diagnostics),
Text(AppLocalizations.of(context)!.general_run_diagnostics),
onPressed: () async { onPressed: () async {
_log.info('Running diagnostics...'); _log.info('Running diagnostics...');
final response = await ref final response = await ref
@ -169,10 +169,7 @@ class AboutPage extends ConsumerWidget {
await ref.read(clipboardProvider).setText(text); await ref.read(clipboardProvider).setText(text);
await ref.read(withContextProvider)( await ref.read(withContextProvider)(
(context) async { (context) async {
showMessage( showMessage(context, l10n.general_diagnostics_copied);
context,
AppLocalizations.of(context)!
.general_diagnostics_copied);
}, },
); );
}, },
@ -183,8 +180,7 @@ class AboutPage extends ConsumerWidget {
if (isAndroid) ...[ if (isAndroid) ...[
const SizedBox(height: 12.0), const SizedBox(height: 12.0),
FilterChip( FilterChip(
label: Text( label: Text(l10n.general_allow_screenshots),
AppLocalizations.of(context)!.general_allow_screenshots),
selected: ref.watch(androidAllowScreenshotsProvider), selected: ref.watch(androidAllowScreenshotsProvider),
onSelected: (value) async { onSelected: (value) async {
ref ref
@ -205,6 +201,7 @@ class LoggingPanel extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final logLevel = ref.watch(logLevelProvider); final logLevel = ref.watch(logLevelProvider);
return Wrap( return Wrap(
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
@ -220,7 +217,7 @@ class LoggingPanel extends ConsumerWidget {
items: Levels.LEVELS, items: Levels.LEVELS,
selected: logLevel != Level.INFO, selected: logLevel != Level.INFO,
labelBuilder: (value) => Text( labelBuilder: (value) => Text(
'${AppLocalizations.of(context)!.general_log_level}: ${value.name[0]}${value.name.substring(1).toLowerCase()}'), '${l10n.general_log_level}: ${value.name[0]}${value.name.substring(1).toLowerCase()}'),
itemBuilder: (value) => itemBuilder: (value) =>
Text('${value.name[0]}${value.name.substring(1).toLowerCase()}'), Text('${value.name[0]}${value.name.substring(1).toLowerCase()}'),
onChanged: (level) { onChanged: (level) {
@ -230,7 +227,7 @@ class LoggingPanel extends ConsumerWidget {
), ),
ActionChip( ActionChip(
avatar: const Icon(Icons.copy), avatar: const Icon(Icons.copy),
label: Text(AppLocalizations.of(context)!.general_copy_log), label: Text(l10n.general_copy_log),
onPressed: () async { onPressed: () async {
_log.info('Copying log to clipboard ($version)...'); _log.info('Copying log to clipboard ($version)...');
final logs = await ref.read(logLevelProvider.notifier).getLogs(); final logs = await ref.read(logLevelProvider.notifier).getLogs();
@ -239,8 +236,7 @@ class LoggingPanel extends ConsumerWidget {
if (!clipboard.platformGivesFeedback()) { if (!clipboard.platformGivesFeedback()) {
await ref.read(withContextProvider)( await ref.read(withContextProvider)(
(context) async { (context) async {
showMessage(context, showMessage(context, l10n.general_log_copied);
AppLocalizations.of(context)!.general_log_copied);
}, },
); );
} }

View File

@ -20,6 +20,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import '../../app/logging.dart'; import '../../app/logging.dart';
@ -189,12 +190,15 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
if (_isUsbAttached) { if (_isUsbAttached) {
void triggerTouchPrompt() async { void triggerTouchPrompt() async {
controller = await _withContext( controller = await _withContext(
(context) async => promptUserInteraction( (context) async {
context, final l10n = AppLocalizations.of(context)!;
icon: const Icon(Icons.touch_app), return promptUserInteraction(
title: 'Touch Required', context,
description: 'Touch the button on your YubiKey now.', icon: const Icon(Icons.touch_app),
), title: l10n.oath_touch_required,
description: l10n.oath_touch_now,
);
},
); );
} }

View File

@ -15,6 +15,7 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'qr_scanner_scan_status.dart'; import 'qr_scanner_scan_status.dart';
import 'qr_scanner_util.dart'; import 'qr_scanner_util.dart';
@ -32,7 +33,8 @@ class QRScannerPermissionsUI extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var scannerAreaWidth = getScannerAreaWidth(screenSize); final l10n = AppLocalizations.of(context)!;
final scannerAreaWidth = getScannerAreaWidth(screenSize);
return Stack(children: [ return Stack(children: [
/// instruction text under the scanner area /// instruction text under the scanner area
@ -42,11 +44,11 @@ class QRScannerPermissionsUI extends StatelessWidget {
screenSize.height - scannerAreaWidth / 2.0 + 8.0), screenSize.height - scannerAreaWidth / 2.0 + 8.0),
width: screenSize.width, width: screenSize.width,
height: screenSize.height), height: screenSize.height),
child: const Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 36), padding: const EdgeInsets.symmetric(horizontal: 36),
child: Text( child: Text(
'Yubico Authenticator needs Camera permissions for scanning QR codes.', l10n.androidQrScanner_need_camera_permission,
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
)), )),
@ -63,32 +65,36 @@ class QRScannerPermissionsUI extends StatelessWidget {
children: [ children: [
Column( Column(
children: [ children: [
const Text( Text(
'Have account info?', l10n.androidQrScanner_have_account_info,
textScaleFactor: 0.7, textScaleFactor: 0.7,
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
OutlinedButton( OutlinedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(''); Navigator.of(context).pop('');
}, },
child: const Text('Enter manually', child: Text(
style: TextStyle(color: Colors.white))), l10n.androidQrScanner_enter_manually,
style: const TextStyle(color: Colors.white),
)),
], ],
), ),
Column( Column(
children: [ children: [
const Text( Text(
'Would like to scan?', l10n.androidQrScanner_want_to_scan,
textScaleFactor: 0.7, textScaleFactor: 0.7,
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
OutlinedButton( OutlinedButton(
onPressed: () { onPressed: () {
onPermissionRequest(); onPermissionRequest();
}, },
child: const Text('Review permissions', child: Text(
style: TextStyle(color: Colors.white))), l10n.androidQrScanner_review_permissions,
style: const TextStyle(color: Colors.white),
)),
], ],
) )
]), ]),

View File

@ -15,6 +15,7 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../keys.dart' as keys; import '../keys.dart' as keys;
import 'qr_scanner_scan_status.dart'; import 'qr_scanner_scan_status.dart';
@ -32,7 +33,8 @@ class QRScannerUI extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var scannerAreaWidth = getScannerAreaWidth(screenSize); final l10n = AppLocalizations.of(context)!;
final scannerAreaWidth = getScannerAreaWidth(screenSize);
return Stack(children: [ return Stack(children: [
/// instruction text under the scanner area /// instruction text under the scanner area
@ -44,8 +46,8 @@ class QRScannerUI extends StatelessWidget {
height: screenSize.height), height: screenSize.height),
child: Text( child: Text(
status != ScanStatus.error status != ScanStatus.error
? 'Point your camera at a QR code to scan it' ? l10n.androidQrScanner_point_and_scan
: 'Invalid QR code', : l10n.androidQrScanner_invalid_code,
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@ -60,18 +62,20 @@ class QRScannerUI extends StatelessWidget {
height: screenSize.height), height: screenSize.height),
child: Column( child: Column(
children: [ children: [
const Text( Text(
'No QR code?', l10n.androidQrScanner_no_code,
textScaleFactor: 0.7, textScaleFactor: 0.7,
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
OutlinedButton( OutlinedButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(''); Navigator.of(context).pop('');
}, },
key: keys.manualEntryButton, key: keys.manualEntryButton,
child: const Text('Enter manually', child: Text(
style: TextStyle(color: Colors.white))), l10n.androidQrScanner_enter_manually,
style: const TextStyle(color: Colors.white),
)),
], ],
), ),
), ),

View File

@ -15,6 +15,7 @@
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:qrscanner_zxing/qrscanner_zxing_view.dart'; import 'package:qrscanner_zxing/qrscanner_zxing_view.dart';
import '../../oath/models.dart'; import '../../oath/models.dart';
@ -63,7 +64,6 @@ class _QrScannerViewState extends State<QrScannerView> {
_status = ScanStatus.scanning; _status = ScanStatus.scanning;
_zxingViewKey.currentState?.resumeScanning(); _zxingViewKey.currentState?.resumeScanning();
}); });
} }
@ -106,15 +106,16 @@ class _QrScannerViewState extends State<QrScannerView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
extendBody: true, extendBody: true,
appBar: AppBar( appBar: AppBar(
title: const Text( title: Text(
'Add account', l10n.oath_add_account,
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
foregroundColor: Colors.white, foregroundColor: Colors.white,

View File

@ -17,6 +17,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/state.dart'; import '../../app/state.dart';
import '../../core/state.dart'; import '../../core/state.dart';
@ -34,14 +35,14 @@ enum _TapAction {
copy, copy,
both; both;
String get description { String getDescription(AppLocalizations l10n) {
switch (this) { switch (this) {
case _TapAction.launch: case _TapAction.launch:
return 'Launch Yubico Authenticator'; return l10n.androidSettings_launch_app;
case _TapAction.copy: case _TapAction.copy:
return 'Copy OTP to clipboard'; return l10n.androidSettings_copy_otp;
case _TapAction.both: case _TapAction.both:
return 'Launch app and copy OTP'; return l10n.androidSettings_launch_and_copy;
} }
} }
@ -76,14 +77,14 @@ enum _TapAction {
} }
extension on ThemeMode { extension on ThemeMode {
String get displayName { String getDisplayName(AppLocalizations l10n) {
switch (this) { switch (this) {
case ThemeMode.system: case ThemeMode.system:
return 'System default'; return l10n.general_system_default;
case ThemeMode.light: case ThemeMode.light:
return 'Light theme'; return l10n.general_light_mode;
case ThemeMode.dark: case ThemeMode.dark:
return 'Dark theme'; return l10n.general_dark_mode;
} }
} }
} }
@ -99,6 +100,7 @@ class AndroidSettingsPage extends ConsumerStatefulWidget {
class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> { class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final prefs = ref.watch(prefProvider); final prefs = ref.watch(prefProvider);
final tapAction = _TapAction.load(prefs); final tapAction = _TapAction.load(prefs);
@ -112,7 +114,7 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
final theme = Theme.of(context); final theme = Theme.of(context);
return ResponsiveDialog( return ResponsiveDialog(
title: const Text('Settings'), title: Text(l10n.general_settings),
child: Theme( child: Theme(
// Make the headers use the primary color to pop a bit. // Make the headers use the primary color to pop a bit.
// Once M3 is implemented this will probably not be needed. // Once M3 is implemented this will probably not be needed.
@ -125,10 +127,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const ListTitle('NFC options'), ListTitle(l10n.androidSettings_nfc_options),
ListTile( ListTile(
title: const Text('On YubiKey NFC tap'), title: Text(l10n.androidSettings_nfc_on_tap),
subtitle: Text(tapAction.description), subtitle: Text(tapAction.getDescription(l10n)),
key: keys.nfcTapSetting, key: keys.nfcTapSetting,
onTap: () async { onTap: () async {
final newTapAction = await _selectTapAction(context, tapAction); final newTapAction = await _selectTapAction(context, tapAction);
@ -138,7 +140,7 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
}, },
), ),
ListTile( ListTile(
title: const Text('Keyboard Layout (for static password)'), title: Text(l10n.androidSettings_keyboard_layout),
subtitle: Text(clipKbdLayout), subtitle: Text(clipKbdLayout),
key: keys.nfcKeyboardLayoutSetting, key: keys.nfcKeyboardLayoutSetting,
enabled: tapAction != _TapAction.launch, enabled: tapAction != _TapAction.launch,
@ -152,12 +154,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
}, },
), ),
SwitchListTile( SwitchListTile(
title: const Text('Bypass touch requirement'), title: Text(l10n.androidSettings_bypass_touch),
subtitle: nfcBypassTouch subtitle: Text(nfcBypassTouch
? const Text( ? l10n.androidSettings_bypass_touch_on
'Accounts that require touch are automatically shown over NFC') : l10n.androidSettings_bypass_touch_off),
: const Text(
'Accounts that require touch need an additional tap over NFC'),
value: nfcBypassTouch, value: nfcBypassTouch,
key: keys.nfcBypassTouchSetting, key: keys.nfcBypassTouchSetting,
onChanged: (value) { onChanged: (value) {
@ -166,11 +166,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
}); });
}), }),
SwitchListTile( SwitchListTile(
title: const Text('Silence NFC sounds'), title: Text(l10n.androidSettings_silence_nfc),
subtitle: nfcSilenceSounds subtitle: Text(nfcSilenceSounds
? const Text( ? l10n.androidSettings_silence_nfc_on
'No sounds will be played on NFC tap') : l10n.androidSettings_silence_nfc_off),
: const Text('Sound will play on NFC tap'),
value: nfcSilenceSounds, value: nfcSilenceSounds,
key: keys.nfcSilenceSoundsSettings, key: keys.nfcSilenceSoundsSettings,
onChanged: (value) { onChanged: (value) {
@ -178,13 +177,12 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
prefs.setBool(prefNfcSilenceSounds, value); prefs.setBool(prefNfcSilenceSounds, value);
}); });
}), }),
const ListTitle('USB options'), ListTitle(l10n.androidSettings_usb_options),
SwitchListTile( SwitchListTile(
title: const Text('Launch when YubiKey is connected'), title: Text(l10n.androidSettings_usb_launch),
subtitle: usbOpenApp subtitle: Text(usbOpenApp
? const Text( ? l10n.androidSettings_usb_launch_on
'This prevents other apps from using the YubiKey over USB') : l10n.androidSettings_usb_launch_off),
: const Text('Other apps can use the YubiKey over USB'),
value: usbOpenApp, value: usbOpenApp,
key: keys.usbOpenApp, key: keys.usbOpenApp,
onChanged: (value) { onChanged: (value) {
@ -192,10 +190,10 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
prefs.setBool(prefUsbOpenApp, value); prefs.setBool(prefUsbOpenApp, value);
}); });
}), }),
const ListTitle('Appearance'), ListTitle(l10n.general_appearance),
ListTile( ListTile(
title: const Text('App theme'), title: Text(l10n.androidSettings_app_theme),
subtitle: Text(themeMode.displayName), subtitle: Text(themeMode.getDisplayName(l10n)),
key: keys.themeModeSetting, key: keys.themeModeSetting,
onTap: () async { onTap: () async {
final newMode = await _selectAppearance( final newMode = await _selectAppearance(
@ -214,12 +212,13 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
await showDialog<_TapAction>( await showDialog<_TapAction>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog( return SimpleDialog(
title: const Text('On YubiKey NFC tap'), title: Text(l10n.androidSettings_nfc_on_tap),
children: _TapAction.values children: _TapAction.values
.map( .map(
(e) => RadioListTile<_TapAction>( (e) => RadioListTile<_TapAction>(
title: Text(e.description), title: Text(e.getDescription(l10n)),
key: e.key, key: e.key,
value: e, value: e,
groupValue: tapAction, groupValue: tapAction,
@ -238,8 +237,9 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
await showDialog<String>( await showDialog<String>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog( return SimpleDialog(
title: const Text('Choose keyboard layout'), title: Text(l10n.androidSettings_choose_keyboard_layout),
children: _keyboardLayouts children: _keyboardLayouts
.map( .map(
(e) => RadioListTile<String>( (e) => RadioListTile<String>(
@ -262,11 +262,12 @@ class _AndroidSettingsPageState extends ConsumerState<AndroidSettingsPage> {
await showDialog<ThemeMode>( await showDialog<ThemeMode>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
final l10n = AppLocalizations.of(context)!;
return SimpleDialog( return SimpleDialog(
title: const Text('Choose app theme'), title: Text(l10n.androidSettings_choose_app_theme),
children: supportedThemes children: supportedThemes
.map((e) => RadioListTile( .map((e) => RadioListTile(
title: Text(e.displayName), title: Text(e.getDisplayName(l10n)),
value: e, value: e,
key: Key('android.keys.theme_mode_${e.name}'), key: Key('android.keys.theme_mode_${e.name}'),
groupValue: themeMode, groupValue: themeMode,

View File

@ -17,6 +17,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../management/models.dart'; import '../../management/models.dart';
import '../core/models.dart'; import '../core/models.dart';
@ -28,16 +29,15 @@ const _listEquality = ListEquality();
enum Availability { enabled, disabled, unsupported } enum Availability { enabled, disabled, unsupported }
enum Application { enum Application {
oath('Authenticator'), oath,
fido('WebAuthn'), fido,
otp('One-Time Passwords'), otp,
piv('Certificates'), piv,
openpgp('OpenPGP'), openpgp,
hsmauth('YubiHSM Auth'), hsmauth,
management('Toggle Applications'); management;
final String displayName; const Application();
const Application(this.displayName);
bool _inCapabilities(int capabilities) { bool _inCapabilities(int capabilities) {
switch (this) { switch (this) {
@ -59,6 +59,17 @@ enum Application {
} }
} }
String getDisplayName(AppLocalizations l10n) {
switch (this) {
case Application.oath:
return l10n.oath_authenticator;
case Application.fido:
return l10n.fido_webauthn;
default:
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
}
Availability getAvailability(YubiKeyData data) { Availability getAvailability(YubiKeyData data) {
if (this == Application.management) { if (this == Application.management) {
final version = data.info.version; final version = data.info.version;

View File

@ -34,10 +34,11 @@ class AppFailurePage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final reason = cause; final reason = cause;
Widget? graphic = const Icon(Icons.error); Widget? graphic = const Icon(Icons.error);
String? header = 'An error has occured'; String? header = l10n.appFailurePage_error_occured;
String? message = reason.toString(); String? message = reason.toString();
List<Widget> actions = []; List<Widget> actions = [];
@ -45,13 +46,13 @@ class AppFailurePage extends ConsumerWidget {
if (reason.status == 'connection-error') { if (reason.status == 'connection-error') {
switch (reason.body['connection']) { switch (reason.body['connection']) {
case 'ccid': case 'ccid':
header = 'Failed to open smart card connection'; header = l10n.appFailurePage_ccid_failed;
if (Platform.isMacOS) { if (Platform.isMacOS) {
message = 'Try to remove and re-insert your YubiKey.'; message = l10n.appFailurePage_msg_reinsert;
} else if (Platform.isLinux) { } else if (Platform.isLinux) {
message = 'Make sure pcscd is installed and running.'; message = l10n.appFailurePage_pcscd_unavailable;
} else { } else {
message = 'Make sure your smart card service is functioning.'; message = l10n.appFailurePage_ccid_unavailable;
} }
break; break;
case 'fido': case 'fido':
@ -59,17 +60,14 @@ class AppFailurePage extends ConsumerWidget {
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) { !ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
graphic = noPermission; graphic = noPermission;
header = null; header = null;
message = AppLocalizations.of(context)!.appFailurePage_txt_info; message = l10n.appFailurePage_txt_info;
actions = [ actions = [
ElevatedButton.icon( ElevatedButton.icon(
label: Text( label: Text(l10n.appFailurePage_btn_unlock),
AppLocalizations.of(context)!.appFailurePage_btn_unlock),
icon: const Icon(Icons.lock_open), icon: const Icon(Icons.lock_open),
onPressed: () async { onPressed: () async {
final closeMessage = showMessage( final closeMessage = showMessage(
context, context, l10n.appFailurePage_msg_permission,
AppLocalizations.of(context)!
.appFailurePage_msg_permission,
duration: const Duration(seconds: 30)); duration: const Duration(seconds: 30));
try { try {
if (await ref.read(rpcProvider).requireValue.elevate()) { if (await ref.read(rpcProvider).requireValue.elevate()) {
@ -77,7 +75,10 @@ class AppFailurePage extends ConsumerWidget {
} else { } else {
await ref.read(withContextProvider)( await ref.read(withContextProvider)(
(context) async { (context) async {
showMessage(context, 'Permission denied'); showMessage(
context,
l10n.general_permission_denied,
);
}, },
); );
} }
@ -90,8 +91,8 @@ class AppFailurePage extends ConsumerWidget {
} }
break; break;
default: default:
header = 'Failed to open connection'; header = l10n.appFailurePage_failed_connection;
message = 'Try to remove and re-insert your YubiKey.'; message = l10n.appFailurePage_msg_reinsert;
} }
} }
} }

View File

@ -18,6 +18,7 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../core/models.dart'; import '../../core/models.dart';
import '../../desktop/state.dart'; import '../../desktop/state.dart';
@ -34,26 +35,27 @@ class DeviceErrorScreen extends ConsumerWidget {
const DeviceErrorScreen(this.node, {this.error, super.key}); const DeviceErrorScreen(this.node, {this.error, super.key});
Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) { Widget _buildUsbPid(BuildContext context, WidgetRef ref, UsbPid pid) {
final l10n = AppLocalizations.of(context)!;
if (pid.usbInterfaces == UsbInterface.fido.value) { if (pid.usbInterfaces == UsbInterface.fido.value) {
if (Platform.isWindows && if (Platform.isWindows &&
!ref.watch(rpcStateProvider.select((state) => state.isAdmin))) { !ref.watch(rpcStateProvider.select((state) => state.isAdmin))) {
return MessagePage( return MessagePage(
graphic: noPermission, graphic: noPermission,
message: 'Managing this device requires elevated privileges.', message: l10n.general_elevated_permissions_required,
actions: [ actions: [
ElevatedButton.icon( ElevatedButton.icon(
label: const Text('Unlock'), label: Text(l10n.appFailurePage_btn_unlock),
icon: const Icon(Icons.lock_open), icon: const Icon(Icons.lock_open),
onPressed: () async { onPressed: () async {
final closeMessage = showMessage( final closeMessage = showMessage(
context, 'Elevating permissions...', context, l10n.appFailurePage_msg_permission,
duration: const Duration(seconds: 30)); duration: const Duration(seconds: 30));
try { try {
if (await ref.read(rpcProvider).requireValue.elevate()) { if (await ref.read(rpcProvider).requireValue.elevate()) {
ref.invalidate(rpcProvider); ref.invalidate(rpcProvider);
} else { } else {
await ref.read(withContextProvider)((context) async => await ref.read(withContextProvider)((context) async =>
showMessage(context, 'Permission denied')); showMessage(context, l10n.general_permission_denied));
} }
} finally { } finally {
closeMessage(); closeMessage();
@ -64,24 +66,25 @@ class DeviceErrorScreen extends ConsumerWidget {
); );
} }
} }
return const MessagePage( return MessagePage(
graphic: DeviceAvatar(child: Icon(Icons.usb_off)), graphic: const DeviceAvatar(child: Icon(Icons.usb_off)),
message: 'This YubiKey cannot be accessed', message: l10n.general_yubikey_no_access,
); );
} }
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
return node.map( return node.map(
usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid), usbYubiKey: (node) => _buildUsbPid(context, ref, node.pid),
nfcReader: (node) { nfcReader: (node) {
final String message; final String message;
switch (error) { switch (error) {
case 'unknown-device': case 'unknown-device':
message = 'Unrecognized device'; message = l10n.devicePicker_unknown_device;
break; break;
default: default:
message = 'Place your YubiKey on the NFC reader'; message = l10n.general_place_on_nfc_reader;
} }
return MessagePage(message: message); return MessagePage(message: message);
}, },

View File

@ -71,6 +71,7 @@ class MainPageDrawer extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
final supportedApps = ref.watch(supportedAppsProvider); final supportedApps = ref.watch(supportedAppsProvider);
final data = ref.watch(currentDeviceDataProvider).valueOrNull; final data = ref.watch(currentDeviceDataProvider).valueOrNull;
final color = final color =
@ -132,7 +133,7 @@ class MainPageDrawer extends ConsumerWidget {
if (data != null) ...[ if (data != null) ...[
// Normal YubiKey Applications // Normal YubiKey Applications
...availableApps.map((app) => NavigationDrawerDestination( ...availableApps.map((app) => NavigationDrawerDestination(
label: Text(app.displayName), label: Text(app.getDisplayName(l10n)),
icon: Icon(app._icon), icon: Icon(app._icon),
selectedIcon: Icon(app._filledIcon), selectedIcon: Icon(app._filledIcon),
)), )),
@ -141,7 +142,7 @@ class MainPageDrawer extends ConsumerWidget {
NavigationDrawerDestination( NavigationDrawerDestination(
key: managementAppDrawer, key: managementAppDrawer,
label: Text( label: Text(
AppLocalizations.of(context)!.mainDrawer_txt_applications, l10n.mainDrawer_txt_applications,
), ),
icon: Icon(Application.management._icon), icon: Icon(Application.management._icon),
selectedIcon: Icon(Application.management._filledIcon), selectedIcon: Icon(Application.management._filledIcon),
@ -151,11 +152,11 @@ class MainPageDrawer extends ConsumerWidget {
], ],
// Non-YubiKey pages // Non-YubiKey pages
NavigationDrawerDestination( NavigationDrawerDestination(
label: Text(AppLocalizations.of(context)!.mainDrawer_txt_settings), label: Text(l10n.mainDrawer_txt_settings),
icon: const Icon(Icons.settings_outlined), icon: const Icon(Icons.settings_outlined),
), ),
NavigationDrawerDestination( NavigationDrawerDestination(
label: Text(AppLocalizations.of(context)!.mainDrawer_txt_help), label: Text(l10n.mainDrawer_txt_help),
icon: const Icon(Icons.help_outline), icon: const Icon(Icons.help_outline),
), ),
], ],

View File

@ -16,16 +16,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:yubico_authenticator/android/app_methods.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:yubico_authenticator/android/state.dart';
import 'package:yubico_authenticator/widgets/custom_icons.dart';
import '../../android/app_methods.dart';
import '../../android/state.dart';
import '../../exception/cancellation_exception.dart'; import '../../exception/cancellation_exception.dart';
import '../../core/state.dart'; import '../../core/state.dart';
import '../../fido/views/fido_screen.dart'; import '../../fido/views/fido_screen.dart';
import '../../oath/models.dart'; import '../../oath/models.dart';
import '../../oath/views/add_account_page.dart'; import '../../oath/views/add_account_page.dart';
import '../../oath/views/oath_screen.dart'; import '../../oath/views/oath_screen.dart';
import '../../widgets/custom_icons.dart';
import '../message.dart'; import '../message.dart';
import '../models.dart'; import '../models.dart';
import '../state.dart'; import '../state.dart';
@ -37,6 +38,7 @@ class MainPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context)!;
ref.listen<Function(BuildContext)?>( ref.listen<Function(BuildContext)?>(
contextConsumer, contextConsumer,
(previous, next) { (previous, next) {
@ -45,7 +47,8 @@ class MainPage extends ConsumerWidget {
); );
if (isAndroid) { if (isAndroid) {
isNfcEnabled().then((value) => ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value)); isNfcEnabled().then((value) =>
ref.read(androidNfcStateProvider.notifier).setNfcEnabled(value));
} }
// If the current device changes, we need to pop any open dialogs. // If the current device changes, we need to pop any open dialogs.
@ -75,18 +78,17 @@ class MainPage extends ConsumerWidget {
); );
if (deviceNode == null) { if (deviceNode == null) {
if (isAndroid) { if (isAndroid) {
var hasNfcSupport = ref.watch(androidNfcSupportProvider); var hasNfcSupport = ref.watch(androidNfcSupportProvider);
var isNfcEnabled = ref.watch(androidNfcStateProvider); var isNfcEnabled = ref.watch(androidNfcStateProvider);
return MessagePage( return MessagePage(
graphic: noKeyImage, graphic: noKeyImage,
message: hasNfcSupport && isNfcEnabled message: hasNfcSupport && isNfcEnabled
? 'Tap or insert your YubiKey' ? l10n.devicePicker_insert_or_tap
: 'Insert your YubiKey', : l10n.devicePicker_insert_yubikey,
actions: [ actions: [
if (hasNfcSupport && !isNfcEnabled) if (hasNfcSupport && !isNfcEnabled)
ElevatedButton.icon( ElevatedButton.icon(
label: const Text('Enable NFC'), label: Text(l10n.general_enable_nfc),
icon: nfcIcon, icon: nfcIcon,
onPressed: () async { onPressed: () async {
await openNfcSettings(); await openNfcSettings();
@ -94,7 +96,7 @@ class MainPage extends ConsumerWidget {
], ],
actionButtonBuilder: (context) => IconButton( actionButtonBuilder: (context) => IconButton(
icon: const Icon(Icons.person_add_alt_1), icon: const Icon(Icons.person_add_alt_1),
tooltip: 'Add account', tooltip: l10n.oath_add_account,
onPressed: () async { onPressed: () async {
CredentialData? otpauth; CredentialData? otpauth;
final scanner = ref.read(qrScannerProvider); final scanner = ref.read(qrScannerProvider);
@ -130,7 +132,7 @@ class MainPage extends ConsumerWidget {
return MessagePage( return MessagePage(
delayedContent: true, delayedContent: true,
graphic: noKeyImage, graphic: noKeyImage,
message: 'Insert your YubiKey', message: l10n.devicePicker_insert_yubikey,
); );
} }
} else { } else {
@ -139,21 +141,19 @@ class MainPage extends ConsumerWidget {
final app = ref.watch(currentAppProvider); final app = ref.watch(currentAppProvider);
if (data.info.supportedCapabilities.isEmpty && if (data.info.supportedCapabilities.isEmpty &&
data.name == 'Unrecognized device') { data.name == 'Unrecognized device') {
return const MessagePage( return MessagePage(
header: 'Device not recognized', header: l10n.mainPage_not_recognized,
); );
} else if (app.getAvailability(data) == } else if (app.getAvailability(data) ==
Availability.unsupported) { Availability.unsupported) {
return MessagePage( return MessagePage(
header: 'Application not supported', header: l10n.mainPage_app_not_supported,
message: message: l10n.mainPage_app_not_supported_on_yubikey(app.name),
'The used YubiKey does not support \'${app.name}\' application',
); );
} else if (app.getAvailability(data) != Availability.enabled) { } else if (app.getAvailability(data) != Availability.enabled) {
return MessagePage( return MessagePage(
header: 'Application disabled', header: l10n.mainPage_app_not_enabled,
message: message: l10n.mainPage_app_not_enabled_desc(app.name),
'Enable the \'${app.name}\' application on your YubiKey to access',
); );
} }
@ -163,9 +163,9 @@ class MainPage extends ConsumerWidget {
case Application.fido: case Application.fido:
return FidoScreen(data); return FidoScreen(data);
default: default:
return const MessagePage( return MessagePage(
header: 'Not supported', header: l10n.mainPage_app_not_supported,
message: 'This application is not supported', message: l10n.mainPage_app_not_supported_desc,
); );
} }
}, },

View File

@ -16,7 +16,6 @@
"oath_fail_add_account": "Failed adding account", "oath_fail_add_account": "Failed adding account",
"oath_add_account": "Add account", "oath_add_account": "Add account",
"oath_save": "Save", "oath_save": "Save",
"oath_no_qr_code": "No QR code found",
"oath_duplicate_name": "This name already exists for the Issuer", "oath_duplicate_name": "This name already exists for the Issuer",
"oath_issuer_optional": "Issuer (optional)", "oath_issuer_optional": "Issuer (optional)",
"oath_account_name": "Account name", "oath_account_name": "Account name",
@ -139,12 +138,20 @@
"general_allow_screenshots": "Allow screenshots", "general_allow_screenshots": "Allow screenshots",
"general_usb": "USB", "general_usb": "USB",
"general_nfc": "NFC", "general_nfc": "NFC",
"general_enable_nfc": "Enable NFC",
"general_place_on_nfc_reader": "Place your YubiKey on the NFC reader",
"general_setup": "Setup", "general_setup": "Setup",
"general_manage": "Manage", "general_manage": "Manage",
"general_configure_yubikey": "Configure YubiKey", "general_configure_yubikey": "Configure YubiKey",
"general_show_window": "Show window", "general_show_window": "Show window",
"general_hide_window": "Hide window", "general_hide_window": "Hide window",
"general_quit": "Quit", "general_quit": "Quit",
"general_character_count": "Character count",
"general_please_wait": "Please wait\u2026",
"general_unsupported_yubikey": "Unsupported YubiKey",
"general_yubikey_no_access": "This YubiKey cannot be accessed",
"general_permission_denied": "Permission denied",
"general_elevated_permissions_required": "Managing this device requires elevated privileges.",
"fido_press_fingerprint_begin": "Press your finger against the YubiKey to begin.", "fido_press_fingerprint_begin": "Press your finger against the YubiKey to begin.",
"fido_keep_touching_yubikey": "Keep touching your YubiKey repeatedly\u2026", "fido_keep_touching_yubikey": "Keep touching your YubiKey repeatedly\u2026",
@ -253,22 +260,39 @@
}, },
"mainPage_not_recognized": "Device not recognized",
"mainPage_app_not_supported": "Application not supported",
"mainPage_app_not_supported_on_yubikey": "The used YubiKey does not support '${app}' application",
"@mainPage_app_not_supported_on_yubikey" : {
"placeholders": {
"app": {}
}
},
"mainPage_app_not_supported_desc": "This application is not supported",
"mainPage_app_not_enabled": "Application disabled",
"mainPage_app_not_enabled_desc": "Enable the '{app}' application on your YubiKey to access",
"@mainPage_app_not_enabled_desc" : {
"placeholders": {
"app": {}
}
},
"appFailurePage_btn_unlock": "Unlock", "appFailurePage_btn_unlock": "Unlock",
"appFailurePage_txt_info": "WebAuthn management requires elevated privileges.", "appFailurePage_txt_info": "WebAuthn management requires elevated privileges.",
"appFailurePage_msg_permission": "Elevating permissions\u2026", "appFailurePage_msg_permission": "Elevating permissions\u2026",
"appFailurePage_error_occured": "An error has occured",
"appFailurePage_failed_connection": "Failed to open connection",
"appFailurePage_msg_reinsert": "Try to remove and re-insert your YubiKey.",
"appFailurePage_ccid_failed": "Failed to open smart card connection",
"appFailurePage_ccid_unavailable": "Make sure your smart card service is functioning.",
"appFailurePage_pcscd_unavailable": "Make sure pcscd is installed and running.",
"mainDrawer_txt_applications": "Toggle applications", "mainDrawer_txt_applications": "Toggle applications",
"mainDrawer_txt_settings": "Settings", "mainDrawer_txt_settings": "Settings",
"mainDrawer_txt_help": "Help and about", "mainDrawer_txt_help": "Help and about",
"devicePicker_no_yubikey": "No YubiKey present", "devicePicker_no_yubikey": "No YubiKey present",
"devicePicker_insert_yubikey": "Insert your YubiKey",
"devicePicker_insert_or_tap": "Insert or tap a YubiKey", "devicePicker_insert_or_tap": "Insert or tap a YubiKey",
"devicePicker_select_to_scan": "Select to scan", "devicePicker_select_to_scan": "Select to scan",
"devicePicker_inaccessible": "Device inaccessible", "devicePicker_inaccessible": "Device inaccessible",
@ -297,5 +321,36 @@
"label": {} "label": {}
} }
}, },
"systray_no_pinned": "No pinned accounts" "systray_no_pinned": "No pinned accounts",
"androidSettings_launch_app": "Launch Yubico Authenticator",
"androidSettings_copy_otp": "Copy OTP to clipboard",
"androidSettings_launch_and_copy": "Launch app and copy OTP",
"androidSettings_nfc_options": "NFC options",
"androidSettings_nfc_on_tap": "On YubiKey NFC tap",
"androidSettings_keyboard_layout": "Keyboard layout (for static password)",
"androidSettings_choose_keyboard_layout": "Choose keyboard layout",
"androidSettings_bypass_touch": "Bypass touch requirement",
"androidSettings_bypass_touch_on": "Accounts that require touch are automatically shown over NFC",
"androidSettings_bypass_touch_off": "Accounts that require touch need an additional tap over NFC",
"androidSettings_silence_nfc": "Silence NFC sounds",
"androidSettings_silence_nfc_on": "No sounds will be played on NFC tap",
"androidSettings_silence_nfc_off": "Sound will play on NFC tap",
"androidSettings_usb_options": "USB options",
"androidSettings_usb_launch": "Launch when YubiKey is connected",
"androidSettings_usb_launch_on": "This prevents other apps from using the YubiKey over USB",
"androidSettings_usb_launch_off": "Other apps can use the YubiKey over USB",
"androidSettings_app_theme": "App theme",
"androidSettings_choose_app_theme": "Choose app theme",
"androidQrScanner_point_and_scan": "Point your camera at a QR code to scan it",
"androidQrScanner_invalid_code": "Invalid QR code",
"androidQrScanner_no_code": "No QR code?",
"androidQrScanner_enter_manually": "Enter manually",
"androidQrScanner_need_camera_permission": "Yubico Authenticator needs Camera permissions for scanning QR codes.",
"androidQrScanner_have_account_info": "Have account info?",
"androidQrScanner_want_to_scan": "Would like to scan?",
"androidQrScanner_review_permissions": "Review permissions",
"@_EOF": {}
} }

View File

@ -113,10 +113,10 @@ class _CapabilitiesForm extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (usbCapabilities != 0) ...[ if (usbCapabilities != 0) ...[
const ListTile( ListTile(
leading: Icon(Icons.usb), leading: const Icon(Icons.usb),
title: Text('USB'), title: Text(AppLocalizations.of(context)!.general_usb),
contentPadding: EdgeInsets.only(bottom: 8), contentPadding: const EdgeInsets.only(bottom: 8),
horizontalTitleGap: 0, horizontalTitleGap: 0,
), ),
_CapabilityForm( _CapabilityForm(
@ -136,7 +136,7 @@ class _CapabilitiesForm extends StatelessWidget {
), ),
ListTile( ListTile(
leading: nfcIcon, leading: nfcIcon,
title: const Text('NFC'), title: Text(AppLocalizations.of(context)!.general_nfc),
contentPadding: const EdgeInsets.only(bottom: 8), contentPadding: const EdgeInsets.only(bottom: 8),
horizontalTitleGap: 0, horizontalTitleGap: 0,
), ),

View File

@ -18,6 +18,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart'; import '../../app/message.dart';
import '../../app/shortcuts.dart'; import '../../app/shortcuts.dart';
@ -37,13 +38,16 @@ class AccountDialog extends ConsumerWidget {
const AccountDialog(this.credential, {super.key}); const AccountDialog(this.credential, {super.key});
List<Widget> _buildActions(BuildContext context, AccountHelper helper) { List<Widget> _buildActions(BuildContext context, AccountHelper helper) {
final l10n = AppLocalizations.of(context)!;
final actions = helper.buildActions(); final actions = helper.buildActions();
final theme = final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme; ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
final copy = actions.firstWhere(((e) => e.text.startsWith('Copy'))); final copy =
final delete = actions.firstWhere(((e) => e.text.startsWith('Delete'))); actions.firstWhere(((e) => e.text == l10n.oath_copy_to_clipboard));
final delete =
actions.firstWhere(((e) => e.text == l10n.oath_delete_account));
final colors = { final colors = {
copy: Pair(theme.primary, theme.onPrimary), copy: Pair(theme.primary, theme.onPrimary),
delete: Pair(theme.error, theme.onError), delete: Pair(theme.error, theme.onError),
@ -51,7 +55,7 @@ class AccountDialog extends ConsumerWidget {
// If we can't copy, but can calculate, highlight that button instead // If we can't copy, but can calculate, highlight that button instead
if (copy.intent == null) { if (copy.intent == null) {
final calculates = actions.where(((e) => e.text.startsWith('Calculate'))); final calculates = actions.where(((e) => e.text == l10n.oath_calculate));
if (calculates.isNotEmpty) { if (calculates.isNotEmpty) {
colors[calculates.first] = Pair(theme.primary, theme.onPrimary); colors[calculates.first] = Pair(theme.primary, theme.onPrimary);
} }

View File

@ -62,25 +62,23 @@ class AccountHelper {
final ready = expired || credential.oathType == OathType.hotp; final ready = expired || credential.oathType == OathType.hotp;
final pinned = _ref.watch(favoritesProvider).contains(credential.id); final pinned = _ref.watch(favoritesProvider).contains(credential.id);
final appLocalizations = AppLocalizations.of(_context)!; final l10n = AppLocalizations.of(_context)!;
final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C'; final shortcut = Platform.isMacOS ? '\u2318 C' : 'Ctrl+C';
return [ return [
MenuAction( MenuAction(
text: appLocalizations.oath_copy_to_clipboard, text: l10n.oath_copy_to_clipboard,
icon: const Icon(Icons.copy), icon: const Icon(Icons.copy),
intent: code == null || expired ? null : const CopyIntent(), intent: code == null || expired ? null : const CopyIntent(),
trailing: shortcut, trailing: shortcut,
), ),
if (manual) if (manual)
MenuAction( MenuAction(
text: appLocalizations.oath_calculate, text: l10n.oath_calculate,
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
intent: ready ? const CalculateIntent() : null, intent: ready ? const CalculateIntent() : null,
), ),
MenuAction( MenuAction(
text: pinned text: pinned ? l10n.oath_unpin_account : l10n.oath_pin_account,
? appLocalizations.oath_unpin_account
: appLocalizations.oath_pin_account,
icon: pinned icon: pinned
? pushPinStrokeIcon ? pushPinStrokeIcon
: const Icon(Icons.push_pin_outlined), : const Icon(Icons.push_pin_outlined),
@ -89,11 +87,11 @@ class AccountHelper {
if (data.info.version.isAtLeast(5, 3)) if (data.info.version.isAtLeast(5, 3))
MenuAction( MenuAction(
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.edit_outlined),
text: appLocalizations.oath_rename_account, text: l10n.oath_rename_account,
intent: const EditIntent(), intent: const EditIntent(),
), ),
MenuAction( MenuAction(
text: appLocalizations.oath_delete_account, text: l10n.oath_delete_account,
icon: const Icon(Icons.delete_outline), icon: const Icon(Icons.delete_outline),
intent: const DeleteIntent(), intent: const DeleteIntent(),
), ),

View File

@ -185,7 +185,6 @@ class _AccountViewState extends ConsumerState<AccountView> {
setState(() { setState(() {
_lastTap = 0; _lastTap = 0;
}); });
//triggerCopy();
Actions.maybeInvoke(context, const CopyIntent()); Actions.maybeInvoke(context, const CopyIntent());
} else { } else {
_focusNode.requestFocus(); _focusNode.requestFocus();

View File

@ -108,6 +108,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
} }
_scanQrCode(QrScanner qrScanner) async { _scanQrCode(QrScanner qrScanner) async {
final l10n = AppLocalizations.of(context)!;
try { try {
setState(() { setState(() {
// If we have a previous scan result stored, clear it // If we have a previous scan result stored, clear it
@ -125,7 +126,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
final otpauth = await qrScanner.scanQr(); final otpauth = await qrScanner.scanQr();
if (otpauth == null) { if (otpauth == null) {
if (!mounted) return; if (!mounted) return;
showMessage(context, AppLocalizations.of(context)!.oath_no_qr_code); showMessage(context, l10n.oath_no_qr_code);
setState(() { setState(() {
_qrState = _QrScanState.failed; _qrState = _QrScanState.failed;
}); });
@ -145,7 +146,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
if (e is! CancellationException) { if (e is! CancellationException) {
showMessage( showMessage(
context, context,
'${AppLocalizations.of(context)!.oath_failed_reading_qr}: $errorMessage', '${l10n.oath_failed_reading_qr}: $errorMessage',
duration: const Duration(seconds: 4), duration: const Duration(seconds: 4),
); );
} }
@ -173,6 +174,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
Future<void> _doAddCredential( Future<void> _doAddCredential(
{DevicePath? devicePath, required Uri credUri}) async { {DevicePath? devicePath, required Uri credUri}) async {
final l10n = AppLocalizations.of(context)!;
try { try {
if (devicePath == null) { if (devicePath == null) {
assert(Platform.isAndroid, 'devicePath is only optional for Android'); assert(Platform.isAndroid, 'devicePath is only optional for Android');
@ -186,8 +188,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
} }
if (!mounted) return; if (!mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
showMessage( showMessage(context, l10n.oath_success_add_account);
context, AppLocalizations.of(context)!.oath_success_add_account);
} on CancellationException catch (_) { } on CancellationException catch (_) {
// ignored // ignored
} catch (e) { } catch (e) {
@ -203,7 +204,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
} }
showMessage( showMessage(
context, context,
'${AppLocalizations.of(context)!.oath_fail_add_account}: $errorMessage', '${l10n.oath_fail_add_account}: $errorMessage',
duration: const Duration(seconds: 4), duration: const Duration(seconds: 4),
); );
} }
@ -211,6 +212,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final deviceNode = ref.watch(currentDeviceProvider); final deviceNode = ref.watch(currentDeviceProvider);
if (widget.devicePath != null && widget.devicePath != deviceNode?.path) { if (widget.devicePath != null && widget.devicePath != deviceNode?.path) {
// If the dialog was started for a specific device and it was // If the dialog was started for a specific device and it was
@ -233,7 +235,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
} }
final otpauthUri = _otpauthUri; final otpauthUri = _otpauthUri;
_promptController?.updateContent(title: 'Insert YubiKey'); _promptController?.updateContent(title: l10n.devicePicker_insert_yubikey);
if (otpauthUri != null && deviceNode != null) { if (otpauthUri != null && deviceNode != null) {
final deviceData = ref.watch(currentDeviceDataProvider); final deviceData = ref.watch(currentDeviceDataProvider);
deviceData.when(data: (data) { deviceData.when(data: (data) {
@ -242,7 +244,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
0) != 0) !=
0) { 0) {
if (oathState == null) { if (oathState == null) {
_promptController?.updateContent(title: 'Please wait...'); _promptController?.updateContent(title: l10n.general_please_wait);
} else if (oathState.locked) { } else if (oathState.locked) {
_promptController?.close(); _promptController?.close();
} else { } else {
@ -254,12 +256,14 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
)); ));
} }
} else { } else {
_promptController?.updateContent(title: 'Unsupported YubiKey'); _promptController?.updateContent(
title: l10n.general_unsupported_yubikey);
} }
}, error: (error, _) { }, error: (error, _) {
_promptController?.updateContent(title: 'Unsupported YubiKey'); _promptController?.updateContent(
title: l10n.general_unsupported_yubikey);
}, loading: () { }, loading: () {
_promptController?.updateContent(title: 'Please wait...'); _promptController?.updateContent(title: l10n.general_please_wait);
}); });
} }
@ -340,8 +344,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
_otpauthUri = cred.toUri(); _otpauthUri = cred.toUri();
_promptController = promptUserInteraction( _promptController = promptUserInteraction(
context, context,
title: 'Insert YubiKey', title: l10n.devicePicker_insert_yubikey,
description: 'Add account', description: l10n.oath_add_account,
icon: const Icon(Icons.usb), icon: const Icon(Icons.usb),
onCancel: () { onCancel: () {
_otpauthUri = null; _otpauthUri = null;
@ -356,12 +360,11 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
} }
return ResponsiveDialog( return ResponsiveDialog(
title: Text(AppLocalizations.of(context)!.oath_add_account), title: Text(l10n.oath_add_account),
actions: [ actions: [
TextButton( TextButton(
onPressed: isValid ? submit : null, onPressed: isValid ? submit : null,
child: Text(AppLocalizations.of(context)!.oath_save, child: Text(l10n.oath_save, key: keys.saveButton),
key: keys.saveButton),
), ),
], ],
child: FileDropTarget( child: FileDropTarget(
@ -371,8 +374,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
final otpauth = await qrScanner.scanQr(b64Image); final otpauth = await qrScanner.scanQr(b64Image);
if (otpauth == null) { if (otpauth == null) {
if (!mounted) return; if (!mounted) return;
showMessage( showMessage(context, l10n.oath_no_qr_code);
context, AppLocalizations.of(context)!.oath_no_qr_code);
} else { } else {
final data = CredentialData.fromUri(Uri.parse(otpauth)); final data = CredentialData.fromUri(Uri.parse(otpauth));
_loadCredentialData(data); _loadCredentialData(data);
@ -402,8 +404,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
buildCounter: buildByteCounterFor(issuerText), buildCounter: buildByteCounterFor(issuerText),
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
labelText: labelText: l10n.oath_issuer_optional,
AppLocalizations.of(context)!.oath_issuer_optional,
helperText: helperText:
'', // Prevents dialog resizing when disabled '', // Prevents dialog resizing when disabled
prefixIcon: const Icon(Icons.business_outlined), prefixIcon: const Icon(Icons.business_outlined),
@ -411,8 +412,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
? '' // needs empty string to render as error ? '' // needs empty string to render as error
: issuerNoColon : issuerNoColon
? null ? null
: AppLocalizations.of(context)! : l10n.oath_invalid_character_issuer,
.oath_invalid_character_issuer,
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onChanged: (value) { onChanged: (value) {
@ -433,16 +433,14 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
decoration: InputDecoration( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.person_outline), prefixIcon: const Icon(Icons.person_outline),
labelText: labelText: l10n.oath_account_name,
AppLocalizations.of(context)!.oath_account_name,
helperText: helperText:
'', // Prevents dialog resizing when disabled '', // Prevents dialog resizing when disabled
errorText: (byteLength(nameText) > nameMaxLength) errorText: (byteLength(nameText) > nameMaxLength)
? '' // needs empty string to render as error ? '' // needs empty string to render as error
: isUnique : isUnique
? null ? null
: AppLocalizations.of(context)! : l10n.oath_duplicate_name,
.oath_duplicate_name,
), ),
textInputAction: TextInputAction.next, textInputAction: TextInputAction.next,
onChanged: (value) { onChanged: (value) {
@ -478,11 +476,9 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
), ),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.key_outlined), prefixIcon: const Icon(Icons.key_outlined),
labelText: labelText: l10n.oath_secret_key,
AppLocalizations.of(context)!.oath_secret_key,
errorText: _validateSecretLength && !secretLengthValid errorText: _validateSecretLength && !secretLengthValid
? AppLocalizations.of(context)! ? l10n.oath_invalid_length
.oath_invalid_length
: null), : null),
readOnly: _qrState == _QrScanState.success, readOnly: _qrState == _QrScanState.success,
textInputAction: TextInputAction.done, textInputAction: TextInputAction.done,
@ -507,10 +503,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
: const CircularProgressIndicator( : const CircularProgressIndicator(
strokeWidth: 2.0), strokeWidth: 2.0),
label: _qrState == _QrScanState.success label: _qrState == _QrScanState.success
? Text(AppLocalizations.of(context)! ? Text(l10n.oath_scanned_qr)
.oath_scanned_qr) : Text(l10n.oath_scan_qr),
: Text(
AppLocalizations.of(context)!.oath_scan_qr),
onPressed: () { onPressed: () {
_scanQrCode(qrScanner); _scanQrCode(qrScanner);
}), }),
@ -523,8 +517,7 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
children: [ children: [
if (oathState?.version.isAtLeast(4, 2) ?? true) if (oathState?.version.isAtLeast(4, 2) ?? true)
FilterChip( FilterChip(
label: Text(AppLocalizations.of(context)! label: Text(l10n.oath_require_touch),
.oath_require_touch),
selected: _touch, selected: _touch,
onSelected: (value) { onSelected: (value) {
setState(() { setState(() {
@ -565,8 +558,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
defaultPeriod, defaultPeriod,
selected: int.tryParse(_periodController.text) != selected: int.tryParse(_periodController.text) !=
defaultPeriod, defaultPeriod,
itemBuilder: ((value) => Text( itemBuilder: ((value) =>
'$value ${AppLocalizations.of(context)!.oath_sec}')), Text('$value ${l10n.oath_sec}')),
onChanged: _qrState != _QrScanState.success onChanged: _qrState != _QrScanState.success
? (period) { ? (period) {
setState(() { setState(() {
@ -579,8 +572,8 @@ class _OathAddAccountPageState extends ConsumerState<OathAddAccountPage> {
items: _digitsValues, items: _digitsValues,
value: _digits, value: _digits,
selected: _digits != defaultDigits, selected: _digits != defaultDigits,
itemBuilder: (value) => Text( itemBuilder: (value) =>
'$value ${AppLocalizations.of(context)!.oath_digits}'), Text('$value ${l10n.oath_digits}'),
onChanged: _qrState != _QrScanState.success onChanged: _qrState != _QrScanState.success
? (digits) { ? (digits) {
setState(() { setState(() {

View File

@ -18,6 +18,7 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/// Get the number of bytes used by a String when encoded to UTF-8. /// Get the number of bytes used by a String when encoded to UTF-8.
int byteLength(String value) => utf8.encode(value).length; int byteLength(String value) => utf8.encode(value).length;
@ -37,7 +38,7 @@ InputCounterWidgetBuilder buildByteCounterFor(String currentValue) =>
return Text( return Text(
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '', maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
style: style, style: style,
semanticsLabel: 'Character count', semanticsLabel: AppLocalizations.of(context)!.general_character_count,
); );
}; };