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

View File

@ -20,6 +20,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:logging/logging.dart';
import '../../app/logging.dart';
@ -189,12 +190,15 @@ class _AndroidCredentialListNotifier extends OathCredentialListNotifier {
if (_isUsbAttached) {
void triggerTouchPrompt() async {
controller = await _withContext(
(context) async => promptUserInteraction(
context,
icon: const Icon(Icons.touch_app),
title: 'Touch Required',
description: 'Touch the button on your YubiKey now.',
),
(context) async {
final l10n = AppLocalizations.of(context)!;
return promptUserInteraction(
context,
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_gen/gen_l10n/app_localizations.dart';
import 'qr_scanner_scan_status.dart';
import 'qr_scanner_util.dart';
@ -32,7 +33,8 @@ class QRScannerPermissionsUI extends StatelessWidget {
@override
Widget build(BuildContext context) {
var scannerAreaWidth = getScannerAreaWidth(screenSize);
final l10n = AppLocalizations.of(context)!;
final scannerAreaWidth = getScannerAreaWidth(screenSize);
return Stack(children: [
/// instruction text under the scanner area
@ -42,11 +44,11 @@ class QRScannerPermissionsUI extends StatelessWidget {
screenSize.height - scannerAreaWidth / 2.0 + 8.0),
width: screenSize.width,
height: screenSize.height),
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 36),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 36),
child: Text(
'Yubico Authenticator needs Camera permissions for scanning QR codes.',
style: TextStyle(color: Colors.white),
l10n.androidQrScanner_need_camera_permission,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
)),
@ -63,32 +65,36 @@ class QRScannerPermissionsUI extends StatelessWidget {
children: [
Column(
children: [
const Text(
'Have account info?',
Text(
l10n.androidQrScanner_have_account_info,
textScaleFactor: 0.7,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
OutlinedButton(
onPressed: () {
Navigator.of(context).pop('');
},
child: const Text('Enter manually',
style: TextStyle(color: Colors.white))),
child: Text(
l10n.androidQrScanner_enter_manually,
style: const TextStyle(color: Colors.white),
)),
],
),
Column(
children: [
const Text(
'Would like to scan?',
Text(
l10n.androidQrScanner_want_to_scan,
textScaleFactor: 0.7,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
OutlinedButton(
onPressed: () {
onPermissionRequest();
},
child: const Text('Review permissions',
style: TextStyle(color: Colors.white))),
child: Text(
l10n.androidQrScanner_review_permissions,
style: const TextStyle(color: Colors.white),
)),
],
)
]),

View File

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

View File

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

View File

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

View File

@ -17,6 +17,7 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../management/models.dart';
import '../core/models.dart';
@ -28,16 +29,15 @@ const _listEquality = ListEquality();
enum Availability { enabled, disabled, unsupported }
enum Application {
oath('Authenticator'),
fido('WebAuthn'),
otp('One-Time Passwords'),
piv('Certificates'),
openpgp('OpenPGP'),
hsmauth('YubiHSM Auth'),
management('Toggle Applications');
oath,
fido,
otp,
piv,
openpgp,
hsmauth,
management;
final String displayName;
const Application(this.displayName);
const Application();
bool _inCapabilities(int capabilities) {
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) {
if (this == Application.management) {
final version = data.info.version;

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,6 @@
"oath_fail_add_account": "Failed adding account",
"oath_add_account": "Add account",
"oath_save": "Save",
"oath_no_qr_code": "No QR code found",
"oath_duplicate_name": "This name already exists for the Issuer",
"oath_issuer_optional": "Issuer (optional)",
"oath_account_name": "Account name",
@ -139,12 +138,20 @@
"general_allow_screenshots": "Allow screenshots",
"general_usb": "USB",
"general_nfc": "NFC",
"general_enable_nfc": "Enable NFC",
"general_place_on_nfc_reader": "Place your YubiKey on the NFC reader",
"general_setup": "Setup",
"general_manage": "Manage",
"general_configure_yubikey": "Configure YubiKey",
"general_show_window": "Show window",
"general_hide_window": "Hide window",
"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_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_txt_info": "WebAuthn management requires elevated privileges.",
"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_settings": "Settings",
"mainDrawer_txt_help": "Help and about",
"devicePicker_no_yubikey": "No YubiKey present",
"devicePicker_insert_yubikey": "Insert your YubiKey",
"devicePicker_insert_or_tap": "Insert or tap a YubiKey",
"devicePicker_select_to_scan": "Select to scan",
"devicePicker_inaccessible": "Device inaccessible",
@ -297,5 +321,36 @@
"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,
children: [
if (usbCapabilities != 0) ...[
const ListTile(
leading: Icon(Icons.usb),
title: Text('USB'),
contentPadding: EdgeInsets.only(bottom: 8),
ListTile(
leading: const Icon(Icons.usb),
title: Text(AppLocalizations.of(context)!.general_usb),
contentPadding: const EdgeInsets.only(bottom: 8),
horizontalTitleGap: 0,
),
_CapabilityForm(
@ -136,7 +136,7 @@ class _CapabilitiesForm extends StatelessWidget {
),
ListTile(
leading: nfcIcon,
title: const Text('NFC'),
title: Text(AppLocalizations.of(context)!.general_nfc),
contentPadding: const EdgeInsets.only(bottom: 8),
horizontalTitleGap: 0,
),

View File

@ -18,6 +18,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../../app/message.dart';
import '../../app/shortcuts.dart';
@ -37,13 +38,16 @@ class AccountDialog extends ConsumerWidget {
const AccountDialog(this.credential, {super.key});
List<Widget> _buildActions(BuildContext context, AccountHelper helper) {
final l10n = AppLocalizations.of(context)!;
final actions = helper.buildActions();
final theme =
ButtonTheme.of(context).colorScheme ?? Theme.of(context).colorScheme;
final copy = actions.firstWhere(((e) => e.text.startsWith('Copy')));
final delete = actions.firstWhere(((e) => e.text.startsWith('Delete')));
final copy =
actions.firstWhere(((e) => e.text == l10n.oath_copy_to_clipboard));
final delete =
actions.firstWhere(((e) => e.text == l10n.oath_delete_account));
final colors = {
copy: Pair(theme.primary, theme.onPrimary),
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 (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) {
colors[calculates.first] = Pair(theme.primary, theme.onPrimary);
}

View File

@ -62,25 +62,23 @@ class AccountHelper {
final ready = expired || credential.oathType == OathType.hotp;
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';
return [
MenuAction(
text: appLocalizations.oath_copy_to_clipboard,
text: l10n.oath_copy_to_clipboard,
icon: const Icon(Icons.copy),
intent: code == null || expired ? null : const CopyIntent(),
trailing: shortcut,
),
if (manual)
MenuAction(
text: appLocalizations.oath_calculate,
text: l10n.oath_calculate,
icon: const Icon(Icons.refresh),
intent: ready ? const CalculateIntent() : null,
),
MenuAction(
text: pinned
? appLocalizations.oath_unpin_account
: appLocalizations.oath_pin_account,
text: pinned ? l10n.oath_unpin_account : l10n.oath_pin_account,
icon: pinned
? pushPinStrokeIcon
: const Icon(Icons.push_pin_outlined),
@ -89,11 +87,11 @@ class AccountHelper {
if (data.info.version.isAtLeast(5, 3))
MenuAction(
icon: const Icon(Icons.edit_outlined),
text: appLocalizations.oath_rename_account,
text: l10n.oath_rename_account,
intent: const EditIntent(),
),
MenuAction(
text: appLocalizations.oath_delete_account,
text: l10n.oath_delete_account,
icon: const Icon(Icons.delete_outline),
intent: const DeleteIntent(),
),

View File

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

View File

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

View File

@ -18,6 +18,7 @@ import 'dart:convert';
import 'package:flutter/material.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.
int byteLength(String value) => utf8.encode(value).length;
@ -37,7 +38,7 @@ InputCounterWidgetBuilder buildByteCounterFor(String currentValue) =>
return Text(
maxLength != null ? '${byteLength(currentValue)}/$maxLength' : '',
style: style,
semanticsLabel: 'Character count',
semanticsLabel: AppLocalizations.of(context)!.general_character_count,
);
};