i18n: Add 247 missing translation keys across all 13 languages

Replace hardcoded English strings in 32 Dart source files with l10n
references. Covers settings labels, UI actions, map/SAR features,
device config, notifications, profiles, spectrum scan, and status
messages for sl, de, hr, pl, es, fr, it, el, ru, tr, uk, zh, pt.
This commit is contained in:
Janez T
2026-03-17 10:36:58 +01:00
parent bbd5d108cb
commit fa2b586ab7
70 changed files with 16366 additions and 742 deletions

View File

@@ -133,7 +133,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
onPressed: _isCreating
? null
: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
icon: Icon(Icons.close),
tooltip: l10n.close,
),
],
@@ -158,7 +158,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
size: 20,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: Text(
l10n.channelTypesInfo,
@@ -170,7 +170,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
],
),
),
const SizedBox(height: 16),
SizedBox(height: 16),
TextFormField(
controller: _nameController,
decoration: InputDecoration(
@@ -196,7 +196,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
},
),
if (!isHashChannel) ...[
const SizedBox(height: 16),
SizedBox(height: 16),
TextFormField(
controller: _secretController,
decoration: InputDecoration(
@@ -211,7 +211,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
textInputAction: TextInputAction.done,
onFieldSubmitted: (_) => _handleCreate(),
),
const SizedBox(height: 8),
SizedBox(height: 8),
Text(
l10n.channelSecretHelp,
style: theme.textTheme.bodySmall?.copyWith(
@@ -236,7 +236,7 @@ class _AddChannelSheetState extends State<AddChannelSheet> {
size: 20,
color: Colors.blue,
),
const SizedBox(width: 8),
SizedBox(width: 8),
Expanded(
child: Text(
l10n.hashChannelInfo,

View File

@@ -7,6 +7,7 @@ import 'package:provider/provider.dart';
import '../../models/contact.dart';
import '../../models/path_history.dart';
import '../../l10n/app_localizations.dart';
import '../../providers/app_provider.dart';
import '../../providers/connection_provider.dart';
import '../../services/contact_route_resolver.dart';
@@ -213,6 +214,16 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
);
}
ParsedContactRoute? get _effectiveRoute {
if (_parsedRoute != null) {
return _parsedRoute;
}
if (_errorText != null || _controller.text.trim().isNotEmpty) {
return null;
}
return ContactRouteCodec.direct(hashSize: _selectedHashSize);
}
void _toggleHop(Contact contact) {
setState(() {
if (_selectedMapHops.any(
@@ -415,13 +426,14 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
),
trailing: FilledButton.tonal(
onPressed: () => _applyHistoryRecord(record),
child: const Text('Use'),
child: Text(AppLocalizations.of(context)!.use),
),
),
);
}
Widget _buildPreviewSection() {
final previewRoute = _effectiveRoute;
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
@@ -435,20 +447,21 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_parsedRoute == null ? 'Route preview' : _parsedRoute!.summary,
previewRoute == null ? 'Route preview' : previewRoute.summary,
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 4),
Text(
_parsedRoute == null
previewRoute == null
? 'Pick relays from the list below or open manual edit if you need exact hop tokens.'
: '${_parsedRoute!.byteLength} bytes • descriptor 0x${_parsedRoute!.encodedPathLen.toRadixString(16).padLeft(2, '0').toUpperCase()}',
: '${previewRoute.byteLength} bytes • descriptor 0x${previewRoute.encodedPathLen.toRadixString(16).padLeft(2, '0').toUpperCase()}',
style: Theme.of(context).textTheme.bodySmall,
),
if (_parsedRoute != null) ...[
if (previewRoute != null &&
previewRoute.canonicalText.isNotEmpty) ...[
const SizedBox(height: 10),
SelectableText(
_parsedRoute!.canonicalText,
previewRoute.canonicalText,
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontFamily: 'monospace'),
@@ -469,7 +482,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
border: Border.all(color: Theme.of(context).dividerColor),
),
child: Text(
'No relays selected yet. Add repeaters below or let auto resolve build a route.',
'No relays selected. Save now to use a direct path, or add repeaters below.',
style: Theme.of(context).textTheme.bodyMedium,
),
);
@@ -590,7 +603,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
_showManualEditor = expanded;
});
},
title: const Text('Manual route edit'),
title: Text(AppLocalizations.of(context)!.manualRouteEdit),
subtitle: const Text(
'Use this when you need to paste or tweak hop tokens directly.',
),
@@ -728,8 +741,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
children: [
FilledButton.tonalIcon(
onPressed: _resolvePathAutomatically,
icon: const Icon(Icons.auto_fix_high),
label: const Text('Auto resolve'),
icon: Icon(Icons.auto_fix_high),
label: Text(AppLocalizations.of(context)!.autoResolve),
),
OutlinedButton.icon(
onPressed: _selectedMapHops.isEmpty
@@ -740,8 +753,8 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
_syncControllerFromSelectedHops();
});
},
icon: const Icon(Icons.clear_all),
label: const Text('Clear relays'),
icon: Icon(Icons.clear_all),
label: Text(AppLocalizations.of(context)!.clearRelays),
),
],
),
@@ -796,7 +809,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (observedRecord != null) ...[
_buildHistoryRecordTile(observedRecord, title: 'Observed mesh route'),
_buildHistoryRecordTile(observedRecord, title: AppLocalizations.of(context)!.observedMeshRoute),
const SizedBox(height: 16),
],
if (remainingRecords.isEmpty)
@@ -822,6 +835,7 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
@override
Widget build(BuildContext context) {
final effectiveRoute = _effectiveRoute;
final appProvider = context.watch<AppProvider>();
final connectionProvider = context.watch<ConnectionProvider>();
final selfPoint =
@@ -939,22 +953,24 @@ class _ContactRouteDialogState extends State<ContactRouteDialog> {
TextButton(
onPressed: () => Navigator.of(
context,
).pop(const ContactRouteDialogResult.clear()),
child: const Text('Clear Route'),
).pop(ContactRouteDialogResult.clear()),
child: Text(
'${AppLocalizations.of(context)!.clearRoute} (${AppLocalizations.of(context)!.flood})',
),
)
else
const SizedBox.shrink(),
FilledButton(
onPressed: _parsedRoute == null
onPressed: effectiveRoute == null
? null
: () => Navigator.of(context).pop(
ContactRouteDialogResult.setWithFallback(
_parsedRoute!,
effectiveRoute,
inferredFallbackLocation:
_buildSyntheticFallbackLocation(),
),
),
child: const Text('Save Path'),
child: Text(AppLocalizations.of(context)!.savePath),
),
],
),

View File

@@ -159,7 +159,7 @@ class ContactTile extends StatelessWidget {
Row(children: [_buildRoutePill(context, contact)]),
] else
Padding(
padding: const EdgeInsets.only(top: 4),
padding: EdgeInsets.only(top: 4),
child: Text(
AppLocalizations.of(context)!.noGpsData,
style: Theme.of(
@@ -361,8 +361,8 @@ class ContactTile extends StatelessWidget {
onTap: () async {
Navigator.pop(sheetContext);
final toggled = contact.toggleFavourite();
final connectionProvider =
context.read<ConnectionProvider>();
final connectionProvider = context
.read<ConnectionProvider>();
await connectionProvider.addOrUpdateContact(toggled);
if (context.mounted) {
// Refresh contact from device so the local cache is updated
@@ -372,12 +372,12 @@ class ContactTile extends StatelessWidget {
),
if (!contact.isChannel)
ListTile(
leading: const Icon(Icons.share_outlined),
title: const Text('Share Contact'),
leading: Icon(Icons.share_outlined),
title: Text(l10n.shareContact),
onTap: () async {
Navigator.pop(sheetContext);
final connectionProvider =
context.read<ConnectionProvider>();
final connectionProvider = context
.read<ConnectionProvider>();
final url = await connectionProvider.exportContactUrl(
contact.publicKey,
);
@@ -385,22 +385,20 @@ class ContactTile extends StatelessWidget {
await Clipboard.setData(ClipboardData(text: url));
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Contact link copied to clipboard'),
SnackBar(
content: Text(l10n.contactLinkCopiedToClipboard),
),
);
}
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Failed to export contact'),
),
SnackBar(content: Text(l10n.failedToExportContact)),
);
}
},
),
ListTile(
leading: const Icon(Icons.message_outlined),
leading: Icon(Icons.message_outlined),
title: Text(l10n.messages),
enabled: canMessage,
onTap: !canMessage
@@ -412,7 +410,7 @@ class ContactTile extends StatelessWidget {
),
if (contact.displayLocation != null)
ListTile(
leading: const Icon(Icons.map_outlined),
leading: Icon(Icons.map_outlined),
title: Text(l10n.viewOnMap),
onTap: () {
Navigator.pop(sheetContext);
@@ -438,8 +436,8 @@ class ContactTile extends StatelessWidget {
),
if (canPreviewSensor)
ListTile(
leading: const Icon(Icons.visibility_outlined),
title: const Text('Preview'),
leading: Icon(Icons.visibility_outlined),
title: Text(l10n.preview),
onTap: () async {
Navigator.pop(sheetContext);
await Future<void>.delayed(Duration.zero);
@@ -467,7 +465,7 @@ class ContactTile extends StatelessWidget {
),
if (canSetPath)
ListTile(
leading: const Icon(Icons.alt_route),
leading: Icon(Icons.alt_route),
title: Text(l10n.contactSetPath),
onTap: () {
Navigator.pop(sheetContext);
@@ -476,8 +474,8 @@ class ContactTile extends StatelessWidget {
),
if (!contact.isChannel)
ListTile(
leading: const Icon(Icons.route),
title: const Text('Trace'),
leading: Icon(Icons.route),
title: Text(l10n.trace),
onTap: () {
Navigator.pop(sheetContext);
_showTraceSheet(context, contact);
@@ -494,8 +492,8 @@ class ContactTile extends StatelessWidget {
),
if (!contact.isPublicChannel)
ListTile(
leading: const Icon(Icons.edit_outlined),
title: const Text('Edit name'),
leading: Icon(Icons.edit_outlined),
title: Text(l10n.editName),
onTap: () {
Navigator.pop(sheetContext);
_showNameOverrideDialog(context, contact);
@@ -503,7 +501,7 @@ class ContactTile extends StatelessWidget {
),
if (!contact.isPublicChannel)
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
leading: Icon(Icons.delete, color: Colors.red),
title: Text(
contact.isChannel ? l10n.deleteChannel : l10n.deleteContact,
style: const TextStyle(color: Colors.red),
@@ -788,12 +786,13 @@ class ContactTile extends StatelessWidget {
paddedPathBytes: parsedRoute.paddedPathBytes,
);
if (context.mounted) {
final routeLabel = parsedRoute.hopCount == 0
? AppLocalizations.of(context)!.direct
: parsedRoute.canonicalText;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(
context,
)!.contactRouteSet(parsedRoute.canonicalText),
AppLocalizations.of(context)!.contactRouteSet(routeLabel),
),
),
);
@@ -1321,14 +1320,16 @@ class _NeighboursSheetState extends State<_NeighboursSheet> {
final timestampOrMs = int.tryParse(parts[1]);
final snrRaw = int.tryParse(parts[2]);
final isMs = timestampOrMs != null && timestampOrMs < 1e9;
parsed.add(_Neighbour(
publicKeyHex: keyHex,
lastSeenAt: !isMs && timestampOrMs != null
? DateTime.fromMillisecondsSinceEpoch(timestampOrMs * 1000)
: null,
lastSeenMs: isMs ? timestampOrMs : null,
snrDb: snrRaw != null ? snrRaw / 4.0 : null,
));
parsed.add(
_Neighbour(
publicKeyHex: keyHex,
lastSeenAt: !isMs && timestampOrMs != null
? DateTime.fromMillisecondsSinceEpoch(timestampOrMs * 1000)
: null,
lastSeenMs: isMs ? timestampOrMs : null,
snrDb: snrRaw != null ? snrRaw / 4.0 : null,
),
);
}
}
@@ -1401,41 +1402,35 @@ class _NeighboursSheetState extends State<_NeighboursSheet> {
child: _loading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
_error!,
textAlign: TextAlign.center,
),
),
)
: _neighbours.isEmpty
? const Center(child: Text('No neighbours found'))
: ListView.builder(
padding: const EdgeInsets.only(bottom: 16),
itemCount: _neighbours.length,
itemBuilder: (context, index) {
final n = _neighbours[index];
final name =
_resolveNeighbourName(n.publicKeyHex);
final parts = <String>[
if (n.snrDb != null)
'SNR ${n.snrDb!.toStringAsFixed(1)} dB',
if (n.lastSeenAt != null)
_formatAge(n.lastSeenAt!),
if (n.lastSeenMs != null)
'${n.lastSeenMs}ms ago',
];
return ListTile(
leading: const Icon(Icons.router_outlined),
title: Text(name),
subtitle: parts.isNotEmpty
? Text(parts.join(''))
: null,
);
},
),
? Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(_error!, textAlign: TextAlign.center),
),
)
: _neighbours.isEmpty
? const Center(child: Text('No neighbours found'))
: ListView.builder(
padding: const EdgeInsets.only(bottom: 16),
itemCount: _neighbours.length,
itemBuilder: (context, index) {
final n = _neighbours[index];
final name = _resolveNeighbourName(n.publicKeyHex);
final parts = <String>[
if (n.snrDb != null)
'SNR ${n.snrDb!.toStringAsFixed(1)} dB',
if (n.lastSeenAt != null) _formatAge(n.lastSeenAt!),
if (n.lastSeenMs != null) '${n.lastSeenMs}ms ago',
];
return ListTile(
leading: const Icon(Icons.router_outlined),
title: Text(name),
subtitle: parts.isNotEmpty
? Text(parts.join(''))
: null,
);
},
),
),
],
),

View File

@@ -401,7 +401,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
color: colorScheme.onPrimaryContainer,
size: 20,
),
const SizedBox(width: 12),
SizedBox(width: 12),
Expanded(
child: Text(
AppLocalizations.of(context)!.enterPasswordInfo,
@@ -488,7 +488,7 @@ class _RoomLoginSheetState extends State<RoomLoginSheet> {
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.login),
: Icon(Icons.login),
label: Text(
_isLoggingIn
? AppLocalizations.of(context)!.loggingInDots