From 79ffc21bd68a78272adf899663f01f7bf91951fd Mon Sep 17 00:00:00 2001 From: Zach Date: Sun, 1 Feb 2026 16:57:17 -0700 Subject: [PATCH] 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