mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-19 17:05:33 +10:00
Add localization support and translation script
- Introduced a new extension for localization in Flutter with `LocalizationExtension` in `l10n.dart`. - Added a Python script `translate.py` for translating ARB/JSON localization files using a local Ollama model, preserving keys and placeholders, and handling ICU format rules.
This commit is contained in:
+107
-108
@@ -4,6 +4,7 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -141,18 +142,18 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BatteryIndicator(connector: connector),
|
||||
title: const Text('Node Map'),
|
||||
title: Text(context.l10n.map_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bluetooth_disabled),
|
||||
tooltip: 'Disconnect',
|
||||
tooltip: context.l10n.common_disconnect,
|
||||
onPressed: () => _disconnect(context, connector),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune),
|
||||
tooltip: 'Settings',
|
||||
tooltip: context.l10n.common_settings,
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||
@@ -180,7 +181,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
defaultLabel: 'Point of interest',
|
||||
defaultLabel: context.l10n.map_pointOfInterest,
|
||||
flags: 'poi',
|
||||
);
|
||||
}
|
||||
@@ -194,7 +195,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
defaultLabel: 'Point of interest',
|
||||
defaultLabel: context.l10n.map_pointOfInterest,
|
||||
flags: 'poi',
|
||||
);
|
||||
return;
|
||||
@@ -265,7 +266,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No nodes with location data',
|
||||
context.l10n.map_noNodesWithLocation,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.grey[600],
|
||||
@@ -273,7 +274,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Nodes need to share their GPS coordinates\nto appear on the map',
|
||||
context.l10n.map_nodesNeedGps,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
@@ -381,27 +382,27 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Nodes: $nodeCount',
|
||||
context.l10n.map_nodesCount(nodeCount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Pins: $markerCount',
|
||||
context.l10n.map_pinsCount(markerCount),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildLegendItem(Icons.person, 'Chat', Colors.blue),
|
||||
_buildLegendItem(Icons.router, 'Repeater', Colors.green),
|
||||
_buildLegendItem(Icons.meeting_room, 'Room', Colors.purple),
|
||||
_buildLegendItem(Icons.sensors, 'Sensor', Colors.orange),
|
||||
_buildLegendItem(Icons.flag, 'Pin (DM)', Colors.blue),
|
||||
_buildLegendItem(Icons.flag, 'Pin (Private)', Colors.purple),
|
||||
_buildLegendItem(Icons.flag, 'Pin (Public)', Colors.orange),
|
||||
_buildLegendItem(Icons.person, context.l10n.map_chat, Colors.blue),
|
||||
_buildLegendItem(Icons.router, context.l10n.map_repeater, Colors.green),
|
||||
_buildLegendItem(Icons.meeting_room, context.l10n.map_room, Colors.purple),
|
||||
_buildLegendItem(Icons.sensors, context.l10n.map_sensor, Colors.orange),
|
||||
_buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue),
|
||||
_buildLegendItem(Icons.flag, context.l10n.map_pinPrivate, Colors.purple),
|
||||
_buildLegendItem(Icons.flag, context.l10n.map_pinPublic, Colors.orange),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -501,7 +502,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final flags = parts.length > 2 ? parts[2].trim() : '';
|
||||
return _MarkerPayload(
|
||||
position: LatLng(lat, lon),
|
||||
label: label.isEmpty ? 'Shared pin' : label,
|
||||
label: label.isEmpty ? context.l10n.map_sharedPin : label,
|
||||
flags: flags,
|
||||
);
|
||||
}
|
||||
@@ -595,7 +596,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
void _showNodeInfo(BuildContext context, Contact contact) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(
|
||||
@@ -614,19 +615,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_buildInfoRow('Path', contact.pathLabel),
|
||||
_buildInfoRow('Location',
|
||||
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}'),
|
||||
_buildInfoRow('Last Seen', _formatLastSeen(contact.lastSeen)),
|
||||
_buildInfoRow(context.l10n.map_lastSeen, _formatLastSeen(contact.lastSeen)),
|
||||
_buildInfoRow('Public Key', contact.publicKeyHex),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
if (contact.type == advTypeChat) // Only show chat button for chat nodes
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(dialogContext);
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -634,23 +635,23 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Open Chat'),
|
||||
child: Text(context.l10n.contacts_openChat),
|
||||
),
|
||||
if (contact.type == advTypeRepeater)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(dialogContext);
|
||||
_showRepeaterLogin(context, contact);
|
||||
},
|
||||
child: const Text('Manage Repeater'),
|
||||
child: Text(context.l10n.map_manageRepeater),
|
||||
),
|
||||
if (contact.type == advTypeRoom)
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(dialogContext);
|
||||
_showRoomLogin(context, contact);
|
||||
},
|
||||
child: const Text('Join Room'),
|
||||
child: Text(context.l10n.map_joinRoom),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -685,17 +686,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Disconnect'),
|
||||
content: const Text('Are you sure you want to disconnect from this device?'),
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.common_disconnect),
|
||||
content: Text(context.l10n.map_disconnectConfirm),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: const Text('Disconnect'),
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: Text(context.l10n.common_disconnect),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -709,19 +710,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
void _showMarkerInfo(_SharedMarker marker) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(marker.label),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow('From', marker.fromName),
|
||||
_buildInfoRow('Source', marker.sourceLabel),
|
||||
_buildInfoRow(context.l10n.map_from, marker.fromName),
|
||||
_buildInfoRow(context.l10n.map_source, marker.sourceLabel),
|
||||
_buildInfoRow(
|
||||
'Location',
|
||||
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
|
||||
),
|
||||
if (marker.flags.isNotEmpty) _buildInfoRow('Flags', marker.flags),
|
||||
if (marker.flags.isNotEmpty) _buildInfoRow(context.l10n.map_flags, marker.flags),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
@@ -730,9 +731,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
setState(() {
|
||||
_hiddenMarkerIds.add(marker.id);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: const Text('Hide'),
|
||||
child: Text(context.l10n.common_hide),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
@@ -741,15 +742,15 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_removedMarkerIds.add(marker.id);
|
||||
});
|
||||
await _markerService.saveRemovedIds(_removedMarkerIds);
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
if (dialogContext.mounted) {
|
||||
Navigator.pop(dialogContext);
|
||||
}
|
||||
},
|
||||
child: const Text('Remove'),
|
||||
child: Text(context.l10n.common_remove),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Close'),
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -785,13 +786,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final difference = now.difference(lastSeen);
|
||||
|
||||
if (difference.inSeconds < 60) {
|
||||
return 'Just now';
|
||||
return context.l10n.time_justNow;
|
||||
} else if (difference.inMinutes < 60) {
|
||||
return '${difference.inMinutes}m ago';
|
||||
return context.l10n.time_minutesAgo(difference.inMinutes);
|
||||
} else if (difference.inHours < 24) {
|
||||
return '${difference.inHours}h ago';
|
||||
return context.l10n.time_hoursAgo(difference.inHours);
|
||||
} else {
|
||||
return '${difference.inDays}d ago';
|
||||
return context.l10n.time_daysAgo(difference.inDays);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,21 +809,21 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.place),
|
||||
title: const Text('Share marker here'),
|
||||
title: Text(context.l10n.map_shareMarkerHere),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_shareMarker(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: position,
|
||||
defaultLabel: 'Point of interest',
|
||||
defaultLabel: context.l10n.map_pointOfInterest,
|
||||
flags: 'poi',
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.close),
|
||||
title: const Text('Cancel'),
|
||||
title: Text(context.l10n.common_cancel),
|
||||
onTap: () => Navigator.pop(sheetContext),
|
||||
),
|
||||
],
|
||||
@@ -840,7 +841,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}) async {
|
||||
if (!connector.isConnected) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Connect to a device to share markers')),
|
||||
SnackBar(content: Text(context.l10n.map_connectToShareMarkers)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -864,25 +865,25 @@ class _MapScreenState extends State<MapScreen> {
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Pin label'),
|
||||
title: Text(context.l10n.map_pinLabel),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Label',
|
||||
border: OutlineInputBorder(),
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.map_label,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final label = controller.text.trim().replaceAll('|', '/');
|
||||
Navigator.pop(dialogContext, label.isEmpty ? defaultLabel : label);
|
||||
},
|
||||
child: const Text('Continue'),
|
||||
child: Text(context.l10n.common_continue),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -910,7 +911,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
builder: (sheetContext) => StatefulBuilder(
|
||||
builder: (sheetContext, setSheetState) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, liveConnector, child) {
|
||||
builder: (consumerContext, liveConnector, child) {
|
||||
final allContacts = liveConnector.contacts
|
||||
.where((contact) =>
|
||||
contact.type != advTypeRepeater && contact.type != advTypeRoom)
|
||||
@@ -921,15 +922,15 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text('Send to contact', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(context.l10n.map_sendToContact, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search contacts...',
|
||||
hintText: context.l10n.contacts_searchContacts,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@@ -956,9 +957,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
);
|
||||
}),
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text('Send to channel', style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
|
||||
child: Text(context.l10n.map_sendToChannel, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
),
|
||||
if (liveConnector.isLoadingChannels)
|
||||
const Padding(
|
||||
@@ -966,9 +967,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: LinearProgressIndicator(),
|
||||
)
|
||||
else if (liveConnector.channels.where((c) => !c.isEmpty).isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text('No channels available'),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Text(context.l10n.map_noChannelsAvailable),
|
||||
)
|
||||
else
|
||||
...liveConnector.channels.where((c) => !c.isEmpty).map((channel) {
|
||||
@@ -980,7 +981,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
color: isPublic ? Colors.orange : Colors.blue,
|
||||
),
|
||||
title: Text(label),
|
||||
subtitle: isPublic ? const Text('Public channel') : null,
|
||||
subtitle: isPublic ? Text(context.l10n.channels_publicChannel) : null,
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
final canSend = isPublic
|
||||
@@ -1011,19 +1012,16 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final result = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Public location share'),
|
||||
content: Text(
|
||||
'You are about to share a location in $channelLabel. '
|
||||
'This channel is public and anyone with the PSK can see it.',
|
||||
),
|
||||
title: Text(context.l10n.map_publicLocationShare),
|
||||
content: Text(context.l10n.map_publicLocationShareConfirm(channelLabel)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, false),
|
||||
child: const Text('Cancel'),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext, true),
|
||||
child: const Text('Share'),
|
||||
child: Text(context.l10n.common_share),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1035,26 +1033,26 @@ class _MapScreenState extends State<MapScreen> {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Filter Nodes'),
|
||||
title: Text(context.l10n.map_filterNodes),
|
||||
contentPadding: const EdgeInsets.fromLTRB(24, 20, 24, 0),
|
||||
content: SingleChildScrollView(
|
||||
child: Consumer<AppSettingsService>(
|
||||
builder: (context, service, child) {
|
||||
builder: (consumerContext, service, child) {
|
||||
final settings = service.settings;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Node Types',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
context.l10n.map_nodeTypes,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
title: const Text('Chat Nodes'),
|
||||
title: Text(context.l10n.map_chatNodes),
|
||||
value: settings.mapShowChatNodes,
|
||||
onChanged: (value) {
|
||||
service.setMapShowChatNodes(value ?? true);
|
||||
@@ -1062,7 +1060,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('Repeaters'),
|
||||
title: Text(context.l10n.map_repeaters),
|
||||
value: settings.mapShowRepeaters,
|
||||
onChanged: (value) {
|
||||
service.setMapShowRepeaters(value ?? true);
|
||||
@@ -1070,7 +1068,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: const Text('Other Nodes'),
|
||||
title: Text(context.l10n.map_otherNodes),
|
||||
value: settings.mapShowOtherNodes,
|
||||
onChanged: (value) {
|
||||
service.setMapShowOtherNodes(value ?? true);
|
||||
@@ -1078,16 +1076,16 @@ class _MapScreenState extends State<MapScreen> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Key Prefix',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
context.l10n.map_keyPrefix,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
title: const Text('Filter by key prefix'),
|
||||
title: Text(context.l10n.map_filterByKeyPrefix),
|
||||
value: settings.mapKeyPrefixEnabled,
|
||||
onChanged: (value) {
|
||||
service.setMapKeyPrefixEnabled(value ?? false);
|
||||
@@ -1097,10 +1095,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
TextFormField(
|
||||
initialValue: settings.mapKeyPrefix,
|
||||
enabled: settings.mapKeyPrefixEnabled,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Public key prefix',
|
||||
decoration: InputDecoration(
|
||||
labelText: context.l10n.map_publicKeyPrefix,
|
||||
hintText: 'e.g. ab12',
|
||||
border: OutlineInputBorder(),
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
@@ -1108,16 +1106,16 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Markers',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
context.l10n.map_markers,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
title: const Text('Show shared markers'),
|
||||
title: Text(context.l10n.map_showSharedMarkers),
|
||||
value: settings.mapShowMarkers,
|
||||
onChanged: (value) {
|
||||
service.setMapShowMarkers(value ?? true);
|
||||
@@ -1125,9 +1123,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Last Seen Time',
|
||||
style: TextStyle(
|
||||
Text(
|
||||
context.l10n.map_lastSeenTime,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
@@ -1158,7 +1156,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: const Text('Close'),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1205,23 +1203,24 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
String _getTimeFilterLabel(double hours) {
|
||||
if (hours == 0) return 'All Time';
|
||||
if (hours == 0) return context.l10n.time_allTime;
|
||||
|
||||
if (hours < 1) {
|
||||
return '${(hours * 60).round()} minutes';
|
||||
return '${(hours * 60).round()} ${context.l10n.time_minutes}';
|
||||
} else if (hours < 24) {
|
||||
return '${hours.round()} ${hours.round() == 1 ? 'hour' : 'hours'}';
|
||||
final h = hours.round();
|
||||
return '$h ${h == 1 ? context.l10n.time_hour : context.l10n.time_hours}';
|
||||
} else if (hours < 168) {
|
||||
final days = (hours / 24).round();
|
||||
return '$days ${days == 1 ? 'day' : 'days'}';
|
||||
return '$days ${days == 1 ? context.l10n.time_day : context.l10n.time_days}';
|
||||
} else if (hours < 720) {
|
||||
final weeks = (hours / 168).round();
|
||||
return '$weeks ${weeks == 1 ? 'week' : 'weeks'}';
|
||||
return '$weeks ${weeks == 1 ? context.l10n.time_week : context.l10n.time_weeks}';
|
||||
} else if (hours < 4380) {
|
||||
final months = (hours / 730).round();
|
||||
return '$months ${months == 1 ? 'month' : 'months'}';
|
||||
return '$months ${months == 1 ? context.l10n.time_month : context.l10n.time_months}';
|
||||
} else {
|
||||
return 'All Time';
|
||||
return context.l10n.time_allTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user