mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user