From c37abb63e37b26ba25893f71fa97bcfd90047059 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 11:56:42 -0800 Subject: [PATCH 01/12] add export and import contact frame builders in meshcore_protocol.dart and implement contact export functionality in contacts_screen.dart --- lib/connector/meshcore_protocol.dart | 18 ++++++ lib/screens/contacts_screen.dart | 89 ++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index f9241e8..0e43e46 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -708,3 +708,21 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { } return writer.toBytes(); } + +// Build a export contact frame +// [cmd][pub_key x32 / if empty exports your contact info] +Uint8List buildExportContactFrame(Uint8List pubKey) { + final writer = BufferWriter(); + writer.writeByte(cmdExportContact); + writer.writeBytes(pubKey); + return writer.toBytes(); +} + +// Build a import contact frame +// [cmd][contact_frame x148] +Uint8List buildImportContactFrame(Uint8List contactFrame) { + final writer = BufferWriter(); + writer.writeByte(cmdImportContact); + writer.writeBytes(contactFrame); + return writer.toBytes(); +} \ No newline at end of file diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 54f819c..4c8e396 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -77,6 +78,13 @@ class _ContactsScreenState extends State await _groupStore.saveGroups(_groups); } + Future _contactExport(Uint8List pubKey) async { + final connector = Provider.of(context, listen: false); + final exportContactFrame = buildExportContactFrame(pubKey); + await connector.sendFrame(exportContactFrame); + return; + } + @override Widget build(BuildContext context) { final connector = context.watch(); @@ -96,18 +104,77 @@ class _ContactsScreenState extends State centerTitle: true, automaticallyImplyLeading: false, actions: [ - IconButton( - icon: const Icon(Icons.bluetooth_disabled), - tooltip: context.l10n.common_disconnect, - onPressed: () => _disconnect(context, connector), - ), - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.common_settings, - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), + PopupMenuButton(itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.connect_without_contact), + const SizedBox(width: 8), + Text("Zero Hop Advert"), + ], + ), + onTap: () => { + connector.sendSelfAdvert(flood: false), + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), + }, ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.cell_tower), + const SizedBox(width: 8), + Text("Flood Advert"), + ], + ), + onTap: () => { + connector.sendSelfAdvert(flood: true), + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(context.l10n.settings_advertisementSent))), + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.copy), + const SizedBox(width: 8), + Text("Copy Advert to Clipboard"), + ], + ), + onTap: () => _contactExport(Uint8List.fromList([])), + ), + ], + icon: const Icon(Icons.connect_without_contact), + ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + const Text('Disconnect'), + ], + ), + onTap: () => _disconnect(context, connector), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 8), + const Text('Settings'), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + ], + icon: const Icon(Icons.more_vert), ), ], ), From 641307a31632574a81dc59ca18f26ff9342eaf1b Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 12:19:45 -0800 Subject: [PATCH 02/12] Added response code for exporting contacts and implement frame listener in contacts_screen.dart --- lib/connector/meshcore_protocol.dart | 1 + lib/screens/contacts_screen.dart | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0e43e46..dda07bd 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -159,6 +159,7 @@ const int respCodeContactMsgRecv = 7; const int respCodeChannelMsgRecv = 8; const int respCodeCurrTime = 9; const int respCodeNoMoreMessages = 10; +const int respCodeExportContact = 11; const int respCodeBattAndStorage = 12; const int respCodeDeviceInfo = 13; const int respCodeContactMsgRecvV3 = 16; diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 4c8e396..d5d3377 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../connector/meshcore_connector.dart'; @@ -53,16 +54,20 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; + StreamSubscription? _frameSubscription; + @override void initState() { super.initState(); _loadGroups(); + _setupFrameListener(); } @override void dispose() { _searchDebounce?.cancel(); _searchController.dispose(); + _frameSubscription?.cancel(); super.dispose(); } @@ -78,6 +83,22 @@ class _ContactsScreenState extends State await _groupStore.saveGroups(_groups); } + void _setupFrameListener() { + final connector = Provider.of(context, listen: false); + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + final frameBuffer = BufferReader(frame); + final code = frameBuffer.readUInt8(); + + if (code == respCodeExportContact) { + final advertPacket = frameBuffer.readRemainingBytes(); + final hexString = pubKeyToHex(advertPacket); + Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); + } + }); + } + Future _contactExport(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); From eeb8ff34e8959bd060b0ae4aa706fb341d7f6c0a Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Mon, 26 Jan 2026 16:11:21 -0800 Subject: [PATCH 03/12] Implement contact import functionality from clipboard and add relevant UI options --- lib/connector/meshcore_protocol.dart | 14 +++++-- lib/l10n/app_en.arb | 6 ++- lib/screens/contacts_screen.dart | 60 +++++++++++++++++++++++----- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index dda07bd..0609adb 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -98,6 +98,14 @@ class BufferWriter { } writeBytes(bytes); } + + void writeHex(String hex) { + List result = []; + for (int i = 0; i < hex.length ~/ 2; i++) { + result.add(int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)); + } + writeBytes(Uint8List.fromList(result)); + } } // Command codes (to device) @@ -720,10 +728,10 @@ Uint8List buildExportContactFrame(Uint8List pubKey) { } // Build a import contact frame -// [cmd][contact_frame x148] -Uint8List buildImportContactFrame(Uint8List contactFrame) { +// [cmd][contact_frame x98+] +Uint8List buildImportContactFrame(String contactFrame) { final writer = BufferWriter(); writer.writeByte(cmdImportContact); - writer.writeBytes(contactFrame); + writer.writeHex(contactFrame); return writer.toBytes(); } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 56cb1cc..7c8a0a7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1308,5 +1308,9 @@ "listFilter_repeaters": "Repeaters", "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", - "listFilter_newGroup": "New group" + "listFilter_newGroup": "New group", + + "contacts_clipboardEmpty": "Clipboard Is Empty.", + "contacts_invalidAdvertFormat": "Invalid Contact Data", + "contacts_contactAdded": "Contact has been added." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index d5d3377..89a07e0 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -106,6 +106,36 @@ class _ContactsScreenState extends State return; } + Future _contactImport() async { + final clipboardData = await Clipboard.getData('text/plain'); + if (clipboardData == null || clipboardData.text == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), + ); + return; + } + final text = clipboardData.text!.trim(); + if (!text.startsWith('meshcore://')) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + return; + } + final hexString = text.substring('meshcore://'.length); + try { + final connector = Provider.of(context, listen: false); + final importContactFrame = buildImportContactFrame(hexString); + await connector.sendFrame(importContactFrame); + // ScaffoldMessenger.of(context).showSnackBar( + // SnackBar(content: Text(context.l10n.contacts_contactAdded)), + // ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } + } + @override Widget build(BuildContext context) { final connector = context.watch(); @@ -166,21 +196,21 @@ class _ContactsScreenState extends State ), onTap: () => _contactExport(Uint8List.fromList([])), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.paste), + const SizedBox(width: 8), + Text("Add Contact from Clipboard"), + ], + ), + onTap: () => _contactImport(), + ), ], icon: const Icon(Icons.connect_without_contact), ), PopupMenuButton( itemBuilder: (context) => [ - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - const Text('Disconnect'), - ], - ), - onTap: () => _disconnect(context, connector), - ), PopupMenuItem( child: Row( children: [ @@ -194,6 +224,16 @@ class _ContactsScreenState extends State MaterialPageRoute(builder: (context) => const SettingsScreen()), ), ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + const Text('Disconnect'), + ], + ), + onTap: () => _disconnect(context, connector), + ) ], icon: const Icon(Icons.more_vert), ), From d0c8fab6fbe451456fe3e98ce3ab5b9150e7b454 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Tue, 27 Jan 2026 18:43:59 -0800 Subject: [PATCH 04/12] Add contact import functionality and update UI feedback for import status --- lib/connector/meshcore_connector.dart | 7 +++++ lib/l10n/app_en.arb | 17 ++++++++++- lib/screens/contacts_screen.dart | 44 ++++++++++++++++++++++----- 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index de39d7e..a5c5290 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1680,6 +1680,12 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; + case pushCodeNewAdvert: + debugPrint('Got NEW_ADVERT'); + _handleContact(frame); + notifyListeners(); + unawaited(_persistContacts()); + break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1737,6 +1743,7 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); + break; default: debugPrint('Unknown frame code: $code'); } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7c8a0a7..3a9383a 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1309,8 +1309,23 @@ "listFilter_roomServers": "Room servers", "listFilter_unreadOnly": "Unread only", "listFilter_newGroup": "New group", + + "contacts_pathTrace": "Path Trace", + "contacts_ping": "Ping", + "contacts_repeaterPathTrace": "Path trace to repeater", + "contacts_repeaterPing": "Ping repeater", + "contacts_roomPathTrace": "Path trace to room server", + "contacts_roomPing": "Ping room server", + "contacts_chatTraceRoute": "Path trace route", + "contacts_pathTraceTo": "Trace route to {name}", + "@contacts_pathTraceTo": { + "placeholders": { + "name": {"type": "String"} + } + }, "contacts_clipboardEmpty": "Clipboard Is Empty.", "contacts_invalidAdvertFormat": "Invalid Contact Data", - "contacts_contactAdded": "Contact has been added." + "contacts_contactImported": "Contact has been Imported.", + "contacts_contactImportFailed": "Contact Failed to Imported." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 89a07e0..64c2924 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -54,6 +54,7 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; + bool _imported = false; StreamSubscription? _frameSubscription; @override @@ -96,6 +97,23 @@ class _ContactsScreenState extends State final hexString = pubKeyToHex(advertPacket); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } + + if(code == respCodeOk && _imported) { + // Show a snackbar indicating success + _imported = false; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImported)), + ); + } + + if(code == respCodeErr && _imported) { + // Show a snackbar indicating success + _imported = false; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), + ); + } + }); } @@ -126,9 +144,7 @@ class _ContactsScreenState extends State final connector = Provider.of(context, listen: false); final importContactFrame = buildImportContactFrame(hexString); await connector.sendFrame(importContactFrame); - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar(content: Text(context.l10n.contacts_contactAdded)), - // ); + _imported = true; } catch (e) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), @@ -923,6 +939,7 @@ class _ContactsScreenState extends State _openChat(context, contact); }, ), + ], ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: Text( @@ -997,7 +1014,7 @@ class _ContactTile extends StatelessWidget { ), title: Text(contact.name), subtitle: Text( - '${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey', + '${contact.typeLabel}\n${contact.shortPubKeyHex}', ), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. @@ -1019,13 +1036,24 @@ class _ContactTile extends StatelessWidget { _formatLastSeen(context, lastSeen), style: TextStyle(fontSize: 12, color: Colors.grey[600]), ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(contact.pathLabel, + style: TextStyle(fontSize: 12, color: Colors.grey[600])), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), + ], + ), + Text( + _formatLastSeen(context, lastSeen), + style: TextStyle(fontSize: 12, color: Colors.grey[600]), + ), + if (contact.hasLocation) + Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), ), - onTap: onTap, - onLongPress: onLongPress, ); } From 42115bf200e4421c3d914b9c6ebd74345b5c8060 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Wed, 28 Jan 2026 11:04:34 -0800 Subject: [PATCH 05/12] Refactor contact handling and enhance UI with new advert options and localized strings --- lib/connector/meshcore_connector.dart | 7 ---- lib/connector/meshcore_protocol.dart | 21 +++++++++++ lib/l10n/app_en.arb | 6 +++- lib/models/contact.dart | 4 +++ lib/screens/contacts_screen.dart | 51 ++++++++++++--------------- 5 files changed, 52 insertions(+), 37 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index a5c5290..de39d7e 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1680,12 +1680,6 @@ class MeshCoreConnector extends ChangeNotifier { _isLoadingContacts = true; notifyListeners(); break; - case pushCodeNewAdvert: - debugPrint('Got NEW_ADVERT'); - _handleContact(frame); - notifyListeners(); - unawaited(_persistContacts()); - break; case respCodeContact: debugPrint('Got CONTACT'); _handleContact(frame); @@ -1743,7 +1737,6 @@ class MeshCoreConnector extends ChangeNotifier { break; case respCodeCustomVars: _handleCustomVars(frame); - break; default: debugPrint('Unknown frame code: $code'); } diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 0609adb..df07758 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -18,6 +18,10 @@ class BufferReader { return data; } + void skipBytes(int count) { + _pointer += count; + } + Uint8List readRemainingBytes() => readBytes(remaining); String readString() => @@ -135,6 +139,7 @@ const int cmdSendStatusReq = 27; const int cmdGetContactByKey = 30; const int cmdGetChannel = 31; const int cmdSetChannel = 32; +const int cmdSendTracePath = 36; const int cmdGetRadioSettings = 57; const int cmdGetTelemetryReq = 39; const int cmdGetCustomVar = 40; @@ -185,6 +190,7 @@ const int pushCodeLoginSuccess = 0x85; const int pushCodeLoginFail = 0x86; const int pushCodeStatusResponse = 0x87; const int pushCodeLogRxData = 0x88; +const int pushCodeTraceData = 0x89; const int pushCodeNewAdvert = 0x8A; const int pushCodeTelemetryResponse = 0x8B; const int pushCodeBinaryResponse = 0x8C; @@ -718,6 +724,21 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) { return writer.toBytes(); } +//Build a trace request frame +//[cmd][tag x4][auth x4][flag][payload] +Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) +{ + final writer = BufferWriter(); + writer.writeByte(cmdSendTracePath); + writer.writeUInt32LE(tag); + writer.writeUInt32LE(auth); + writer.writeByte(flag); + if (payload != null && payload.isNotEmpty) { + writer.writeBytes(payload); + } + return writer.toBytes(); +} + // Build a export contact frame // [cmd][pub_key x32 / if empty exports your contact info] Uint8List buildExportContactFrame(Uint8List pubKey) { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3a9383a..706d361 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1327,5 +1327,9 @@ "contacts_clipboardEmpty": "Clipboard Is Empty.", "contacts_invalidAdvertFormat": "Invalid Contact Data", "contacts_contactImported": "Contact has been Imported.", - "contacts_contactImportFailed": "Contact Failed to Imported." + "contacts_contactImportFailed": "Contact Failed to Imported.", + "contacts_zeroHopAdvert":"Zero Hop Advert", + "contacts_floodAdvert":"Flood Advert", + "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", + "contacts_addContactFromClipboard":"Add Contact from Clipboard" } diff --git a/lib/models/contact.dart b/lib/models/contact.dart index 364deff..9599e01 100644 --- a/lib/models/contact.dart +++ b/lib/models/contact.dart @@ -102,6 +102,10 @@ class Contact { return parts.join(','); } + String get shortPubKeyHex { + return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>"; + } + Uint8List get _pathBytesForDisplay { if (pathOverride != null) { if (pathOverride! < 0) return Uint8List(0); diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 64c2924..e9aef26 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -177,7 +177,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.connect_without_contact), const SizedBox(width: 8), - Text("Zero Hop Advert"), + Text(context.l10n.contacts_zeroHopAdvert), ], ), onTap: () => { @@ -192,7 +192,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.cell_tower), const SizedBox(width: 8), - Text("Flood Advert"), + Text(context.l10n.contacts_floodAdvert), ], ), onTap: () => { @@ -207,7 +207,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.copy), const SizedBox(width: 8), - Text("Copy Advert to Clipboard"), + Text(context.l10n.contacts_copyAdvertToClipboard), ], ), onTap: () => _contactExport(Uint8List.fromList([])), @@ -217,7 +217,7 @@ class _ContactsScreenState extends State children: [ const Icon(Icons.paste), const SizedBox(width: 8), - Text("Add Contact from Clipboard"), + Text(context.l10n.contacts_addContactFromClipboard), ], ), onTap: () => _contactImport(), @@ -227,12 +227,22 @@ class _ContactsScreenState extends State ), PopupMenuButton( itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.common_disconnect), + ], + ), + onTap: () => _disconnect(context, connector), + ), PopupMenuItem( child: Row( children: [ const Icon(Icons.settings), const SizedBox(width: 8), - const Text('Settings'), + Text(context.l10n.settings_title), ], ), onTap: () => Navigator.push( @@ -240,16 +250,6 @@ class _ContactsScreenState extends State MaterialPageRoute(builder: (context) => const SettingsScreen()), ), ), - PopupMenuItem( - child: Row( - children: [ - const Icon(Icons.logout, color: Colors.red), - const SizedBox(width: 8), - const Text('Disconnect'), - ], - ), - onTap: () => _disconnect(context, connector), - ) ], icon: const Icon(Icons.more_vert), ), @@ -930,7 +930,7 @@ class _ContactsScreenState extends State _showRoomLogin(context, contact, RoomLoginDestination.management); }, ), - ] else + ] else ...[ ListTile( leading: const Icon(Icons.chat), title: Text(context.l10n.contacts_openChat), @@ -1005,17 +1005,16 @@ class _ContactTile extends StatelessWidget { @override Widget build(BuildContext context) { - final shotPublicKey = - "<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>"; return ListTile( leading: CircleAvatar( backgroundColor: _getTypeColor(contact.type), child: _buildContactAvatar(contact), ), title: Text(contact.name), - subtitle: Text( - '${contact.typeLabel}\n${contact.shortPubKeyHex}', - ), + subtitle: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(contact.pathLabel), + Text(contact.shortPubKeyHex, style: TextStyle(fontSize: 12)) + ],), // Clamp text scaling in trailing section to prevent overflow while // maintaining accessibility. Primary content (title/subtitle) scales normally. trailing: MediaQuery( @@ -1039,21 +1038,15 @@ class _ContactTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Text(contact.pathLabel, - style: TextStyle(fontSize: 12, color: Colors.grey[600])), if (contact.hasLocation) Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), - Text( - _formatLastSeen(context, lastSeen), - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - if (contact.hasLocation) - Icon(Icons.location_on, size: 14, color: Colors.grey[400]), ], ), ), + onTap: onTap, + onLongPress: onLongPress, ); } From 6712088fcda3d0abaf8cefd0f303dd9e6dc3e793 Mon Sep 17 00:00:00 2001 From: 446564 Date: Fri, 30 Jan 2026 08:44:03 -0800 Subject: [PATCH 06/12] add obtainium badge allow users to easily add app to obtainium https://apps.obtainium.imranr.dev --- README.md | 28 ++++++++++++++++++++++++++-- assets/badges/badge_obtainium.png | Bin 0 -> 21824 bytes 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 assets/badges/badge_obtainium.png diff --git a/README.md b/README.md index 2acb390..984e6ba 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices. MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities. + + Get it on Obtainium + + ## Screenshots @@ -21,6 +25,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Features ### Core Functionality + - **Direct Messaging**: Private encrypted conversations with individual contacts - **Public Channels**: Broadcast messages to channel subscribers on the mesh network - **Contact Management**: Organize contacts, track last seen times, and manage conversation history @@ -29,6 +34,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Message Replies**: Thread conversations with inline reply functionality ### Mesh Network + - **Path Visualization**: View routing paths and signal quality for each contact - **Route Management**: Manual path overriding and automatic route rotation - **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking @@ -36,6 +42,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Repeater Support**: Connect to and manage repeater nodes for extended range ### Map & Location + - **Live Map View**: Real-time visualization of mesh network nodes on an interactive map - **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range - **Location Sharing**: Share GPS coordinates and custom markers with contacts @@ -43,12 +50,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **MGRS Coordinates**: Support for Military Grid Reference System coordinate format ### Device Management + - **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth - **Device Settings**: Configure radio parameters, power settings, and network options - **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves - **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon) ### Repeater Hub + - **CLI Access**: Full command-line interface to repeater nodes - **Settings Management**: Configure repeater behavior, power limits, and network settings - **Statistics Dashboard**: View repeater traffic, connected clients, and system health @@ -57,6 +66,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Technical Details ### Architecture + - **Framework**: Flutter 3.38.5 / Dart 3.10.4 - **State Management**: Provider pattern with ChangeNotifier - **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy @@ -64,11 +74,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh - **Encryption**: End-to-end encryption for private messages using the MeshCore protocol ### Platform Support + - ✅ **Android**: Full support (API 21+) - ✅ **iOS**: Full support (iOS 12+) - 🚧 **Desktop**: Limited support (macOS/Linux/Windows) ### Dependencies + | Package | Purpose | |---------|---------| | flutter_blue_plus | Bluetooth Low Energy communication | @@ -84,6 +96,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ## Getting Started ### Prerequisites + - Flutter SDK 3.38.5 or later - Android Studio / Xcode (for mobile development) - A MeshCore-compatible LoRa device @@ -91,17 +104,20 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Installation 1. **Clone the repository** + ```bash git clone https://github.com/zjs81/meshcore-open.git cd meshcore-open ``` 2. **Install dependencies** + ```bash flutter pub get ``` 3. **Run the app** + ```bash flutter run ``` @@ -109,11 +125,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh ### Building for Release **Android APK:** + ```bash flutter build apk --release ``` **iOS:** + ```bash flutter build ios --release ``` @@ -152,25 +170,30 @@ lib/ ## BLE Protocol ### Nordic UART Service (NUS) + - **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e` - **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device) - **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device) ### Device Discovery + Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-` ### Message Format + Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions. ## Configuration ### App Settings + - **Theme**: System default, light, or dark mode - **Notifications**: Configurable for messages, channels, and node advertisements - **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types - **Message Retry**: Automatic retry with configurable path clearing ### Device Settings + - **Radio Power**: Transmit power adjustment (10-30 dBm) - **Frequency**: LoRa frequency configuration - **Bandwidth**: Channel bandwidth selection @@ -182,22 +205,23 @@ Messages are transmitted as binary frames using a custom protocol optimized for This is an open-source project. Contributions are welcome! ### Development Guidelines + - Follow the Flutter style guide - Use Material 3 design components - Write clear commit messages - Test on both Android and iOS before submitting PRs ### Code Style + - Prefer `StatelessWidget` with `Consumer` for reactive UI - Use `const` constructors where possible - Keep functions small and focused - Avoid premature abstractions - ## Support For issues, questions, or feature requests, please open an issue on GitHub: -https://github.com/zjs81/meshcore-open/issues + ## Donate diff --git a/assets/badges/badge_obtainium.png b/assets/badges/badge_obtainium.png new file mode 100644 index 0000000000000000000000000000000000000000..cc3a0ed0e00bbae1f0c02d22f449529d1e1fa6f4 GIT binary patch literal 21824 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 Tiv|2W2NXqFHJM^5v*-T<+0V|B literal 0 HcmV?d00001 From d30e7c4e2c4cb7b12b3d2a76265a460a2027656f Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 14:55:55 -0800 Subject: [PATCH 07/12] Prevent disconnection handling when already disconnected, curing a race condition. --- lib/connector/meshcore_connector.dart | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 1bab130..d5a36e3 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -706,7 +706,7 @@ class MeshCoreConnector extends ChangeNotifier { try { _connectionSubscription = device.connectionState.listen((state) { - if (state == BluetoothConnectionState.disconnected) { + if (state == BluetoothConnectionState.disconnected && isConnected) { _handleDisconnection(); } }); @@ -959,12 +959,7 @@ class MeshCoreConnector extends ChangeNotifier { if (!isConnected) return; if (_batteryRequested && !force) return; _batteryRequested = true; - try { - await sendFrame(buildGetBattAndStorageFrame()); - } catch (e) { - // Connection likely lost - trigger disconnection handling - _handleDisconnection(); - } + await sendFrame(buildGetBattAndStorageFrame()); } void _startBatteryPolling() { From 5115d8bbe350f3ecb6ca93460380eeb03c31c6b2 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 15:00:33 -0800 Subject: [PATCH 08/12] Added zero-hop contact sharing functionality and related UI updates --- lib/connector/meshcore_protocol.dart | 9 +++ lib/l10n/app_en.arb | 8 +- lib/screens/contacts_screen.dart | 105 ++++++++++++++++++++++----- 3 files changed, 101 insertions(+), 21 deletions(-) diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index df07758..64a9963 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -755,4 +755,13 @@ Uint8List buildImportContactFrame(String contactFrame) { writer.writeByte(cmdImportContact); writer.writeHex(contactFrame); return writer.toBytes(); +} + +// Build a export contact frame +// [cmd][pub_key x32] +Uint8List buildZeroHopContact(Uint8List pubKey) { + final writer = BufferWriter(); + writer.writeByte(cmdShareContact); + writer.writeBytes(pubKey); + return writer.toBytes(); } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1203b20..ba6afc4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1338,5 +1338,11 @@ "contacts_zeroHopAdvert":"Zero Hop Advert", "contacts_floodAdvert":"Flood Advert", "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", - "contacts_addContactFromClipboard":"Add Contact from Clipboard" + "contacts_addContactFromClipboard":"Add Contact from Clipboard", + "contacts_ShareContact": "Copy contact to Clipboard", + "contacts_ShareContactZeroHop": "Share contact by advert", + "contacts_zeroHopContactAdvertSent": "Sent contact by advert.", + "contacts_zeroHopContactAdvertFailed": "Failed to send contact.", + "contacts_contactAdvertCopied": "Advert copied to Clipboard.", + "contacts_contactAdvertCopyFailed": "Copying advert to Clipboard failed." } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 59a8d05..99bad87 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -55,6 +55,9 @@ class _ContactsScreenState extends State Timer? _searchDebounce; bool _imported = false; + bool _zeroHopContact = false; + bool _copyedContact = false; + StreamSubscription? _frameSubscription; @override @@ -98,20 +101,53 @@ class _ContactsScreenState extends State Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } - if(code == respCodeOk && _imported) { + if(code == respCodeOk) { // Show a snackbar indicating success + if(_imported && mounted){ + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImported)), + ); + } + + if(_zeroHopContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertSent)), + ); + } + + if(_copyedContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), + ); + } + + _copyedContact = false; + _zeroHopContact = false; _imported = false; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImported)), - ); } - if(code == respCodeErr && _imported) { - // Show a snackbar indicating success + if(code == respCodeErr) { + // Show a snackbar indicating failure + if(_imported && mounted){ + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), + ); + } + + if(_zeroHopContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertFailed)), + ); + } + if(_copyedContact && mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_contactAdvertCopyFailed)), + ); + } + + _copyedContact = false; _imported = false; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), - ); + _zeroHopContact = false; } }); @@ -120,35 +156,49 @@ class _ContactsScreenState extends State Future _contactExport(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); + _copyedContact = true; await connector.sendFrame(exportContactFrame); return; } + Future _contactZeroHop(Uint8List pubKey) async { + final connector = Provider.of(context, listen: false); + final exportContactZeroHopFrame = buildZeroHopContact(pubKey); + _zeroHopContact = true; + await connector.sendFrame(exportContactZeroHopFrame); + } + Future _contactImport() async { + final connector = Provider.of(context, listen: false); final clipboardData = await Clipboard.getData('text/plain'); if (clipboardData == null || clipboardData.text == null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), - ); + if(mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)), + ); + } return; } final text = clipboardData.text!.trim(); if (!text.startsWith('meshcore://')) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), - ); + if(mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } return; } final hexString = text.substring('meshcore://'.length); try { - final connector = Provider.of(context, listen: false); final importContactFrame = buildImportContactFrame(hexString); await connector.sendFrame(importContactFrame); _imported = true; } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), - ); + if(mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } } } @@ -977,6 +1027,22 @@ class _ContactsScreenState extends State }, ), ], + ListTile( + leading: const Icon(Icons.copy), + title: Text(context.l10n.contacts_ShareContact), + onTap: () { + Navigator.pop(sheetContext); + _contactExport(contact.publicKey); + }, + ), + ListTile( + leading: const Icon(Icons.connect_without_contact), + title: Text(context.l10n.contacts_ShareContactZeroHop), + onTap: () { + Navigator.pop(sheetContext); + _contactZeroHop(contact.publicKey); + }, + ), ListTile( leading: const Icon(Icons.delete, color: Colors.red), title: Text( @@ -988,7 +1054,6 @@ class _ContactsScreenState extends State _confirmDelete(context, connector, contact); }, ), - ], ], ), ), From 33680f0cb99e595d077aa3b6d208b2f3bda2a1c8 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 15:25:34 -0800 Subject: [PATCH 09/12] Replace action buttons with a popup menu for better UI/UX on channels and map screens --- lib/screens/channels_screen.dart | 56 ++++++++++++++++++++++---------- lib/screens/map_screen.dart | 39 ++++++++++++++-------- 2 files changed, 64 insertions(+), 31 deletions(-) diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index c302fb3..efd7340 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -121,24 +121,44 @@ class _ChannelsScreenState extends State centerTitle: true, automaticallyImplyLeading: false, actions: [ - if (_communities.isNotEmpty) - IconButton( - icon: const Icon(Icons.groups), - tooltip: context.l10n.community_manageCommunities, - onPressed: () => _showManageCommunitiesDialog(context), - ), - IconButton( - icon: const Icon(Icons.bluetooth_disabled), - tooltip: context.l10n.common_disconnect, - onPressed: () => _disconnect(context), - ), - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.common_settings, - onPressed: () => Navigator.push( - context, - MaterialPageRoute(builder: (context) => const SettingsScreen()), - ), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.common_disconnect), + ], + ), + onTap: () => _disconnect(context), + ), + if (_communities.isNotEmpty) + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.groups), + const SizedBox(width: 8), + Text(context.l10n.community_manageCommunities), + ], + ), + onTap: () => _showManageCommunitiesDialog(context), + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 8), + Text(context.l10n.settings_title), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + ], + icon: const Icon(Icons.more_vert), ), ], ), diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart index 74e5cf9..edef811 100644 --- a/lib/screens/map_screen.dart +++ b/lib/screens/map_screen.dart @@ -245,20 +245,33 @@ class _MapScreenState extends State { centerTitle: true, automaticallyImplyLeading: false, actions: [ - IconButton( - icon: const Icon(Icons.bluetooth_disabled), - tooltip: context.l10n.common_disconnect, - onPressed: () => _disconnect(context, connector), - ), - IconButton( - icon: const Icon(Icons.tune), - tooltip: context.l10n.common_settings, - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const SettingsScreen(), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.logout, color: Colors.red), + const SizedBox(width: 8), + Text(context.l10n.common_disconnect), + ], + ), + onTap: () => _disconnect(context, connector), ), - ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Icons.settings), + const SizedBox(width: 8), + Text(context.l10n.settings_title), + ], + ), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => const SettingsScreen()), + ), + ), + ], + icon: const Icon(Icons.more_vert), ), ], ), From 6d7d51f0a4cd1c5f44cd7b425540d5602fb4cd91 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 16:03:05 -0800 Subject: [PATCH 10/12] _requestDeviceInfo added isConnected not already _awaitingSelfInfo --- lib/connector/meshcore_connector.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index d5a36e3..3a36a92 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -990,6 +990,7 @@ class MeshCoreConnector extends ChangeNotifier { } Future _requestDeviceInfo() async { + if (!isConnected || _awaitingSelfInfo) return; _awaitingSelfInfo = true; await sendFrame(buildDeviceQueryFrame()); await sendFrame(buildAppStartFrame()); From 8d8b938878fef633980a1e7cf6893f3b704b9e94 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sat, 31 Jan 2026 22:19:01 -0800 Subject: [PATCH 11/12] Ran translation script --- lib/l10n/app_bg.arb | 18 +++++++++++++++++- lib/l10n/app_de.arb | 18 +++++++++++++++++- lib/l10n/app_es.arb | 18 +++++++++++++++++- lib/l10n/app_fr.arb | 18 +++++++++++++++++- lib/l10n/app_it.arb | 18 +++++++++++++++++- lib/l10n/app_nl.arb | 18 +++++++++++++++++- lib/l10n/app_pl.arb | 18 +++++++++++++++++- lib/l10n/app_pt.arb | 18 +++++++++++++++++- lib/l10n/app_ru.arb | 17 ++++++++++++++++- lib/l10n/app_sk.arb | 18 +++++++++++++++++- lib/l10n/app_sl.arb | 18 +++++++++++++++++- lib/l10n/app_sv.arb | 18 +++++++++++++++++- lib/l10n/app_uk.arb | 17 ++++++++++++++++- lib/l10n/app_zh.arb | 18 +++++++++++++++++- 14 files changed, 236 insertions(+), 14 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 958d963..460bc9d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1552,5 +1552,21 @@ "contacts_chatTraceRoute": "Трасиране на път", "contacts_roomPathTrace": "Трасиране на път до съ", "contacts_roomPing": "Ping на сървъра на стаята", - "contacts_pathTraceTo": "Проследи маршрут към {name}" + "contacts_pathTraceTo": "Проследи маршрут към {name}", + "appSettings_languageUk": "Украински", + "contacts_clipboardEmpty": "Клипборда е празна.", + "contacts_invalidAdvertFormat": "Невалидни данни за контакт", + "appSettings_languageRu": "Руски", + "contacts_contactImported": "Контактът е импортиран.", + "contacts_zeroHopAdvert": "Реклама без скок", + "contacts_contactImportFailed": "Контактът не е успешно импортиран.", + "contacts_floodAdvert": "Потопна реклама", + "contacts_addContactFromClipboard": "Добави контакт от клипборда", + "contacts_copyAdvertToClipboard": "Копирай обявата в клипборда", + "contacts_ShareContact": "Копирай контакт в клипборда", + "contacts_ShareContactZeroHop": "Сподели контакт чрез обява", + "contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.", + "contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.", + "contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.", + "contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 2586877..0a72559 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Pfadverfolgung zum Raumserver", "contacts_roomPing": "Raumserver anpingen", "contacts_pathTraceTo": "Route nach {name} verfolgen", - "contacts_chatTraceRoute": "Pfadverfolgungsroute" + "contacts_chatTraceRoute": "Pfadverfolgungsroute", + "appSettings_languageRu": "Russisch", + "contacts_invalidAdvertFormat": "Ungültige Kontaktdaten", + "contacts_clipboardEmpty": "Die Zwischenablage ist leer.", + "appSettings_languageUk": "Ukrainisch", + "contacts_contactImported": "Kontakt wurde importiert.", + "contacts_contactImportFailed": "Kontakt konnte nicht importiert werden", + "contacts_zeroHopAdvert": "Zero-Hop-Anzeige", + "contacts_floodAdvert": "Überflutungsanzeige", + "contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen", + "contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen", + "contacts_copyAdvertToClipboard": "Werbung in die Zwischenablage kopieren", + "contacts_ShareContact": "Kontakt in die Zwischenablage kopieren", + "contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.", + "contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet", + "contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.", + "contacts_contactAdvertCopyFailed": "Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen." } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1cdfb7b..c6dad1f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1552,5 +1552,21 @@ "contacts_roomPing": "Pingar servidor de sala", "contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación", "contacts_pathTraceTo": "Rastrear ruta a {name}", - "contacts_chatTraceRoute": "Ruta de trazado" + "contacts_chatTraceRoute": "Ruta de trazado", + "appSettings_languageUk": "Ucraniano", + "contacts_clipboardEmpty": "El portapapeles está vacío.", + "appSettings_languageRu": "Ruso", + "contacts_invalidAdvertFormat": "Datos de contacto no válidos", + "contacts_floodAdvert": "Anuncio de inundación", + "contacts_contactImported": "El contacto ha sido importado.", + "contacts_contactImportFailed": "Contacto no se importó correctamente.", + "contacts_zeroHopAdvert": "Anuncio de Zero Hop", + "contacts_ShareContactZeroHop": "Compartir contacto por anuncio", + "contacts_ShareContact": "Copiar contacto al Portapapeles", + "contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles", + "contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles", + "contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.", + "contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.", + "contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.", + "contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 88c65d6..c1157ed 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1552,5 +1552,21 @@ "contacts_chatTraceRoute": "Tracer le chemin", "contacts_pathTraceTo": "Tracer l'itinéraire vers {name}", "contacts_ping": "Ping", - "contacts_roomPing": "Pinguer le serveur de la salle" + "contacts_roomPing": "Pinguer le serveur de la salle", + "contacts_invalidAdvertFormat": "Données de contact non valides", + "appSettings_languageUk": "Ukrainien", + "appSettings_languageRu": "Russe", + "contacts_clipboardEmpty": "Le presse-papiers est vide.", + "contacts_contactImported": "Le contact a été importé.", + "contacts_floodAdvert": "Annonce de crue", + "contacts_contactImportFailed": "Échec de l'importation du contact.", + "contacts_zeroHopAdvert": "Annonce Zero Hop", + "contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers", + "contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers", + "contacts_ShareContact": "Copier le contact dans le presse-papiers", + "contacts_ShareContactZeroHop": "Partager un contact par annonce", + "contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.", + "contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.", + "contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.", + "contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index acd440b..c32e863 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1552,5 +1552,21 @@ "contacts_repeaterPing": "Ripetitore ping", "contacts_pathTraceTo": "Traccia percorso verso {name}", "contacts_roomPing": "Ping al server della stanza", - "contacts_chatTraceRoute": "Traccia percorso path" + "contacts_chatTraceRoute": "Traccia percorso path", + "appSettings_languageRu": "Russo", + "contacts_invalidAdvertFormat": "Dati di contatto non validi", + "appSettings_languageUk": "Ucraino", + "contacts_zeroHopAdvert": "Annuncio Zero Hop", + "contacts_floodAdvert": "Annuncio alluvionale", + "contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti", + "contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard", + "contacts_clipboardEmpty": "La clipboard è vuota.", + "contacts_ShareContact": "Copia contatto negli Appunti", + "contacts_contactImported": "Il contatto è stato importato.", + "contacts_contactImportFailed": "Contatto non importato con successo.", + "contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.", + "contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.", + "contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio", + "contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.", + "contacts_contactAdvertCopied": "Annuncio copiato negli Appunti." } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index b28d668..e94deb3 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Padtrace naar room server", "contacts_roomPing": "Ping kamer server", "contacts_chatTraceRoute": "Route traceren", - "contacts_pathTraceTo": "Trace route to {name}" + "contacts_pathTraceTo": "Trace route to {name}", + "appSettings_languageUk": "Oekraïens", + "contacts_invalidAdvertFormat": "Ongeldige contactgegevens", + "contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.", + "contacts_zeroHopAdvert": "Zero Hop Reclame", + "contacts_floodAdvert": "Overstromingsadvertentie", + "contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren", + "appSettings_languageRu": "Russisch", + "contacts_clipboardEmpty": "Knipbord is leeg.", + "contacts_addContactFromClipboard": "Contact uit klembord toevoegen", + "contacts_contactImported": "Contact is geïmporteerd.", + "contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie", + "contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.", + "contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.", + "contacts_ShareContact": "Kontakt naar Klembord kopiëren", + "contacts_ShareContactZeroHop": "Contact delen via advertentie", + "contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 8070ac3..44552c3 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1552,5 +1552,21 @@ "pathTrace_refreshTooltip": "Odśwież ścieżkę.", "contacts_repeaterPing": "Repeater pingowy", "contacts_pathTraceTo": "Śledź trasę do {name}", - "contacts_chatTraceRoute": "Śledź trasę promienia" + "contacts_chatTraceRoute": "Śledź trasę promienia", + "appSettings_languageRu": "Rosyjski", + "appSettings_languageUk": "Ukraińska", + "contacts_contactImportFailed": "Kontakt nie został zaimportowany.", + "contacts_zeroHopAdvert": "Reklama Zero Hop", + "contacts_floodAdvert": "Reklama powodziowa", + "contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka", + "contacts_clipboardEmpty": "Schowek jest pusty.", + "contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe", + "contacts_addContactFromClipboard": "Dodaj kontakt z schowka", + "contacts_contactImported": "Kontakt został zaimportowany.", + "contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.", + "contacts_contactAdvertCopied": "Reklama skopiowana do schowka.", + "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.", + "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie", + "contacts_ShareContact": "Kopiuj kontakt do schowka", + "contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 6994bea..56a7f2b 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Traçar caminho para o servidor da sala", "contacts_roomPing": "Pingar servidor da sala", "contacts_chatTraceRoute": "Rastrear rota do caminho", - "contacts_pathTraceTo": "Rastrear rota para {name}" + "contacts_pathTraceTo": "Rastrear rota para {name}", + "contacts_invalidAdvertFormat": "Dados de Contato Inválidos", + "contacts_clipboardEmpty": "Área de Transferência Está Vazia.", + "appSettings_languageUk": "Ucraniano", + "contacts_contactImported": "Contato foi importado.", + "contacts_zeroHopAdvert": "Anúncio Zero Hop", + "contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência", + "contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência", + "appSettings_languageRu": "Russo", + "contacts_ShareContact": "Copiar contato para Área de Transferência", + "contacts_contactImportFailed": "Contato falhou ao ser importado.", + "contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.", + "contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.", + "contacts_floodAdvert": "Anúncio de Inundação", + "contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.", + "contacts_ShareContactZeroHop": "Compartilhar contato por anúncio", + "contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato." } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index f007aa7..0bca5ef 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -793,5 +793,20 @@ "contacts_roomPathTrace": "Трассировка пути к серверу комнаты", "contacts_roomPing": "Пинговать сервер комнаты", "contacts_chatTraceRoute": "Трассировка маршрута", - "contacts_pathTraceTo": "Показать маршрут к {name}" + "contacts_pathTraceTo": "Показать маршрут к {name}", + "contacts_contactImported": "Контакт был импортирован", + "contacts_contactImportFailed": "Контакт не удалось импортировать", + "contacts_invalidAdvertFormat": "Недействительные контактные данные", + "contacts_zeroHopAdvert": "Реклама Zero Hop", + "appSettings_languageUk": "Українська", + "contacts_floodAdvert": "Рекламный поток", + "contacts_clipboardEmpty": "Буфер обмена пуст.", + "contacts_copyAdvertToClipboard": "Копировать рекламу в буфер обмена", + "contacts_ShareContact": "Копировать контакт в буфер обмена", + "contacts_zeroHopContactAdvertFailed": "Не удалось отправить контакт.", + "contacts_contactAdvertCopied": "Реклама скопирована в буфер обмена.", + "contacts_contactAdvertCopyFailed": "Копирование рекламы в буфер обмена не удалось.", + "contacts_addContactFromClipboard": "Добавить контакт из буфера обмена", + "contacts_ShareContactZeroHop": "Поделиться контактом по объявлению", + "contacts_zeroHopContactAdvertSent": "Отправлено сообщение по объявлению." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 4e66af0..d61cca6 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti", "contacts_roomPing": "Ping server miestnosti", "contacts_chatTraceRoute": "Sledovať trasu lúča", - "contacts_pathTraceTo": "Sledovať trasu k {name}" + "contacts_pathTraceTo": "Sledovať trasu k {name}", + "contacts_clipboardEmpty": "Schránka je prázdna.", + "appSettings_languageUk": "Ukrajinská", + "contacts_contactImportFailed": "Kontakt sa nepodarilo importovať.", + "contacts_zeroHopAdvert": "Inzerát Zero Hop", + "contacts_floodAdvert": "Inzerát povodní", + "contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky", + "contacts_invalidAdvertFormat": "Neplatné kontaktné údaje", + "appSettings_languageRu": "Ruština", + "contacts_addContactFromClipboard": "Pridať kontakt z schránky", + "contacts_contactImported": "Kontakt bol importovaný.", + "contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.", + "contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.", + "contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.", + "contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.", + "contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát", + "contacts_ShareContact": "Kopírovať kontakt do schránky" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 805621b..cbc4e3f 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Sledenje poti do strežnika sobe", "contacts_roomPing": "Ping strežnik sobe", "contacts_chatTraceRoute": "Slediti poti žarkov", - "contacts_pathTraceTo": "Trace route to {name}" + "contacts_pathTraceTo": "Trace route to {name}", + "appSettings_languageRu": "Ruščina", + "appSettings_languageUk": "Ukrajinsko", + "contacts_contactImported": "Kontakt je bil uvožen.", + "contacts_contactImportFailed": "Kontakt ni bil uspešno uvožen.", + "contacts_zeroHopAdvert": "Reklama brez posrednikov", + "contacts_floodAdvert": "Poplavna oglás", + "contacts_invalidAdvertFormat": "Neveljavni kontaktne podatke", + "contacts_clipboardEmpty": "Odložišče je prazno.", + "contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče", + "contacts_addContactFromClipboard": "Dodaj stik iz odložišča", + "contacts_zeroHopContactAdvertSent": "Poslano po oglasu.", + "contacts_zeroHopContactAdvertFailed": "Pošiljanje kontakta ni uspelo.", + "contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.", + "contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.", + "contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa", + "contacts_ShareContact": "Kopiraj stik v Odložišče" } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index da017be..05f77cb 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "Vägspårning till rumserver", "contacts_roomPing": "Ping rumsserver", "contacts_chatTraceRoute": "Spåra rutt", - "contacts_pathTraceTo": "Spåra rutt till {name}" + "contacts_pathTraceTo": "Spåra rutt till {name}", + "contacts_clipboardEmpty": "Urklipp är tomt.", + "appSettings_languageRu": "Ryska", + "contacts_contactImportFailed": "Kontakt kunde inte importeras.", + "contacts_zeroHopAdvert": "Reklam med nollhopp", + "contacts_floodAdvert": "Översvämningsannons", + "contacts_copyAdvertToClipboard": "Kopiera annons till urklipp", + "contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter", + "appSettings_languageUk": "Ukrainska", + "contacts_addContactFromClipboard": "Lägg till kontakt från urklipp", + "contacts_contactImported": "Kontakt har importerats.", + "contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.", + "contacts_contactAdvertCopied": "Annons kopierad till Urklipp.", + "contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.", + "contacts_ShareContact": "Kopiera kontakt till Urklipp", + "contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.", + "contacts_ShareContactZeroHop": "Dela kontakt via annons" } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index 85ce4a2..3362d40 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -1553,5 +1553,20 @@ "contacts_roomPathTrace": "Трасування шляху до серверу кімнати", "contacts_roomPing": "Пінг сервера кімнати", "contacts_chatTraceRoute": "Трасування шляху", - "contacts_pathTraceTo": "Відстежити маршрут до {name}" + "contacts_pathTraceTo": "Відстежити маршрут до {name}", + "contacts_invalidAdvertFormat": "Недійсні контактні дані", + "contacts_contactImported": "Контакт було імпортовано.", + "contacts_contactImportFailed": "Контакт не вдалося імпортувати", + "contacts_zeroHopAdvert": "Реклама без перехоплення", + "contacts_floodAdvert": "Залив реклами", + "contacts_copyAdvertToClipboard": "Копіювати оголошення в буфер обміну", + "contacts_clipboardEmpty": "Буфер обміну порожній", + "appSettings_languageRu": "Російська", + "contacts_ShareContact": "Копіювати контакт у буфер обміну", + "contacts_zeroHopContactAdvertFailed": "Не вдалося надіслати контакт.", + "contacts_contactAdvertCopied": "Рекламу скопійовано до буфера обміну.", + "contacts_contactAdvertCopyFailed": "Копіювання оголошення в буфер обміну завершилося невдало", + "contacts_zeroHopContactAdvertSent": "Відправлено контакт за оголошенням", + "contacts_addContactFromClipboard": "Додати контакт з буфера обміну", + "contacts_ShareContactZeroHop": "Поділитися контактом за оголошенням" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5f0c797..c118b9d 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1552,5 +1552,21 @@ "contacts_roomPathTrace": "路径追踪至房间服务器", "contacts_roomPing": "Ping 房间服务器", "contacts_chatTraceRoute": "路径追踪", - "contacts_pathTraceTo": "追踪路由到 {name}" + "contacts_pathTraceTo": "追踪路由到 {name}", + "appSettings_languageUk": "乌克兰语", + "appSettings_languageRu": "俄语", + "contacts_contactImported": "联系人已导入", + "contacts_contactImportFailed": "联系人导入失败", + "contacts_zeroHopAdvert": "零跳广告", + "contacts_floodAdvert": "洪水广告", + "contacts_clipboardEmpty": "剪贴板为空。", + "contacts_invalidAdvertFormat": "无效联系人数据", + "contacts_addContactFromClipboard": "从剪贴板添加联系人", + "contacts_zeroHopContactAdvertSent": "通过广告发送的联系人", + "contacts_zeroHopContactAdvertFailed": "发送联系人失败", + "contacts_contactAdvertCopied": "广告已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "复制广告到剪贴板失败。", + "contacts_copyAdvertToClipboard": "复制广告到剪贴板", + "contacts_ShareContactZeroHop": "通过广告分享联系人", + "contacts_ShareContact": "复制联系人到剪贴板" } From 79ffc21bd68a78272adf899663f01f7bf91951fd Mon Sep 17 00:00:00 2001 From: Zach Date: Sun, 1 Feb 2026 16:57:17 -0700 Subject: [PATCH 12/12] fix commit --- ios/Podfile.lock | 7 - lib/connector/meshcore_protocol.dart | 13 +- lib/l10n/app_en.arb | 9 +- lib/l10n/app_localizations.dart | 84 ++ lib/l10n/app_localizations_bg.dart | 50 +- lib/l10n/app_localizations_de.dart | 53 +- lib/l10n/app_localizations_en.dart | 43 + lib/l10n/app_localizations_es.dart | 50 +- lib/l10n/app_localizations_fr.dart | 54 +- lib/l10n/app_localizations_it.dart | 52 +- lib/l10n/app_localizations_nl.dart | 52 +- lib/l10n/app_localizations_pl.dart | 51 +- lib/l10n/app_localizations_pt.dart | 51 +- lib/l10n/app_localizations_ru.dart | 50 ++ lib/l10n/app_localizations_sk.dart | 50 +- lib/l10n/app_localizations_sl.dart | 49 +- lib/l10n/app_localizations_sv.dart | 49 +- lib/l10n/app_localizations_uk.dart | 51 +- lib/l10n/app_localizations_zh.dart | 966 ++++++++++++----------- lib/l10n/app_zh.arb | 1076 +++++++++++++------------- lib/screens/contacts_screen.dart | 53 +- tools/translate.py | 1059 +++++++++---------------- untranslated.json | 70 +- 23 files changed, 2211 insertions(+), 1831 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aef2502..cf8bbca 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -57,9 +57,6 @@ PODS: - nanopb/encode (3.30910.0) - package_info_plus (0.4.5): - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - PromisesObjC (2.4.0) - shared_preferences_foundation (0.0.1): - Flutter @@ -79,7 +76,6 @@ DEPENDENCIES: - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -112,8 +108,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/mobile_scanner/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: @@ -140,7 +134,6 @@ SPEC CHECKSUMS: mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart index 64a9963..dfe787e 100644 --- a/lib/connector/meshcore_protocol.dart +++ b/lib/connector/meshcore_protocol.dart @@ -104,9 +104,18 @@ class BufferWriter { } void writeHex(String hex) { + // Validate hex string length is even and not empty + if (hex.isEmpty || hex.length % 2 != 0) { + throw FormatException('Invalid hex string length: ${hex.length}'); + } List result = []; for (int i = 0; i < hex.length ~/ 2; i++) { - result.add(int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16)); + final hexByte = hex.substring(i * 2, i * 2 + 2); + final byte = int.tryParse(hexByte, radix: 16); + if (byte == null) { + throw FormatException('Invalid hex characters at position $i: $hexByte'); + } + result.add(byte); } writeBytes(Uint8List.fromList(result)); } @@ -764,4 +773,4 @@ Uint8List buildZeroHopContact(Uint8List pubKey) { writer.writeByte(cmdShareContact); writer.writeBytes(pubKey); return writer.toBytes(); -} \ No newline at end of file +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ba6afc4..ee5cf7d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1316,7 +1316,6 @@ "pathTrace_failed": "Path trace failed.", "pathTrace_notAvailable": "Path trace not available.", "pathTrace_refreshTooltip": "Refresh Path Trace.", - "contacts_pathTrace": "Path Trace", "contacts_ping": "Ping", "contacts_repeaterPathTrace": "Path trace to repeater", @@ -1331,10 +1330,10 @@ } }, - "contacts_clipboardEmpty": "Clipboard Is Empty.", - "contacts_invalidAdvertFormat": "Invalid Contact Data", - "contacts_contactImported": "Contact has been Imported.", - "contacts_contactImportFailed": "Contact Failed to Imported.", + "contacts_clipboardEmpty": "Clipboard is empty.", + "contacts_invalidAdvertFormat": "Invalid contact data", + "contacts_contactImported": "Contact has been imported.", + "contacts_contactImportFailed": "Failed to import contact.", "contacts_zeroHopAdvert":"Zero Hop Advert", "contacts_floodAdvert":"Flood Advert", "contacts_copyAdvertToClipboard":"Copy Advert to Clipboard", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ac3eb99..055667f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4771,6 +4771,90 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Trace route to {name}'** String contacts_pathTraceTo(String name); + + /// No description provided for @contacts_clipboardEmpty. + /// + /// In en, this message translates to: + /// **'Clipboard is empty.'** + String get contacts_clipboardEmpty; + + /// No description provided for @contacts_invalidAdvertFormat. + /// + /// In en, this message translates to: + /// **'Invalid contact data'** + String get contacts_invalidAdvertFormat; + + /// No description provided for @contacts_contactImported. + /// + /// In en, this message translates to: + /// **'Contact has been imported.'** + String get contacts_contactImported; + + /// No description provided for @contacts_contactImportFailed. + /// + /// In en, this message translates to: + /// **'Failed to import contact.'** + String get contacts_contactImportFailed; + + /// No description provided for @contacts_zeroHopAdvert. + /// + /// In en, this message translates to: + /// **'Zero Hop Advert'** + String get contacts_zeroHopAdvert; + + /// No description provided for @contacts_floodAdvert. + /// + /// In en, this message translates to: + /// **'Flood Advert'** + String get contacts_floodAdvert; + + /// No description provided for @contacts_copyAdvertToClipboard. + /// + /// In en, this message translates to: + /// **'Copy Advert to Clipboard'** + String get contacts_copyAdvertToClipboard; + + /// No description provided for @contacts_addContactFromClipboard. + /// + /// In en, this message translates to: + /// **'Add Contact from Clipboard'** + String get contacts_addContactFromClipboard; + + /// No description provided for @contacts_ShareContact. + /// + /// In en, this message translates to: + /// **'Copy contact to Clipboard'** + String get contacts_ShareContact; + + /// No description provided for @contacts_ShareContactZeroHop. + /// + /// In en, this message translates to: + /// **'Share contact by advert'** + String get contacts_ShareContactZeroHop; + + /// No description provided for @contacts_zeroHopContactAdvertSent. + /// + /// In en, this message translates to: + /// **'Sent contact by advert.'** + String get contacts_zeroHopContactAdvertSent; + + /// No description provided for @contacts_zeroHopContactAdvertFailed. + /// + /// In en, this message translates to: + /// **'Failed to send contact.'** + String get contacts_zeroHopContactAdvertFailed; + + /// No description provided for @contacts_contactAdvertCopied. + /// + /// In en, this message translates to: + /// **'Advert copied to Clipboard.'** + String get contacts_contactAdvertCopied; + + /// No description provided for @contacts_contactAdvertCopyFailed. + /// + /// In en, this message translates to: + /// **'Copying advert to Clipboard failed.'** + String get contacts_contactAdvertCopyFailed; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 27b2007..701429e 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -451,10 +451,10 @@ class AppLocalizationsBg extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Руски'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Украински'; @override String get appSettings_notifications => 'Уведомления'; @@ -2720,4 +2720,50 @@ class AppLocalizationsBg extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Проследи маршрут към $name'; } + + @override + String get contacts_clipboardEmpty => 'Клипборда е празна.'; + + @override + String get contacts_invalidAdvertFormat => 'Невалидни данни за контакт'; + + @override + String get contacts_contactImported => 'Контактът е импортиран.'; + + @override + String get contacts_contactImportFailed => + 'Контактът не е успешно импортиран.'; + + @override + String get contacts_zeroHopAdvert => 'Реклама без скок'; + + @override + String get contacts_floodAdvert => 'Потопна реклама'; + + @override + String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда'; + + @override + String get contacts_addContactFromClipboard => 'Добави контакт от клипборда'; + + @override + String get contacts_ShareContact => 'Копирай контакт в клипборда'; + + @override + String get contacts_ShareContactZeroHop => 'Сподели контакт чрез обява'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Изпратен контакт по обява.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Неуспешно изпращане на контакт.'; + + @override + String get contacts_contactAdvertCopied => + 'Рекламата е копирана в клипборда.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Копирането на обявата в клипборда не успя.'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 69e6a59..514a7a1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -445,10 +445,10 @@ class AppLocalizationsDe extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russisch'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrainisch'; @override String get appSettings_notifications => 'Benachrichtigungen'; @@ -2724,4 +2724,53 @@ class AppLocalizationsDe extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Route nach $name verfolgen'; } + + @override + String get contacts_clipboardEmpty => 'Die Zwischenablage ist leer.'; + + @override + String get contacts_invalidAdvertFormat => 'Ungültige Kontaktdaten'; + + @override + String get contacts_contactImported => 'Kontakt wurde importiert.'; + + @override + String get contacts_contactImportFailed => + 'Kontakt konnte nicht importiert werden'; + + @override + String get contacts_zeroHopAdvert => 'Zero-Hop-Anzeige'; + + @override + String get contacts_floodAdvert => 'Überflutungsanzeige'; + + @override + String get contacts_copyAdvertToClipboard => + 'Werbung in die Zwischenablage kopieren'; + + @override + String get contacts_addContactFromClipboard => + 'Kontakt aus Zwischenablage hinzufügen'; + + @override + String get contacts_ShareContact => 'Kontakt in die Zwischenablage kopieren'; + + @override + String get contacts_ShareContactZeroHop => 'Kontakt über Anzeige teilen'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Kontakt über Anzeige gesendet'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Kontakt konnte nicht gesendet werden.'; + + @override + String get contacts_contactAdvertCopied => + 'Anzeige in die Zwischenablage kopiert.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopieren des Werbeinhalts in die Zwischenablage fehlgeschlagen.'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a609dd8..040d809 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2680,4 +2680,47 @@ class AppLocalizationsEn extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Trace route to $name'; } + + @override + String get contacts_clipboardEmpty => 'Clipboard is empty.'; + + @override + String get contacts_invalidAdvertFormat => 'Invalid contact data'; + + @override + String get contacts_contactImported => 'Contact has been imported.'; + + @override + String get contacts_contactImportFailed => 'Failed to import contact.'; + + @override + String get contacts_zeroHopAdvert => 'Zero Hop Advert'; + + @override + String get contacts_floodAdvert => 'Flood Advert'; + + @override + String get contacts_copyAdvertToClipboard => 'Copy Advert to Clipboard'; + + @override + String get contacts_addContactFromClipboard => 'Add Contact from Clipboard'; + + @override + String get contacts_ShareContact => 'Copy contact to Clipboard'; + + @override + String get contacts_ShareContactZeroHop => 'Share contact by advert'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Sent contact by advert.'; + + @override + String get contacts_zeroHopContactAdvertFailed => 'Failed to send contact.'; + + @override + String get contacts_contactAdvertCopied => 'Advert copied to Clipboard.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Copying advert to Clipboard failed.'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 28d3e9d..e65cbcd 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -448,10 +448,10 @@ class AppLocalizationsEs extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ruso'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ucraniano'; @override String get appSettings_notifications => 'Notificaciones'; @@ -2720,4 +2720,50 @@ class AppLocalizationsEs extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Rastrear ruta a $name'; } + + @override + String get contacts_clipboardEmpty => 'El portapapeles está vacío.'; + + @override + String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos'; + + @override + String get contacts_contactImported => 'El contacto ha sido importado.'; + + @override + String get contacts_contactImportFailed => + 'Contacto no se importó correctamente.'; + + @override + String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop'; + + @override + String get contacts_floodAdvert => 'Anuncio de inundación'; + + @override + String get contacts_copyAdvertToClipboard => 'Copiar anuncio al portapapeles'; + + @override + String get contacts_addContactFromClipboard => + 'Agregar contacto desde el portapapeles'; + + @override + String get contacts_ShareContact => 'Copiar contacto al Portapapeles'; + + @override + String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'No se pudo enviar el contacto.'; + + @override + String get contacts_contactAdvertCopied => 'Anuncio copiado al Portapapeles.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Copiar anuncio al Portapapeles ha fallado.'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index ce6f6a9..4496fc8 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -449,10 +449,10 @@ class AppLocalizationsFr extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russe'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrainien'; @override String get appSettings_notifications => 'Notifications'; @@ -2737,4 +2737,54 @@ class AppLocalizationsFr extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Tracer l\'itinéraire vers $name'; } + + @override + String get contacts_clipboardEmpty => 'Le presse-papiers est vide.'; + + @override + String get contacts_invalidAdvertFormat => 'Données de contact non valides'; + + @override + String get contacts_contactImported => 'Le contact a été importé.'; + + @override + String get contacts_contactImportFailed => + 'Échec de l\'importation du contact.'; + + @override + String get contacts_zeroHopAdvert => 'Annonce Zero Hop'; + + @override + String get contacts_floodAdvert => 'Annonce de crue'; + + @override + String get contacts_copyAdvertToClipboard => + 'Copier l\'annonce dans le presse-papiers'; + + @override + String get contacts_addContactFromClipboard => + 'Ajouter un contact depuis le presse-papiers'; + + @override + String get contacts_ShareContact => + 'Copier le contact dans le presse-papiers'; + + @override + String get contacts_ShareContactZeroHop => 'Partager un contact par annonce'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Envoyer un contact par annonce.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Échec de l\'envoi du contact.'; + + @override + String get contacts_contactAdvertCopied => + 'Annonce copiée dans le presse-papiers.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'La copie de l\'annonce vers le presse-papiers a échoué.'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a7ac6a6..02345c4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -447,10 +447,10 @@ class AppLocalizationsIt extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russo'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ucraino'; @override String get appSettings_notifications => 'Notifiche'; @@ -2721,4 +2721,52 @@ class AppLocalizationsIt extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Traccia percorso verso $name'; } + + @override + String get contacts_clipboardEmpty => 'La clipboard è vuota.'; + + @override + String get contacts_invalidAdvertFormat => 'Dati di contatto non validi'; + + @override + String get contacts_contactImported => 'Il contatto è stato importato.'; + + @override + String get contacts_contactImportFailed => + 'Contatto non importato con successo.'; + + @override + String get contacts_zeroHopAdvert => 'Annuncio Zero Hop'; + + @override + String get contacts_floodAdvert => 'Annuncio alluvionale'; + + @override + String get contacts_copyAdvertToClipboard => 'Copia Annuncio negli Appunti'; + + @override + String get contacts_addContactFromClipboard => + 'Aggiungere contatto dalla clipboard'; + + @override + String get contacts_ShareContact => 'Copia contatto negli Appunti'; + + @override + String get contacts_ShareContactZeroHop => + 'Condividi contatto tramite annuncio'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Inviato contatto tramite annuncio.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Invio del contatto non riuscito.'; + + @override + String get contacts_contactAdvertCopied => 'Annuncio copiato negli Appunti.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Copia dell\'annuncio nella Clipboard non riuscita.'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index b55dc41..292181f 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -445,10 +445,10 @@ class AppLocalizationsNl extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russisch'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Oekraïens'; @override String get appSettings_notifications => 'Notificaties'; @@ -2710,4 +2710,52 @@ class AppLocalizationsNl extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Trace route to $name'; } + + @override + String get contacts_clipboardEmpty => 'Knipbord is leeg.'; + + @override + String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens'; + + @override + String get contacts_contactImported => 'Contact is geïmporteerd.'; + + @override + String get contacts_contactImportFailed => + 'Contact kon niet geïmporteerd worden.'; + + @override + String get contacts_zeroHopAdvert => 'Zero Hop Reclame'; + + @override + String get contacts_floodAdvert => 'Overstromingsadvertentie'; + + @override + String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren'; + + @override + String get contacts_addContactFromClipboard => + 'Contact uit klembord toevoegen'; + + @override + String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren'; + + @override + String get contacts_ShareContactZeroHop => 'Contact delen via advertentie'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Contact verzonden via advertentie'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Mislukt om contact te verzenden'; + + @override + String get contacts_contactAdvertCopied => + 'Reclame gekopieerd naar Klembord.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiëren van advertentie naar Clipboard is mislukt.'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 0f7a704..0832329 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -449,10 +449,10 @@ class AppLocalizationsPl extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Rosyjski'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukraińska'; @override String get appSettings_notifications => 'Powiadomienia'; @@ -2719,4 +2719,51 @@ class AppLocalizationsPl extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Śledź trasę do $name'; } + + @override + String get contacts_clipboardEmpty => 'Schowek jest pusty.'; + + @override + String get contacts_invalidAdvertFormat => 'Nieprawidłowe dane kontaktowe'; + + @override + String get contacts_contactImported => 'Kontakt został zaimportowany.'; + + @override + String get contacts_contactImportFailed => + 'Kontakt nie został zaimportowany.'; + + @override + String get contacts_zeroHopAdvert => 'Reklama Zero Hop'; + + @override + String get contacts_floodAdvert => 'Reklama powodziowa'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka'; + + @override + String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka'; + + @override + String get contacts_ShareContact => 'Kopiuj kontakt do schowka'; + + @override + String get contacts_ShareContactZeroHop => + 'Udostępnij kontakt przez ogłoszenie'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Wysłano kontakt przez ogłoszenie.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Nie udało się wysłać kontaktu.'; + + @override + String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiowanie ogłoszenia do schowka nie powiodło się.'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5c25276..eadea3b 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -449,10 +449,10 @@ class AppLocalizationsPt extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Russo'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ucraniano'; @override String get appSettings_notifications => 'Notificações'; @@ -2721,4 +2721,51 @@ class AppLocalizationsPt extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Rastrear rota para $name'; } + + @override + String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.'; + + @override + String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos'; + + @override + String get contacts_contactImported => 'Contato foi importado.'; + + @override + String get contacts_contactImportFailed => 'Contato falhou ao ser importado.'; + + @override + String get contacts_zeroHopAdvert => 'Anúncio Zero Hop'; + + @override + String get contacts_floodAdvert => 'Anúncio de Inundação'; + + @override + String get contacts_copyAdvertToClipboard => + 'Copiar Anúncio para Área de Transferência'; + + @override + String get contacts_addContactFromClipboard => + 'Adicionar Contato da Área de Transferência'; + + @override + String get contacts_ShareContact => + 'Copiar contato para Área de Transferência'; + + @override + String get contacts_ShareContactZeroHop => 'Compartilhar contato por anúncio'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Enviou contato por anúncio.'; + + @override + String get contacts_zeroHopContactAdvertFailed => 'Falha ao enviar contato.'; + + @override + String get contacts_contactAdvertCopied => + 'Anúncio copiado para a Área de Transferência.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Cópia do anúncio para a Área de Transferência falhou.'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index a944fab..ec0f1ba 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2723,4 +2723,54 @@ class AppLocalizationsRu extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Показать маршрут к $name'; } + + @override + String get contacts_clipboardEmpty => 'Буфер обмена пуст.'; + + @override + String get contacts_invalidAdvertFormat => + 'Недействительные контактные данные'; + + @override + String get contacts_contactImported => 'Контакт был импортирован'; + + @override + String get contacts_contactImportFailed => 'Контакт не удалось импортировать'; + + @override + String get contacts_zeroHopAdvert => 'Реклама Zero Hop'; + + @override + String get contacts_floodAdvert => 'Рекламный поток'; + + @override + String get contacts_copyAdvertToClipboard => + 'Копировать рекламу в буфер обмена'; + + @override + String get contacts_addContactFromClipboard => + 'Добавить контакт из буфера обмена'; + + @override + String get contacts_ShareContact => 'Копировать контакт в буфер обмена'; + + @override + String get contacts_ShareContactZeroHop => + 'Поделиться контактом по объявлению'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Отправлено сообщение по объявлению.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Не удалось отправить контакт.'; + + @override + String get contacts_contactAdvertCopied => + 'Реклама скопирована в буфер обмена.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Копирование рекламы в буфер обмена не удалось.'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 02f2b62..346047b 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -445,10 +445,10 @@ class AppLocalizationsSk extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ruština'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrajinská'; @override String get appSettings_notifications => 'Upozornenia'; @@ -2706,4 +2706,50 @@ class AppLocalizationsSk extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Sledovať trasu k $name'; } + + @override + String get contacts_clipboardEmpty => 'Schránka je prázdna.'; + + @override + String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje'; + + @override + String get contacts_contactImported => 'Kontakt bol importovaný.'; + + @override + String get contacts_contactImportFailed => + 'Kontakt sa nepodarilo importovať.'; + + @override + String get contacts_zeroHopAdvert => 'Inzerát Zero Hop'; + + @override + String get contacts_floodAdvert => 'Inzerát povodní'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopírovať reklamu do schránky'; + + @override + String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky'; + + @override + String get contacts_ShareContact => 'Kopírovať kontakt do schránky'; + + @override + String get contacts_ShareContactZeroHop => 'Zdieľať kontakt cez inzerát'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Poslal kontakt cez inzerát.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Zlyhalo odoslanie kontaktu.'; + + @override + String get contacts_contactAdvertCopied => + 'Inzerát bol skopírovaný do schránky.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopírovanie inzerátu do schránky zlyhalo.'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 21d7b6f..ed71122 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -444,10 +444,10 @@ class AppLocalizationsSl extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ruščina'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrajinsko'; @override String get appSettings_notifications => 'Obvestila'; @@ -2709,4 +2709,49 @@ class AppLocalizationsSl extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Trace route to $name'; } + + @override + String get contacts_clipboardEmpty => 'Odložišče je prazno.'; + + @override + String get contacts_invalidAdvertFormat => 'Neveljavni kontaktne podatke'; + + @override + String get contacts_contactImported => 'Kontakt je bil uvožen.'; + + @override + String get contacts_contactImportFailed => 'Kontakt ni bil uspešno uvožen.'; + + @override + String get contacts_zeroHopAdvert => 'Reklama brez posrednikov'; + + @override + String get contacts_floodAdvert => 'Poplavna oglás'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopiraj oglas v odložišče'; + + @override + String get contacts_addContactFromClipboard => 'Dodaj stik iz odložišča'; + + @override + String get contacts_ShareContact => 'Kopiraj stik v Odložišče'; + + @override + String get contacts_ShareContactZeroHop => 'Deliti kontakt prek oglasa'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Poslano po oglasu.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Pošiljanje kontakta ni uspelo.'; + + @override + String get contacts_contactAdvertCopied => + 'Oglas je bil kopiran v odložišče.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiranje oglasa v odložišče je spodletelo.'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index a96d7dc..97b849f 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -442,10 +442,10 @@ class AppLocalizationsSv extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Ryska'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => 'Ukrainska'; @override String get appSettings_notifications => 'Meddelanden'; @@ -2694,4 +2694,49 @@ class AppLocalizationsSv extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Spåra rutt till $name'; } + + @override + String get contacts_clipboardEmpty => 'Urklipp är tomt.'; + + @override + String get contacts_invalidAdvertFormat => 'Ogiltiga kontaktuppgifter'; + + @override + String get contacts_contactImported => 'Kontakt har importerats.'; + + @override + String get contacts_contactImportFailed => 'Kontakt kunde inte importeras.'; + + @override + String get contacts_zeroHopAdvert => 'Reklam med nollhopp'; + + @override + String get contacts_floodAdvert => 'Översvämningsannons'; + + @override + String get contacts_copyAdvertToClipboard => 'Kopiera annons till urklipp'; + + @override + String get contacts_addContactFromClipboard => + 'Lägg till kontakt från urklipp'; + + @override + String get contacts_ShareContact => 'Kopiera kontakt till Urklipp'; + + @override + String get contacts_ShareContactZeroHop => 'Dela kontakt via annons'; + + @override + String get contacts_zeroHopContactAdvertSent => 'Skickat kontakt via annons.'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Misslyckades med att skicka kontakt.'; + + @override + String get contacts_contactAdvertCopied => 'Annons kopierad till Urklipp.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Kopiering av annons till Urklipp misslyckades.'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 6107c5b..899d540 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -447,7 +447,7 @@ class AppLocalizationsUk extends AppLocalizations { String get appSettings_languageBg => 'Български'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => 'Російська'; @override String get appSettings_languageUk => 'Українська'; @@ -2730,4 +2730,53 @@ class AppLocalizationsUk extends AppLocalizations { String contacts_pathTraceTo(String name) { return 'Відстежити маршрут до $name'; } + + @override + String get contacts_clipboardEmpty => 'Буфер обміну порожній'; + + @override + String get contacts_invalidAdvertFormat => 'Недійсні контактні дані'; + + @override + String get contacts_contactImported => 'Контакт було імпортовано.'; + + @override + String get contacts_contactImportFailed => 'Контакт не вдалося імпортувати'; + + @override + String get contacts_zeroHopAdvert => 'Реклама без перехоплення'; + + @override + String get contacts_floodAdvert => 'Залив реклами'; + + @override + String get contacts_copyAdvertToClipboard => + 'Копіювати оголошення в буфер обміну'; + + @override + String get contacts_addContactFromClipboard => + 'Додати контакт з буфера обміну'; + + @override + String get contacts_ShareContact => 'Копіювати контакт у буфер обміну'; + + @override + String get contacts_ShareContactZeroHop => + 'Поділитися контактом за оголошенням'; + + @override + String get contacts_zeroHopContactAdvertSent => + 'Відправлено контакт за оголошенням'; + + @override + String get contacts_zeroHopContactAdvertFailed => + 'Не вдалося надіслати контакт.'; + + @override + String get contacts_contactAdvertCopied => + 'Рекламу скопійовано до буфера обміну.'; + + @override + String get contacts_contactAdvertCopyFailed => + 'Копіювання оголошення в буфер обміну завершилося невдало'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index c10a745..7746792 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -12,7 +12,7 @@ class AppLocalizationsZh extends AppLocalizations { String get appTitle => 'MeshCore Open'; @override - String get nav_contacts => '联系人'; + String get nav_contacts => '联系方式'; @override String get nav_channels => '频道'; @@ -54,13 +54,13 @@ class AppLocalizationsZh extends AppLocalizations { String get common_disconnect => '断开'; @override - String get common_connected => '已连接'; + String get common_connected => '连接'; @override String get common_disconnected => '断开'; @override - String get common_create => '创建'; + String get common_create => '创造'; @override String get common_continue => '继续'; @@ -78,7 +78,7 @@ class AppLocalizationsZh extends AppLocalizations { String get common_hide => '隐藏'; @override - String get common_remove => '删除'; + String get common_remove => '移除'; @override String get common_enable => '启用'; @@ -87,7 +87,7 @@ class AppLocalizationsZh extends AppLocalizations { String get common_disable => '禁用'; @override - String get common_reboot => '重启'; + String get common_reboot => '重新启动'; @override String get common_loading => '正在加载...'; @@ -106,34 +106,34 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get scanner_title => 'MeshCore Open'; + String get scanner_title => 'MeshCore 开放'; @override - String get scanner_scanning => '扫描设备…'; + String get scanner_scanning => '正在搜索设备...'; @override - String get scanner_connecting => '连接中...'; + String get scanner_connecting => '正在连接...'; @override - String get scanner_disconnecting => '断开中...'; + String get scanner_disconnecting => '断开连接...'; @override String get scanner_notConnected => '未连接'; @override String scanner_connectedTo(String deviceName) { - return '已连接至 $deviceName'; + return '已连接到 $deviceName'; } @override - String get scanner_searchingDevices => '搜索 MeshCore 设备...'; + String get scanner_searchingDevices => '正在搜索 MeshCore 设备...'; @override - String get scanner_tapToScan => '点击扫描以查找MeshCore设备'; + String get scanner_tapToScan => '点击“扫描”功能,以查找 MeshCore 设备。'; @override String scanner_connectionFailed(String error) { - return '连接失败:$error'; + return 'Connection failed: $error'; } @override @@ -146,7 +146,7 @@ class AppLocalizationsZh extends AppLocalizations { String get device_quickSwitch => '快速切换'; @override - String get device_meshcore => 'MeshCore'; + String get device_meshcore => '网格核心'; @override String get settings_title => '设置'; @@ -176,40 +176,40 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_nodeNameUpdated => '姓名已更新'; @override - String get settings_radioSettings => '无线设置'; + String get settings_radioSettings => '收音机设置'; @override - String get settings_radioSettingsSubtitle => '频率,功率,扩展因子'; + String get settings_radioSettingsSubtitle => '频率、功率、扩频因子'; @override - String get settings_radioSettingsUpdated => '射频设置已更新'; + String get settings_radioSettingsUpdated => '收音机设置已更新'; @override - String get settings_location => '位置'; + String get settings_location => '地点'; @override - String get settings_locationSubtitle => 'GPS坐标'; + String get settings_locationSubtitle => 'GPS 坐标'; @override - String get settings_locationUpdated => '位置已更新'; + String get settings_locationUpdated => '位置和 GPS 设置已更新'; @override - String get settings_locationBothRequired => '请输入纬度和经度。'; + String get settings_locationBothRequired => '请输入经度和纬度。'; @override - String get settings_locationInvalid => '无效的纬度或经度。'; + String get settings_locationInvalid => '无效的经度和纬度。'; @override - String get settings_locationGPSEnable => '启用GPS'; + String get settings_locationGPSEnable => '开启 GPS 功能'; @override - String get settings_locationGPSEnableSubtitle => '启用GPS自动更新位置。'; + String get settings_locationGPSEnableSubtitle => '使 GPS 能够自动更新位置。'; @override - String get settings_locationIntervalSec => 'GPS 间隔(秒)'; + String get settings_locationIntervalSec => 'GPS 间隔时间(秒)'; @override - String get settings_locationIntervalInvalid => '时间间隔必须至少为60秒,且小于86400秒。'; + String get settings_locationIntervalInvalid => '间隔时间必须至少为 60 秒,但不超过 86400 秒。'; @override String get settings_latitude => '纬度'; @@ -221,34 +221,34 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_privacyMode => '隐私模式'; @override - String get settings_privacyModeSubtitle => '隐藏在广告中的姓名/位置'; + String get settings_privacyModeSubtitle => '在广告中隐藏姓名/位置'; @override - String get settings_privacyModeToggle => '开启隐私模式以隐藏您的姓名和位置在广告中的显示。'; + String get settings_privacyModeToggle => '切换隐私模式,以隐藏您的姓名和位置,从而在广告中保护您的个人信息。'; @override String get settings_privacyModeEnabled => '隐私模式已启用'; @override - String get settings_privacyModeDisabled => '隐私模式已禁用'; + String get settings_privacyModeDisabled => '隐私模式已关闭'; @override - String get settings_actions => '操作'; + String get settings_actions => '行动'; @override - String get settings_sendAdvertisement => '发送广告'; + String get settings_sendAdvertisement => '发布广告'; @override - String get settings_sendAdvertisementSubtitle => '现在已广播'; + String get settings_sendAdvertisementSubtitle => '现已开始进行广播节目'; @override - String get settings_advertisementSent => '广告已发送'; + String get settings_advertisementSent => '已发送广告'; @override String get settings_syncTime => '同步时间'; @override - String get settings_syncTimeSubtitle => '将设备时钟设置为手机时间'; + String get settings_syncTimeSubtitle => '将设备时钟设置为与手机时间一致'; @override String get settings_timeSynchronized => '时间同步'; @@ -257,31 +257,31 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_refreshContacts => '刷新联系人'; @override - String get settings_refreshContactsSubtitle => '从设备重新加载联系人列表'; + String get settings_refreshContactsSubtitle => '从设备中重新加载联系人列表'; @override String get settings_rebootDevice => '重启设备'; @override - String get settings_rebootDeviceSubtitle => '重启 MeshCore 设备'; + String get settings_rebootDeviceSubtitle => '重新启动 MeshCore 设备'; @override - String get settings_rebootDeviceConfirm => '您确定要重启设备吗?您将会断开连接。'; + String get settings_rebootDeviceConfirm => '您确定要重启设备吗?这将导致您与设备断开连接。'; @override String get settings_debug => '调试'; @override - String get settings_bleDebugLog => '蓝牙调试日志'; + String get settings_bleDebugLog => 'BLE 调试日志'; @override - String get settings_bleDebugLogSubtitle => '蓝牙命令、响应和原始数据'; + String get settings_bleDebugLogSubtitle => 'BLE 命令、响应和原始数据'; @override - String get settings_appDebugLog => '应用调试日志'; + String get settings_appDebugLog => '应用程序调试日志'; @override - String get settings_appDebugLogSubtitle => '应用调试消息'; + String get settings_appDebugLogSubtitle => '应用程序调试消息'; @override String get settings_about => '关于'; @@ -292,11 +292,11 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get settings_aboutLegalese => '2024 MeshCore 开放源代码项目'; + String get settings_aboutLegalese => '2026 MeshCore 开源项目'; @override String get settings_aboutDescription => - '一个开源的 Flutter 客户端,用于 MeshCore LoRa 网状网络设备。'; + '一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。'; @override String get settings_infoName => '姓名'; @@ -317,19 +317,19 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_infoContactsCount => '联系人数量'; @override - String get settings_infoChannelCount => '频道数量'; + String get settings_infoChannelCount => '通道数量'; @override String get settings_presets => '预设'; @override - String get settings_preset915Mhz => '915 MHz'; + String get settings_preset915Mhz => '915 兆赫'; @override - String get settings_preset868Mhz => '868 MHz'; + String get settings_preset868Mhz => '868 兆赫'; @override - String get settings_preset433Mhz => '433 MHz'; + String get settings_preset433Mhz => '433 兆赫'; @override String get settings_frequency => '频率 (MHz)'; @@ -338,35 +338,35 @@ class AppLocalizationsZh extends AppLocalizations { String get settings_frequencyHelper => '300.0 - 2500.0'; @override - String get settings_frequencyInvalid => '无效频率 (300-2500 MHz)'; + String get settings_frequencyInvalid => '无效频率(300-2500 MHz)'; @override String get settings_bandwidth => '带宽'; @override - String get settings_spreadingFactor => '扩散因子'; + String get settings_spreadingFactor => '传播系数'; @override String get settings_codingRate => '编码速率'; @override - String get settings_txPower => 'TX Power (dBm)'; + String get settings_txPower => 'TX 功率(dBm)'; @override String get settings_txPowerHelper => '0 - 22'; @override - String get settings_txPowerInvalid => '无效的 TX 电功率 (0-22 dBm)'; + String get settings_txPowerInvalid => '无效的发射功率(0-22 dBm)'; @override String get settings_longRange => '远距离'; @override - String get settings_fastSpeed => '快速速度'; + String get settings_fastSpeed => '高速'; @override String settings_error(String message) { - return '错误:$message'; + return '[保存:$message]\n错误:$message'; } @override @@ -379,64 +379,64 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_theme => '主题'; @override - String get appSettings_themeSystem => '系统默认'; + String get appSettings_themeSystem => '系统默认设置'; @override String get appSettings_themeLight => '光'; @override - String get appSettings_themeDark => '深色'; + String get appSettings_themeDark => '黑暗'; @override String get appSettings_language => '语言'; @override - String get appSettings_languageSystem => '系统默认'; + String get appSettings_languageSystem => '系统默认设置'; @override - String get appSettings_languageEn => 'English'; + String get appSettings_languageEn => '英语'; @override - String get appSettings_languageFr => 'Français'; + String get appSettings_languageFr => '法语'; @override - String get appSettings_languageEs => 'Español'; + String get appSettings_languageEs => '西班牙语'; @override - String get appSettings_languageDe => 'Deutsch'; + String get appSettings_languageDe => '德语'; @override - String get appSettings_languagePl => 'Polski'; + String get appSettings_languagePl => '波兰语'; @override - String get appSettings_languageSl => 'Slovenščina'; + String get appSettings_languageSl => '斯洛文语'; @override - String get appSettings_languagePt => 'Português'; + String get appSettings_languagePt => '葡萄牙语'; @override - String get appSettings_languageIt => 'Italiano'; + String get appSettings_languageIt => '意大利语'; @override String get appSettings_languageZh => '中文'; @override - String get appSettings_languageSv => 'Svenska'; + String get appSettings_languageSv => '瑞典语'; @override - String get appSettings_languageNl => 'Nederlands'; + String get appSettings_languageNl => '荷兰语'; @override - String get appSettings_languageSk => 'Slovenčina'; + String get appSettings_languageSk => '斯洛伐克语'; @override - String get appSettings_languageBg => 'Български'; + String get appSettings_languageBg => '保加利亚'; @override - String get appSettings_languageRu => 'Русский'; + String get appSettings_languageRu => '俄语'; @override - String get appSettings_languageUk => 'Українська'; + String get appSettings_languageUk => '乌克兰'; @override String get appSettings_notifications => '通知'; @@ -448,7 +448,7 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_enableNotificationsSubtitle => '接收消息和广告的通知'; @override - String get appSettings_notificationPermissionDenied => '通知权限被拒绝'; + String get appSettings_notificationPermissionDenied => '权限被拒绝'; @override String get appSettings_notificationsEnabled => '通知已启用'; @@ -460,40 +460,41 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_messageNotifications => '消息通知'; @override - String get appSettings_messageNotificationsSubtitle => '显示收到新消息时的通知'; + String get appSettings_messageNotificationsSubtitle => '在收到新消息时显示通知'; @override String get appSettings_channelMessageNotifications => '频道消息通知'; @override - String get appSettings_channelMessageNotificationsSubtitle => '显示接收频道消息时的通知'; + String get appSettings_channelMessageNotificationsSubtitle => + '在收到频道消息时,显示通知。'; @override String get appSettings_advertisementNotifications => '广告通知'; @override - String get appSettings_advertisementNotificationsSubtitle => '显示当新节点被发现时通知'; + String get appSettings_advertisementNotificationsSubtitle => '在发现新的节点时,显示通知。'; @override - String get appSettings_messaging => '消息'; + String get appSettings_messaging => '信息传递'; @override - String get appSettings_clearPathOnMaxRetry => '清除最大重试路径'; + String get appSettings_clearPathOnMaxRetry => '关于“最大重试”的清晰说明'; @override - String get appSettings_clearPathOnMaxRetrySubtitle => '重置联系人路径,在5次发送失败尝试后'; + String get appSettings_clearPathOnMaxRetrySubtitle => '在尝试发送失败后 5 次,重置联系路径。'; @override - String get appSettings_pathsWillBeCleared => '路径将在5次失败重试后清除'; + String get appSettings_pathsWillBeCleared => '如果尝试 5 次后仍然失败,则将重新规划路径。'; @override - String get appSettings_pathsWillNotBeCleared => '路径不会自动清理'; + String get appSettings_pathsWillNotBeCleared => '路径不会自动清除。'; @override - String get appSettings_autoRouteRotation => '自动路径旋转'; + String get appSettings_autoRouteRotation => '自动路径轮换'; @override - String get appSettings_autoRouteRotationSubtitle => '在最佳路径和洪水模式之间切换'; + String get appSettings_autoRouteRotationSubtitle => '在最佳路径和防洪模式之间切换'; @override String get appSettings_autoRouteRotationEnabled => '自动路径轮换已启用'; @@ -509,26 +510,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String appSettings_batteryChemistryPerDevice(String deviceName) { - return '设置每个设备 ($deviceName)'; + return '为每个设备设置 ($deviceName)'; } @override - String get appSettings_batteryChemistryConnectFirst => '连接设备以选择'; + String get appSettings_batteryChemistryConnectFirst => '连接到设备以进行选择'; @override - String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)'; + String get appSettings_batteryNmc => '18650 型号,NMC 电池(3.0-4.2V)'; @override String get appSettings_batteryLifepo4 => '磷酸铁锂 (2.6-3.65V)'; @override - String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; + String get appSettings_batteryLipo => '锂离子电池 (3.0-4.2V)'; @override - String get appSettings_mapDisplay => '地图显示'; + String get appSettings_mapDisplay => '地图展示'; @override - String get appSettings_showRepeaters => '显示循环器'; + String get appSettings_showRepeaters => '显示重复'; @override String get appSettings_showRepeatersSubtitle => '在地图上显示重复节点'; @@ -543,36 +544,36 @@ class AppLocalizationsZh extends AppLocalizations { String get appSettings_showOtherNodes => '显示其他节点'; @override - String get appSettings_showOtherNodesSubtitle => '显示其他节点类型在地图上'; + String get appSettings_showOtherNodesSubtitle => '在地图上显示其他节点类型'; @override - String get appSettings_timeFilter => '时间筛选'; + String get appSettings_timeFilter => '时间过滤器'; @override String get appSettings_timeFilterShowAll => '显示所有节点'; @override String appSettings_timeFilterShowLast(int hours) { - return '显示来自过去 $hours 小时的节点'; + return 'Show nodes from last $hours hours'; } @override String get appSettings_mapTimeFilter => '地图时间筛选'; @override - String get appSettings_showNodesDiscoveredWithin => '显示发现的节点在:'; + String get appSettings_showNodesDiscoveredWithin => '显示在以下范围内发现的节点:'; @override String get appSettings_allTime => '所有时间'; @override - String get appSettings_lastHour => '最后小时'; + String get appSettings_lastHour => '过去一小时'; @override - String get appSettings_last6Hours => '最后6小时'; + String get appSettings_last6Hours => '过去6小时'; @override - String get appSettings_last24Hours => '最后24小时'; + String get appSettings_last24Hours => '过去24小时'; @override String get appSettings_lastWeek => '上周'; @@ -585,38 +586,38 @@ class AppLocalizationsZh extends AppLocalizations { @override String appSettings_areaSelectedZoom(int minZoom, int maxZoom) { - return '选中的区域(缩放至 $minZoom - $maxZoom)'; + return '已选择区域(缩放至 $minZoom - $maxZoom)'; } @override String get appSettings_debugCard => '调试'; @override - String get appSettings_appDebugLogging => '应用调试日志'; + String get appSettings_appDebugLogging => '应用程序调试日志'; @override - String get appSettings_appDebugLoggingSubtitle => '记录应用调试消息以供故障排除'; + String get appSettings_appDebugLoggingSubtitle => '用于故障排除的日志应用程序调试消息'; @override - String get appSettings_appDebugLoggingEnabled => '应用调试日志已启用'; + String get appSettings_appDebugLoggingEnabled => '调试日志已启用'; @override - String get appSettings_appDebugLoggingDisabled => '应用调试日志已禁用'; + String get appSettings_appDebugLoggingDisabled => '应用程序调试日志已禁用'; @override - String get contacts_title => '联系人'; + String get contacts_title => '联系方式'; @override - String get contacts_noContacts => '还没有联系人'; + String get contacts_noContacts => '目前还没有联系人'; @override - String get contacts_contactsWillAppear => '设备会广播时,联系人会显示'; + String get contacts_contactsWillAppear => '当设备发布广告时,联系方式会显示。'; @override String get contacts_searchContacts => '搜索联系人...'; @override - String get contacts_noUnreadContacts => '未读联系人'; + String get contacts_noUnreadContacts => '没有未读通讯'; @override String get contacts_noContactsFound => '未找到任何联系人或群组'; @@ -626,26 +627,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String contacts_removeConfirm(String contactName) { - return '从联系人中删除 $contactName 吗?'; + return 'Remove $contactName from contacts?'; } @override - String get contacts_manageRepeater => '管理重复项'; + String get contacts_manageRepeater => '管理重复器'; @override String get contacts_manageRoom => '管理房间服务器'; @override - String get contacts_roomLogin => '房间登录'; + String get contacts_roomLogin => '服务器登录'; @override - String get contacts_openChat => '打开聊天'; + String get contacts_openChat => '开放聊天'; @override - String get contacts_editGroup => '编辑组'; + String get contacts_editGroup => '编辑小组'; @override - String get contacts_deleteGroup => '删除分组'; + String get contacts_deleteGroup => '删除群组'; @override String contacts_deleteGroupConfirm(String groupName) { @@ -653,50 +654,50 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get contacts_newGroup => '新组'; + String get contacts_newGroup => '新的团体'; @override - String get contacts_groupName => '组名'; + String get contacts_groupName => '团体名称'; @override - String get contacts_groupNameRequired => '组名不能为空'; + String get contacts_groupNameRequired => '需要提供组名称'; @override String contacts_groupAlreadyExists(String name) { - return '组“$name”已存在'; + return '名为\"$name\"的组已经存在'; } @override String get contacts_filterContacts => '筛选联系人...'; @override - String get contacts_noContactsMatchFilter => '未找到匹配您的筛选条件的结果'; + String get contacts_noContactsMatchFilter => '未找到符合您筛选条件的联系人'; @override String get contacts_noMembers => '没有会员'; @override - String get contacts_lastSeenNow => '最后一次登录时间现在'; + String get contacts_lastSeenNow => '最后一次被看到的时间'; @override String contacts_lastSeenMinsAgo(int minutes) { - return '最后一次出现 $minutes 分前'; + return 'Last seen $minutes mins ago'; } @override - String get contacts_lastSeenHourAgo => '最后一次出现前1小时'; + String get contacts_lastSeenHourAgo => '最后一次被看到的时间:1小时前'; @override String contacts_lastSeenHoursAgo(int hours) { - return '最后一次出现 $hours 小时前'; + return 'Last seen $hours hours ago'; } @override - String get contacts_lastSeenDayAgo => '最后一次登录前一天'; + String get contacts_lastSeenDayAgo => '最后一次被看到的时间是1天前'; @override String contacts_lastSeenDaysAgo(int days) { - return '最后一次出现 $days 天前'; + return 'Last seen $days days ago'; } @override @@ -706,7 +707,7 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_noChannelsConfigured => '未配置任何频道'; @override - String get channels_addPublicChannel => '添加公开频道'; + String get channels_addPublicChannel => '添加公共频道'; @override String get channels_searchChannels => '搜索频道...'; @@ -720,19 +721,19 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channels_hashtagChannel => '话题频道'; + String get channels_hashtagChannel => '话题标签频道'; @override - String get channels_public => '公开'; + String get channels_public => '公众'; @override - String get channels_private => '私有'; + String get channels_private => '私人'; @override - String get channels_publicChannel => '公开频道'; + String get channels_publicChannel => '公共频道'; @override - String get channels_privateChannel => '私聊频道'; + String get channels_privateChannel => '私密频道'; @override String get channels_editChannel => '编辑频道'; @@ -742,12 +743,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String channels_deleteChannelConfirm(String name) { - return '删除\"$name\"?此操作无法撤销。'; + return 'Delete \"$name\"? This cannot be undone.'; } @override String channels_channelDeleted(String name) { - return '频道“$name”已删除'; + return '删除频道 \"$name\"'; } @override @@ -763,23 +764,23 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_usePublicChannel => '使用公共频道'; @override - String get channels_standardPublicPsk => '标准公钥共享密钥'; + String get channels_standardPublicPsk => '标准公共PSK'; @override String get channels_pskHex => 'PSK (十六进制)'; @override - String get channels_generateRandomPsk => '生成随机PSK'; + String get channels_generateRandomPsk => '生成随机的PSK(正交相移键控)'; @override - String get channels_enterChannelName => '请输入频道名称'; + String get channels_enterChannelName => '请在此处输入频道名称'; @override - String get channels_pskMustBe32Hex => 'PSK 必须是 32 个十六进制字符'; + String get channels_pskMustBe32Hex => 'PSK 必须包含 32 个十六进制字符。'; @override String channels_channelAdded(String name) { - return '频道“$name”已添加'; + return '添加频道 \"$name\"'; } @override @@ -792,20 +793,20 @@ class AppLocalizationsZh extends AppLocalizations { @override String channels_channelUpdated(String name) { - return '频道“$name”已更新'; + return '频道 \"$name\" 已更新'; } @override - String get channels_publicChannelAdded => '公共频道已添加'; + String get channels_publicChannelAdded => '已添加公共频道'; @override - String get channels_sortBy => '按类型排序'; + String get channels_sortBy => '按排序'; @override - String get channels_sortManual => '手动'; + String get channels_sortManual => '手册'; @override - String get channels_sortAZ => 'A-Z'; + String get channels_sortAZ => 'A 到 Z'; @override String get channels_sortLatestMessages => '最新消息'; @@ -814,10 +815,10 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_sortUnread => '未读'; @override - String get channels_createPrivateChannel => '创建私聊频道'; + String get channels_createPrivateChannel => '创建私密频道'; @override - String get channels_createPrivateChannelDesc => '使用密钥保护。'; + String get channels_createPrivateChannelDesc => '使用秘密密钥进行保护。'; @override String get channels_joinPrivateChannel => '加入私密频道'; @@ -832,48 +833,48 @@ class AppLocalizationsZh extends AppLocalizations { String get channels_joinPublicChannelDesc => '任何人都可以加入这个频道。'; @override - String get channels_joinHashtagChannel => '加入标签频道'; + String get channels_joinHashtagChannel => '加入一个带有特定标签的频道'; @override - String get channels_joinHashtagChannelDesc => '任何人都可以加入话题频道。'; + String get channels_joinHashtagChannelDesc => '任何人都可以加入带有特定标签的频道。'; @override String get channels_scanQrCode => '扫描二维码'; @override - String get channels_scanQrCodeComingSoon => '即将到来'; + String get channels_scanQrCodeComingSoon => '即将发布'; @override String get channels_enterHashtag => '输入标签'; @override - String get channels_hashtagHint => '例如 #团队'; + String get channels_hashtagHint => '例如:#团队'; @override - String get chat_noMessages => '目前还没有消息'; + String get chat_noMessages => '目前还没有收到任何消息。'; @override - String get chat_sendMessageToStart => '发送消息开始'; + String get chat_sendMessageToStart => '发送消息以开始'; @override - String get chat_originalMessageNotFound => '找不到原始消息'; + String get chat_originalMessageNotFound => '无法找到原始消息'; @override String chat_replyingTo(String name) { - return '回复 $name'; + return 'Replying to $name'; } @override String chat_replyTo(String name) { - return '回复 $name'; + return 'Reply to $name'; } @override - String get chat_location => '位置'; + String get chat_location => '地点'; @override String chat_sendMessageTo(String contactName) { - return '向$contactName发送消息'; + return 'Send a message to $contactName'; } @override @@ -881,7 +882,7 @@ class AppLocalizationsZh extends AppLocalizations { @override String chat_messageTooLong(int maxBytes) { - return '消息太长了(最大 $maxBytes 字节)。'; + return '消息内容过长(最大 $maxBytes 字节)。'; } @override @@ -891,21 +892,21 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_messageDeleted => '消息已删除'; @override - String get chat_retryingMessage => '重试'; + String get chat_retryingMessage => '重试消息'; @override String chat_retryCount(int current, int max) { - return '重试 $current/$max'; + return 'Retry $current/$max'; } @override - String get chat_sendGif => '发送GIF'; + String get chat_sendGif => '发送 GIF 动画'; @override String get chat_reply => '回复'; @override - String get chat_addReaction => '添加反应'; + String get chat_addReaction => '添加评论'; @override String get chat_me => '我'; @@ -917,16 +918,16 @@ class AppLocalizationsZh extends AppLocalizations { String get emojiCategoryGestures => '手势'; @override - String get emojiCategoryHearts => '心'; + String get emojiCategoryHearts => '心脏'; @override - String get emojiCategoryObjects => '对象'; + String get emojiCategoryObjects => '物体'; @override - String get gifPicker_title => '选择一个 GIF'; + String get gifPicker_title => '选择一个 GIF 动画'; @override - String get gifPicker_searchHint => '搜索GIF...'; + String get gifPicker_searchHint => '搜索 GIF 动画...'; @override String get gifPicker_poweredBy => '由 GIPHY 提供支持'; @@ -935,46 +936,46 @@ class AppLocalizationsZh extends AppLocalizations { String get gifPicker_noGifsFound => '未找到 GIF 动画'; @override - String get gifPicker_failedLoad => 'GIF 加载失败'; + String get gifPicker_failedLoad => '无法加载 GIF 动画'; @override - String get gifPicker_failedSearch => '搜索GIF失败'; + String get gifPicker_failedSearch => '未能搜索 GIF 动画'; @override - String get gifPicker_noInternet => '无网络连接'; + String get gifPicker_noInternet => '没有互联网连接'; @override - String get debugLog_appTitle => '应用调试日志'; + String get debugLog_appTitle => '应用程序调试日志'; @override - String get debugLog_bleTitle => '蓝牙调试日志'; + String get debugLog_bleTitle => 'BLE 调试日志'; @override String get debugLog_copyLog => '复制日志'; @override - String get debugLog_clearLog => '清除日志'; + String get debugLog_clearLog => '清晰的日志'; @override String get debugLog_copied => '调试日志已复制'; @override - String get debugLog_bleCopied => '蓝牙日志复制'; + String get debugLog_bleCopied => 'BLE 日志已复制'; @override - String get debugLog_noEntries => '尚未生成调试日志'; + String get debugLog_noEntries => '目前还没有调试日志'; @override - String get debugLog_enableInSettings => '启用应用调试日志记录设置'; + String get debugLog_enableInSettings => '在设置中启用应用程序调试日志功能。'; @override - String get debugLog_frames => '帧'; + String get debugLog_frames => '框架'; @override String get debugLog_rawLogRx => '原始日志-RX'; @override - String get debugLog_noBleActivity => '目前还没有蓝牙活动。'; + String get debugLog_noBleActivity => '目前尚未有蓝牙低功耗(BLE)活动。'; @override String debugFrame_length(int count) { @@ -987,16 +988,16 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get debugFrame_textMessageHeader => '短信框'; + String get debugFrame_textMessageHeader => '短信模板:'; @override String debugFrame_destinationPubKey(String pubKey) { - return '- 目的地公钥:$pubKey'; + return '- 目标公钥:$pubKey'; } @override String debugFrame_timestamp(int timestamp) { - return '- 时间戳:$timestamp'; + return '- Timestamp: $timestamp'; } @override @@ -1006,22 +1007,22 @@ class AppLocalizationsZh extends AppLocalizations { @override String debugFrame_textType(int type, String label) { - return '- 文本类型:$type ($label)'; + return '- Text Type: $type ($label)'; } @override - String get debugFrame_textTypeCli => 'CLI'; + String get debugFrame_textTypeCli => '命令行界面'; @override - String get debugFrame_textTypePlain => '简洁'; + String get debugFrame_textTypePlain => '简单'; @override String debugFrame_text(String text) { - return '- 文本:\"$text\"'; + return '- 文本:“$text”'; } @override - String get debugFrame_hexDump => '十六进制数据'; + String get debugFrame_hexDump => '十六进制数据:'; @override String get chat_pathManagement => '路径管理'; @@ -1030,30 +1031,30 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_routingMode => '路由模式'; @override - String get chat_autoUseSavedPath => '自动(使用已保存路径)'; + String get chat_autoUseSavedPath => '自动(使用已保存的路径)'; @override String get chat_forceFloodMode => '强制洪水模式'; @override - String get chat_recentAckPaths => '最近的 ACK 路径 (点击以使用):'; + String get chat_recentAckPaths => '最近使用的 ACK 路径(点击使用):'; @override - String get chat_pathHistoryFull => '路径历史已满。删除条目以添加新条目。'; + String get chat_pathHistoryFull => '路径历史已满。删除条目以添加新的条目。'; @override - String get chat_hopSingular => '跳转'; + String get chat_hopSingular => '跳跃'; @override - String get chat_hopPlural => '跳跃'; + String get chat_hopPlural => '啤酒花'; @override String chat_hopsCount(int count) { String _temp0 = intl.Intl.pluralLogic( count, locale: localeName, - other: '跳跃', - one: '跳跃', + other: 'hops', + one: 'hop', ); return '$count $_temp0'; } @@ -1065,7 +1066,7 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_removePath => '删除路径'; @override - String get chat_noPathHistoryYet => '还没有历史记录。\n发送消息以发现路径。'; + String get chat_noPathHistoryYet => '目前还没有历史记录。\n发送消息以查找路径。'; @override String get chat_pathActions => '路径操作:'; @@ -1077,25 +1078,25 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_setCustomPathSubtitle => '手动指定路由路径'; @override - String get chat_clearPath => '清除路径'; + String get chat_clearPath => '明确的道路'; @override - String get chat_clearPathSubtitle => '强制下次发送时重新发现'; + String get chat_clearPathSubtitle => '在下一次发送时,重新尝试。'; @override - String get chat_pathCleared => '路径已清除。下一条消息将重新发现路线。'; + String get chat_pathCleared => '路径已清理。下一条消息将重新确定路线。'; @override - String get chat_floodModeSubtitle => '使用应用栏中的路由切换开关'; + String get chat_floodModeSubtitle => '使用应用程序栏中的路由切换功能'; @override - String get chat_floodModeEnabled => '防洪模式已启用。通过应用程序栏中的路由图标进行反转。'; + String get chat_floodModeEnabled => '防洪模式已启用。通过应用程序栏中的路由图标进行切换。'; @override String get chat_fullPath => '完整路径'; @override - String get chat_pathDetailsNotAvailable => '路径详情尚未获取。请尝试发送消息以刷新。'; + String get chat_pathDetailsNotAvailable => '路径信息尚未提供。请尝试发送消息以刷新。'; @override String chat_pathSetHops(int hopCount, String status) { @@ -1105,20 +1106,20 @@ class AppLocalizationsZh extends AppLocalizations { other: 'hops', one: 'hop', ); - return '路径设置:$hopCount $_temp0 - $status'; + return 'Path set: $hopCount $_temp0 - $status'; } @override - String get chat_pathSavedLocally => '已本地保存。连接以同步。'; + String get chat_pathSavedLocally => '已本地保存。连接以进行同步。'; @override String get chat_pathDeviceConfirmed => '设备已确认。'; @override - String get chat_pathDeviceNotConfirmed => '设备尚未确认。'; + String get chat_pathDeviceNotConfirmed => '该设备尚未得到确认。'; @override - String get chat_type => '输入'; + String get chat_type => '类型'; @override String get chat_path => '路径'; @@ -1130,64 +1131,64 @@ class AppLocalizationsZh extends AppLocalizations { String get chat_compressOutgoingMessages => '压缩发送的消息'; @override - String get chat_floodForced => '强制溢出'; + String get chat_floodForced => '洪水(被迫)'; @override - String get chat_directForced => '强制直接'; + String get chat_directForced => '直接(强制性的)'; @override String chat_hopsForced(int count) { - return '$count 次跳跃 (强制)'; + return '$count 根啤酒花(人工种植)'; } @override - String get chat_floodAuto => '自动防洪'; + String get chat_floodAuto => '自动洪水'; @override String get chat_direct => '直接'; @override - String get chat_poiShared => '共享位置信息'; + String get chat_poiShared => '共享位置'; @override String chat_unread(int count) { - return '未读:$count'; + return 'Unread: $count'; } @override String get chat_openLink => '打开链接?'; @override - String get chat_openLinkConfirmation => '您想在浏览器中打开此链接吗?'; + String get chat_openLinkConfirmation => '您想用浏览器打开这个链接吗?'; @override - String get chat_open => '打开'; + String get chat_open => '开放'; @override String chat_couldNotOpenLink(String url) { - return '无法打开链接:$url'; + return '[保存:$url]\n无法打开链接:$url'; } @override - String get chat_invalidLink => '链接格式无效'; + String get chat_invalidLink => '无效的链接格式'; @override - String get map_title => '节点地图'; + String get map_title => '节点图'; @override - String get map_noNodesWithLocation => '没有具有位置数据的节点'; + String get map_noNodesWithLocation => '没有包含位置信息的节点'; @override - String get map_nodesNeedGps => '节点需要共享它们的 GPS 坐标\n才能在地图上显示'; + String get map_nodesNeedGps => '节点需要共享其 GPS 坐标,以便在地图上显示'; @override String map_nodesCount(int count) { - return '节点:$count'; + return 'Nodes: $count'; } @override String map_pinsCount(int count) { - return '针:$count'; + return 'Pins: $count'; } @override @@ -1203,16 +1204,16 @@ class AppLocalizationsZh extends AppLocalizations { String get map_sensor => '传感器'; @override - String get map_pinDm => '私信 (DM)'; + String get map_pinDm => 'PIN (直接消息)'; @override - String get map_pinPrivate => '私密模式'; + String get map_pinPrivate => '私密'; @override - String get map_pinPublic => '公开(公版)'; + String get map_pinPublic => '公开'; @override - String get map_lastSeen => '最后一次登录'; + String get map_lastSeen => '最后一次被看到'; @override String get map_disconnectConfirm => '您确定要断开与此设备的连接吗?'; @@ -1227,19 +1228,19 @@ class AppLocalizationsZh extends AppLocalizations { String get map_flags => '旗帜'; @override - String get map_shareMarkerHere => '分享标记在此'; + String get map_shareMarkerHere => '在此分享标记'; @override - String get map_pinLabel => '固定标签'; + String get map_pinLabel => '标签'; @override String get map_label => '标签'; @override - String get map_pointOfInterest => '兴趣点'; + String get map_pointOfInterest => '值得参观的地方'; @override - String get map_sendToContact => '发送给联系人'; + String get map_sendToContact => '发送给联系'; @override String get map_sendToChannel => '发送到频道'; @@ -1248,18 +1249,18 @@ class AppLocalizationsZh extends AppLocalizations { String get map_noChannelsAvailable => '没有可用的频道'; @override - String get map_publicLocationShare => '公共位置共享'; + String get map_publicLocationShare => '公共场所共享'; @override String map_publicLocationShareConfirm(String channelLabel) { - return '您即将分享一个位置在 $channelLabel。此频道公开,任何拥有 PSK 的人都可以看到它。'; + return '[保存:$channelLabel]\n您即将分享一个位置,该位置位于 $channelLabel。 此频道是公开的,任何拥有 PSK 的人都可以看到它。'; } @override String get map_connectToShareMarkers => '连接设备以共享标记'; @override - String get map_filterNodes => '筛选节点'; + String get map_filterNodes => '过滤节点'; @override String get map_nodeTypes => '节点类型'; @@ -1274,10 +1275,10 @@ class AppLocalizationsZh extends AppLocalizations { String get map_otherNodes => '其他节点'; @override - String get map_keyPrefix => '键前缀'; + String get map_keyPrefix => '关键前缀'; @override - String get map_filterByKeyPrefix => '按关键词前缀筛选'; + String get map_filterByKeyPrefix => '按关键前缀筛选'; @override String get map_publicKeyPrefix => '公钥前缀'; @@ -1289,32 +1290,32 @@ class AppLocalizationsZh extends AppLocalizations { String get map_showSharedMarkers => '显示共享标记'; @override - String get map_lastSeenTime => '最后一次查看时间'; + String get map_lastSeenTime => '最后一次被看到的时间'; @override - String get map_sharedPin => '共享 PIN'; + String get map_sharedPin => '共享密码'; @override String get map_joinRoom => '加入房间'; @override - String get map_manageRepeater => '管理重复项'; + String get map_manageRepeater => '管理重复器'; @override String get mapCache_title => '离线地图缓存'; @override - String get mapCache_selectAreaFirst => '选择一个区域进行缓存'; + String get mapCache_selectAreaFirst => '选择一个用于缓存的区域'; @override - String get mapCache_noTilesToDownload => '该区域没有可下载的瓦片。'; + String get mapCache_noTilesToDownload => '此区域没有可下载的瓦片。'; @override - String get mapCache_downloadTilesTitle => '下载瓦片'; + String get mapCache_downloadTilesTitle => '下载瓷砖'; @override String mapCache_downloadTilesPrompt(int count) { - return '下载 $count 个瓦片用于离线使用?'; + return '[保存:$count]\n下载 $count 个图片用于离线使用?'; } @override @@ -1322,19 +1323,19 @@ class AppLocalizationsZh extends AppLocalizations { @override String mapCache_cachedTiles(int count) { - return '已缓存 $count 个瓦片'; + return '缓存 $count 个瓦片'; } @override String mapCache_cachedTilesWithFailed(int downloaded, int failed) { - return '已缓存 $downloaded 个瓦片 ($failed 失败)'; + return 'Cached $downloaded tiles ($failed failed)'; } @override String get mapCache_clearOfflineCacheTitle => '清除离线缓存'; @override - String get mapCache_clearOfflineCachePrompt => '删除所有缓存地图瓦片?'; + String get mapCache_clearOfflineCachePrompt => '清除所有缓存的地图瓦片'; @override String get mapCache_offlineCacheCleared => '离线缓存已清除'; @@ -1349,27 +1350,27 @@ class AppLocalizationsZh extends AppLocalizations { String get mapCache_useCurrentView => '使用当前视图'; @override - String get mapCache_zoomRange => '缩放范围'; + String get mapCache_zoomRange => '变焦范围'; @override String mapCache_estimatedTiles(int count) { - return '预计瓦片数量:$count'; + return 'Estimated tiles: $count'; } @override String mapCache_downloadedTiles(int completed, int total) { - return '已下载 $completed / $total'; + return 'Downloaded $completed / $total'; } @override - String get mapCache_downloadTilesButton => '下载瓦片'; + String get mapCache_downloadTilesButton => '下载瓷砖'; @override String get mapCache_clearCacheButton => '清除缓存'; @override String mapCache_failedDownloads(int count) { - return '下载失败:$count'; + return 'Failed downloads: $count'; } @override @@ -1379,7 +1380,7 @@ class AppLocalizationsZh extends AppLocalizations { String east, String west, ) { - return '北 $north, 南 $south, 东 $east, 西 $west'; + return 'N $north, S $south, E $east, W $west'; } @override @@ -1387,17 +1388,17 @@ class AppLocalizationsZh extends AppLocalizations { @override String time_minutesAgo(int minutes) { - return '$minutes分钟前'; + return '${minutes}m ago'; } @override String time_hoursAgo(int hours) { - return '$hours小时前'; + return '${hours}h ago'; } @override String time_daysAgo(int days) { - return '$days 天前'; + return '$days天前'; } @override @@ -1407,16 +1408,16 @@ class AppLocalizationsZh extends AppLocalizations { String get time_hours => '小时'; @override - String get time_day => '今天'; + String get time_day => '一天'; @override String get time_days => '天'; @override - String get time_week => '本周'; + String get time_week => '一周'; @override - String get time_weeks => '几周'; + String get time_weeks => '周'; @override String get time_month => '月份'; @@ -1440,7 +1441,7 @@ class AppLocalizationsZh extends AppLocalizations { String get login_repeaterLogin => '重复登录'; @override - String get login_roomLogin => '房间登录'; + String get login_roomLogin => '服务器登录'; @override String get login_password => '密码'; @@ -1452,13 +1453,13 @@ class AppLocalizationsZh extends AppLocalizations { String get login_savePassword => '保存密码'; @override - String get login_savePasswordSubtitle => '密码将安全地存储在这个设备上'; + String get login_savePasswordSubtitle => '密码将安全地存储在 данном设备上'; @override - String get login_repeaterDescription => '输入重复密码以访问设置和状态。'; + String get login_repeaterDescription => '输入重复器密码,即可访问设置和状态。'; @override - String get login_roomDescription => '输入房间密码以访问设置和状态。'; + String get login_roomDescription => '输入密码进入房间,即可访问设置和状态。'; @override String get login_routing => '路由'; @@ -1467,7 +1468,7 @@ class AppLocalizationsZh extends AppLocalizations { String get login_routingMode => '路由模式'; @override - String get login_autoUseSavedPath => '自动(使用已保存路径)'; + String get login_autoUseSavedPath => '自动(使用已保存的路径)'; @override String get login_forceFloodMode => '强制洪水模式'; @@ -1480,26 +1481,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String login_attempt(int current, int max) { - return '尝试 $current/$max'; + return 'Attempt $current/$max'; } @override String login_failed(String error) { - return '登录失败:$error'; + return 'Login failed: $error'; } @override - String get login_failedMessage => '登录失败。密码不正确或中继器不可达。'; + String get login_failedMessage => '登录失败。可能是密码错误,也可能是无法连接到服务器。'; @override String get common_reload => '重新加载'; @override - String get common_clear => '清除'; + String get common_clear => '清晰'; @override String path_currentPath(String path) { - return '当前路径:$path'; + return 'Current path: $path'; } @override @@ -1510,7 +1511,7 @@ class AppLocalizationsZh extends AppLocalizations { other: 'hops', one: 'hop', ); - return '使用 $count $_temp0 路径'; + return '使用 $count $_temp0 条路径'; } @override @@ -1520,29 +1521,29 @@ class AppLocalizationsZh extends AppLocalizations { String get path_currentPathLabel => '当前路径'; @override - String get path_hexPrefixInstructions => '输入2个字符的十六进制前缀,每个前缀之间用逗号分隔。'; + String get path_hexPrefixInstructions => '请输入每个跳跃步骤的 2 个字符的十六进制前缀,用逗号分隔。'; @override - String get path_hexPrefixExample => 'A1,F2,3C (每个节点使用其公钥的第一字节)'; + String get path_hexPrefixExample => '例如:A1, F2, 3C (每个节点使用其公钥的第一字节)'; @override - String get path_labelHexPrefixes => '十六进制前缀'; + String get path_labelHexPrefixes => '路径(十六进制前缀)'; @override - String get path_helperMaxHops => '最大 64 步跳。每个前缀是 2 个十六进制字符(1 字节)'; + String get path_helperMaxHops => '最大 64 个“hop”(跳跃)。每个前缀由 2 个十六进制字符(1 字节)组成。'; @override - String get path_selectFromContacts => '或从联系人中选择:'; + String get path_selectFromContacts => '或者从联系人列表中选择:'; @override - String get path_noRepeatersFound => '未找到任何重复器或房间服务器。'; + String get path_noRepeatersFound => '未找到任何重复设备或房间服务器。'; @override - String get path_customPathsRequire => '自定义路径需要中间跳转,这些跳转可以传递消息。'; + String get path_customPathsRequire => '自定义路径需要中间节点,这些节点可以转发消息。'; @override String path_invalidHexPrefixes(String prefixes) { - return '无效的十六进制前缀:$prefixes'; + return 'Invalid hex prefixes: $prefixes'; } @override @@ -1555,7 +1556,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_management => '重复器管理'; @override - String get room_management => '房间服务器管理'; + String get room_management => '服务器管理'; @override String get repeater_managementTools => '管理工具'; @@ -1567,22 +1568,22 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_statusSubtitle => '查看重复器状态、统计信息和邻居'; @override - String get repeater_telemetry => '遥测'; + String get repeater_telemetry => '远程监控'; @override - String get repeater_telemetrySubtitle => '查看传感器和系统状态的Telemetry数据'; + String get repeater_telemetrySubtitle => '查看传感器和系统状态的数据。'; @override - String get repeater_cli => 'CLI'; + String get repeater_cli => '命令行界面'; @override - String get repeater_cliSubtitle => '发送命令到重复器'; + String get repeater_cliSubtitle => '向复用器发送指令'; @override String get repeater_neighbours => '邻居'; @override - String get repeater_neighboursSubtitle => '查看零跳邻居。'; + String get repeater_neighboursSubtitle => '查看邻居节点(无需中间节点)。'; @override String get repeater_settings => '设置'; @@ -1597,7 +1598,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_routingMode => '路由模式'; @override - String get repeater_autoUseSavedPath => '自动(使用已保存路径)'; + String get repeater_autoUseSavedPath => '自动(使用已保存的路径)'; @override String get repeater_forceFloodMode => '强制洪水模式'; @@ -1606,14 +1607,14 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_pathManagement => '路径管理'; @override - String get repeater_refresh => '刷新'; + String get repeater_refresh => '更新'; @override String get repeater_statusRequestTimeout => '状态请求超时。'; @override String repeater_errorLoadingStatus(String error) { - return '错误加载状态:$error'; + return 'Error loading status: $error'; } @override @@ -1623,10 +1624,10 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_battery => '电池'; @override - String get repeater_clockAtLogin => '时间 (登录时)'; + String get repeater_clockAtLogin => '登录时的时间'; @override - String get repeater_uptime => '可用时间'; + String get repeater_uptime => '正常运行时间'; @override String get repeater_queueLength => '排队长度'; @@ -1635,28 +1636,28 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_debugFlags => '调试标志'; @override - String get repeater_radioStatistics => '无线电统计'; + String get repeater_radioStatistics => '广播统计'; @override - String get repeater_lastRssi => '上次RSSI'; + String get repeater_lastRssi => '上次的 RSSI 值'; @override - String get repeater_lastSnr => '最后 SNR'; + String get repeater_lastSnr => '最后一次信噪比'; @override - String get repeater_noiseFloor => '噪声地板'; + String get repeater_noiseFloor => '噪声水平'; @override - String get repeater_txAirtime => 'TX Airtime'; + String get repeater_txAirtime => 'TX 频道预留时间'; @override - String get repeater_rxAirtime => 'RX Airtime'; + String get repeater_rxAirtime => 'RX 空时'; @override String get repeater_packetStatistics => '数据包统计'; @override - String get repeater_sent => '已发送'; + String get repeater_sent => '发送'; @override String get repeater_received => '已收到'; @@ -1676,26 +1677,26 @@ class AppLocalizationsZh extends AppLocalizations { @override String repeater_packetTxTotal(int total, String flood, String direct) { - return '总计:$total, 洪流:$flood, 直连:$direct'; + return 'Total: $total, Flood: $flood, Direct: $direct'; } @override String repeater_packetRxTotal(int total, String flood, String direct) { - return '总计:$total, 洪流:$flood, 直连:$direct'; + return 'Total: $total, Flood: $flood, Direct: $direct'; } @override String repeater_duplicatesFloodDirect(String flood, String direct) { - return '洪水:$flood, 直通:$direct'; + return 'Flood: $flood, Direct: $direct'; } @override String repeater_duplicatesTotal(int total) { - return '总计:$total'; + return 'Total: $total'; } @override - String get repeater_settingsTitle => '重复设置'; + String get repeater_settingsTitle => '重复器设置'; @override String get repeater_basicSettings => '基本设置'; @@ -1704,7 +1705,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_repeaterName => '重复器名称'; @override - String get repeater_repeaterNameHelper => '显示此重复器的名称'; + String get repeater_repeaterNameHelper => '此复播器的显示名称'; @override String get repeater_adminPassword => '管理员密码'; @@ -1719,16 +1720,16 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_guestPasswordHelper => '只读访问密码'; @override - String get repeater_radioSettings => '射频设置'; + String get repeater_radioSettings => '收音机设置'; @override String get repeater_frequencyMhz => '频率 (MHz)'; @override - String get repeater_frequencyHelper => '300-2500 MHz'; + String get repeater_frequencyHelper => '300-2500 兆赫'; @override - String get repeater_txPower => 'TX Power'; + String get repeater_txPower => 'TX 功率'; @override String get repeater_txPowerHelper => '1-30 dBm'; @@ -1737,7 +1738,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_bandwidth => '带宽'; @override - String get repeater_spreadingFactor => '扩散因子'; + String get repeater_spreadingFactor => '传播系数'; @override String get repeater_codingRate => '编码速率'; @@ -1749,56 +1750,56 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_latitude => '纬度'; @override - String get repeater_latitudeHelper => '十进度的数字(例如:37.7749)'; + String get repeater_latitudeHelper => '十进制度(例如:37.7749)'; @override String get repeater_longitude => '经度'; @override - String get repeater_longitudeHelper => '十进度的数字(例如:-122.4194)'; + String get repeater_longitudeHelper => '十进制度(例如:-122.4194)'; @override - String get repeater_features => '功能'; + String get repeater_features => '特点'; @override String get repeater_packetForwarding => '数据包转发'; @override - String get repeater_packetForwardingSubtitle => '启用重复器以转发数据包'; + String get repeater_packetForwardingSubtitle => '启用重复器,使其能够转发数据包'; @override String get repeater_guestAccess => '访客访问'; @override - String get repeater_guestAccessSubtitle => '允许访客仅读访问'; + String get repeater_guestAccessSubtitle => '允许访客仅限读取权限'; @override String get repeater_privacyMode => '隐私模式'; @override - String get repeater_privacyModeSubtitle => '隐藏在广告中的姓名/位置'; + String get repeater_privacyModeSubtitle => '在广告中隐藏姓名/位置'; @override String get repeater_advertisementSettings => '广告设置'; @override - String get repeater_localAdvertInterval => '本地广告间隔'; + String get repeater_localAdvertInterval => '本地广告投放时间段'; @override String repeater_localAdvertIntervalMinutes(int minutes) { - return '$minutes 分钟'; + return '$minutes minutes'; } @override - String get repeater_floodAdvertInterval => '洪水广告间隔'; + String get repeater_floodAdvertInterval => '洪水广告播放间隔'; @override String repeater_floodAdvertIntervalHours(int hours) { - return '$hours 小时'; + return '$hours hours'; } @override - String get repeater_encryptedAdvertInterval => '加密广告间隔'; + String get repeater_encryptedAdvertInterval => '加密的广告投放时间段'; @override String get repeater_dangerZone => '危险区域'; @@ -1807,10 +1808,10 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_rebootRepeater => '重启重复器'; @override - String get repeater_rebootRepeaterSubtitle => '重启重复器设备'; + String get repeater_rebootRepeaterSubtitle => '重新启动重复器设备'; @override - String get repeater_rebootRepeaterConfirm => '您确定要重启这个中继器吗?'; + String get repeater_rebootRepeaterConfirm => '您确定要重新启动这个中继器吗?'; @override String get repeater_regenerateIdentityKey => '重新生成身份密钥'; @@ -1819,7 +1820,7 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_regenerateIdentityKeySubtitle => '生成新的公钥/私钥对'; @override - String get repeater_regenerateIdentityKeyConfirm => '这将生成一个重复器的新身份。继续吗?'; + String get repeater_regenerateIdentityKeyConfirm => '这将为复用器生成一个新的身份。继续吗?'; @override String get repeater_eraseFileSystem => '删除文件系统'; @@ -1828,109 +1829,109 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_eraseFileSystemSubtitle => '格式化重复文件系统'; @override - String get repeater_eraseFileSystemConfirm => '警告:这将擦除重复器上的所有数据。 这无法撤销!'; + String get repeater_eraseFileSystemConfirm => '警告:此操作将清除复用器上的所有数据。 无法恢复!'; @override - String get repeater_eraseSerialOnly => '通过串行控制台才能删除。'; + String get repeater_eraseSerialOnly => '“Erase”功能仅可通过串行控制台使用。'; @override String repeater_commandSent(String command) { - return '命令已发送:$command'; + return 'Command sent: $command'; } @override String repeater_errorSendingCommand(String error) { - return '发送命令时出错:$error'; + return 'Error sending command: $error'; } @override String get repeater_confirm => '确认'; @override - String get repeater_settingsSaved => '设置已保存成功'; + String get repeater_settingsSaved => '设置已成功保存'; @override String repeater_errorSavingSettings(String error) { - return '保存设置出错:$error'; + return 'Error saving settings: $error'; } @override - String get repeater_refreshBasicSettings => '刷新基本设置'; + String get repeater_refreshBasicSettings => '重置基本设置'; @override - String get repeater_refreshRadioSettings => '刷新无线电设置'; + String get repeater_refreshRadioSettings => '重置收音机设置'; @override - String get repeater_refreshTxPower => '刷新 TX 电量'; + String get repeater_refreshTxPower => '重置 TX 电源'; @override - String get repeater_refreshLocationSettings => '刷新位置设置'; + String get repeater_refreshLocationSettings => '重置位置设置'; @override String get repeater_refreshPacketForwarding => '刷新包转发'; @override - String get repeater_refreshGuestAccess => '刷新访客访问'; + String get repeater_refreshGuestAccess => '重新获取访客访问权限'; @override - String get repeater_refreshPrivacyMode => '刷新隐私模式'; + String get repeater_refreshPrivacyMode => '重置隐私模式'; @override - String get repeater_refreshAdvertisementSettings => '刷新广告设置'; + String get repeater_refreshAdvertisementSettings => '重置广告设置'; @override String repeater_refreshed(String label) { - return '$label 已刷新'; + return '$label refreshed'; } @override String repeater_errorRefreshing(String label) { - return '刷新 $label 时出错'; + return '[保存:$label]\n刷新 $label 时出错'; } @override - String get repeater_cliTitle => '重复器命令行工具'; + String get repeater_cliTitle => '重复器命令行界面'; @override - String get repeater_debugNextCommand => '调试下一步命令'; + String get repeater_debugNextCommand => '调试下一条命令'; @override String get repeater_commandHelp => '帮助'; @override - String get repeater_clearHistory => '清除历史'; + String get repeater_clearHistory => '清晰的历史'; @override - String get repeater_noCommandsSent => '尚未发送任何命令'; + String get repeater_noCommandsSent => '尚未发送任何指令'; @override - String get repeater_typeCommandOrUseQuick => '输入命令或使用快捷命令'; + String get repeater_typeCommandOrUseQuick => '在下方输入命令,或使用快捷命令。'; @override String get repeater_enterCommandHint => '输入命令...'; @override - String get repeater_previousCommand => '上一个命令'; + String get repeater_previousCommand => '之前的命令'; @override - String get repeater_nextCommand => '下一步命令'; + String get repeater_nextCommand => '下一个指令'; @override - String get repeater_enterCommandFirst => '请输入一个命令'; + String get repeater_enterCommandFirst => '首先输入一个命令'; @override - String get repeater_cliCommandFrameTitle => 'CLI 命令窗口'; + String get repeater_cliCommandFrameTitle => 'CLI 命令框架'; @override String repeater_cliCommandError(String error) { - return '错误:$error'; + return 'Error: $error'; } @override String get repeater_cliQuickGetName => '获取姓名'; @override - String get repeater_cliQuickGetRadio => '获取收音机'; + String get repeater_cliQuickGetRadio => '收听广播'; @override String get repeater_cliQuickGetTx => '获取 TX'; @@ -1942,196 +1943,199 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_cliQuickVersion => '版本'; @override - String get repeater_cliQuickAdvertise => '发布'; + String get repeater_cliQuickAdvertise => '发布广告'; @override String get repeater_cliQuickClock => '时钟'; @override - String get repeater_cliHelpAdvert => '发送广告包'; + String get repeater_cliHelpAdvert => '发送广告资料包'; @override - String get repeater_cliHelpReboot => '重启设备。(请注意,可能会出现“超时”现象,这是正常现象)'; + String get repeater_cliHelpReboot => '重置设备。 (请注意,您可能会收到“超时”错误,这是正常的现象)'; @override String get repeater_cliHelpClock => '显示每个设备的当前时间。'; @override - String get repeater_cliHelpPassword => '设置设备的新管理员密码。'; + String get repeater_cliHelpPassword => '为设备设置新的管理员密码。'; @override String get repeater_cliHelpVersion => '显示设备版本和固件构建日期。'; @override - String get repeater_cliHelpClearStats => '重置各种统计数值为零。'; + String get repeater_cliHelpClearStats => '重置各种统计指标,将其设置为零。'; @override - String get repeater_cliHelpSetAf => '设置空闲时间因子。'; + String get repeater_cliHelpSetAf => '设置时间因素。'; @override - String get repeater_cliHelpSetTx => '设置 LoRa 传输功率 (重置生效)'; + String get repeater_cliHelpSetTx => + '设置 LoRa 传输功率,单位为 dBm (相对于参考值)。 (重启以应用更改)'; @override - String get repeater_cliHelpSetRepeat => '启用或禁用此节点的重复器角色。'; + String get repeater_cliHelpSetRepeat => '启用或禁用此节点的重复器功能。'; @override String get repeater_cliHelpSetAllowReadOnly => - '(房间服务器) 如果“开”了,则空密码登录将被允许,但不能向房间发布内容。(仅限读取)'; + '(房间服务器)如果设置为“开启”,则允许使用空密码登录,但无法向房间发送消息(只能进行读取)。'; @override - String get repeater_cliHelpSetFloodMax => '设置最大换路包数量(如果 >= 最大,则不转发包)。'; + String get repeater_cliHelpSetFloodMax => '设置最大传入数据包的跳数(如果大于或等于最大值,则不进行转发)。'; @override String get repeater_cliHelpSetIntThresh => - '设置干扰阈值(以 dB 为单位)。默认值为 14。将设置为 0 以禁用通道干扰检测。'; + '设置干扰阈值(以dB为单位)。默认值为14。将设置为0以禁用频道干扰检测。'; @override String get repeater_cliHelpSetAgcResetInterval => - '设置间隔以重置自动增益控制器。将设置为 0 以禁用。'; + '设置间隔时间,用于重置自动增益控制器。设置为 0 以禁用。'; @override - String get repeater_cliHelpSetMultiAcks => '启用或禁用“双 ACKs”功能。'; + String get repeater_cliHelpSetMultiAcks => '启用或禁用“双重确认”功能。'; @override String get repeater_cliHelpSetAdvertInterval => - '设置定时器间隔时间为分钟,以发送本地(零跳)广告包。将设置为0以禁用。'; + '设置定时器间隔,单位为分钟,用于发送本地(无中继)的广告数据包。 将设置为 0 以禁用。'; @override String get repeater_cliHelpSetFloodAdvertInterval => - '设置定时器间隔时间为小时,以发送洪水广告包。将设置为 0 以禁用。'; + '设置定时器间隔时间为小时,以便发送广告信息包。将设置为 0 以禁用。'; @override String get repeater_cliHelpSetGuestPassword => - '设置/更新客人密码。(对于重复器,客人在登录时可以发送“获取统计”请求)'; + '设置/更新访客密码。 (对于访客,登录请求可以发送“获取统计”请求)'; @override String get repeater_cliHelpSetName => '设置广告名称。'; @override - String get repeater_cliHelpSetLat => '设置广告地图纬度(十进制度)'; + String get repeater_cliHelpSetLat => '设置广告地图的纬度。(以十进制表示)'; @override - String get repeater_cliHelpSetLon => '设置广告地图经度 (十进位)'; + String get repeater_cliHelpSetLon => '设置广告地图的经度。 (十进制度)'; @override - String get repeater_cliHelpSetRadio => '设置全新的无线电参数,并保存到偏好设置。需要执行“重启”命令才能应用。'; + String get repeater_cliHelpSetRadio => '完全重新设置无线电参数,并保存到偏好设置。需要执行“重启”命令才能生效。'; @override String get repeater_cliHelpSetRxDelay => - '设置(实验性)的基础(必须大于 1 才能生效)是用于对接收到的数据包应用轻微延迟,基于信号强度/得分。将设置为 0 以禁用。'; + '设置(实验性):设置一个基础值(必须大于1才能生效),用于对接收到的数据包进行轻微延迟处理,该延迟值基于信号强度/评分。将该值设置为0以禁用。'; @override String get repeater_cliHelpSetTxDelay => - '设置一个与时间-在空气中(time-on-air)的系数,用于洪水模式的数据包,并结合随机插槽系统,以延迟其转发。(以降低碰撞的可能性)'; + '通过将一个因子与“浮动模式”数据包的时间在空中停留时间相乘,并结合随机的“时隙”系统,来延迟其转发,从而降低数据包冲突的概率。'; @override String get repeater_cliHelpSetDirectTxDelay => - '与txdelay相同,但用于为直接模式包的转发应用随机延迟。'; + '与txdelay相同,但用于对直接模式数据包的转发进行随机延迟。'; @override - String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥梁'; + String get repeater_cliHelpSetBridgeEnabled => '启用/禁用桥接。'; @override - String get repeater_cliHelpSetBridgeDelay => '设置在重新发送数据包之前延迟时间。'; + String get repeater_cliHelpSetBridgeDelay => '在重新发送数据包之前,设置延迟时间。'; @override - String get repeater_cliHelpSetBridgeSource => '选择桥梁是否会重传接收到的数据包或发送的数据包。'; + String get repeater_cliHelpSetBridgeSource => '选择桥接器是否会转发收到的数据包,还是转发发送的数据包。'; @override - String get repeater_cliHelpSetBridgeBaud => '设置rs232桥接的串口链路波特率。'; + String get repeater_cliHelpSetBridgeBaud => '为 RS232 桥接设置串行链路的波特率。'; @override - String get repeater_cliHelpSetBridgeSecret => '设置 espnow 桥的秘密。'; + String get repeater_cliHelpSetBridgeSecret => '设置 ESPNOW 桥的秘密。'; @override - String get repeater_cliHelpSetAdcMultiplier => '设置自定义因子以调整报告的电池电压(仅限部分板卡支持)。'; + String get repeater_cliHelpSetAdcMultiplier => + '设置自定义因子,用于调整报告的电池电压(仅在特定板上支持)。'; @override String get repeater_cliHelpTempRadio => - '设置临时无线电参数,持续指定的分钟数,之后恢复为原始无线电参数。(不保存到偏好设置)。'; + '设置临时收音机参数,持续指定分钟数,之后恢复到原始收音机参数。(不保存到偏好设置)。'; @override String get repeater_cliHelpSetPerm => - '修改ACL。如果“权限”为零,则删除匹配的条目(通过pubkey前缀)。如果pubkey-hex的完整长度且当前不在ACL中,则添加新条目。通过匹配pubkey前缀更新条目。权限位因固件角色而异,但低2位为:0(Guest)、1(只读)、2(读写)、3(Admin)'; + '修改 ACL。如果 \"permissions\" 的值为 0,则删除与 pubkey 相关的条目。如果 pubkey-hex 完整且当前不在 ACL 中,则添加新的条目。通过匹配 pubkey 相关的前缀来更新条目。不同固件角色的权限位有所不同,但低 2 位分别对应:0 (访客)、1 (只读)、2 (读写)、3 (管理员)。'; @override - String get repeater_cliHelpGetBridgeType => '获取桥接类型:无,RS232,ESPNow'; + String get repeater_cliHelpGetBridgeType => '支持桥接模式、RS232、ESPNOW。'; @override String get repeater_cliHelpLogStart => '开始将数据包记录到文件系统。'; @override - String get repeater_cliHelpLogStop => '停止将数据包记录到文件系统。'; + String get repeater_cliHelpLogStop => '停止将数据包记录写入文件系统。'; @override - String get repeater_cliHelpLogErase => '删除文件系统中的包日志。'; + String get repeater_cliHelpLogErase => '从文件系统中删除所有已记录的包信息。'; @override String get repeater_cliHelpNeighbors => - '显示通过零跳广告收听的其他重复节点列表。 每行是 id-prefix-hex:时间戳:snr-times-4'; + '显示了通过零跳广告收到的其他复用节点列表。 每行包含:id-前缀-十六进制:时间戳:信噪比(4次)'; @override String get repeater_cliHelpNeighborRemove => - '移除邻居列表中第一个匹配的条目(通过十六进制 pubkey 前缀)。'; + '从邻居列表中删除第一个匹配项(通过十六进制的 pubkey 前缀)。'; @override - String get repeater_cliHelpRegion => '(仅显示区域) 列出所有已定义的区域和当前的防洪权限。'; + String get repeater_cliHelpRegion => '(仅限序列)列出所有已定义的区域以及当前的防洪许可。'; @override String get repeater_cliHelpRegionLoad => - '注意:这是一个特殊的多命令调用。 随后的每个命令都是一个区域名称(用空格缩进以指示父级层次结构,至少需要一个空格)。 以发送一个空行/命令结束。'; + '请注意:这是一个特殊的、包含多个命令的调用方式。 之后的每个命令都是一个区域名称(使用空格进行缩进,以表示父级关系,至少需要一个空格)。 结束方式是通过发送一个空行/命令。'; @override String get repeater_cliHelpRegionGet => - '搜索具有给定名称前缀的区域(或“”用于全局范围)。回复为“-> region-name (parent-name) ‘F’”'; + '搜索具有指定名称前缀的区域(或使用“*”表示全局范围)。 返回结果为“-> region-name (parent-name) \'F\'”'; @override - String get repeater_cliHelpRegionPut => '添加或更新区域定义,使用指定名称。'; + String get repeater_cliHelpRegionPut => '添加或更新一个区域定义,并指定其名称。'; @override - String get repeater_cliHelpRegionRemove => '删除指定名称的区域定义。(必须没有子区域)'; + String get repeater_cliHelpRegionRemove => + '删除具有指定名称的区域定义。 (必须与指定名称完全匹配,且不能有子区域)'; @override - String get repeater_cliHelpRegionAllowf => '设置指定区域的“洪水”权限。(“”代表全局/遗留范围)'; + String get repeater_cliHelpRegionAllowf => '为指定区域设置“洪水”权限。(“*”表示全局/旧版本范围)'; @override String get repeater_cliHelpRegionDenyf => - '移除指定区域的‘F’lood权限。 (注意:目前阶段不建议在此范围内使用,尤其是全局/旧版范围!!)'; + '移除指定区域的“洪水”权限。(请注意:目前不建议在全局/旧版本中使用此功能!!)'; @override - String get repeater_cliHelpRegionHome => '回复当前“主页”区域。 (注意尚未应用,保留用于未来)'; + String get repeater_cliHelpRegionHome => '回复当前“主区域”。(此功能尚未应用,仅供未来使用)'; @override - String get repeater_cliHelpRegionHomeSet => '设置‘主页’区域。'; + String get repeater_cliHelpRegionHomeSet => '设置“主”区域。'; @override - String get repeater_cliHelpRegionSave => '保存区域列表/地图到存储。'; + String get repeater_cliHelpRegionSave => '将区域列表/地图保存到存储中。'; @override String get repeater_cliHelpGps => - '显示GPS状态。当GPS关闭时,回复仅为“关闭”,如果已开启,则回复为“开启”、“状态”、“定位”和卫星数量。'; + '显示 GPS 状态。当 GPS 处于关闭状态时,它只会显示“关闭”;当 GPS 处于开启状态时,它会显示“开启”、“状态”、“定位”、“卫星数量”等信息。'; @override - String get repeater_cliHelpGpsOnOff => '切换 GPS 开启状态。'; + String get repeater_cliHelpGpsOnOff => '切换 GPS 设备的电源状态。'; @override - String get repeater_cliHelpGpsSync => '同步节点时间与 GPS 钟。'; + String get repeater_cliHelpGpsSync => '将节点时间与 GPS 钟同步。'; @override - String get repeater_cliHelpGpsSetLoc => '设置节点位置至 GPS 坐标并保存偏好设置。'; + String get repeater_cliHelpGpsSetLoc => '将节点的坐标设置为 GPS 坐标,并保存设置。'; @override String get repeater_cliHelpGpsAdvert => - '提供节点广告配置位置:\n- none:不包含位置在广告中\n- share:分享 GPS 位置(来自 SensorManager)\n- prefs:在偏好设置中投放位置'; + '设置节点的位置广告配置:\n- none:不将位置信息包含在广告中\n- share:共享 GPS 位置(从 SensorManager 获取)\n- prefs:在偏好设置中展示的位置'; @override - String get repeater_cliHelpGpsAdvertSet => '设置广告位置配置。'; + String get repeater_cliHelpGpsAdvertSet => '设置广告的位置配置。'; @override String get repeater_commandsListTitle => '命令列表'; @override - String get repeater_commandsListNote => '注意:对于各种“设置...”命令,也存在“获取...”命令。'; + String get repeater_commandsListNote => '请注意:对于各种“set ...”命令,也存在“get ...”命令。'; @override String get repeater_general => '通用'; @@ -2146,29 +2150,29 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_logging => '记录'; @override - String get repeater_neighborsRepeaterOnly => '邻居(仅限重复器)'; + String get repeater_neighborsRepeaterOnly => '邻居(仅限重复功能)'; @override - String get repeater_regionManagementRepeaterOnly => '区域管理(仅限重复器)'; + String get repeater_regionManagementRepeaterOnly => '区域管理(仅限重复站点)'; @override - String get repeater_regionNote => '区域命令已推出,用于管理区域定义和权限。'; + String get repeater_regionNote => '区域命令已引入,用于管理区域定义和权限。'; @override - String get repeater_gpsManagement => 'GPS管理'; + String get repeater_gpsManagement => 'GPS 管理'; @override - String get repeater_gpsNote => 'GPS 命令已引入用于管理与位置相关的主题。'; + String get repeater_gpsNote => '已引入 GPS 命令,用于管理与位置相关的任务。'; @override - String get telemetry_receivedData => '接收遥测数据'; + String get telemetry_receivedData => '接收到的遥测数据'; @override String get telemetry_requestTimeout => '遥测请求超时。'; @override String telemetry_errorLoading(String error) { - return '错误加载遥测数据:$error'; + return 'Error loading telemetry: $error'; } @override @@ -2186,7 +2190,7 @@ class AppLocalizationsZh extends AppLocalizations { String get telemetry_voltageLabel => '电压'; @override - String get telemetry_mcuTemperatureLabel => 'MCU 温度'; + String get telemetry_mcuTemperatureLabel => 'MCU 的温度'; @override String get telemetry_temperatureLabel => '温度'; @@ -2215,30 +2219,30 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get neighbors_receivedData => '收到邻居数据'; + String get neighbors_receivedData => '已收到邻居信息'; @override - String get neighbors_requestTimedOut => '邻居请求超时处理。'; + String get neighbors_requestTimedOut => '邻居要求停止干扰。'; @override String neighbors_errorLoading(String error) { - return '加载邻居时出错:$error'; + return 'Error loading neighbors: $error'; } @override - String get neighbors_repeatersNeighbours => '重复器邻居'; + String get neighbors_repeatersNeighbours => '重复使用的邻居'; @override - String get neighbors_noData => '没有可用的邻居数据。'; + String get neighbors_noData => '没有可用的邻居信息。'; @override String neighbors_unknownContact(String pubkey) { - return '未知$pubkey'; + return 'Unknown $pubkey'; } @override String neighbors_heardAgo(String time) { - return '听到的时间:$time前'; + return 'Heard: $time ago'; } @override @@ -2251,10 +2255,10 @@ class AppLocalizationsZh extends AppLocalizations { String get channelPath_otherObservedPaths => '其他观察到的路径'; @override - String get channelPath_repeaterHops => '重复跳跃'; + String get channelPath_repeaterHops => '复用跳跃'; @override - String get channelPath_noHopDetails => '此包的详细信息未提供。'; + String get channelPath_noHopDetails => '对于此包,未提供详细信息。'; @override String get channelPath_messageDetails => '消息详情'; @@ -2274,15 +2278,15 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channelPath_observedLabel => '已观察'; + String get channelPath_observedLabel => '观察到的'; @override String channelPath_observedPathTitle(int index, String hops) { - return '观察路径 $index • $hops'; + return 'Observed path $index • $hops'; } @override - String get channelPath_noLocationData => '没有位置数据'; + String get channelPath_noLocationData => '没有位置信息'; @override String channelPath_timeWithDate(int day, int month, String time) { @@ -2305,30 +2309,30 @@ class AppLocalizationsZh extends AppLocalizations { @override String channelPath_observedZeroOf(int total) { - return '0 of $total 跳跃'; + return '0 of $total hops'; } @override String channelPath_observedSomeOf(int observed, int total) { - return '已观察到 $observed 步中的 $total 步'; + return '$observed of $total hops'; } @override - String get channelPath_mapTitle => '路径地图'; + String get channelPath_mapTitle => '路线图'; @override - String get channelPath_noRepeaterLocations => '此路径没有可用的重复器位置。'; + String get channelPath_noRepeaterLocations => '这条路径上没有可用的中继器位置。'; @override String channelPath_primaryPath(int index) { - return '路径 $index (主)'; + return '路径 $index (主要路径)'; } @override String get channelPath_pathLabelTitle => '路径'; @override - String get channelPath_observedPathHeader => '已观察路径'; + String get channelPath_observedPathHeader => '观察路径'; @override String channelPath_selectedPathLabel(String label, String prefixes) { @@ -2336,19 +2340,19 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get channelPath_noHopDetailsAvailable => '此包的跳跃详情不可用。'; + String get channelPath_noHopDetailsAvailable => '对于此包裹,尚无详细信息。'; @override - String get channelPath_unknownRepeater => '未知重复器'; + String get channelPath_unknownRepeater => '未知的重复设备'; @override String get community_title => '社区'; @override - String get community_create => '创建社区'; + String get community_create => '建立社区'; @override - String get community_createDesc => '创建新的社区并可通过二维码分享。'; + String get community_createDesc => '创建一个新的社群,并通过二维码进行分享。'; @override String get community_join => '加入'; @@ -2358,20 +2362,20 @@ class AppLocalizationsZh extends AppLocalizations { @override String community_joinConfirmation(String name) { - return '您想加入社区 \"$name\" 吗?'; + return 'Do you want to join the community \"$name\"?'; } @override String get community_scanQr => '扫描社区二维码'; @override - String get community_scanInstructions => '将相机对准社区二维码'; + String get community_scanInstructions => '将相机对准社区的二维码。'; @override String get community_showQr => '显示二维码'; @override - String get community_publicChannel => '社区公开'; + String get community_publicChannel => '社区公共'; @override String get community_hashtagChannel => '社区标签'; @@ -2384,12 +2388,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String community_created(String name) { - return '社区“$name”已创建'; + return 'Community \"$name\" created'; } @override String community_joined(String name) { - return '加入社区 \"$name\"'; + return 'Joined community \"$name\"'; } @override @@ -2397,128 +2401,128 @@ class AppLocalizationsZh extends AppLocalizations { @override String community_qrInstructions(String name) { - return '扫描此二维码加入$name'; + return 'Scan this QR code to join \"$name\"'; } @override - String get community_hashtagPrivacyHint => '社区标签频道仅社区成员可加入'; + String get community_hashtagPrivacyHint => '仅社区成员才能加入社区话题标签的频道。'; @override String get community_invalidQrCode => '无效的社区二维码'; @override - String get community_alreadyMember => '已经是会员了'; + String get community_alreadyMember => '已经是会员'; @override String community_alreadyMemberMessage(String name) { - return '您已经是 \"$name\" 的会员。'; + return 'You are already a member of \"$name\".'; } @override - String get community_addPublicChannel => '添加社区公共频道'; + String get community_addPublicChannel => '添加公共频道'; @override String get community_addPublicChannelHint => '自动添加该社区的公共频道'; @override - String get community_noCommunities => '尚未加入任何社区'; + String get community_noCommunities => '目前还没有任何社区加入。'; @override - String get community_scanOrCreate => '扫描二维码或创建社区开始'; + String get community_scanOrCreate => '扫描二维码或创建社群,即可开始。'; @override - String get community_manageCommunities => '管理社群'; + String get community_manageCommunities => '管理社区'; @override String get community_delete => '退出社区'; @override String community_deleteConfirm(String name) { - return '退出 \"$name\"?'; + return '是否要删除\"$name\"?'; } @override String community_deleteChannelsWarning(int count) { - return '这也将删除 $count 个频道及其消息。'; + return '这将同时删除 $count 个频道及其所有消息。'; } @override String community_deleted(String name) { - return '已退出社区 \"$name\"'; + return 'Left community \"$name\"'; } @override - String get community_regenerateSecret => '重新生成密钥'; + String get community_regenerateSecret => '恢复秘密'; @override String community_regenerateSecretConfirm(String name) { - return '重新生成“$name”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。'; + return '[保存:$name]\n是否需要重新生成\"$name\"的密钥?所有成员都需要扫描新的二维码才能继续进行通信。'; } @override - String get community_regenerate => '重新生成'; + String get community_regenerate => '再生'; @override String community_secretRegenerated(String name) { - return '密码已重置为“$name”'; + return '[保护对象:$name]\n秘密已恢复至\"$name\"'; } @override - String get community_updateSecret => '更新密钥'; + String get community_updateSecret => '更新秘密'; @override String community_secretUpdated(String name) { - return '密码已更新为“$name”'; + return '“$name”的秘密已更新'; } @override String community_scanToUpdateSecret(String name) { - return '扫描新的二维码更新\"$name\"的密码'; + return 'Scan the new QR code to update the secret for \"$name\"'; } @override String get community_addHashtagChannel => '添加社区标签'; @override - String get community_addHashtagChannelDesc => '添加一个话题频道给此社区'; + String get community_addHashtagChannelDesc => '为这个社区创建一个带有话题标签的频道'; @override String get community_selectCommunity => '选择社区'; @override - String get community_regularHashtag => '常规话题标签'; + String get community_regularHashtag => '常用标签'; @override - String get community_regularHashtagDesc => '公共话题(任何人都可以加入)'; + String get community_regularHashtagDesc => '公共话题标签(任何人都可以参与)'; @override String get community_communityHashtag => '社区标签'; @override - String get community_communityHashtagDesc => '仅限社区成员使用'; + String get community_communityHashtagDesc => '仅限社区成员'; @override String community_forCommunity(String name) { - return '对于 $name'; + return 'For $name'; } @override String get listFilter_tooltip => '筛选和排序'; @override - String get listFilter_sortBy => '按类型排序'; + String get listFilter_sortBy => '按排序'; @override String get listFilter_latestMessages => '最新消息'; @override - String get listFilter_heardRecently => '最近听说'; + String get listFilter_heardRecently => '最近听到的'; @override - String get listFilter_az => 'A-Z'; + String get listFilter_az => 'A 到 Z'; @override - String get listFilter_filters => '筛选'; + String get listFilter_filters => '过滤器'; @override String get listFilter_all => '全部'; @@ -2533,46 +2537,88 @@ class AppLocalizationsZh extends AppLocalizations { String get listFilter_roomServers => '房间服务器'; @override - String get listFilter_unreadOnly => '未读消息'; + String get listFilter_unreadOnly => '仅显示未读消息'; @override - String get listFilter_newGroup => '新组'; + String get listFilter_newGroup => '新的团体'; @override - String get pathTrace_you => '你'; + String get pathTrace_you => '您'; @override String get pathTrace_failed => '路径追踪失败。'; @override - String get pathTrace_notAvailable => '路径追踪不可用'; + String get pathTrace_notAvailable => '无法获取路径信息。'; @override - String get pathTrace_refreshTooltip => '刷新路径追踪'; + String get pathTrace_refreshTooltip => '重新绘制路径。'; @override String get contacts_pathTrace => '路径追踪'; @override - String get contacts_ping => 'ping'; + String get contacts_ping => '乒'; @override - String get contacts_repeaterPathTrace => '路径追踪到中继器'; + String get contacts_repeaterPathTrace => '追踪路径至中继器'; @override - String get contacts_repeaterPing => 'Ping 中继器'; + String get contacts_repeaterPing => '中继器'; @override - String get contacts_roomPathTrace => '路径追踪至房间服务器'; + String get contacts_roomPathTrace => '追踪到房间服务器'; @override - String get contacts_roomPing => 'Ping 房间服务器'; + String get contacts_roomPing => '会议室服务器'; @override - String get contacts_chatTraceRoute => '路径追踪'; + String get contacts_chatTraceRoute => '路径跟踪路线'; @override String contacts_pathTraceTo(String name) { - return '追踪路由到 $name'; + return '追踪路径至 $name'; } + + @override + String get contacts_clipboardEmpty => '剪贴板为空。'; + + @override + String get contacts_invalidAdvertFormat => '无效的联系信息'; + + @override + String get contacts_contactImported => '已建立联系。'; + + @override + String get contacts_contactImportFailed => '未能导入联系人。'; + + @override + String get contacts_zeroHopAdvert => '零跳广告'; + + @override + String get contacts_floodAdvert => '防洪广告'; + + @override + String get contacts_copyAdvertToClipboard => '复制广告到剪贴板'; + + @override + String get contacts_addContactFromClipboard => '从剪贴板添加联系人'; + + @override + String get contacts_ShareContact => '复制联系方式到剪贴板'; + + @override + String get contacts_ShareContactZeroHop => '通过广告分享联系方式'; + + @override + String get contacts_zeroHopContactAdvertSent => '通过广告获取联系方式。'; + + @override + String get contacts_zeroHopContactAdvertFailed => '发送联系方式失败。'; + + @override + String get contacts_contactAdvertCopied => '广告内容已复制到剪贴板。'; + + @override + String get contacts_contactAdvertCopyFailed => '将广告复制到剪贴板操作失败。'; } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index c118b9d..c941461 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1,10 +1,11 @@ { "@@locale": "zh", "appTitle": "MeshCore Open", - "nav_contacts": "联系人", + "nav_contacts": "联系方式", "nav_channels": "频道", "nav_map": "地图", "common_cancel": "取消", + "common_ok": "好的", "common_connect": "连接", "common_unknownDevice": "未知设备", "common_save": "保存", @@ -14,18 +15,18 @@ "common_add": "添加", "common_settings": "设置", "common_disconnect": "断开", - "common_connected": "已连接", + "common_connected": "连接", "common_disconnected": "断开", - "common_create": "创建", + "common_create": "创造", "common_continue": "继续", "common_share": "分享", "common_copy": "复制", "common_retry": "重试", "common_hide": "隐藏", - "common_remove": "删除", + "common_remove": "移除", "common_enable": "启用", "common_disable": "禁用", - "common_reboot": "重启", + "common_reboot": "重新启动", "common_loading": "正在加载...", "common_notAvailable": "—", "common_voltageValue": "{volts} V", @@ -44,12 +45,12 @@ } } }, - "scanner_title": "MeshCore Open", - "scanner_scanning": "扫描设备…", - "scanner_connecting": "连接中...", - "scanner_disconnecting": "断开中...", + "scanner_title": "MeshCore 开放", + "scanner_scanning": "正在搜索设备...", + "scanner_connecting": "正在连接...", + "scanner_disconnecting": "断开连接...", "scanner_notConnected": "未连接", - "scanner_connectedTo": "已连接至 {deviceName}", + "scanner_connectedTo": "已连接到 {deviceName}", "@scanner_connectedTo": { "placeholders": { "deviceName": { @@ -57,9 +58,9 @@ } } }, - "scanner_searchingDevices": "搜索 MeshCore 设备...", - "scanner_tapToScan": "点击扫描以查找MeshCore设备", - "scanner_connectionFailed": "连接失败:{error}", + "scanner_searchingDevices": "正在搜索 MeshCore 设备...", + "scanner_tapToScan": "点击“扫描”功能,以查找 MeshCore 设备。", + "scanner_connectionFailed": "Connection failed: {error}", "@scanner_connectionFailed": { "placeholders": { "error": { @@ -70,7 +71,7 @@ "scanner_stop": "停止", "scanner_scan": "扫描", "device_quickSwitch": "快速切换", - "device_meshcore": "MeshCore", + "device_meshcore": "网格核心", "settings_title": "设置", "settings_deviceInfo": "设备信息", "settings_appSettings": "应用设置", @@ -80,38 +81,42 @@ "settings_nodeNameNotSet": "未设置", "settings_nodeNameHint": "请输入节点名称", "settings_nodeNameUpdated": "姓名已更新", - "settings_radioSettings": "无线设置", - "settings_radioSettingsSubtitle": "频率,功率,扩展因子", - "settings_radioSettingsUpdated": "射频设置已更新", - "settings_location": "位置", - "settings_locationSubtitle": "GPS坐标", - "settings_locationUpdated": "位置已更新", - "settings_locationBothRequired": "请输入纬度和经度。", - "settings_locationInvalid": "无效的纬度或经度。", + "settings_radioSettings": "收音机设置", + "settings_radioSettingsSubtitle": "频率、功率、扩频因子", + "settings_radioSettingsUpdated": "收音机设置已更新", + "settings_location": "地点", + "settings_locationSubtitle": "GPS 坐标", + "settings_locationUpdated": "位置和 GPS 设置已更新", + "settings_locationBothRequired": "请输入经度和纬度。", + "settings_locationInvalid": "无效的经度和纬度。", + "settings_locationGPSEnable": "开启 GPS 功能", + "settings_locationGPSEnableSubtitle": "使 GPS 能够自动更新位置。", + "settings_locationIntervalSec": "GPS 间隔时间(秒)", + "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_latitude": "纬度", "settings_longitude": "经度", "settings_privacyMode": "隐私模式", - "settings_privacyModeSubtitle": "隐藏在广告中的姓名/位置", - "settings_privacyModeToggle": "开启隐私模式以隐藏您的姓名和位置在广告中的显示。", + "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", + "settings_privacyModeToggle": "切换隐私模式,以隐藏您的姓名和位置,从而在广告中保护您的个人信息。", "settings_privacyModeEnabled": "隐私模式已启用", - "settings_privacyModeDisabled": "隐私模式已禁用", - "settings_actions": "操作", - "settings_sendAdvertisement": "发送广告", - "settings_sendAdvertisementSubtitle": "现在已广播", - "settings_advertisementSent": "广告已发送", + "settings_privacyModeDisabled": "隐私模式已关闭", + "settings_actions": "行动", + "settings_sendAdvertisement": "发布广告", + "settings_sendAdvertisementSubtitle": "现已开始进行广播节目", + "settings_advertisementSent": "已发送广告", "settings_syncTime": "同步时间", - "settings_syncTimeSubtitle": "将设备时钟设置为手机时间", + "settings_syncTimeSubtitle": "将设备时钟设置为与手机时间一致", "settings_timeSynchronized": "时间同步", "settings_refreshContacts": "刷新联系人", - "settings_refreshContactsSubtitle": "从设备重新加载联系人列表", + "settings_refreshContactsSubtitle": "从设备中重新加载联系人列表", "settings_rebootDevice": "重启设备", - "settings_rebootDeviceSubtitle": "重启 MeshCore 设备", - "settings_rebootDeviceConfirm": "您确定要重启设备吗?您将会断开连接。", + "settings_rebootDeviceSubtitle": "重新启动 MeshCore 设备", + "settings_rebootDeviceConfirm": "您确定要重启设备吗?这将导致您与设备断开连接。", "settings_debug": "调试", - "settings_bleDebugLog": "蓝牙调试日志", - "settings_bleDebugLogSubtitle": "蓝牙命令、响应和原始数据", - "settings_appDebugLog": "应用调试日志", - "settings_appDebugLogSubtitle": "应用调试消息", + "settings_bleDebugLog": "BLE 调试日志", + "settings_bleDebugLogSubtitle": "BLE 命令、响应和原始数据", + "settings_appDebugLog": "应用程序调试日志", + "settings_appDebugLogSubtitle": "应用程序调试消息", "settings_about": "关于", "settings_aboutVersion": "MeshCore Open v{version}", "@settings_aboutVersion": { @@ -121,31 +126,31 @@ } } }, - "settings_aboutLegalese": "2024 MeshCore 开放源代码项目", - "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 网状网络设备。", + "settings_aboutLegalese": "2026 MeshCore 开源项目", + "settings_aboutDescription": "一个开源的 Flutter 客户端,用于 MeshCore LoRa 无线网络设备。", "settings_infoName": "姓名", "settings_infoId": "ID", "settings_infoStatus": "状态", "settings_infoBattery": "电池", "settings_infoPublicKey": "公钥", "settings_infoContactsCount": "联系人数量", - "settings_infoChannelCount": "频道数量", + "settings_infoChannelCount": "通道数量", "settings_presets": "预设", - "settings_preset915Mhz": "915 MHz", - "settings_preset868Mhz": "868 MHz", - "settings_preset433Mhz": "433 MHz", + "settings_preset915Mhz": "915 兆赫", + "settings_preset868Mhz": "868 兆赫", + "settings_preset433Mhz": "433 兆赫", "settings_frequency": "频率 (MHz)", "settings_frequencyHelper": "300.0 - 2500.0", - "settings_frequencyInvalid": "无效频率 (300-2500 MHz)", + "settings_frequencyInvalid": "无效频率(300-2500 MHz)", "settings_bandwidth": "带宽", - "settings_spreadingFactor": "扩散因子", + "settings_spreadingFactor": "传播系数", "settings_codingRate": "编码速率", - "settings_txPower": "TX Power (dBm)", + "settings_txPower": "TX 功率(dBm)", "settings_txPowerHelper": "0 - 22", - "settings_txPowerInvalid": "无效的 TX 电功率 (0-22 dBm)", + "settings_txPowerInvalid": "无效的发射功率(0-22 dBm)", "settings_longRange": "远距离", - "settings_fastSpeed": "快速速度", - "settings_error": "错误:{message}", + "settings_fastSpeed": "高速", + "settings_error": "[保存:{message}]\n错误:{message}", "@settings_error": { "placeholders": { "message": { @@ -156,48 +161,50 @@ "appSettings_title": "应用设置", "appSettings_appearance": "外观", "appSettings_theme": "主题", - "appSettings_themeSystem": "系统默认", + "appSettings_themeSystem": "系统默认设置", "appSettings_themeLight": "光", - "appSettings_themeDark": "深色", + "appSettings_themeDark": "黑暗", "appSettings_language": "语言", - "appSettings_languageSystem": "系统默认", - "appSettings_languageEn": "English", - "appSettings_languageFr": "Français", - "appSettings_languageEs": "Español", - "appSettings_languageDe": "Deutsch", - "appSettings_languagePl": "Polski", - "appSettings_languageSl": "Slovenščina", - "appSettings_languagePt": "Português", - "appSettings_languageIt": "Italiano", + "appSettings_languageSystem": "系统默认设置", + "appSettings_languageEn": "英语", + "appSettings_languageFr": "法语", + "appSettings_languageEs": "西班牙语", + "appSettings_languageDe": "德语", + "appSettings_languagePl": "波兰语", + "appSettings_languageSl": "斯洛文语", + "appSettings_languagePt": "葡萄牙语", + "appSettings_languageIt": "意大利语", "appSettings_languageZh": "中文", - "appSettings_languageSv": "Svenska", - "appSettings_languageNl": "Nederlands", - "appSettings_languageSk": "Slovenčina", - "appSettings_languageBg": "Български", + "appSettings_languageSv": "瑞典语", + "appSettings_languageNl": "荷兰语", + "appSettings_languageSk": "斯洛伐克语", + "appSettings_languageBg": "保加利亚", + "appSettings_languageRu": "俄语", + "appSettings_languageUk": "乌克兰", "appSettings_notifications": "通知", "appSettings_enableNotifications": "启用通知", "appSettings_enableNotificationsSubtitle": "接收消息和广告的通知", - "appSettings_notificationPermissionDenied": "通知权限被拒绝", + "appSettings_notificationPermissionDenied": "权限被拒绝", "appSettings_notificationsEnabled": "通知已启用", "appSettings_notificationsDisabled": "通知已关闭", "appSettings_messageNotifications": "消息通知", - "appSettings_messageNotificationsSubtitle": "显示收到新消息时的通知", + "appSettings_messageNotificationsSubtitle": "在收到新消息时显示通知", "appSettings_channelMessageNotifications": "频道消息通知", - "appSettings_channelMessageNotificationsSubtitle": "显示接收频道消息时的通知", + "appSettings_channelMessageNotificationsSubtitle": "在收到频道消息时,显示通知。", "appSettings_advertisementNotifications": "广告通知", - "appSettings_advertisementNotificationsSubtitle": "显示当新节点被发现时通知", - "appSettings_messaging": "消息", - "appSettings_clearPathOnMaxRetry": "清除最大重试路径", - "appSettings_clearPathOnMaxRetrySubtitle": "重置联系人路径,在5次发送失败尝试后", - "appSettings_pathsWillBeCleared": "路径将在5次失败重试后清除", - "appSettings_pathsWillNotBeCleared": "路径不会自动清理", - "appSettings_autoRouteRotation": "自动路径旋转", - "appSettings_autoRouteRotationSubtitle": "在最佳路径和洪水模式之间切换", + "appSettings_advertisementNotificationsSubtitle": "在发现新的节点时,显示通知。", + "appSettings_messaging": "信息传递", + "appSettings_clearPathOnMaxRetry": "关于“最大重试”的清晰说明", + "appSettings_clearPathOnMaxRetrySubtitle": "在尝试发送失败后 5 次,重置联系路径。", + "appSettings_pathsWillBeCleared": "如果尝试 5 次后仍然失败,则将重新规划路径。", + "appSettings_pathsWillNotBeCleared": "路径不会自动清除。", + "appSettings_autoRouteRotation": "自动路径轮换", + "appSettings_autoRouteRotationSubtitle": "在最佳路径和防洪模式之间切换", "appSettings_autoRouteRotationEnabled": "自动路径轮换已启用", "appSettings_autoRouteRotationDisabled": "自动路径轮换已禁用", "appSettings_battery": "电池", "appSettings_batteryChemistry": "电池化学", - "appSettings_batteryChemistryPerDevice": "设置每个设备 ({deviceName})", + "appSettings_batteryChemistryPerDevice": "为每个设备设置 ({deviceName})", "@appSettings_batteryChemistryPerDevice": { "placeholders": { "deviceName": { @@ -205,20 +212,20 @@ } } }, - "appSettings_batteryChemistryConnectFirst": "连接设备以选择", - "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", + "appSettings_batteryChemistryConnectFirst": "连接到设备以进行选择", + "appSettings_batteryNmc": "18650 型号,NMC 电池(3.0-4.2V)", "appSettings_batteryLifepo4": "磷酸铁锂 (2.6-3.65V)", - "appSettings_batteryLipo": "LiPo (3.0-4.2V)", - "appSettings_mapDisplay": "地图显示", - "appSettings_showRepeaters": "显示循环器", + "appSettings_batteryLipo": "锂离子电池 (3.0-4.2V)", + "appSettings_mapDisplay": "地图展示", + "appSettings_showRepeaters": "显示重复", "appSettings_showRepeatersSubtitle": "在地图上显示重复节点", "appSettings_showChatNodes": "显示聊天节点", "appSettings_showChatNodesSubtitle": "在地图上显示聊天节点", "appSettings_showOtherNodes": "显示其他节点", - "appSettings_showOtherNodesSubtitle": "显示其他节点类型在地图上", - "appSettings_timeFilter": "时间筛选", + "appSettings_showOtherNodesSubtitle": "在地图上显示其他节点类型", + "appSettings_timeFilter": "时间过滤器", "appSettings_timeFilterShowAll": "显示所有节点", - "appSettings_timeFilterShowLast": "显示来自过去 {hours} 小时的节点", + "appSettings_timeFilterShowLast": "Show nodes from last {hours} hours", "@appSettings_timeFilterShowLast": { "placeholders": { "hours": { @@ -227,15 +234,15 @@ } }, "appSettings_mapTimeFilter": "地图时间筛选", - "appSettings_showNodesDiscoveredWithin": "显示发现的节点在:", + "appSettings_showNodesDiscoveredWithin": "显示在以下范围内发现的节点:", "appSettings_allTime": "所有时间", - "appSettings_lastHour": "最后小时", - "appSettings_last6Hours": "最后6小时", - "appSettings_last24Hours": "最后24小时", + "appSettings_lastHour": "过去一小时", + "appSettings_last6Hours": "过去6小时", + "appSettings_last24Hours": "过去24小时", "appSettings_lastWeek": "上周", "appSettings_offlineMapCache": "离线地图缓存", "appSettings_noAreaSelected": "未选择任何区域", - "appSettings_areaSelectedZoom": "选中的区域(缩放至 {minZoom} - {maxZoom})", + "appSettings_areaSelectedZoom": "已选择区域(缩放至 {minZoom} - {maxZoom})", "@appSettings_areaSelectedZoom": { "placeholders": { "minZoom": { @@ -247,18 +254,18 @@ } }, "appSettings_debugCard": "调试", - "appSettings_appDebugLogging": "应用调试日志", - "appSettings_appDebugLoggingSubtitle": "记录应用调试消息以供故障排除", - "appSettings_appDebugLoggingEnabled": "应用调试日志已启用", - "appSettings_appDebugLoggingDisabled": "应用调试日志已禁用", - "contacts_title": "联系人", - "contacts_noContacts": "还没有联系人", - "contacts_contactsWillAppear": "设备会广播时,联系人会显示", + "appSettings_appDebugLogging": "应用程序调试日志", + "appSettings_appDebugLoggingSubtitle": "用于故障排除的日志应用程序调试消息", + "appSettings_appDebugLoggingEnabled": "调试日志已启用", + "appSettings_appDebugLoggingDisabled": "应用程序调试日志已禁用", + "contacts_title": "联系方式", + "contacts_noContacts": "目前还没有联系人", + "contacts_contactsWillAppear": "当设备发布广告时,联系方式会显示。", "contacts_searchContacts": "搜索联系人...", - "contacts_noUnreadContacts": "未读联系人", + "contacts_noUnreadContacts": "没有未读通讯", "contacts_noContactsFound": "未找到任何联系人或群组", "contacts_deleteContact": "删除联系人", - "contacts_removeConfirm": "从联系人中删除 {contactName} 吗?", + "contacts_removeConfirm": "Remove {contactName} from contacts?", "@contacts_removeConfirm": { "placeholders": { "contactName": { @@ -266,11 +273,12 @@ } } }, - "contacts_manageRepeater": "管理重复项", - "contacts_roomLogin": "房间登录", - "contacts_openChat": "打开聊天", - "contacts_editGroup": "编辑组", - "contacts_deleteGroup": "删除分组", + "contacts_manageRepeater": "管理重复器", + "contacts_manageRoom": "管理房间服务器", + "contacts_roomLogin": "服务器登录", + "contacts_openChat": "开放聊天", + "contacts_editGroup": "编辑小组", + "contacts_deleteGroup": "删除群组", "contacts_deleteGroupConfirm": "删除\"{groupName}\"?", "@contacts_deleteGroupConfirm": { "placeholders": { @@ -279,10 +287,10 @@ } } }, - "contacts_newGroup": "新组", - "contacts_groupName": "组名", - "contacts_groupNameRequired": "组名不能为空", - "contacts_groupAlreadyExists": "组“{name}”已存在", + "contacts_newGroup": "新的团体", + "contacts_groupName": "团体名称", + "contacts_groupNameRequired": "需要提供组名称", + "contacts_groupAlreadyExists": "名为\"{name}\"的组已经存在", "@contacts_groupAlreadyExists": { "placeholders": { "name": { @@ -291,10 +299,10 @@ } }, "contacts_filterContacts": "筛选联系人...", - "contacts_noContactsMatchFilter": "未找到匹配您的筛选条件的结果", + "contacts_noContactsMatchFilter": "未找到符合您筛选条件的联系人", "contacts_noMembers": "没有会员", - "contacts_lastSeenNow": "最后一次登录时间现在", - "contacts_lastSeenMinsAgo": "最后一次出现 {minutes} 分前", + "contacts_lastSeenNow": "最后一次被看到的时间", + "contacts_lastSeenMinsAgo": "Last seen {minutes} mins ago", "@contacts_lastSeenMinsAgo": { "placeholders": { "minutes": { @@ -302,8 +310,8 @@ } } }, - "contacts_lastSeenHourAgo": "最后一次出现前1小时", - "contacts_lastSeenHoursAgo": "最后一次出现 {hours} 小时前", + "contacts_lastSeenHourAgo": "最后一次被看到的时间:1小时前", + "contacts_lastSeenHoursAgo": "Last seen {hours} hours ago", "@contacts_lastSeenHoursAgo": { "placeholders": { "hours": { @@ -311,8 +319,8 @@ } } }, - "contacts_lastSeenDayAgo": "最后一次登录前一天", - "contacts_lastSeenDaysAgo": "最后一次出现 {days} 天前", + "contacts_lastSeenDayAgo": "最后一次被看到的时间是1天前", + "contacts_lastSeenDaysAgo": "Last seen {days} days ago", "@contacts_lastSeenDaysAgo": { "placeholders": { "days": { @@ -322,7 +330,7 @@ }, "channels_title": "频道", "channels_noChannelsConfigured": "未配置任何频道", - "channels_addPublicChannel": "添加公开频道", + "channels_addPublicChannel": "添加公共频道", "channels_searchChannels": "搜索频道...", "channels_noChannelsFound": "未找到任何频道", "channels_channelIndex": "频道 {index}", @@ -333,14 +341,14 @@ } } }, - "channels_hashtagChannel": "话题频道", - "channels_public": "公开", - "channels_private": "私有", - "channels_publicChannel": "公开频道", - "channels_privateChannel": "私聊频道", + "channels_hashtagChannel": "话题标签频道", + "channels_public": "公众", + "channels_private": "私人", + "channels_publicChannel": "公共频道", + "channels_privateChannel": "私密频道", "channels_editChannel": "编辑频道", "channels_deleteChannel": "删除频道", - "channels_deleteChannelConfirm": "删除\"{name}\"?此操作无法撤销。", + "channels_deleteChannelConfirm": "Delete \"{name}\"? This cannot be undone.", "@channels_deleteChannelConfirm": { "placeholders": { "name": { @@ -348,7 +356,7 @@ } } }, - "channels_channelDeleted": "频道“{name}”已删除", + "channels_channelDeleted": "删除频道 \"{name}\"", "@channels_channelDeleted": { "placeholders": { "name": { @@ -360,12 +368,12 @@ "channels_channelIndexLabel": "频道索引", "channels_channelName": "频道名称", "channels_usePublicChannel": "使用公共频道", - "channels_standardPublicPsk": "标准公钥共享密钥", + "channels_standardPublicPsk": "标准公共PSK", "channels_pskHex": "PSK (十六进制)", - "channels_generateRandomPsk": "生成随机PSK", - "channels_enterChannelName": "请输入频道名称", - "channels_pskMustBe32Hex": "PSK 必须是 32 个十六进制字符", - "channels_channelAdded": "频道“{name}”已添加", + "channels_generateRandomPsk": "生成随机的PSK(正交相移键控)", + "channels_enterChannelName": "请在此处输入频道名称", + "channels_pskMustBe32Hex": "PSK 必须包含 32 个十六进制字符。", + "channels_channelAdded": "添加频道 \"{name}\"", "@channels_channelAdded": { "placeholders": { "name": { @@ -382,7 +390,7 @@ } }, "channels_smazCompression": "SMAZ 压缩", - "channels_channelUpdated": "频道“{name}”已更新", + "channels_channelUpdated": "频道 \"{name}\" 已更新", "@channels_channelUpdated": { "placeholders": { "name": { @@ -390,16 +398,28 @@ } } }, - "channels_publicChannelAdded": "公共频道已添加", - "channels_sortBy": "按类型排序", - "channels_sortManual": "手动", - "channels_sortAZ": "A-Z", + "channels_publicChannelAdded": "已添加公共频道", + "channels_sortBy": "按排序", + "channels_sortManual": "手册", + "channels_sortAZ": "A 到 Z", "channels_sortLatestMessages": "最新消息", "channels_sortUnread": "未读", - "chat_noMessages": "目前还没有消息", - "chat_sendMessageToStart": "发送消息开始", - "chat_originalMessageNotFound": "找不到原始消息", - "chat_replyingTo": "回复 {name}", + "channels_createPrivateChannel": "创建私密频道", + "channels_createPrivateChannelDesc": "使用秘密密钥进行保护。", + "channels_joinPrivateChannel": "加入私密频道", + "channels_joinPrivateChannelDesc": "手动输入密钥。", + "channels_joinPublicChannel": "加入公共频道", + "channels_joinPublicChannelDesc": "任何人都可以加入这个频道。", + "channels_joinHashtagChannel": "加入一个带有特定标签的频道", + "channels_joinHashtagChannelDesc": "任何人都可以加入带有特定标签的频道。", + "channels_scanQrCode": "扫描二维码", + "channels_scanQrCodeComingSoon": "即将发布", + "channels_enterHashtag": "输入标签", + "channels_hashtagHint": "例如:#团队", + "chat_noMessages": "目前还没有收到任何消息。", + "chat_sendMessageToStart": "发送消息以开始", + "chat_originalMessageNotFound": "无法找到原始消息", + "chat_replyingTo": "Replying to {name}", "@chat_replyingTo": { "placeholders": { "name": { @@ -407,7 +427,7 @@ } } }, - "chat_replyTo": "回复 {name}", + "chat_replyTo": "Reply to {name}", "@chat_replyTo": { "placeholders": { "name": { @@ -415,8 +435,8 @@ } } }, - "chat_location": "位置", - "chat_sendMessageTo": "向{contactName}发送消息", + "chat_location": "地点", + "chat_sendMessageTo": "Send a message to {contactName}", "@chat_sendMessageTo": { "placeholders": { "contactName": { @@ -425,7 +445,7 @@ } }, "chat_typeMessage": "输入消息...", - "chat_messageTooLong": "消息太长了(最大 {maxBytes} 字节)。", + "chat_messageTooLong": "消息内容过长(最大 {maxBytes} 字节)。", "@chat_messageTooLong": { "placeholders": { "maxBytes": { @@ -435,8 +455,8 @@ }, "chat_messageCopied": "消息已复制", "chat_messageDeleted": "消息已删除", - "chat_retryingMessage": "重试", - "chat_retryCount": "重试 {current}/{max}", + "chat_retryingMessage": "重试消息", + "chat_retryCount": "Retry {current}/{max}", "@chat_retryCount": { "placeholders": { "current": { @@ -447,32 +467,32 @@ } } }, - "chat_sendGif": "发送GIF", + "chat_sendGif": "发送 GIF 动画", "chat_reply": "回复", - "chat_addReaction": "添加反应", + "chat_addReaction": "添加评论", "chat_me": "我", "emojiCategorySmileys": "表情符号", "emojiCategoryGestures": "手势", - "emojiCategoryHearts": "心", - "emojiCategoryObjects": "对象", - "gifPicker_title": "选择一个 GIF", - "gifPicker_searchHint": "搜索GIF...", + "emojiCategoryHearts": "心脏", + "emojiCategoryObjects": "物体", + "gifPicker_title": "选择一个 GIF 动画", + "gifPicker_searchHint": "搜索 GIF 动画...", "gifPicker_poweredBy": "由 GIPHY 提供支持", "gifPicker_noGifsFound": "未找到 GIF 动画", - "gifPicker_failedLoad": "GIF 加载失败", - "gifPicker_failedSearch": "搜索GIF失败", - "gifPicker_noInternet": "无网络连接", - "debugLog_appTitle": "应用调试日志", - "debugLog_bleTitle": "蓝牙调试日志", + "gifPicker_failedLoad": "无法加载 GIF 动画", + "gifPicker_failedSearch": "未能搜索 GIF 动画", + "gifPicker_noInternet": "没有互联网连接", + "debugLog_appTitle": "应用程序调试日志", + "debugLog_bleTitle": "BLE 调试日志", "debugLog_copyLog": "复制日志", - "debugLog_clearLog": "清除日志", + "debugLog_clearLog": "清晰的日志", "debugLog_copied": "调试日志已复制", - "debugLog_bleCopied": "蓝牙日志复制", - "debugLog_noEntries": "尚未生成调试日志", - "debugLog_enableInSettings": "启用应用调试日志记录设置", - "debugLog_frames": "帧", + "debugLog_bleCopied": "BLE 日志已复制", + "debugLog_noEntries": "目前还没有调试日志", + "debugLog_enableInSettings": "在设置中启用应用程序调试日志功能。", + "debugLog_frames": "框架", "debugLog_rawLogRx": "原始日志-RX", - "debugLog_noBleActivity": "目前还没有蓝牙活动。", + "debugLog_noBleActivity": "目前尚未有蓝牙低功耗(BLE)活动。", "debugFrame_length": "帧长度:{count} 字节", "@debugFrame_length": { "placeholders": { @@ -489,8 +509,8 @@ } } }, - "debugFrame_textMessageHeader": "短信框", - "debugFrame_destinationPubKey": "- 目的地公钥:{pubKey}", + "debugFrame_textMessageHeader": "短信模板:", + "debugFrame_destinationPubKey": "- 目标公钥:{pubKey}", "@debugFrame_destinationPubKey": { "placeholders": { "pubKey": { @@ -498,7 +518,7 @@ } } }, - "debugFrame_timestamp": "- 时间戳:{timestamp}", + "debugFrame_timestamp": "- Timestamp: {timestamp}", "@debugFrame_timestamp": { "placeholders": { "timestamp": { @@ -514,7 +534,7 @@ } } }, - "debugFrame_textType": "- 文本类型:{type} ({label})", + "debugFrame_textType": "- Text Type: {type} ({label})", "@debugFrame_textType": { "placeholders": { "type": { @@ -525,9 +545,9 @@ } } }, - "debugFrame_textTypeCli": "CLI", - "debugFrame_textTypePlain": "简洁", - "debugFrame_text": "- 文本:\"{text}\"", + "debugFrame_textTypeCli": "命令行界面", + "debugFrame_textTypePlain": "简单", + "debugFrame_text": "- 文本:“{text}”", "@debugFrame_text": { "placeholders": { "text": { @@ -535,16 +555,16 @@ } } }, - "debugFrame_hexDump": "十六进制数据", + "debugFrame_hexDump": "十六进制数据:", "chat_pathManagement": "路径管理", "chat_routingMode": "路由模式", - "chat_autoUseSavedPath": "自动(使用已保存路径)", + "chat_autoUseSavedPath": "自动(使用已保存的路径)", "chat_forceFloodMode": "强制洪水模式", - "chat_recentAckPaths": "最近的 ACK 路径 (点击以使用):", - "chat_pathHistoryFull": "路径历史已满。删除条目以添加新条目。", - "chat_hopSingular": "跳转", - "chat_hopPlural": "跳跃", - "chat_hopsCount": "{count} {count, plural, =1{跳跃} other{跳跃}}", + "chat_recentAckPaths": "最近使用的 ACK 路径(点击使用):", + "chat_pathHistoryFull": "路径历史已满。删除条目以添加新的条目。", + "chat_hopSingular": "跳跃", + "chat_hopPlural": "啤酒花", + "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}", "@chat_hopsCount": { "placeholders": { "count": { @@ -554,18 +574,18 @@ }, "chat_successes": "成功", "chat_removePath": "删除路径", - "chat_noPathHistoryYet": "还没有历史记录。\n发送消息以发现路径。", + "chat_noPathHistoryYet": "目前还没有历史记录。\n发送消息以查找路径。", "chat_pathActions": "路径操作:", "chat_setCustomPath": "设置自定义路径", "chat_setCustomPathSubtitle": "手动指定路由路径", - "chat_clearPath": "清除路径", - "chat_clearPathSubtitle": "强制下次发送时重新发现", - "chat_pathCleared": "路径已清除。下一条消息将重新发现路线。", - "chat_floodModeSubtitle": "使用应用栏中的路由切换开关", - "chat_floodModeEnabled": "防洪模式已启用。通过应用程序栏中的路由图标进行反转。", + "chat_clearPath": "明确的道路", + "chat_clearPathSubtitle": "在下一次发送时,重新尝试。", + "chat_pathCleared": "路径已清理。下一条消息将重新确定路线。", + "chat_floodModeSubtitle": "使用应用程序栏中的路由切换功能", + "chat_floodModeEnabled": "防洪模式已启用。通过应用程序栏中的路由图标进行切换。", "chat_fullPath": "完整路径", - "chat_pathDetailsNotAvailable": "路径详情尚未获取。请尝试发送消息以刷新。", - "chat_pathSetHops": "路径设置:{hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", + "chat_pathDetailsNotAvailable": "路径信息尚未提供。请尝试发送消息以刷新。", + "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}", "@chat_pathSetHops": { "placeholders": { "hopCount": { @@ -576,16 +596,16 @@ } } }, - "chat_pathSavedLocally": "已本地保存。连接以同步。", + "chat_pathSavedLocally": "已本地保存。连接以进行同步。", "chat_pathDeviceConfirmed": "设备已确认。", - "chat_pathDeviceNotConfirmed": "设备尚未确认。", - "chat_type": "输入", + "chat_pathDeviceNotConfirmed": "该设备尚未得到确认。", + "chat_type": "类型", "chat_path": "路径", "chat_publicKey": "公钥", "chat_compressOutgoingMessages": "压缩发送的消息", - "chat_floodForced": "强制溢出", - "chat_directForced": "强制直接", - "chat_hopsForced": "{count} 次跳跃 (强制)", + "chat_floodForced": "洪水(被迫)", + "chat_directForced": "直接(强制性的)", + "chat_hopsForced": "{count} 根啤酒花(人工种植)", "@chat_hopsForced": { "placeholders": { "count": { @@ -593,10 +613,10 @@ } } }, - "chat_floodAuto": "自动防洪", + "chat_floodAuto": "自动洪水", "chat_direct": "直接", - "chat_poiShared": "共享位置信息", - "chat_unread": "未读:{count}", + "chat_poiShared": "共享位置", + "chat_unread": "Unread: {count}", "@chat_unread": { "placeholders": { "count": { @@ -605,9 +625,9 @@ } }, "chat_openLink": "打开链接?", - "chat_openLinkConfirmation": "您想在浏览器中打开此链接吗?", - "chat_open": "打开", - "chat_couldNotOpenLink": "无法打开链接:{url}", + "chat_openLinkConfirmation": "您想用浏览器打开这个链接吗?", + "chat_open": "开放", + "chat_couldNotOpenLink": "[保存:{url}]\n无法打开链接:{url}", "@chat_couldNotOpenLink": { "placeholders": { "url": { @@ -615,11 +635,11 @@ } } }, - "chat_invalidLink": "链接格式无效", - "map_title": "节点地图", - "map_noNodesWithLocation": "没有具有位置数据的节点", - "map_nodesNeedGps": "节点需要共享它们的 GPS 坐标\n才能在地图上显示", - "map_nodesCount": "节点:{count}", + "chat_invalidLink": "无效的链接格式", + "map_title": "节点图", + "map_noNodesWithLocation": "没有包含位置信息的节点", + "map_nodesNeedGps": "节点需要共享其 GPS 坐标,以便在地图上显示", + "map_nodesCount": "Nodes: {count}", "@map_nodesCount": { "placeholders": { "count": { @@ -627,7 +647,7 @@ } } }, - "map_pinsCount": "针:{count}", + "map_pinsCount": "Pins: {count}", "@map_pinsCount": { "placeholders": { "count": { @@ -639,23 +659,23 @@ "map_repeater": "重复器", "map_room": "房间", "map_sensor": "传感器", - "map_pinDm": "私信 (DM)", - "map_pinPrivate": "私密模式", - "map_pinPublic": "公开(公版)", - "map_lastSeen": "最后一次登录", + "map_pinDm": "PIN (直接消息)", + "map_pinPrivate": "私密", + "map_pinPublic": "公开", + "map_lastSeen": "最后一次被看到", "map_disconnectConfirm": "您确定要断开与此设备的连接吗?", "map_from": "从", "map_source": "来源", "map_flags": "旗帜", - "map_shareMarkerHere": "分享标记在此", - "map_pinLabel": "固定标签", + "map_shareMarkerHere": "在此分享标记", + "map_pinLabel": "标签", "map_label": "标签", - "map_pointOfInterest": "兴趣点", - "map_sendToContact": "发送给联系人", + "map_pointOfInterest": "值得参观的地方", + "map_sendToContact": "发送给联系", "map_sendToChannel": "发送到频道", "map_noChannelsAvailable": "没有可用的频道", - "map_publicLocationShare": "公共位置共享", - "map_publicLocationShareConfirm": "您即将分享一个位置在 {channelLabel}。此频道公开,任何拥有 PSK 的人都可以看到它。", + "map_publicLocationShare": "公共场所共享", + "map_publicLocationShareConfirm": "[保存:{channelLabel}]\n您即将分享一个位置,该位置位于 {channelLabel}。 此频道是公开的,任何拥有 PSK 的人都可以看到它。", "@map_publicLocationShareConfirm": { "placeholders": { "channelLabel": { @@ -664,25 +684,25 @@ } }, "map_connectToShareMarkers": "连接设备以共享标记", - "map_filterNodes": "筛选节点", + "map_filterNodes": "过滤节点", "map_nodeTypes": "节点类型", "map_chatNodes": "聊天节点", "map_repeaters": "重复器", "map_otherNodes": "其他节点", - "map_keyPrefix": "键前缀", - "map_filterByKeyPrefix": "按关键词前缀筛选", + "map_keyPrefix": "关键前缀", + "map_filterByKeyPrefix": "按关键前缀筛选", "map_publicKeyPrefix": "公钥前缀", "map_markers": "标记", "map_showSharedMarkers": "显示共享标记", - "map_lastSeenTime": "最后一次查看时间", - "map_sharedPin": "共享 PIN", + "map_lastSeenTime": "最后一次被看到的时间", + "map_sharedPin": "共享密码", "map_joinRoom": "加入房间", - "map_manageRepeater": "管理重复项", + "map_manageRepeater": "管理重复器", "mapCache_title": "离线地图缓存", - "mapCache_selectAreaFirst": "选择一个区域进行缓存", - "mapCache_noTilesToDownload": "该区域没有可下载的瓦片。", - "mapCache_downloadTilesTitle": "下载瓦片", - "mapCache_downloadTilesPrompt": "下载 {count} 个瓦片用于离线使用?", + "mapCache_selectAreaFirst": "选择一个用于缓存的区域", + "mapCache_noTilesToDownload": "此区域没有可下载的瓦片。", + "mapCache_downloadTilesTitle": "下载瓷砖", + "mapCache_downloadTilesPrompt": "[保存:{count}]\n下载 {count} 个图片用于离线使用?", "@mapCache_downloadTilesPrompt": { "placeholders": { "count": { @@ -691,7 +711,7 @@ } }, "mapCache_downloadAction": "下载", - "mapCache_cachedTiles": "已缓存 {count} 个瓦片", + "mapCache_cachedTiles": "缓存 {count} 个瓦片", "@mapCache_cachedTiles": { "placeholders": { "count": { @@ -699,7 +719,7 @@ } } }, - "mapCache_cachedTilesWithFailed": "已缓存 {downloaded} 个瓦片 ({failed} 失败)", + "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)", "@mapCache_cachedTilesWithFailed": { "placeholders": { "downloaded": { @@ -711,13 +731,13 @@ } }, "mapCache_clearOfflineCacheTitle": "清除离线缓存", - "mapCache_clearOfflineCachePrompt": "删除所有缓存地图瓦片?", + "mapCache_clearOfflineCachePrompt": "清除所有缓存的地图瓦片", "mapCache_offlineCacheCleared": "离线缓存已清除", "mapCache_noAreaSelected": "未选择任何区域", "mapCache_cacheArea": "缓存区域", "mapCache_useCurrentView": "使用当前视图", - "mapCache_zoomRange": "缩放范围", - "mapCache_estimatedTiles": "预计瓦片数量:{count}", + "mapCache_zoomRange": "变焦范围", + "mapCache_estimatedTiles": "Estimated tiles: {count}", "@mapCache_estimatedTiles": { "placeholders": { "count": { @@ -725,7 +745,7 @@ } } }, - "mapCache_downloadedTiles": "已下载 {completed} / {total}", + "mapCache_downloadedTiles": "Downloaded {completed} / {total}", "@mapCache_downloadedTiles": { "placeholders": { "completed": { @@ -736,9 +756,9 @@ } } }, - "mapCache_downloadTilesButton": "下载瓦片", + "mapCache_downloadTilesButton": "下载瓷砖", "mapCache_clearCacheButton": "清除缓存", - "mapCache_failedDownloads": "下载失败:{count}", + "mapCache_failedDownloads": "Failed downloads: {count}", "@mapCache_failedDownloads": { "placeholders": { "count": { @@ -746,7 +766,7 @@ } } }, - "mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}", + "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "@mapCache_boundsLabel": { "placeholders": { "north": { @@ -764,7 +784,7 @@ } }, "time_justNow": "刚才", - "time_minutesAgo": "{minutes}分钟前", + "time_minutesAgo": "{minutes}m ago", "@time_minutesAgo": { "placeholders": { "minutes": { @@ -772,7 +792,7 @@ } } }, - "time_hoursAgo": "{hours}小时前", + "time_hoursAgo": "{hours}h ago", "@time_hoursAgo": { "placeholders": { "hours": { @@ -780,7 +800,7 @@ } } }, - "time_daysAgo": "{days} 天前", + "time_daysAgo": "{days}天前", "@time_daysAgo": { "placeholders": { "days": { @@ -790,10 +810,10 @@ }, "time_hour": "小时", "time_hours": "小时", - "time_day": "今天", + "time_day": "一天", "time_days": "天", - "time_week": "本周", - "time_weeks": "几周", + "time_week": "一周", + "time_weeks": "周", "time_month": "月份", "time_months": "月份", "time_minutes": "分钟", @@ -801,20 +821,20 @@ "dialog_disconnect": "断开", "dialog_disconnectConfirm": "您确定要断开与此设备的连接吗?", "login_repeaterLogin": "重复登录", - "login_roomLogin": "房间登录", + "login_roomLogin": "服务器登录", "login_password": "密码", "login_enterPassword": "请输入密码", "login_savePassword": "保存密码", - "login_savePasswordSubtitle": "密码将安全地存储在这个设备上", - "login_repeaterDescription": "输入重复密码以访问设置和状态。", - "login_roomDescription": "输入房间密码以访问设置和状态。", + "login_savePasswordSubtitle": "密码将安全地存储在 данном设备上", + "login_repeaterDescription": "输入重复器密码,即可访问设置和状态。", + "login_roomDescription": "输入密码进入房间,即可访问设置和状态。", "login_routing": "路由", "login_routingMode": "路由模式", - "login_autoUseSavedPath": "自动(使用已保存路径)", + "login_autoUseSavedPath": "自动(使用已保存的路径)", "login_forceFloodMode": "强制洪水模式", "login_managePaths": "管理路径", "login_login": "登录", - "login_attempt": "尝试 {current}/{max}", + "login_attempt": "Attempt {current}/{max}", "@login_attempt": { "placeholders": { "current": { @@ -825,7 +845,7 @@ } } }, - "login_failed": "登录失败:{error}", + "login_failed": "Login failed: {error}", "@login_failed": { "placeholders": { "error": { @@ -833,10 +853,10 @@ } } }, - "login_failedMessage": "登录失败。密码不正确或中继器不可达。", + "login_failedMessage": "登录失败。可能是密码错误,也可能是无法连接到服务器。", "common_reload": "重新加载", - "common_clear": "清除", - "path_currentPath": "当前路径:{path}", + "common_clear": "清晰", + "path_currentPath": "Current path: {path}", "@path_currentPath": { "placeholders": { "path": { @@ -844,7 +864,7 @@ } } }, - "path_usingHopsPath": "使用 {count} {count, plural, =1{hop} other{hops}} 路径", + "path_usingHopsPath": "使用 {count} {count, plural, =1{hop} other{hops}} 条路径", "@path_usingHopsPath": { "placeholders": { "count": { @@ -854,14 +874,14 @@ }, "path_enterCustomPath": "输入自定义路径", "path_currentPathLabel": "当前路径", - "path_hexPrefixInstructions": "输入2个字符的十六进制前缀,每个前缀之间用逗号分隔。", - "path_hexPrefixExample": "A1,F2,3C (每个节点使用其公钥的第一字节)", - "path_labelHexPrefixes": "十六进制前缀", - "path_helperMaxHops": "最大 64 步跳。每个前缀是 2 个十六进制字符(1 字节)", - "path_selectFromContacts": "或从联系人中选择:", - "path_noRepeatersFound": "未找到任何重复器或房间服务器。", - "path_customPathsRequire": "自定义路径需要中间跳转,这些跳转可以传递消息。", - "path_invalidHexPrefixes": "无效的十六进制前缀:{prefixes}", + "path_hexPrefixInstructions": "请输入每个跳跃步骤的 2 个字符的十六进制前缀,用逗号分隔。", + "path_hexPrefixExample": "例如:A1, F2, 3C (每个节点使用其公钥的第一字节)", + "path_labelHexPrefixes": "路径(十六进制前缀)", + "path_helperMaxHops": "最大 64 个“hop”(跳跃)。每个前缀由 2 个十六进制字符(1 字节)组成。", + "path_selectFromContacts": "或者从联系人列表中选择:", + "path_noRepeatersFound": "未找到任何重复设备或房间服务器。", + "path_customPathsRequire": "自定义路径需要中间节点,这些节点可以转发消息。", + "path_invalidHexPrefixes": "Invalid hex prefixes: {prefixes}", "@path_invalidHexPrefixes": { "placeholders": { "prefixes": { @@ -872,23 +892,26 @@ "path_tooLong": "路径太长。允许的最大跳跃次数为 64 次。", "path_setPath": "设置路径", "repeater_management": "重复器管理", + "room_management": "服务器管理", "repeater_managementTools": "管理工具", "repeater_status": "状态", "repeater_statusSubtitle": "查看重复器状态、统计信息和邻居", - "repeater_telemetry": "遥测", - "repeater_telemetrySubtitle": "查看传感器和系统状态的Telemetry数据", - "repeater_cli": "CLI", - "repeater_cliSubtitle": "发送命令到重复器", + "repeater_telemetry": "远程监控", + "repeater_telemetrySubtitle": "查看传感器和系统状态的数据。", + "repeater_cli": "命令行界面", + "repeater_cliSubtitle": "向复用器发送指令", + "repeater_neighbours": "邻居", + "repeater_neighboursSubtitle": "查看邻居节点(无需中间节点)。", "repeater_settings": "设置", "repeater_settingsSubtitle": "配置重复器参数", "repeater_statusTitle": "重复器状态", "repeater_routingMode": "路由模式", - "repeater_autoUseSavedPath": "自动(使用已保存路径)", + "repeater_autoUseSavedPath": "自动(使用已保存的路径)", "repeater_forceFloodMode": "强制洪水模式", "repeater_pathManagement": "路径管理", - "repeater_refresh": "刷新", + "repeater_refresh": "更新", "repeater_statusRequestTimeout": "状态请求超时。", - "repeater_errorLoadingStatus": "错误加载状态:{error}", + "repeater_errorLoadingStatus": "Error loading status: {error}", "@repeater_errorLoadingStatus": { "placeholders": { "error": { @@ -898,18 +921,18 @@ }, "repeater_systemInformation": "系统信息", "repeater_battery": "电池", - "repeater_clockAtLogin": "时间 (登录时)", - "repeater_uptime": "可用时间", + "repeater_clockAtLogin": "登录时的时间", + "repeater_uptime": "正常运行时间", "repeater_queueLength": "排队长度", "repeater_debugFlags": "调试标志", - "repeater_radioStatistics": "无线电统计", - "repeater_lastRssi": "上次RSSI", - "repeater_lastSnr": "最后 SNR", - "repeater_noiseFloor": "噪声地板", - "repeater_txAirtime": "TX Airtime", - "repeater_rxAirtime": "RX Airtime", + "repeater_radioStatistics": "广播统计", + "repeater_lastRssi": "上次的 RSSI 值", + "repeater_lastSnr": "最后一次信噪比", + "repeater_noiseFloor": "噪声水平", + "repeater_txAirtime": "TX 频道预留时间", + "repeater_rxAirtime": "RX 空时", "repeater_packetStatistics": "数据包统计", - "repeater_sent": "已发送", + "repeater_sent": "发送", "repeater_received": "已收到", "repeater_duplicates": "重复", "repeater_daysHoursMinsSecs": "{days}天 {hours}小时 {minutes}分 {seconds}秒", @@ -929,7 +952,7 @@ } } }, - "repeater_packetTxTotal": "总计:{total}, 洪流:{flood}, 直连:{direct}", + "repeater_packetTxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetTxTotal": { "placeholders": { "total": { @@ -943,7 +966,7 @@ } } }, - "repeater_packetRxTotal": "总计:{total}, 洪流:{flood}, 直连:{direct}", + "repeater_packetRxTotal": "Total: {total}, Flood: {flood}, Direct: {direct}", "@repeater_packetRxTotal": { "placeholders": { "total": { @@ -957,7 +980,7 @@ } } }, - "repeater_duplicatesFloodDirect": "洪水:{flood}, 直通:{direct}", + "repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}", "@repeater_duplicatesFloodDirect": { "placeholders": { "flood": { @@ -968,7 +991,7 @@ } } }, - "repeater_duplicatesTotal": "总计:{total}", + "repeater_duplicatesTotal": "Total: {total}", "@repeater_duplicatesTotal": { "placeholders": { "total": { @@ -976,37 +999,37 @@ } } }, - "repeater_settingsTitle": "重复设置", + "repeater_settingsTitle": "重复器设置", "repeater_basicSettings": "基本设置", "repeater_repeaterName": "重复器名称", - "repeater_repeaterNameHelper": "显示此重复器的名称", + "repeater_repeaterNameHelper": "此复播器的显示名称", "repeater_adminPassword": "管理员密码", "repeater_adminPasswordHelper": "完整访问密码", "repeater_guestPassword": "访客密码", "repeater_guestPasswordHelper": "只读访问密码", - "repeater_radioSettings": "射频设置", + "repeater_radioSettings": "收音机设置", "repeater_frequencyMhz": "频率 (MHz)", - "repeater_frequencyHelper": "300-2500 MHz", - "repeater_txPower": "TX Power", + "repeater_frequencyHelper": "300-2500 兆赫", + "repeater_txPower": "TX 功率", "repeater_txPowerHelper": "1-30 dBm", "repeater_bandwidth": "带宽", - "repeater_spreadingFactor": "扩散因子", + "repeater_spreadingFactor": "传播系数", "repeater_codingRate": "编码速率", "repeater_locationSettings": "位置设置", "repeater_latitude": "纬度", - "repeater_latitudeHelper": "十进度的数字(例如:37.7749)", + "repeater_latitudeHelper": "十进制度(例如:37.7749)", "repeater_longitude": "经度", - "repeater_longitudeHelper": "十进度的数字(例如:-122.4194)", - "repeater_features": "功能", + "repeater_longitudeHelper": "十进制度(例如:-122.4194)", + "repeater_features": "特点", "repeater_packetForwarding": "数据包转发", - "repeater_packetForwardingSubtitle": "启用重复器以转发数据包", + "repeater_packetForwardingSubtitle": "启用重复器,使其能够转发数据包", "repeater_guestAccess": "访客访问", - "repeater_guestAccessSubtitle": "允许访客仅读访问", + "repeater_guestAccessSubtitle": "允许访客仅限读取权限", "repeater_privacyMode": "隐私模式", - "repeater_privacyModeSubtitle": "隐藏在广告中的姓名/位置", + "repeater_privacyModeSubtitle": "在广告中隐藏姓名/位置", "repeater_advertisementSettings": "广告设置", - "repeater_localAdvertInterval": "本地广告间隔", - "repeater_localAdvertIntervalMinutes": "{minutes} 分钟", + "repeater_localAdvertInterval": "本地广告投放时间段", + "repeater_localAdvertIntervalMinutes": "{minutes} minutes", "@repeater_localAdvertIntervalMinutes": { "placeholders": { "minutes": { @@ -1014,8 +1037,8 @@ } } }, - "repeater_floodAdvertInterval": "洪水广告间隔", - "repeater_floodAdvertIntervalHours": "{hours} 小时", + "repeater_floodAdvertInterval": "洪水广告播放间隔", + "repeater_floodAdvertIntervalHours": "{hours} hours", "@repeater_floodAdvertIntervalHours": { "placeholders": { "hours": { @@ -1023,19 +1046,19 @@ } } }, - "repeater_encryptedAdvertInterval": "加密广告间隔", + "repeater_encryptedAdvertInterval": "加密的广告投放时间段", "repeater_dangerZone": "危险区域", "repeater_rebootRepeater": "重启重复器", - "repeater_rebootRepeaterSubtitle": "重启重复器设备", - "repeater_rebootRepeaterConfirm": "您确定要重启这个中继器吗?", + "repeater_rebootRepeaterSubtitle": "重新启动重复器设备", + "repeater_rebootRepeaterConfirm": "您确定要重新启动这个中继器吗?", "repeater_regenerateIdentityKey": "重新生成身份密钥", "repeater_regenerateIdentityKeySubtitle": "生成新的公钥/私钥对", - "repeater_regenerateIdentityKeyConfirm": "这将生成一个重复器的新身份。继续吗?", + "repeater_regenerateIdentityKeyConfirm": "这将为复用器生成一个新的身份。继续吗?", "repeater_eraseFileSystem": "删除文件系统", "repeater_eraseFileSystemSubtitle": "格式化重复文件系统", - "repeater_eraseFileSystemConfirm": "警告:这将擦除重复器上的所有数据。 这无法撤销!", - "repeater_eraseSerialOnly": "通过串行控制台才能删除。", - "repeater_commandSent": "命令已发送:{command}", + "repeater_eraseFileSystemConfirm": "警告:此操作将清除复用器上的所有数据。 无法恢复!", + "repeater_eraseSerialOnly": "“Erase”功能仅可通过串行控制台使用。", + "repeater_commandSent": "Command sent: {command}", "@repeater_commandSent": { "placeholders": { "command": { @@ -1043,7 +1066,7 @@ } } }, - "repeater_errorSendingCommand": "发送命令时出错:{error}", + "repeater_errorSendingCommand": "Error sending command: {error}", "@repeater_errorSendingCommand": { "placeholders": { "error": { @@ -1052,8 +1075,8 @@ } }, "repeater_confirm": "确认", - "repeater_settingsSaved": "设置已保存成功", - "repeater_errorSavingSettings": "保存设置出错:{error}", + "repeater_settingsSaved": "设置已成功保存", + "repeater_errorSavingSettings": "Error saving settings: {error}", "@repeater_errorSavingSettings": { "placeholders": { "error": { @@ -1061,15 +1084,15 @@ } } }, - "repeater_refreshBasicSettings": "刷新基本设置", - "repeater_refreshRadioSettings": "刷新无线电设置", - "repeater_refreshTxPower": "刷新 TX 电量", - "repeater_refreshLocationSettings": "刷新位置设置", + "repeater_refreshBasicSettings": "重置基本设置", + "repeater_refreshRadioSettings": "重置收音机设置", + "repeater_refreshTxPower": "重置 TX 电源", + "repeater_refreshLocationSettings": "重置位置设置", "repeater_refreshPacketForwarding": "刷新包转发", - "repeater_refreshGuestAccess": "刷新访客访问", - "repeater_refreshPrivacyMode": "刷新隐私模式", - "repeater_refreshAdvertisementSettings": "刷新广告设置", - "repeater_refreshed": "{label} 已刷新", + "repeater_refreshGuestAccess": "重新获取访客访问权限", + "repeater_refreshPrivacyMode": "重置隐私模式", + "repeater_refreshAdvertisementSettings": "重置广告设置", + "repeater_refreshed": "{label} refreshed", "@repeater_refreshed": { "placeholders": { "label": { @@ -1077,7 +1100,7 @@ } } }, - "repeater_errorRefreshing": "刷新 {label} 时出错", + "repeater_errorRefreshing": "[保存:{label}]\n刷新 {label} 时出错", "@repeater_errorRefreshing": { "placeholders": { "label": { @@ -1085,18 +1108,18 @@ } } }, - "repeater_cliTitle": "重复器命令行工具", - "repeater_debugNextCommand": "调试下一步命令", + "repeater_cliTitle": "重复器命令行界面", + "repeater_debugNextCommand": "调试下一条命令", "repeater_commandHelp": "帮助", - "repeater_clearHistory": "清除历史", - "repeater_noCommandsSent": "尚未发送任何命令", - "repeater_typeCommandOrUseQuick": "输入命令或使用快捷命令", + "repeater_clearHistory": "清晰的历史", + "repeater_noCommandsSent": "尚未发送任何指令", + "repeater_typeCommandOrUseQuick": "在下方输入命令,或使用快捷命令。", "repeater_enterCommandHint": "输入命令...", - "repeater_previousCommand": "上一个命令", - "repeater_nextCommand": "下一步命令", - "repeater_enterCommandFirst": "请输入一个命令", - "repeater_cliCommandFrameTitle": "CLI 命令窗口", - "repeater_cliCommandError": "错误:{error}", + "repeater_previousCommand": "之前的命令", + "repeater_nextCommand": "下一个指令", + "repeater_enterCommandFirst": "首先输入一个命令", + "repeater_cliCommandFrameTitle": "CLI 命令框架", + "repeater_cliCommandError": "Error: {error}", "@repeater_cliCommandError": { "placeholders": { "error": { @@ -1105,80 +1128,80 @@ } }, "repeater_cliQuickGetName": "获取姓名", - "repeater_cliQuickGetRadio": "获取收音机", + "repeater_cliQuickGetRadio": "收听广播", "repeater_cliQuickGetTx": "获取 TX", "repeater_cliQuickNeighbors": "邻居", "repeater_cliQuickVersion": "版本", - "repeater_cliQuickAdvertise": "发布", + "repeater_cliQuickAdvertise": "发布广告", "repeater_cliQuickClock": "时钟", - "repeater_cliHelpAdvert": "发送广告包", - "repeater_cliHelpReboot": "重启设备。(请注意,可能会出现“超时”现象,这是正常现象)", + "repeater_cliHelpAdvert": "发送广告资料包", + "repeater_cliHelpReboot": "重置设备。 (请注意,您可能会收到“超时”错误,这是正常的现象)", "repeater_cliHelpClock": "显示每个设备的当前时间。", - "repeater_cliHelpPassword": "设置设备的新管理员密码。", + "repeater_cliHelpPassword": "为设备设置新的管理员密码。", "repeater_cliHelpVersion": "显示设备版本和固件构建日期。", - "repeater_cliHelpClearStats": "重置各种统计数值为零。", - "repeater_cliHelpSetAf": "设置空闲时间因子。", - "repeater_cliHelpSetTx": "设置 LoRa 传输功率 (重置生效)", - "repeater_cliHelpSetRepeat": "启用或禁用此节点的重复器角色。", - "repeater_cliHelpSetAllowReadOnly": "(房间服务器) 如果“开”了,则空密码登录将被允许,但不能向房间发布内容。(仅限读取)", - "repeater_cliHelpSetFloodMax": "设置最大换路包数量(如果 >= 最大,则不转发包)。", - "repeater_cliHelpSetIntThresh": "设置干扰阈值(以 dB 为单位)。默认值为 14。将设置为 0 以禁用通道干扰检测。", - "repeater_cliHelpSetAgcResetInterval": "设置间隔以重置自动增益控制器。将设置为 0 以禁用。", - "repeater_cliHelpSetMultiAcks": "启用或禁用“双 ACKs”功能。", - "repeater_cliHelpSetAdvertInterval": "设置定时器间隔时间为分钟,以发送本地(零跳)广告包。将设置为0以禁用。", - "repeater_cliHelpSetFloodAdvertInterval": "设置定时器间隔时间为小时,以发送洪水广告包。将设置为 0 以禁用。", - "repeater_cliHelpSetGuestPassword": "设置/更新客人密码。(对于重复器,客人在登录时可以发送“获取统计”请求)", + "repeater_cliHelpClearStats": "重置各种统计指标,将其设置为零。", + "repeater_cliHelpSetAf": "设置时间因素。", + "repeater_cliHelpSetTx": "设置 LoRa 传输功率,单位为 dBm (相对于参考值)。 (重启以应用更改)", + "repeater_cliHelpSetRepeat": "启用或禁用此节点的重复器功能。", + "repeater_cliHelpSetAllowReadOnly": "(房间服务器)如果设置为“开启”,则允许使用空密码登录,但无法向房间发送消息(只能进行读取)。", + "repeater_cliHelpSetFloodMax": "设置最大传入数据包的跳数(如果大于或等于最大值,则不进行转发)。", + "repeater_cliHelpSetIntThresh": "设置干扰阈值(以dB为单位)。默认值为14。将设置为0以禁用频道干扰检测。", + "repeater_cliHelpSetAgcResetInterval": "设置间隔时间,用于重置自动增益控制器。设置为 0 以禁用。", + "repeater_cliHelpSetMultiAcks": "启用或禁用“双重确认”功能。", + "repeater_cliHelpSetAdvertInterval": "设置定时器间隔,单位为分钟,用于发送本地(无中继)的广告数据包。 将设置为 0 以禁用。", + "repeater_cliHelpSetFloodAdvertInterval": "设置定时器间隔时间为小时,以便发送广告信息包。将设置为 0 以禁用。", + "repeater_cliHelpSetGuestPassword": "设置/更新访客密码。 (对于访客,登录请求可以发送“获取统计”请求)", "repeater_cliHelpSetName": "设置广告名称。", - "repeater_cliHelpSetLat": "设置广告地图纬度(十进制度)", - "repeater_cliHelpSetLon": "设置广告地图经度 (十进位)", - "repeater_cliHelpSetRadio": "设置全新的无线电参数,并保存到偏好设置。需要执行“重启”命令才能应用。", - "repeater_cliHelpSetRxDelay": "设置(实验性)的基础(必须大于 1 才能生效)是用于对接收到的数据包应用轻微延迟,基于信号强度/得分。将设置为 0 以禁用。", - "repeater_cliHelpSetTxDelay": "设置一个与时间-在空气中(time-on-air)的系数,用于洪水模式的数据包,并结合随机插槽系统,以延迟其转发。(以降低碰撞的可能性)", - "repeater_cliHelpSetDirectTxDelay": "与txdelay相同,但用于为直接模式包的转发应用随机延迟。", - "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥梁", - "repeater_cliHelpSetBridgeDelay": "设置在重新发送数据包之前延迟时间。", - "repeater_cliHelpSetBridgeSource": "选择桥梁是否会重传接收到的数据包或发送的数据包。", - "repeater_cliHelpSetBridgeBaud": "设置rs232桥接的串口链路波特率。", - "repeater_cliHelpSetBridgeSecret": "设置 espnow 桥的秘密。", - "repeater_cliHelpSetAdcMultiplier": "设置自定义因子以调整报告的电池电压(仅限部分板卡支持)。", - "repeater_cliHelpTempRadio": "设置临时无线电参数,持续指定的分钟数,之后恢复为原始无线电参数。(不保存到偏好设置)。", - "repeater_cliHelpSetPerm": "修改ACL。如果“权限”为零,则删除匹配的条目(通过pubkey前缀)。如果pubkey-hex的完整长度且当前不在ACL中,则添加新条目。通过匹配pubkey前缀更新条目。权限位因固件角色而异,但低2位为:0(Guest)、1(只读)、2(读写)、3(Admin)", - "repeater_cliHelpGetBridgeType": "获取桥接类型:无,RS232,ESPNow", + "repeater_cliHelpSetLat": "设置广告地图的纬度。(以十进制表示)", + "repeater_cliHelpSetLon": "设置广告地图的经度。 (十进制度)", + "repeater_cliHelpSetRadio": "完全重新设置无线电参数,并保存到偏好设置。需要执行“重启”命令才能生效。", + "repeater_cliHelpSetRxDelay": "设置(实验性):设置一个基础值(必须大于1才能生效),用于对接收到的数据包进行轻微延迟处理,该延迟值基于信号强度/评分。将该值设置为0以禁用。", + "repeater_cliHelpSetTxDelay": "通过将一个因子与“浮动模式”数据包的时间在空中停留时间相乘,并结合随机的“时隙”系统,来延迟其转发,从而降低数据包冲突的概率。", + "repeater_cliHelpSetDirectTxDelay": "与txdelay相同,但用于对直接模式数据包的转发进行随机延迟。", + "repeater_cliHelpSetBridgeEnabled": "启用/禁用桥接。", + "repeater_cliHelpSetBridgeDelay": "在重新发送数据包之前,设置延迟时间。", + "repeater_cliHelpSetBridgeSource": "选择桥接器是否会转发收到的数据包,还是转发发送的数据包。", + "repeater_cliHelpSetBridgeBaud": "为 RS232 桥接设置串行链路的波特率。", + "repeater_cliHelpSetBridgeSecret": "设置 ESPNOW 桥的秘密。", + "repeater_cliHelpSetAdcMultiplier": "设置自定义因子,用于调整报告的电池电压(仅在特定板上支持)。", + "repeater_cliHelpTempRadio": "设置临时收音机参数,持续指定分钟数,之后恢复到原始收音机参数。(不保存到偏好设置)。", + "repeater_cliHelpSetPerm": "修改 ACL。如果 \"permissions\" 的值为 0,则删除与 pubkey 相关的条目。如果 pubkey-hex 完整且当前不在 ACL 中,则添加新的条目。通过匹配 pubkey 相关的前缀来更新条目。不同固件角色的权限位有所不同,但低 2 位分别对应:0 (访客)、1 (只读)、2 (读写)、3 (管理员)。", + "repeater_cliHelpGetBridgeType": "支持桥接模式、RS232、ESPNOW。", "repeater_cliHelpLogStart": "开始将数据包记录到文件系统。", - "repeater_cliHelpLogStop": "停止将数据包记录到文件系统。", - "repeater_cliHelpLogErase": "删除文件系统中的包日志。", - "repeater_cliHelpNeighbors": "显示通过零跳广告收听的其他重复节点列表。 每行是 id-prefix-hex:时间戳:snr-times-4", - "repeater_cliHelpNeighborRemove": "移除邻居列表中第一个匹配的条目(通过十六进制 pubkey 前缀)。", - "repeater_cliHelpRegion": "(仅显示区域) 列出所有已定义的区域和当前的防洪权限。", - "repeater_cliHelpRegionLoad": "注意:这是一个特殊的多命令调用。 随后的每个命令都是一个区域名称(用空格缩进以指示父级层次结构,至少需要一个空格)。 以发送一个空行/命令结束。", - "repeater_cliHelpRegionGet": "搜索具有给定名称前缀的区域(或“”用于全局范围)。回复为“-> region-name (parent-name) ‘F’”", - "repeater_cliHelpRegionPut": "添加或更新区域定义,使用指定名称。", - "repeater_cliHelpRegionRemove": "删除指定名称的区域定义。(必须没有子区域)", - "repeater_cliHelpRegionAllowf": "设置指定区域的“洪水”权限。(“”代表全局/遗留范围)", - "repeater_cliHelpRegionDenyf": "移除指定区域的‘F’lood权限。 (注意:目前阶段不建议在此范围内使用,尤其是全局/旧版范围!!)", - "repeater_cliHelpRegionHome": "回复当前“主页”区域。 (注意尚未应用,保留用于未来)", - "repeater_cliHelpRegionHomeSet": "设置‘主页’区域。", - "repeater_cliHelpRegionSave": "保存区域列表/地图到存储。", - "repeater_cliHelpGps": "显示GPS状态。当GPS关闭时,回复仅为“关闭”,如果已开启,则回复为“开启”、“状态”、“定位”和卫星数量。", - "repeater_cliHelpGpsOnOff": "切换 GPS 开启状态。", - "repeater_cliHelpGpsSync": "同步节点时间与 GPS 钟。", - "repeater_cliHelpGpsSetLoc": "设置节点位置至 GPS 坐标并保存偏好设置。", - "repeater_cliHelpGpsAdvert": "提供节点广告配置位置:\n- none:不包含位置在广告中\n- share:分享 GPS 位置(来自 SensorManager)\n- prefs:在偏好设置中投放位置", - "repeater_cliHelpGpsAdvertSet": "设置广告位置配置。", + "repeater_cliHelpLogStop": "停止将数据包记录写入文件系统。", + "repeater_cliHelpLogErase": "从文件系统中删除所有已记录的包信息。", + "repeater_cliHelpNeighbors": "显示了通过零跳广告收到的其他复用节点列表。 每行包含:id-前缀-十六进制:时间戳:信噪比(4次)", + "repeater_cliHelpNeighborRemove": "从邻居列表中删除第一个匹配项(通过十六进制的 pubkey 前缀)。", + "repeater_cliHelpRegion": "(仅限序列)列出所有已定义的区域以及当前的防洪许可。", + "repeater_cliHelpRegionLoad": "请注意:这是一个特殊的、包含多个命令的调用方式。 之后的每个命令都是一个区域名称(使用空格进行缩进,以表示父级关系,至少需要一个空格)。 结束方式是通过发送一个空行/命令。", + "repeater_cliHelpRegionGet": "搜索具有指定名称前缀的区域(或使用“*”表示全局范围)。 返回结果为“-> region-name (parent-name) 'F'”", + "repeater_cliHelpRegionPut": "添加或更新一个区域定义,并指定其名称。", + "repeater_cliHelpRegionRemove": "删除具有指定名称的区域定义。 (必须与指定名称完全匹配,且不能有子区域)", + "repeater_cliHelpRegionAllowf": "为指定区域设置“洪水”权限。(“*”表示全局/旧版本范围)", + "repeater_cliHelpRegionDenyf": "移除指定区域的“洪水”权限。(请注意:目前不建议在全局/旧版本中使用此功能!!)", + "repeater_cliHelpRegionHome": "回复当前“主区域”。(此功能尚未应用,仅供未来使用)", + "repeater_cliHelpRegionHomeSet": "设置“主”区域。", + "repeater_cliHelpRegionSave": "将区域列表/地图保存到存储中。", + "repeater_cliHelpGps": "显示 GPS 状态。当 GPS 处于关闭状态时,它只会显示“关闭”;当 GPS 处于开启状态时,它会显示“开启”、“状态”、“定位”、“卫星数量”等信息。", + "repeater_cliHelpGpsOnOff": "切换 GPS 设备的电源状态。", + "repeater_cliHelpGpsSync": "将节点时间与 GPS 钟同步。", + "repeater_cliHelpGpsSetLoc": "将节点的坐标设置为 GPS 坐标,并保存设置。", + "repeater_cliHelpGpsAdvert": "设置节点的位置广告配置:\n- none:不将位置信息包含在广告中\n- share:共享 GPS 位置(从 SensorManager 获取)\n- prefs:在偏好设置中展示的位置", + "repeater_cliHelpGpsAdvertSet": "设置广告的位置配置。", "repeater_commandsListTitle": "命令列表", - "repeater_commandsListNote": "注意:对于各种“设置...”命令,也存在“获取...”命令。", + "repeater_commandsListNote": "请注意:对于各种“set ...”命令,也存在“get ...”命令。", "repeater_general": "通用", "repeater_settingsCategory": "设置", "repeater_bridge": "桥", "repeater_logging": "记录", - "repeater_neighborsRepeaterOnly": "邻居(仅限重复器)", - "repeater_regionManagementRepeaterOnly": "区域管理(仅限重复器)", - "repeater_regionNote": "区域命令已推出,用于管理区域定义和权限。", - "repeater_gpsManagement": "GPS管理", - "repeater_gpsNote": "GPS 命令已引入用于管理与位置相关的主题。", - "telemetry_receivedData": "接收遥测数据", + "repeater_neighborsRepeaterOnly": "邻居(仅限重复功能)", + "repeater_regionManagementRepeaterOnly": "区域管理(仅限重复站点)", + "repeater_regionNote": "区域命令已引入,用于管理区域定义和权限。", + "repeater_gpsManagement": "GPS 管理", + "repeater_gpsNote": "已引入 GPS 命令,用于管理与位置相关的任务。", + "telemetry_receivedData": "接收到的遥测数据", "telemetry_requestTimeout": "遥测请求超时。", - "telemetry_errorLoading": "错误加载遥测数据:{error}", + "telemetry_errorLoading": "Error loading telemetry: {error}", "@telemetry_errorLoading": { "placeholders": { "error": { @@ -1197,7 +1220,7 @@ }, "telemetry_batteryLabel": "电池", "telemetry_voltageLabel": "电压", - "telemetry_mcuTemperatureLabel": "MCU 温度", + "telemetry_mcuTemperatureLabel": "MCU 的温度", "telemetry_temperatureLabel": "温度", "telemetry_currentLabel": "当前", "telemetry_batteryValue": "{percent}% / {volts}V", @@ -1238,18 +1261,46 @@ } } }, + "neighbors_receivedData": "已收到邻居信息", + "neighbors_requestTimedOut": "邻居要求停止干扰。", + "neighbors_errorLoading": "Error loading neighbors: {error}", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "neighbors_repeatersNeighbours": "重复使用的邻居", + "neighbors_noData": "没有可用的邻居信息。", + "neighbors_unknownContact": "Unknown {pubkey}", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Heard: {time} ago", + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, "channelPath_title": "数据包路径", "channelPath_viewMap": "查看地图", "channelPath_otherObservedPaths": "其他观察到的路径", - "channelPath_repeaterHops": "重复跳跃", - "channelPath_noHopDetails": "此包的详细信息未提供。", + "channelPath_repeaterHops": "复用跳跃", + "channelPath_noHopDetails": "对于此包,未提供详细信息。", "channelPath_messageDetails": "消息详情", "channelPath_senderLabel": "发件人", "channelPath_timeLabel": "时间", "channelPath_repeatsLabel": "重复", "channelPath_pathLabel": "路径 {index}", - "channelPath_observedLabel": "已观察", - "channelPath_observedPathTitle": "观察路径 {index} • {hops}", + "channelPath_observedLabel": "观察到的", + "channelPath_observedPathTitle": "Observed path {index} • {hops}", "@channelPath_observedPathTitle": { "placeholders": { "index": { @@ -1260,7 +1311,7 @@ } } }, - "channelPath_noLocationData": "没有位置数据", + "channelPath_noLocationData": "没有位置信息", "channelPath_timeWithDate": "{day}/{month} {time}", "@channelPath_timeWithDate": { "placeholders": { @@ -1286,7 +1337,7 @@ "channelPath_unknownPath": "未知", "channelPath_floodPath": "洪水", "channelPath_directPath": "直接", - "channelPath_observedZeroOf": "0 of {total} 跳跃", + "channelPath_observedZeroOf": "0 of {total} hops", "@channelPath_observedZeroOf": { "placeholders": { "total": { @@ -1294,7 +1345,7 @@ } } }, - "channelPath_observedSomeOf": "已观察到 {observed} 步中的 {total} 步", + "channelPath_observedSomeOf": "{observed} of {total} hops", "@channelPath_observedSomeOf": { "placeholders": { "observed": { @@ -1305,9 +1356,9 @@ } } }, - "channelPath_mapTitle": "路径地图", - "channelPath_noRepeaterLocations": "此路径没有可用的重复器位置。", - "channelPath_primaryPath": "路径 {index} (主)", + "channelPath_mapTitle": "路线图", + "channelPath_noRepeaterLocations": "这条路径上没有可用的中继器位置。", + "channelPath_primaryPath": "路径 {index} (主要路径)", "@channelPath_primaryPath": { "placeholders": { "index": { @@ -1323,7 +1374,7 @@ } }, "channelPath_pathLabelTitle": "路径", - "channelPath_observedPathHeader": "已观察路径", + "channelPath_observedPathHeader": "观察路径", "channelPath_selectedPathLabel": "{label} • {prefixes}", "@channelPath_selectedPathLabel": { "placeholders": { @@ -1335,68 +1386,14 @@ } } }, - "channelPath_noHopDetailsAvailable": "此包的跳跃详情不可用。", - "channelPath_unknownRepeater": "未知重复器", - "listFilter_tooltip": "筛选和排序", - "listFilter_sortBy": "按类型排序", - "listFilter_latestMessages": "最新消息", - "listFilter_heardRecently": "最近听说", - "listFilter_az": "A-Z", - "listFilter_filters": "筛选", - "listFilter_all": "全部", - "listFilter_users": "用户", - "listFilter_repeaters": "重复器", - "listFilter_roomServers": "房间服务器", - "listFilter_unreadOnly": "未读消息", - "listFilter_newGroup": "新组", - "@neighbors_errorLoading": { - "placeholders": { - "error": { - "type": "String" - } - } - }, - "repeater_neighboursSubtitle": "查看零跳邻居。", - "repeater_neighbours": "邻居", - "neighbors_receivedData": "收到邻居数据", - "neighbors_requestTimedOut": "邻居请求超时处理。", - "neighbors_errorLoading": "加载邻居时出错:{error}", - "neighbors_repeatersNeighbours": "重复器邻居", - "neighbors_noData": "没有可用的邻居数据。", - "channels_joinPrivateChannel": "加入私密频道", - "channels_createPrivateChannelDesc": "使用密钥保护。", - "channels_joinPrivateChannelDesc": "手动输入密钥。", - "channels_createPrivateChannel": "创建私聊频道", - "channels_joinPublicChannel": "加入公共频道", - "channels_joinPublicChannelDesc": "任何人都可以加入这个频道。", - "channels_joinHashtagChannel": "加入标签频道", - "channels_joinHashtagChannelDesc": "任何人都可以加入话题频道。", - "channels_scanQrCode": "扫描二维码", - "channels_scanQrCodeComingSoon": "即将到来", - "channels_enterHashtag": "输入标签", - "channels_hashtagHint": "例如 #团队", - "@neighbors_unknownContact": { - "placeholders": { - "pubkey": { - "type": "String" - } - } - }, - "@neighbors_heardAgo": { - "placeholders": { - "time": { - "type": "String" - } - } - }, - "neighbors_heardAgo": "听到的时间:{time}前", - "neighbors_unknownContact": "未知{pubkey}", - "settings_locationGPSEnable": "启用GPS", - "settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。", - "settings_locationIntervalSec": "GPS 间隔(秒)", - "settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。", - "contacts_manageRoom": "管理房间服务器", - "room_management": "房间服务器管理", + "channelPath_noHopDetailsAvailable": "对于此包裹,尚无详细信息。", + "channelPath_unknownRepeater": "未知的重复设备", + "community_title": "社区", + "community_create": "建立社区", + "community_createDesc": "创建一个新的社群,并通过二维码进行分享。", + "community_join": "加入", + "community_joinTitle": "加入社区", + "community_joinConfirmation": "Do you want to join the community \"{name}\"?", "@community_joinConfirmation": { "placeholders": { "name": { @@ -1404,6 +1401,14 @@ } } }, + "community_scanQr": "扫描社区二维码", + "community_scanInstructions": "将相机对准社区的二维码。", + "community_showQr": "显示二维码", + "community_publicChannel": "社区公共", + "community_hashtagChannel": "社区标签", + "community_name": "社区名称", + "community_enterName": "请输入社区名称", + "community_created": "Community \"{name}\" created", "@community_created": { "placeholders": { "name": { @@ -1411,6 +1416,7 @@ } } }, + "community_joined": "Joined community \"{name}\"", "@community_joined": { "placeholders": { "name": { @@ -1418,6 +1424,8 @@ } } }, + "community_qrTitle": "分享社区", + "community_qrInstructions": "Scan this QR code to join \"{name}\"", "@community_qrInstructions": { "placeholders": { "name": { @@ -1425,6 +1433,10 @@ } } }, + "community_hashtagPrivacyHint": "仅社区成员才能加入社区话题标签的频道。", + "community_invalidQrCode": "无效的社区二维码", + "community_alreadyMember": "已经是会员", + "community_alreadyMemberMessage": "You are already a member of \"{name}\".", "@community_alreadyMemberMessage": { "placeholders": { "name": { @@ -1432,6 +1444,13 @@ } } }, + "community_addPublicChannel": "添加公共频道", + "community_addPublicChannelHint": "自动添加该社区的公共频道", + "community_noCommunities": "目前还没有任何社区加入。", + "community_scanOrCreate": "扫描二维码或创建社群,即可开始。", + "community_manageCommunities": "管理社区", + "community_delete": "退出社区", + "community_deleteConfirm": "是否要删除\"{name}\"?", "@community_deleteConfirm": { "placeholders": { "name": { @@ -1439,50 +1458,7 @@ } } }, - "@community_deleted": { - "placeholders": { - "name": { - "type": "String" - } - } - }, - "@community_forCommunity": { - "placeholders": { - "name": { - "type": "String" - } - } - }, - "community_create": "创建社区", - "community_title": "社区", - "community_createDesc": "创建新的社区并可通过二维码分享。", - "common_ok": "好的", - "community_join": "加入", - "community_joinTitle": "加入社区", - "community_joinConfirmation": "您想加入社区 \"{name}\" 吗?", - "community_scanQr": "扫描社区二维码", - "community_scanInstructions": "将相机对准社区二维码", - "community_showQr": "显示二维码", - "community_publicChannel": "社区公开", - "community_hashtagChannel": "社区标签", - "community_name": "社区名称", - "community_enterName": "请输入社区名称", - "community_created": "社区“{name}”已创建", - "community_joined": "加入社区 \"{name}\"", - "community_qrTitle": "分享社区", - "community_qrInstructions": "扫描此二维码加入{name}", - "community_hashtagPrivacyHint": "社区标签频道仅社区成员可加入", - "community_invalidQrCode": "无效的社区二维码", - "community_alreadyMember": "已经是会员了", - "community_alreadyMemberMessage": "您已经是 \"{name}\" 的会员。", - "community_addPublicChannel": "添加社区公共频道", - "community_addPublicChannelHint": "自动添加该社区的公共频道", - "community_noCommunities": "尚未加入任何社区", - "community_scanOrCreate": "扫描二维码或创建社区开始", - "community_manageCommunities": "管理社群", - "community_delete": "退出社区", - "community_deleteConfirm": "退出 \"{name}\"?", - "community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。", + "community_deleteChannelsWarning": "这将同时删除 {count} 个频道及其所有消息。", "@community_deleteChannelsWarning": { "placeholders": { "count": { @@ -1490,15 +1466,16 @@ } } }, - "community_deleted": "已退出社区 \"{name}\"", - "community_addHashtagChannel": "添加社区标签", - "community_addHashtagChannelDesc": "添加一个话题频道给此社区", - "community_selectCommunity": "选择社区", - "community_regularHashtag": "常规话题标签", - "community_regularHashtagDesc": "公共话题(任何人都可以加入)", - "community_communityHashtag": "社区标签", - "community_communityHashtagDesc": "仅限社区成员使用", - "community_forCommunity": "对于 {name}", + "community_deleted": "Left community \"{name}\"", + "@community_deleted": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "community_regenerateSecret": "恢复秘密", + "community_regenerateSecretConfirm": "[保存:{name}]\n是否需要重新生成\"{name}\"的密钥?所有成员都需要扫描新的二维码才能继续进行通信。", "@community_regenerateSecretConfirm": { "placeholders": { "name": { @@ -1506,6 +1483,8 @@ } } }, + "community_regenerate": "再生", + "community_secretRegenerated": "[保护对象:{name}]\n秘密已恢复至\"{name}\"", "@community_secretRegenerated": { "placeholders": { "name": { @@ -1513,6 +1492,8 @@ } } }, + "community_updateSecret": "更新秘密", + "community_secretUpdated": "“{name}”的秘密已更新", "@community_secretUpdated": { "placeholders": { "name": { @@ -1520,6 +1501,7 @@ } } }, + "community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"", "@community_scanToUpdateSecret": { "placeholders": { "name": { @@ -1527,13 +1509,45 @@ } } }, - "community_regenerateSecret": "重新生成密钥", - "community_secretRegenerated": "密码已重置为“{name}”", - "community_regenerate": "重新生成", - "community_regenerateSecretConfirm": "重新生成“{name}”的秘密密钥?所有成员将需要扫描新的二维码才能继续沟通。", - "community_scanToUpdateSecret": "扫描新的二维码更新\"{name}\"的密码", - "community_updateSecret": "更新密钥", - "community_secretUpdated": "密码已更新为“{name}”", + "community_addHashtagChannel": "添加社区标签", + "community_addHashtagChannelDesc": "为这个社区创建一个带有话题标签的频道", + "community_selectCommunity": "选择社区", + "community_regularHashtag": "常用标签", + "community_regularHashtagDesc": "公共话题标签(任何人都可以参与)", + "community_communityHashtag": "社区标签", + "community_communityHashtagDesc": "仅限社区成员", + "community_forCommunity": "For {name}", + "@community_forCommunity": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "listFilter_tooltip": "筛选和排序", + "listFilter_sortBy": "按排序", + "listFilter_latestMessages": "最新消息", + "listFilter_heardRecently": "最近听到的", + "listFilter_az": "A 到 Z", + "listFilter_filters": "过滤器", + "listFilter_all": "全部", + "listFilter_users": "用户", + "listFilter_repeaters": "重复器", + "listFilter_roomServers": "房间服务器", + "listFilter_unreadOnly": "仅显示未读消息", + "listFilter_newGroup": "新的团体", + "pathTrace_you": "您", + "pathTrace_failed": "路径追踪失败。", + "pathTrace_notAvailable": "无法获取路径信息。", + "pathTrace_refreshTooltip": "重新绘制路径。", + "contacts_pathTrace": "路径追踪", + "contacts_ping": "乒", + "contacts_repeaterPathTrace": "追踪路径至中继器", + "contacts_repeaterPing": "中继器", + "contacts_roomPathTrace": "追踪到房间服务器", + "contacts_roomPing": "会议室服务器", + "contacts_chatTraceRoute": "路径跟踪路线", + "contacts_pathTraceTo": "追踪路径至 {name}", "@contacts_pathTraceTo": { "placeholders": { "name": { @@ -1541,32 +1555,18 @@ } } }, - "pathTrace_you": "你", - "pathTrace_failed": "路径追踪失败。", - "pathTrace_notAvailable": "路径追踪不可用", - "pathTrace_refreshTooltip": "刷新路径追踪", - "contacts_pathTrace": "路径追踪", - "contacts_ping": "ping", - "contacts_repeaterPathTrace": "路径追踪到中继器", - "contacts_repeaterPing": "Ping 中继器", - "contacts_roomPathTrace": "路径追踪至房间服务器", - "contacts_roomPing": "Ping 房间服务器", - "contacts_chatTraceRoute": "路径追踪", - "contacts_pathTraceTo": "追踪路由到 {name}", - "appSettings_languageUk": "乌克兰语", - "appSettings_languageRu": "俄语", - "contacts_contactImported": "联系人已导入", - "contacts_contactImportFailed": "联系人导入失败", - "contacts_zeroHopAdvert": "零跳广告", - "contacts_floodAdvert": "洪水广告", "contacts_clipboardEmpty": "剪贴板为空。", - "contacts_invalidAdvertFormat": "无效联系人数据", - "contacts_addContactFromClipboard": "从剪贴板添加联系人", - "contacts_zeroHopContactAdvertSent": "通过广告发送的联系人", - "contacts_zeroHopContactAdvertFailed": "发送联系人失败", - "contacts_contactAdvertCopied": "广告已复制到剪贴板。", - "contacts_contactAdvertCopyFailed": "复制广告到剪贴板失败。", + "contacts_invalidAdvertFormat": "无效的联系信息", + "contacts_contactImported": "已建立联系。", + "contacts_contactImportFailed": "未能导入联系人。", + "contacts_zeroHopAdvert": "零跳广告", + "contacts_floodAdvert": "防洪广告", "contacts_copyAdvertToClipboard": "复制广告到剪贴板", - "contacts_ShareContactZeroHop": "通过广告分享联系人", - "contacts_ShareContact": "复制联系人到剪贴板" + "contacts_addContactFromClipboard": "从剪贴板添加联系人", + "contacts_ShareContact": "复制联系方式到剪贴板", + "contacts_ShareContactZeroHop": "通过广告分享联系方式", + "contacts_zeroHopContactAdvertSent": "通过广告获取联系方式。", + "contacts_zeroHopContactAdvertFailed": "发送联系方式失败。", + "contacts_contactAdvertCopied": "广告内容已复制到剪贴板。", + "contacts_contactAdvertCopyFailed": "将广告复制到剪贴板操作失败。" } diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart index 99bad87..48f94f9 100644 --- a/lib/screens/contacts_screen.dart +++ b/lib/screens/contacts_screen.dart @@ -34,6 +34,12 @@ enum RoomLoginDestination { management, } +enum ContactOperationType { + import, + export, + zeroHopShare, +} + class ContactsScreen extends StatefulWidget { final bool hideBackButton; @@ -54,9 +60,7 @@ class _ContactsScreenState extends State List _groups = []; Timer? _searchDebounce; - bool _imported = false; - bool _zeroHopContact = false; - bool _copyedContact = false; + final Set _pendingOperations = {}; StreamSubscription? _frameSubscription; @@ -97,57 +101,67 @@ class _ContactsScreenState extends State if (code == respCodeExportContact) { final advertPacket = frameBuffer.readRemainingBytes(); + // Validate packet has expected minimum size (98+ bytes per protocol) + if (advertPacket.length < 98) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)), + ); + } + _pendingOperations.remove(ContactOperationType.export); + return; + } final hexString = pubKeyToHex(advertPacket); Clipboard.setData(ClipboardData(text: "meshcore://$hexString")); } if(code == respCodeOk) { // Show a snackbar indicating success - if(_imported && mounted){ + if(!mounted) return; + + if(_pendingOperations.contains(ContactOperationType.import)){ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactImported)), ); } - if(_zeroHopContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertSent)), ); } - if(_copyedContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.export)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)), ); } - - _copyedContact = false; - _zeroHopContact = false; - _imported = false; + + _pendingOperations.clear(); } if(code == respCodeErr) { // Show a snackbar indicating failure - if(_imported && mounted){ + if(!mounted) return; + + if(_pendingOperations.contains(ContactOperationType.import)){ ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactImportFailed)), ); } - if(_zeroHopContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.zeroHopShare)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_zeroHopContactAdvertFailed)), ); } - if(_copyedContact && mounted) { + if(_pendingOperations.contains(ContactOperationType.export)) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(context.l10n.contacts_contactAdvertCopyFailed)), ); } - _copyedContact = false; - _imported = false; - _zeroHopContact = false; + _pendingOperations.clear(); } }); @@ -156,15 +170,14 @@ class _ContactsScreenState extends State Future _contactExport(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactFrame = buildExportContactFrame(pubKey); - _copyedContact = true; + _pendingOperations.add(ContactOperationType.export); await connector.sendFrame(exportContactFrame); - return; } Future _contactZeroHop(Uint8List pubKey) async { final connector = Provider.of(context, listen: false); final exportContactZeroHopFrame = buildZeroHopContact(pubKey); - _zeroHopContact = true; + _pendingOperations.add(ContactOperationType.zeroHopShare); await connector.sendFrame(exportContactZeroHopFrame); } @@ -191,8 +204,8 @@ class _ContactsScreenState extends State final hexString = text.substring('meshcore://'.length); try { final importContactFrame = buildImportContactFrame(hexString); + _pendingOperations.add(ContactOperationType.import); await connector.sendFrame(importContactFrame); - _imported = true; } catch (e) { if(mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/tools/translate.py b/tools/translate.py index 84d172a..a51d3d1 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -1,42 +1,21 @@ #!/usr/bin/env python3 """ -translate_arb_with_ollama.py +translate_arb_with_translategemma.py -Translates ARB/JSON localization values using a local Ollama model, while: -- preserving keys -- skipping "@@locale" and all "@key" metadata blocks -- preserving placeholders like {deviceName}, {count, plural, ...} -- writing a new file with updated @@locale -- printing progress as it runs +Translates ARB/JSON localization files using TranslateGemma via Ollama. +Preserves placeholders like {deviceName} and ICU plural/select formats. Usage: # Translate all strings: - python translate_arb_with_ollama.py \ - --in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \ - --out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \ - --to-locale es \ - --model ministral-3:latest \ - --temperature 0 \ - --concurrency 4 + python translate.py --in lib/l10n/app_en.arb --out lib/l10n/app_es.arb --to-locale es - # Translate only missing/untranslated strings: - python translate_arb_with_ollama.py \ - --in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \ - --out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \ - --to-locale es \ - --missing-only \ - --model ministral-3:latest + # Translate only missing strings: + python translate.py --in lib/l10n/app_en.arb --out lib/l10n/app_es.arb --to-locale es --missing-only # Translate all locales (missing strings only): - python translate_arb_with_ollama.py \ - --in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \ - --l10n-dir /home/zjs81/Desktop/meshcore-open/lib/l10n \ - --missing-only \ - --model ministral-3:latest + python translate.py --in lib/l10n/app_en.arb --l10n-dir lib/l10n --missing-only """ -from __future__ import annotations - import argparse import json import os @@ -49,9 +28,8 @@ from typing import Any, Dict, List, Tuple, Optional from urllib import request -# Simple placeholder like {name}, {count}, {deviceName} +# Placeholder patterns SIMPLE_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}") -# ICU plural/select variable name extraction: {count, plural, ...} or {gender, select, ...} ICU_VAR_RE = re.compile(r"\{(\w+)\s*,\s*(?:plural|select|selectordinal)\s*,", re.IGNORECASE) @@ -61,263 +39,46 @@ class OllamaConfig: model: str timeout_s: float temperature: float - num_ctx: int - num_predict: int - top_p: float -def http_post_json(url: str, payload: Dict[str, Any], timeout_s: float) -> Dict[str, Any]: - data = json.dumps(payload).encode("utf-8") - req = request.Request( - url, - data=data, - headers={"Content-Type": "application/json"}, - method="POST", - ) - with request.urlopen(req, timeout=timeout_s) as resp: - body = resp.read().decode("utf-8") - return json.loads(body) - - -def strip_markdown(s: str) -> str: - """Remove common markdown formatting from output.""" - # Remove bold/italic markers - s = re.sub(r'\*\*(.+?)\*\*', r'\1', s) - s = re.sub(r'\*(.+?)\*', r'\1', s) - s = re.sub(r'__(.+?)__', r'\1', s) - s = re.sub(r'_(.+?)_', r'\1', s) - # Remove stray asterisks - s = re.sub(r'\*+', '', s) - return s.strip() - - -def ollama_generate(cfg: OllamaConfig, prompt: str) -> str: - url = cfg.host.rstrip("/") + "/api/generate" - payload = { - "model": cfg.model, - "prompt": prompt, - "stream": False, - "options": { - "temperature": cfg.temperature, - "num_ctx": cfg.num_ctx, - "num_predict": cfg.num_predict, - "top_p": cfg.top_p, - }, - } - resp = http_post_json(url, payload, cfg.timeout_s) - out = resp.get("response", "") - # Clean up common LLM artifacts - out = strip_markdown(out) - return out.strip() - - -def extract_placeholder_names(s: str) -> List[str]: - """Extract placeholder variable names (not the full braced expression). - - For '{name}' returns ['name'] - For '{count} {count, plural, =1{hop} other{hops}}' returns ['count'] - """ - names = set() - # Get ICU variable names first - for m in ICU_VAR_RE.finditer(s): - names.add(m.group(1)) - # Get simple placeholders, but skip if they're inside ICU blocks (text forms like {hop}) - # We do this by checking if the name is also an ICU variable - if not, it's a simple placeholder - # unless it looks like a word (ICU text forms are usually short words) - for m in SIMPLE_PLACEHOLDER_RE.finditer(s): - name = m.group(1) - # Check if this appears as a simple {name} placeholder (not inside ICU) - # by looking at what comes after it - full_match = m.group(0) - pos = m.start() - # Look for pattern like {name, plural/select - if found, skip (handled by ICU_VAR_RE) - rest = s[pos:] - if re.match(r"\{\w+\s*,\s*(?:plural|select|selectordinal)", rest, re.IGNORECASE): - continue - # Check if this is likely a text form inside ICU (preceded by =X{ or other{) - before = s[:pos] - if re.search(r"(?:=\d+|zero|one|two|few|many|other)\s*$", before, re.IGNORECASE): - continue # This is a text form like "=1{hop}", skip it - names.add(name) - return sorted(names) - - -def build_prompt(text: str, target_lang: str, placeholder_names: List[str], has_icu: bool, ask_confidence: bool = False) -> str: - preserve_list = "\n".join(f"- {{{t}}}" for t in placeholder_names) if placeholder_names else "- (none)" - - icu_note = "" - if has_icu: - icu_note = ( - "ICU FORMAT RULES:\n" - f"- This text uses ICU plural/select format: {{var, plural, =1{{singular}} other{{plural}}}}\n" - "- Keep structure keywords EXACTLY: plural, select, =0, =1, =2, zero, one, two, few, many, other\n" - f"- TRANSLATE the words inside each form to {target_lang}\n" - "- Example: =1{item} other{items} -> translate 'item'/'items' but keep =1{{ }} other{{ }} structure\n\n" - ) - - if ask_confidence: - return ( - f"Translate this UI string to {target_lang}.\n\n" - "RULES:\n" - "- Placeholders like {name}, {count} must appear EXACTLY unchanged.\n" - "- Use infinitive verb forms for buttons (Save, Delete, etc.).\n" - f"- Use natural {target_lang} word order.\n" - "- Keep brand names and technical terms unchanged.\n\n" - f"{icu_note}" - f"Placeholders: {', '.join(f'{{{t}}}' for t in placeholder_names) if placeholder_names else 'none'}\n\n" - f"English: {text}\n\n" - "Respond with EXACTLY two lines:\n" - "1. The translation (no quotes)\n" - "2. Confidence score 1-5 (5=certain, 1=unsure)\n\n" - "Example response:\n" - "Guardar archivo\n" - "5" - ) - else: - return ( - f"Translate this UI string to {target_lang}. Return ONLY the translation.\n\n" - "RULES:\n" - "- Output the translated text ONLY. No markdown, no quotes, no explanations.\n" - "- Placeholders like {name}, {count} must appear EXACTLY unchanged.\n" - "- Use infinitive verb forms for buttons (Save, Delete, etc.).\n" - f"- Use natural {target_lang} word order.\n" - "- Keep brand names and technical terms unchanged.\n" - "- Translation length should be similar to the original.\n\n" - f"{icu_note}" - f"Placeholders: {', '.join(f'{{{t}}}' for t in placeholder_names) if placeholder_names else 'none'}\n\n" - f"English: {text}\n" - f"{target_lang}:" - ) - - -def parse_confidence_response(response: str) -> Tuple[str, int]: - """Parse response with translation and confidence score. - - Returns (translation, confidence) where confidence is 1-5, or 0 if unparseable. - """ - lines = response.strip().split('\n') - if len(lines) >= 2: - translation = '\n'.join(lines[:-1]).strip() # All but last line - try: - # Try to extract number from last line - last_line = lines[-1].strip() - # Handle formats like "5", "5/5", "Confidence: 5" - match = re.search(r'\b([1-5])\b', last_line) - if match: - confidence = int(match.group(1)) - return translation, confidence - except ValueError: - pass - # Fallback: treat whole response as translation with unknown confidence - return strip_markdown(response), 0 - - -def looks_like_translation_failed(src: str, out: str) -> bool: - if not out: - return True - if src.strip() == out.strip() and len(src.strip()) > 8: - return True - # Detect hallucination: output much longer than input (3x+ for short strings, 2x for longer) - src_len = len(src.strip()) - out_len = len(out.strip()) - if src_len < 50 and out_len > src_len * 3: - return True - if src_len >= 50 and out_len > src_len * 2: - return True - return False - - -def has_icu_block(s: str) -> bool: - """Check if string contains ICU plural/select block.""" - return bool(ICU_VAR_RE.search(s)) - - -def validate_preserved_tokens(src: str, out: str) -> Tuple[bool, Optional[str]]: - """Validate that placeholder names are preserved in translation.""" - src_names = extract_placeholder_names(src) - - # Check each placeholder name appears in output - for name in src_names: - # Look for {name} or {name, plural/select...} - pattern = r"\{" + re.escape(name) + r"(?:\}|\s*,)" - if not re.search(pattern, out): - return False, f"Missing placeholder: {{{name}}}" - - # If source has ICU block, output should too - if has_icu_block(src) and not has_icu_block(out): - return False, "ICU plural/select block missing in output" - - return True, None - - -def compute_confidence(src: str, out: str) -> Tuple[float, List[str]]: - """ - Compute confidence score (0.0 to 1.0) for a translation. - Returns (score, list of issues). - """ - issues = [] - score = 1.0 - - src_len = len(src.strip()) - out_len = len(out.strip()) - - # Length ratio check - if src_len > 0: - ratio = out_len / src_len - if ratio < 0.3: # Way too short - score -= 0.4 - issues.append("too_short") - elif ratio < 0.5: - score -= 0.2 - issues.append("short") - elif ratio > 2.5: # Way too long - score -= 0.4 - issues.append("too_long") - elif ratio > 1.8: - score -= 0.2 - issues.append("long") - - # Contains question mark when source doesn't (model asking questions) - if '?' in out and '?' not in src: - score -= 0.3 - issues.append("added_question") - - # Contains common LLM artifacts - artifacts = ['```', '**', 'translation:', 'here is', 'certainly', 'i can', 'i\'ll'] - out_lower = out.lower() - for artifact in artifacts: - if artifact in out_lower: - score -= 0.3 - issues.append(f"artifact:{artifact}") - break - - # Output looks like it's in English still (common words) - english_indicators = ['the ', ' is ', ' are ', ' was ', ' were ', ' have ', ' has ', 'you ', ' your '] - english_count = sum(1 for ind in english_indicators if ind in out_lower) - if english_count >= 3 and src_len > 20: - score -= 0.3 - issues.append("likely_english") - - # Contains newlines when source doesn't - if '\n' in out and '\n' not in src: - score -= 0.2 - issues.append("added_newlines") - - # ICU/placeholder validation - ok, _ = validate_preserved_tokens(src, out) - if not ok: - score -= 0.3 - issues.append("placeholder_error") - - return max(0.0, score), issues - - -# Keys to skip translation (brand names) -SKIP_KEYS = { - "appTitle", +# Language mapping (locale_code -> (language_name, translategemma_code)) +LOCALE_MAP = { + "es": ("Spanish", "es"), + "fr": ("French", "fr"), + "de": ("German", "de"), + "it": ("Italian", "it"), + "pt": ("Portuguese", "pt"), + "pt-BR": ("Brazilian Portuguese", "pt"), + "ja": ("Japanese", "ja"), + "ko": ("Korean", "ko"), + "zh": ("Chinese", "zh-Hans"), + "zh-Hant": ("Chinese", "zh-Hant"), + "ru": ("Russian", "ru"), + "uk": ("Ukrainian", "uk"), + "ar": ("Arabic", "ar"), + "hi": ("Hindi", "hi"), + "tr": ("Turkish", "tr"), + "nl": ("Dutch", "nl"), + "sv": ("Swedish", "sv"), + "no": ("Norwegian", "no"), + "da": ("Danish", "da"), + "fi": ("Finnish", "fi"), + "pl": ("Polish", "pl"), + "cs": ("Czech", "cs"), + "sk": ("Slovak", "sk"), + "sl": ("Slovenian", "sl"), + "bg": ("Bulgarian", "bg"), + "el": ("Greek", "el"), + "he": ("Hebrew", "he"), + "th": ("Thai", "th"), + "vi": ("Vietnamese", "vi"), + "id": ("Indonesian", "id"), } -# Manual translations for problematic strings (key -> {locale: translation}) +# Keys to skip translation +SKIP_KEYS = {"appTitle"} + +# Manual translations for complex strings MANUAL_TRANSLATIONS: Dict[str, Dict[str, str]] = { "repeater_daysHoursMinsSecs": { "es": "{days} días {hours}h {minutes}m {seconds}s", @@ -340,98 +101,126 @@ MANUAL_TRANSLATIONS: Dict[str, Dict[str, str]] = { } -def is_translatable_entry(key: str, value: Any) -> bool: - if key == "@@locale": - return False - if key in SKIP_KEYS: - return False - if key.startswith("@"): - return False - if not isinstance(value, str): - return False - if value.strip() == "": - return False - return True +def http_post_json(url: str, payload: Dict[str, Any], timeout_s: float) -> Dict[str, Any]: + data = json.dumps(payload).encode("utf-8") + req = request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST") + with request.urlopen(req, timeout=timeout_s) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def ollama_generate(cfg: OllamaConfig, prompt: str) -> str: + url = cfg.host.rstrip("/") + "/api/generate" + payload = { + "model": cfg.model, + "prompt": prompt, + "stream": False, + "options": {"temperature": cfg.temperature}, + } + resp = http_post_json(url, payload, cfg.timeout_s) + return resp.get("response", "").strip() + + +def extract_placeholder_names(s: str) -> List[str]: + """Extract placeholder variable names from string.""" + names = set() + + # Get ICU variable names + for m in ICU_VAR_RE.finditer(s): + names.add(m.group(1)) + + # Get simple placeholders (excluding ICU text forms) + for m in SIMPLE_PLACEHOLDER_RE.finditer(s): + name = m.group(1) + pos = m.start() + rest = s[pos:] + + # Skip if this is part of an ICU block + if re.match(r"\{\w+\s*,\s*(?:plural|select|selectordinal)", rest, re.IGNORECASE): + continue + + # Skip if this is a text form inside ICU (preceded by =X{ or other{) + before = s[:pos] + if re.search(r"(?:=\d+|zero|one|two|few|many|other)\s*$", before, re.IGNORECASE): + continue + + names.add(name) + + return sorted(names) + + +def has_icu_block(s: str) -> bool: + """Check if string contains ICU plural/select block.""" + return bool(ICU_VAR_RE.search(s)) + + +def build_prompt(text: str, target_lang: str, target_code: str, placeholder_names: List[str], has_icu: bool) -> str: + """Build TranslateGemma-compatible prompt with placeholder preservation instructions.""" + # Build instructions for placeholder preservation + instructions = [] + if placeholder_names: + placeholders = ', '.join(f'{{{t}}}' for t in placeholder_names) + instructions.append(f"CRITICAL: Keep these placeholders EXACTLY as they appear: {placeholders}") + if has_icu: + instructions.append("CRITICAL: Preserve ICU message format structure (plural, select, =0, =1, other, etc.). Only translate the text inside the forms.") + + # Add instructions to the system prompt, not to the text itself + instruction_text = "\n".join(instructions) if instructions else "" + separator = "\n" if instruction_text else "" + + # TranslateGemma expects this exact format (note the two blank lines before text) + return f"""You are a professional English (en) to {target_lang} ({target_code}) translator. Your goal is to accurately convey the meaning and nuances of the original English text while adhering to {target_lang} grammar, vocabulary, and cultural sensitivities. +Produce only the {target_lang} translation, without any additional explanations or commentary.{separator}{instruction_text} +Please translate the following English text into {target_lang}: + + +{text}""" + + +def validate_preserved_tokens(src: str, out: str) -> Tuple[bool, Optional[str]]: + """Validate that placeholder names are preserved.""" + src_names = extract_placeholder_names(src) + + for name in src_names: + pattern = r"\{" + re.escape(name) + r"(?:\}|\s*,)" + if not re.search(pattern, out): + return False, f"Missing placeholder: {{{name}}}" + + if has_icu_block(src) and not has_icu_block(out): + return False, "ICU plural/select block missing" + + return True, None def translate_one( key: str, text: str, target_lang: str, + target_code: str, cfg: OllamaConfig, retries: int, backoff_s: float, fallback_cfg: Optional[OllamaConfig] = None, - confidence_threshold: float = 0.7, - model_confidence_threshold: int = 4, - ask_model_confidence: bool = True, ) -> Tuple[str, str, Optional[str], bool]: - """ - Translate a single string. - Returns (key, translated_text, error_or_none, used_fallback_model). - """ + """Translate a single string. Returns (key, translated_text, error_or_none, used_fallback).""" placeholder_names = extract_placeholder_names(text) text_has_icu = has_icu_block(text) - - # Ask for confidence if we have a fallback model - should_ask_confidence = ask_model_confidence and fallback_cfg and fallback_cfg.model != cfg.model - prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu, ask_confidence=should_ask_confidence) - used_fallback = False + prompt = build_prompt(text, target_lang, target_code, placeholder_names, text_has_icu) last_err: Optional[str] = None for attempt in range(retries + 1): try: - raw_out = ollama_generate(cfg, prompt) - - # Parse confidence if we asked for it - if should_ask_confidence: - out, model_confidence = parse_confidence_response(raw_out) - else: - out = raw_out - model_confidence = 5 # Assume high confidence if not asked - + out = ollama_generate(cfg, prompt) + + # Validate placeholders ok, why = validate_preserved_tokens(text, out) if not ok: last_err = f"Validation failed: {why}" - # Retry without confidence format for simpler response - prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu, ask_confidence=False) - prompt = ( - prompt - + "\n\nIMPORTANT: You MUST keep every {...} segment exactly unchanged. " - "If you cannot, return the original text unchanged." - ) + if attempt < retries: + time.sleep(backoff_s * (attempt + 1)) + continue raise ValueError(last_err) - if looks_like_translation_failed(text, out) and attempt < retries: - last_err = "Output identical/suspicious; retrying" - time.sleep(backoff_s * (attempt + 1)) - continue - - # Check if model reported low confidence - use fallback - if model_confidence > 0 and model_confidence < model_confidence_threshold and fallback_cfg: - fallback_prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu, ask_confidence=False) - fallback_out = ollama_generate(fallback_cfg, fallback_prompt) - fallback_ok, _ = validate_preserved_tokens(text, fallback_out) - if fallback_ok and not looks_like_translation_failed(text, fallback_out): - return key, fallback_out, None, True - - # Also check computed confidence and use fallback model if needed - confidence, issues = compute_confidence(text, out) - if confidence < confidence_threshold and fallback_cfg and fallback_cfg.model != cfg.model: - # Low confidence - try with bigger model - fallback_prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu) - fallback_out = ollama_generate(fallback_cfg, fallback_prompt) - fallback_ok, _ = validate_preserved_tokens(text, fallback_out) - fallback_conf, _ = compute_confidence(text, fallback_out) - - if fallback_ok and fallback_conf > confidence: - # Fallback is better - return key, fallback_out, None, True - elif fallback_ok and not ok: - # Original failed validation but fallback passed - return key, fallback_out, None, True - - return key, out, None, used_fallback + return key, out, None, False except Exception as e: last_err = str(e) @@ -439,21 +228,55 @@ def translate_one( time.sleep(backoff_s * (attempt + 1)) continue - # Last resort: try fallback model - if fallback_cfg and fallback_cfg.model != cfg.model: + # Try fallback model if available + if fallback_cfg: try: - fallback_prompt = build_prompt(text, target_lang, placeholder_names, text_has_icu) + fallback_prompt = build_prompt(text, target_lang, target_code, placeholder_names, text_has_icu) fallback_out = ollama_generate(fallback_cfg, fallback_prompt) fallback_ok, _ = validate_preserved_tokens(text, fallback_out) - if fallback_ok and not looks_like_translation_failed(text, fallback_out): + if fallback_ok: return key, fallback_out, None, True except Exception: pass - return key, text, last_err, False # fallback to original on failure + # Fallback to original + return key, text, last_err, False + + +def is_translatable_entry(key: str, value: Any) -> bool: + """Check if an entry should be translated.""" + if key == "@@locale" or key.startswith("@") or key in SKIP_KEYS: + return False + return isinstance(value, str) and value.strip() != "" + + +def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]: + """Find keys that are missing or empty in target.""" + missing = [] + for key in source_data: + if key == "@@locale" or key.startswith("@"): + continue + if key not in target_data or (isinstance(target_data.get(key), str) and target_data[key].strip() == ""): + missing.append(key) + return missing + + +def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]: + """Find all locale .arb files excluding template. Returns [(locale_code, file_path)].""" + locales = [] + template_basename = os.path.basename(template_file) + + for filename in os.listdir(l10n_dir): + if filename.endswith('.arb') and filename != template_basename: + if filename.startswith('app_'): + locale = filename[4:-4] # app_es.arb -> es + locales.append((locale, os.path.join(l10n_dir, filename))) + + return sorted(locales) def fmt_duration(seconds: float) -> str: + """Format duration as human-readable string.""" if seconds < 60: return f"{seconds:.1f}s" m = int(seconds // 60) @@ -465,226 +288,25 @@ def fmt_duration(seconds: float) -> str: return f"{h}h {m2}m" -def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]: - """Find keys that are in source but not in target, or have empty values (excluding metadata keys).""" - missing = [] - for key in source_data: - if key == "@@locale": - continue - if key.startswith("@"): - continue - if key not in target_data: - missing.append(key) - elif isinstance(target_data.get(key), str) and target_data[key].strip() == "": - # Also include keys with empty string values - missing.append(key) - return missing - - -def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]: - """Find all locale .arb files in the directory, excluding the template. - - Returns list of (locale_code, file_path) tuples. - """ - locales = [] - template_basename = os.path.basename(template_file) - - for filename in os.listdir(l10n_dir): - if not filename.endswith('.arb'): - continue - if filename == template_basename: - continue - # Extract locale from filename like app_es.arb -> es - if filename.startswith('app_') and filename.endswith('.arb'): - locale = filename[4:-4] # Remove 'app_' prefix and '.arb' suffix - filepath = os.path.join(l10n_dir, filename) - locales.append((locale, filepath)) - - return sorted(locales) - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--in", dest="in_path", required=True, help="Input .arb/.json file path (source/template)") - ap.add_argument("--out", dest="out_path", default=None, help="Output .arb/.json file path (required unless using --l10n-dir)") - ap.add_argument("--to-locale", default=None, help="Target locale code, e.g. es, fr, de (required unless using --l10n-dir)") - ap.add_argument("--l10n-dir", default=None, help="Directory containing locale .arb files. When set, translates all locales.") - ap.add_argument("--missing-only", action="store_true", help="Only translate keys missing from target file") - ap.add_argument("--target-lang", default=None, help="Target language name for the model, e.g. Spanish (defaults from locale)") - ap.add_argument("--model", default="gemma3:4b", help="Ollama model name") - ap.add_argument("--fallback-model", default=None, help="Larger model to use for low-confidence translations") - ap.add_argument("--confidence-threshold", type=float, default=0.7, help="Computed confidence threshold to trigger fallback (0.0-1.0)") - ap.add_argument("--model-confidence-threshold", type=int, default=4, help="Model self-reported confidence threshold (1-5, use fallback if below)") - ap.add_argument("--retry-model", default="ministral-3:latest", help="Model to use for end-of-run retries") - ap.add_argument("--host", default="http://localhost:11434", help="Ollama host") - ap.add_argument("--timeout", type=float, default=120.0, help="HTTP timeout seconds") - ap.add_argument("--temperature", type=float, default=0.2, help="Model temperature") - ap.add_argument("--num-ctx", type=int, default=4096, help="Context size") - ap.add_argument("--num-predict", type=int, default=256, help="Max tokens to generate") - ap.add_argument("--top-p", type=float, default=0.9, help="Top-p") - ap.add_argument("--concurrency", type=int, default=4, help="Parallel requests") - ap.add_argument("--retries", type=int, default=2, help="Retries per string") - ap.add_argument("--backoff", type=float, default=0.6, help="Backoff seconds base") - ap.add_argument("--dry-run", action="store_true", help="Do not write file; just print summary") - ap.add_argument("--progress-every", type=int, default=1, help="Print progress every N completed strings (default: 1)") - args = ap.parse_args() - - locale_map = { - "es": "Spanish", - "fr": "French", - "de": "German", - "it": "Italian", - "pt": "Portuguese", - "pt-BR": "Brazilian Portuguese", - "ja": "Japanese", - "ko": "Korean", - "zh": "Chinese (Simplified)", - "zh-Hant": "Chinese (Traditional)", - "ru": "Russian", - "uk": "Ukrainian", - "ar": "Arabic", - "hi": "Hindi", - "tr": "Turkish", - "nl": "Dutch", - "sv": "Swedish", - "no": "Norwegian", - "da": "Danish", - "fi": "Finnish", - "pl": "Polish", - "cs": "Czech", - "sk": "Slovak", - "sl": "Slovenian", - "bg": "Bulgarian", - "el": "Greek", - "he": "Hebrew", - "th": "Thai", - "vi": "Vietnamese", - "id": "Indonesian", - } - - # Read source/template file - try: - with open(args.in_path, "r", encoding="utf-8") as f: - source_data = json.load(f) - except Exception as e: - print(f"Failed to read input: {e}", file=sys.stderr) - return 2 - - if not isinstance(source_data, dict): - print("Input JSON must be an object at top-level.", file=sys.stderr) - return 2 - - # If --l10n-dir is provided, process all locale files - if args.l10n_dir: - locales = get_all_locale_files(args.l10n_dir, args.in_path) - if not locales: - print(f"No locale files found in {args.l10n_dir}", file=sys.stderr) - return 1 - - print(f"Found {len(locales)} locale file(s) to process") - - total_translated = 0 - for locale_code, locale_path in locales: - target_lang = locale_map.get(locale_code, locale_code) - - # Read existing target file - try: - with open(locale_path, "r", encoding="utf-8") as f: - target_data = json.load(f) - except Exception as e: - print(f" [{locale_code}] Failed to read {locale_path}: {e}") - continue - - if args.missing_only: - missing_keys = find_missing_keys(source_data, target_data) - if not missing_keys: - print(f" [{locale_code}] No missing keys") - continue - print(f" [{locale_code}] {len(missing_keys)} missing key(s): {', '.join(missing_keys[:5])}{'...' if len(missing_keys) > 5 else ''}") - else: - missing_keys = None - - # Run translation for this locale - result = translate_locale( - source_data=source_data, - target_data=target_data, - target_locale=locale_code, - target_lang=target_lang, - out_path=locale_path, - args=args, - locale_map=locale_map, - missing_keys=missing_keys, - ) - total_translated += result - - print(f"\nTotal: {total_translated} string(s) translated across {len(locales)} locale(s)") - return 0 - - # Single locale mode - validate required args - if not args.out_path: - print("--out is required when not using --l10n-dir", file=sys.stderr) - return 1 - if not args.to_locale: - print("--to-locale is required when not using --l10n-dir", file=sys.stderr) - return 1 - - target_lang = args.target_lang or locale_map.get(args.to_locale, args.to_locale) - - # Read existing target file if --missing-only and file exists - target_data: Dict[str, Any] = {} - missing_keys: Optional[List[str]] = None - if args.missing_only: - if os.path.exists(args.out_path): - try: - with open(args.out_path, "r", encoding="utf-8") as f: - target_data = json.load(f) - missing_keys = find_missing_keys(source_data, target_data) - if not missing_keys: - print(f"No missing keys in {args.out_path}") - return 0 - print(f"Found {len(missing_keys)} missing key(s) to translate") - except Exception as e: - print(f"Failed to read target file: {e}", file=sys.stderr) - return 2 - else: - print(f"Target file {args.out_path} does not exist. Will translate all strings.") - - result = translate_locale( - source_data=source_data, - target_data=target_data, - target_locale=args.to_locale, - target_lang=target_lang, - out_path=args.out_path, - args=args, - locale_map=locale_map, - missing_keys=missing_keys, - ) - return 0 if result >= 0 else 1 - - def translate_locale( source_data: Dict[str, Any], target_data: Dict[str, Any], target_locale: str, target_lang: str, + target_code: str, out_path: str, args, - locale_map: Dict[str, str], missing_keys: Optional[List[str]] = None, ) -> int: """Translate a single locale. Returns number of strings translated.""" - + cfg = OllamaConfig( host=args.host, model=args.model, timeout_s=args.timeout, temperature=args.temperature, - num_ctx=args.num_ctx, - num_predict=args.num_predict, - top_p=args.top_p, ) - # Fallback model for low-confidence translations fallback_cfg = None if args.fallback_model: fallback_cfg = OllamaConfig( @@ -692,34 +314,27 @@ def translate_locale( model=args.fallback_model, timeout_s=args.timeout, temperature=args.temperature, - num_ctx=args.num_ctx, - num_predict=args.num_predict, - top_p=args.top_p, ) - # Start with target data (preserves existing translations) or source data - if target_data: - out_data: Dict[str, Any] = dict(target_data) - else: - out_data: Dict[str, Any] = dict(source_data) + # Start with target data or source data + out_data: Dict[str, Any] = dict(target_data) if target_data else dict(source_data) out_data["@@locale"] = target_locale # Build list of items to translate if missing_keys is not None: - # Only translate missing keys items: List[Tuple[str, str]] = [ - (k, source_data[k]) for k in missing_keys + (k, source_data[k]) for k in missing_keys if is_translatable_entry(k, source_data.get(k)) ] - # Also copy over any metadata keys for missing items + # Copy metadata for missing items for key in missing_keys: meta_key = f"@{key}" if meta_key in source_data: out_data[meta_key] = source_data[meta_key] else: items: List[Tuple[str, str]] = [(k, v) for k, v in source_data.items() if is_translatable_entry(k, v)] - - # Apply manual translations first + + # Apply manual translations manual_count = 0 items_to_translate: List[Tuple[str, str]] = [] for k, v in items: @@ -728,154 +343,73 @@ def translate_locale( manual_count += 1 else: items_to_translate.append((k, v)) - + if manual_count > 0: print(f"Applied {manual_count} manual translation(s)") - - total = len(items_to_translate) - if total == 0 and manual_count == 0: - print("No translatable string entries found (excluding @@locale and @metadata).") - return 0 - - if total == 0: - print("All strings handled by manual translations.") - else: - fallback_info = f" (fallback: {args.fallback_model})" if args.fallback_model else "" - print(f"Translating {total} strings -> {target_lang} using {cfg.model}{fallback_info} (concurrency={args.concurrency})") - - start = time.time() + total = len(items_to_translate) + if total == 0: + if manual_count > 0: + print("All strings handled by manual translations.") + return manual_count + + fallback_info = f" (fallback: {args.fallback_model})" if args.fallback_model else "" + print(f"Translating {total} strings -> {target_lang} using {cfg.model}{fallback_info} (concurrency={args.concurrency})") + + start = time.time() failures: List[Tuple[str, str]] = [] - translated_ok = manual_count # Count manual translations as OK + translated_ok = manual_count fallback_used = 0 completed = 0 - # Build a lookup for original text by key - items_dict: Dict[str, str] = dict(items_to_translate) + with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex: + future_to_key = { + ex.submit( + translate_one, + key=k, + text=v, + target_lang=target_lang, + target_code=target_code, + cfg=cfg, + retries=args.retries, + backoff_s=args.backoff, + fallback_cfg=fallback_cfg, + ): k + for (k, v) in items_to_translate + } - # Submit all tasks up front - if total > 0: - with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex: - future_to_key = { - ex.submit( - translate_one, - key=k, - text=v, - target_lang=target_lang, - cfg=cfg, - retries=args.retries, - backoff_s=args.backoff, - fallback_cfg=fallback_cfg, - confidence_threshold=args.confidence_threshold, - model_confidence_threshold=args.model_confidence_threshold, - ask_model_confidence=bool(args.fallback_model), - ): k - for (k, v) in items_to_translate - } + for fut in as_completed(future_to_key): + k, translated, err, used_fallback = fut.result() + out_data[k] = translated - for fut in as_completed(future_to_key): - k, translated, err, used_fallback = fut.result() - out_data[k] = translated - - completed += 1 - if err: - failures.append((k, err)) - status = "FAIL" + completed += 1 + if err: + failures.append((k, err)) + status = "FAIL" + else: + translated_ok += 1 + if used_fallback: + fallback_used += 1 + status = "OK*" else: - translated_ok += 1 - if used_fallback: - fallback_used += 1 - status = "OK*" # asterisk indicates fallback model was used - else: - status = "OK" - - if args.progress_every > 0 and (completed % args.progress_every == 0 or completed == total): - elapsed = time.time() - start - rate = completed / elapsed if elapsed > 0 else 0.0 - remaining = (total - completed) / rate if rate > 0 else 0.0 - # Keep it single-line friendly but readable. - print( - f"[{completed:>4}/{total}] {status:<4} {k} | " - f"elapsed {fmt_duration(elapsed)} | ETA {fmt_duration(remaining)}" - ) - - elapsed = time.time() - start - fallback_msg = f", used_fallback_model={fallback_used}" if fallback_used > 0 else "" - print(f"Done in {fmt_duration(elapsed)}. OK={translated_ok}{fallback_msg}, errors={len(failures)}") - - # Retry failed translations at the end with increasing temperature - retry_round = 1 - max_end_retries = 3 - retry_model = args.retry_model - while failures and retry_round <= max_end_retries: - # Increase temperature for each retry round - retry_temp = min(cfg.temperature + (0.2 * retry_round), 1.0) - print(f"\n--- Retry round {retry_round}/{max_end_retries} for {len(failures)} failed key(s) (model={retry_model}, temp={retry_temp:.1f}) ---") - retry_items = [(k, items_dict[k]) for k, _ in failures] - failures = [] - retry_completed = 0 - retry_total = len(retry_items) - retry_start = time.time() - - # Create config with higher temperature (and optionally different model) for retries - retry_cfg = OllamaConfig( - host=cfg.host, - model=retry_model, - timeout_s=cfg.timeout_s, - temperature=retry_temp, - num_ctx=cfg.num_ctx, - num_predict=cfg.num_predict, - top_p=cfg.top_p, - ) - - with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as ex: - future_to_key = { - ex.submit( - translate_one, - key=k, - text=v, - target_lang=target_lang, - cfg=retry_cfg, - retries=args.retries, - backoff_s=args.backoff, - ): k - for (k, v) in retry_items - } - - for fut in as_completed(future_to_key): - k, translated, err, used_fb = fut.result() - out_data[k] = translated - - retry_completed += 1 - if err: - failures.append((k, err)) - status = "FAIL" - else: - translated_ok += 1 status = "OK" - if args.progress_every > 0 and (retry_completed % args.progress_every == 0 or retry_completed == retry_total): - elapsed = time.time() - retry_start - rate = retry_completed / elapsed if elapsed > 0 else 0.0 - remaining = (retry_total - retry_completed) / rate if rate > 0 else 0.0 - print( - f"[{retry_completed:>4}/{retry_total}] {status:<4} {k} | " - f"elapsed {fmt_duration(elapsed)} | ETA {fmt_duration(remaining)}" - ) + if completed % args.progress_every == 0 or completed == total: + elapsed = time.time() - start + rate = completed / elapsed if elapsed > 0 else 0.0 + remaining = (total - completed) / rate if rate > 0 else 0.0 + print(f"[{completed:>4}/{total}] {status:<4} {k} | elapsed {fmt_duration(elapsed)} | ETA {fmt_duration(remaining)}") - retry_elapsed = time.time() - retry_start - print(f"Retry round {retry_round} done in {fmt_duration(retry_elapsed)}. Remaining failures: {len(failures)}") - retry_round += 1 - - total_elapsed = time.time() - start - print(f"\nTotal time: {fmt_duration(total_elapsed)}. OK={translated_ok}, final fallback={len(failures)}") + elapsed = time.time() - start + fallback_msg = f", fallback_used={fallback_used}" if fallback_used > 0 else "" + print(f"Done in {fmt_duration(elapsed)}. OK={translated_ok}{fallback_msg}, errors={len(failures)}") if failures: - print("Fallback keys (kept original English due to errors):") - for k, err in failures[:60]: + print(f"{len(failures)} translation(s) kept original English:") + for k, err in failures[:20]: print(f" - {k}: {err}") - if len(failures) > 60: - print(f" ... and {len(failures) - 60} more") + if len(failures) > 20: + print(f" ... and {len(failures) - 20} more") if args.dry_run: print("Dry run: not writing output file.") @@ -893,5 +427,116 @@ def translate_locale( return translated_ok +def main() -> int: + ap = argparse.ArgumentParser(description="Translate ARB files using TranslateGemma") + ap.add_argument("--in", dest="in_path", required=True, help="Input .arb file (source/template)") + ap.add_argument("--out", dest="out_path", help="Output .arb file (required unless using --l10n-dir)") + ap.add_argument("--to-locale", help="Target locale code (es, fr, de, etc.)") + ap.add_argument("--l10n-dir", help="Directory with locale files (translates all locales)") + ap.add_argument("--missing-only", action="store_true", help="Only translate missing keys") + ap.add_argument("--model", default="translategemma:latest", help="Ollama model (translategemma:latest or specific versions)") + ap.add_argument("--fallback-model", help="Fallback model for failed translations (e.g., translategemma:27b)") + ap.add_argument("--host", default="http://localhost:11434", help="Ollama host") + ap.add_argument("--timeout", type=float, default=120.0, help="HTTP timeout seconds") + ap.add_argument("--temperature", type=float, default=0.0, help="Model temperature (0.0 for deterministic)") + ap.add_argument("--concurrency", type=int, default=4, help="Parallel requests") + ap.add_argument("--retries", type=int, default=2, help="Retries per string") + ap.add_argument("--backoff", type=float, default=0.6, help="Backoff seconds base") + ap.add_argument("--dry-run", action="store_true", help="Don't write output") + ap.add_argument("--progress-every", type=int, default=1, help="Print progress every N strings") + args = ap.parse_args() + + # Read source file + try: + with open(args.in_path, "r", encoding="utf-8") as f: + source_data = json.load(f) + except Exception as e: + print(f"Failed to read input: {e}", file=sys.stderr) + return 2 + + if not isinstance(source_data, dict): + print("Input JSON must be an object at top-level.", file=sys.stderr) + return 2 + + # Process all locales if --l10n-dir is provided + if args.l10n_dir: + locales = get_all_locale_files(args.l10n_dir, args.in_path) + if not locales: + print(f"No locale files found in {args.l10n_dir}", file=sys.stderr) + return 1 + + print(f"Found {len(locales)} locale file(s) to process") + + total_translated = 0 + for locale_code, locale_path in locales: + lang_name, lang_code = LOCALE_MAP.get(locale_code, (locale_code, locale_code)) + + try: + with open(locale_path, "r", encoding="utf-8") as f: + target_data = json.load(f) + except Exception as e: + print(f" [{locale_code}] Failed to read {locale_path}: {e}") + continue + + if args.missing_only: + missing_keys = find_missing_keys(source_data, target_data) + if not missing_keys: + print(f" [{locale_code}] No missing keys") + continue + print(f" [{locale_code}] {len(missing_keys)} missing key(s)") + else: + missing_keys = None + + result = translate_locale( + source_data=source_data, + target_data=target_data, + target_locale=locale_code, + target_lang=lang_name, + target_code=lang_code, + out_path=locale_path, + args=args, + missing_keys=missing_keys, + ) + total_translated += result + + print(f"\nTotal: {total_translated} string(s) translated across {len(locales)} locale(s)") + return 0 + + # Single locale mode + if not args.out_path or not args.to_locale: + print("--out and --to-locale are required when not using --l10n-dir", file=sys.stderr) + return 1 + + lang_name, lang_code = LOCALE_MAP.get(args.to_locale, (args.to_locale, args.to_locale)) + + # Read existing target file if --missing-only + target_data: Dict[str, Any] = {} + missing_keys: Optional[List[str]] = None + if args.missing_only and os.path.exists(args.out_path): + try: + with open(args.out_path, "r", encoding="utf-8") as f: + target_data = json.load(f) + missing_keys = find_missing_keys(source_data, target_data) + if not missing_keys: + print(f"No missing keys in {args.out_path}") + return 0 + print(f"Found {len(missing_keys)} missing key(s) to translate") + except Exception as e: + print(f"Failed to read target file: {e}", file=sys.stderr) + return 2 + + result = translate_locale( + source_data=source_data, + target_data=target_data, + target_locale=args.to_locale, + target_lang=lang_name, + target_code=lang_code, + out_path=args.out_path, + args=args, + missing_keys=missing_keys, + ) + return 0 if result >= 0 else 1 + + if __name__ == "__main__": - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/untranslated.json b/untranslated.json index b9dadf3..9e26dfe 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,69 +1 @@ -{ - "bg": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "de": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "es": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "fr": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "it": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "nl": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "pl": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "pt": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "ru": [ - "appSettings_languageUk" - ], - - "sk": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "sl": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "sv": [ - "appSettings_languageRu", - "appSettings_languageUk" - ], - - "uk": [ - "appSettings_languageRu" - ], - - "zh": [ - "appSettings_languageRu", - "appSettings_languageUk" - ] -} +{} \ No newline at end of file