From 04a713bb76013b4e15e8d30fb374cd5df6295ca9 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 18 Jan 2026 11:17:47 -0800 Subject: [PATCH 1/4] Added a basic neighbours screen for repeaters --- lib/l10n/app_bg.arb | 16 +- lib/l10n/app_de.arb | 16 +- lib/l10n/app_en.arb | 14 + lib/l10n/app_es.arb | 16 +- lib/l10n/app_fr.arb | 16 +- lib/l10n/app_it.arb | 16 +- lib/l10n/app_localizations.dart | 42 +++ lib/l10n/app_localizations_bg.dart | 23 ++ lib/l10n/app_localizations_de.dart | 23 ++ lib/l10n/app_localizations_en.dart | 23 ++ lib/l10n/app_localizations_es.dart | 23 ++ lib/l10n/app_localizations_fr.dart | 23 ++ lib/l10n/app_localizations_it.dart | 23 ++ lib/l10n/app_localizations_nl.dart | 23 ++ lib/l10n/app_localizations_pl.dart | 23 ++ lib/l10n/app_localizations_pt.dart | 23 ++ lib/l10n/app_localizations_sk.dart | 23 ++ lib/l10n/app_localizations_sl.dart | 23 ++ lib/l10n/app_localizations_sv.dart | 23 ++ lib/l10n/app_localizations_zh.dart | 23 ++ lib/l10n/app_nl.arb | 16 +- lib/l10n/app_pl.arb | 16 +- lib/l10n/app_pt.arb | 16 +- lib/l10n/app_sk.arb | 16 +- lib/l10n/app_sl.arb | 16 +- lib/l10n/app_sv.arb | 16 +- lib/l10n/app_zh.arb | 16 +- lib/screens/neighbours_screen.dart | 456 +++++++++++++++++++++++++++ lib/screens/repeater_hub_screen.dart | 23 +- lib/widgets/snr_indicator.dart | 62 ++++ 30 files changed, 1075 insertions(+), 13 deletions(-) create mode 100644 lib/screens/neighbours_screen.dart create mode 100644 lib/widgets/snr_indicator.dart diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 4c747da..c85db5f 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Повторители", "listFilter_roomServers": "Сървъри на стая", "listFilter_unreadOnly": "Само непрочетените", - "listFilter_newGroup": "Нова група" + "listFilter_newGroup": "Нова група", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.", + "repeater_neighbours": "Съседи", + "neighbors_ReceivedData": "Получени данни за съседи", + "neighbors_RequestTimedOut": "Съседите поискат изтичане на време.", + "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", + "neighbors_repeatersNeighbours": "Повторители Съседи", + "neighbors_noData": "Няма налични данни за съседи." } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 9044962..3d58892 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Wiederholer", "listFilter_roomServers": "Raumserver", "listFilter_unreadOnly": "Nur nicht gelesen", - "listFilter_newGroup": "Neue Gruppe" + "listFilter_newGroup": "Neue Gruppe", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Nachbarn", + "repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", + "neighbors_ReceivedData": "Empfangene Nachbarendaten", + "neighbors_RequestTimedOut": "Nachbarn melden zeitweise Ausfall.", + "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", + "neighbors_repeatersNeighbours": "Wiederholer Nachbarn", + "neighbors_noData": "Keine Nachbardaten verfügbar." } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dc53f73..608af2d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -753,6 +753,8 @@ "repeater_telemetrySubtitle": "View telemetry of sensors and system stats", "repeater_cli": "CLI", "repeater_cliSubtitle": "Send commands to the repeater", + "repeater_neighbours": "Neighbors", + "repeater_neighboursSubtitle": "View zero hop neighbors.", "repeater_settings": "Settings", "repeater_settingsSubtitle": "Configure repeater parameters", @@ -1055,6 +1057,18 @@ "fahrenheit": {"type": "String"} } }, + + "neighbors_ReceivedData": "Received Neighbours Data", + "neighbors_RequestTimedOut": "Neighbours request timed out.", + "neighbors_errorLoading": "Error loading neighbors: {error}", + "@neighbors_errorLoading": { + "placeholders": { + "error": {"type": "String"} + } + }, + "neighbors_repeatersNeighbours": "Repeaters Neighbours", + "neighbors_noData": "No neighbours data available.", + "channelPath_title": "Packet Path", "channelPath_viewMap": "View map", "channelPath_otherObservedPaths": "Other Observed Paths", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 1515eb6..b3f188f 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de la sala", "listFilter_unreadOnly": "Solo sin leer", - "listFilter_newGroup": "Nuevo grupo" + "listFilter_newGroup": "Nuevo grupo", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Vecinos", + "repeater_neighboursSubtitle": "Ver vecinos de salto cero.", + "neighbors_ReceivedData": "Recibidas Datos de Vecinos", + "neighbors_RequestTimedOut": "Los vecinos solicitan que se desconecte.", + "neighbors_errorLoading": "Error al cargar vecinos: {error}", + "neighbors_repeatersNeighbours": "Repetidores Vecinos", + "neighbors_noData": "No hay datos de vecinos disponibles." } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 4b8af01..566bf1b 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Répéteurs", "listFilter_roomServers": "Serveurs de pièce", "listFilter_unreadOnly": "Messages non lus seulement", - "listFilter_newGroup": "Nouvelle groupe" + "listFilter_newGroup": "Nouvelle groupe", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Voisins", + "repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.", + "neighbors_ReceivedData": "Données des voisins reçues", + "neighbors_RequestTimedOut": "Les voisins demandent un délai.", + "neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}", + "neighbors_repeatersNeighbours": "Répéteurs Voisins", + "neighbors_noData": "Aucune donnée concernant les voisins disponible." } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 72be091..731be02 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Ripetitori", "listFilter_roomServers": "Server della stanza", "listFilter_unreadOnly": "Solo non letto", - "listFilter_newGroup": "Nuovo gruppo" + "listFilter_newGroup": "Nuovo gruppo", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Vicini", + "repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.", + "neighbors_ReceivedData": "Ricevute dati vicini", + "neighbors_RequestTimedOut": "I vicini richiedono un timeout.", + "neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}", + "neighbors_repeatersNeighbours": "Ripetitori Vicini", + "neighbors_noData": "Nessun dato sugli vicini disponibile." } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a2c7ddb..6e3d895 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2831,6 +2831,18 @@ abstract class AppLocalizations { /// **'Send commands to the repeater'** String get repeater_cliSubtitle; + /// No description provided for @repeater_neighbours. + /// + /// In en, this message translates to: + /// **'Neighbors'** + String get repeater_neighbours; + + /// No description provided for @repeater_neighboursSubtitle. + /// + /// In en, this message translates to: + /// **'View zero hop neighbors.'** + String get repeater_neighboursSubtitle; + /// No description provided for @repeater_settings. /// /// In en, this message translates to: @@ -3970,6 +3982,36 @@ abstract class AppLocalizations { /// **'{celsius}°C / {fahrenheit}°F'** String telemetry_temperatureValue(String celsius, String fahrenheit); + /// No description provided for @neighbors_ReceivedData. + /// + /// In en, this message translates to: + /// **'Received Neighbours Data'** + String get neighbors_ReceivedData; + + /// No description provided for @neighbors_RequestTimedOut. + /// + /// In en, this message translates to: + /// **'Neighbours request timed out.'** + String get neighbors_RequestTimedOut; + + /// No description provided for @neighbors_errorLoading. + /// + /// In en, this message translates to: + /// **'Error loading neighbors: {error}'** + String neighbors_errorLoading(String error); + + /// No description provided for @neighbors_repeatersNeighbours. + /// + /// In en, this message translates to: + /// **'Repeaters Neighbours'** + String get neighbors_repeatersNeighbours; + + /// No description provided for @neighbors_noData. + /// + /// In en, this message translates to: + /// **'No neighbours data available.'** + String get neighbors_noData; + /// No description provided for @channelPath_title. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 5def822..3134001 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1567,6 +1567,12 @@ class AppLocalizationsBg extends AppLocalizations { @override String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Настройки'; @@ -2252,6 +2258,23 @@ class AppLocalizationsBg extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Пътеки пъзел'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 591de39..963debe 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1565,6 +1565,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get repeater_cliSubtitle => 'Sende Befehle an den Repeater'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Einstellungen'; @@ -2254,6 +2260,23 @@ class AppLocalizationsDe extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Paketpfad'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 9e04923..17684e0 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1543,6 +1543,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get repeater_cliSubtitle => 'Send commands to the repeater'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Settings'; @@ -2216,6 +2222,23 @@ class AppLocalizationsEn extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Packet Path'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 82259cf..0b7356b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1564,6 +1564,12 @@ class AppLocalizationsEs extends AppLocalizations { @override String get repeater_cliSubtitle => 'Enviar comandos al repetidor'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Configuración'; @@ -2248,6 +2254,23 @@ class AppLocalizationsEs extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Ruta del Paquete'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index acdb5b0..b26c81d 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1570,6 +1570,12 @@ class AppLocalizationsFr extends AppLocalizations { @override String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Paramètres'; @@ -2261,6 +2267,23 @@ class AppLocalizationsFr extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Chemin de paquet'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index cd2d022..cebdda4 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1562,6 +1562,12 @@ class AppLocalizationsIt extends AppLocalizations { @override String get repeater_cliSubtitle => 'Invia comandi al ripetitore'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Impostazioni'; @@ -2248,6 +2254,23 @@ class AppLocalizationsIt extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Percorso Pacchetto'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 3484c0a..f9296ed 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1558,6 +1558,12 @@ class AppLocalizationsNl extends AppLocalizations { @override String get repeater_cliSubtitle => 'Verzend commando\'s naar de herhaaldere'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Instellingen'; @@ -2241,6 +2247,23 @@ class AppLocalizationsNl extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Pakketpad'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 180d8e2..d77596f 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1566,6 +1566,12 @@ class AppLocalizationsPl extends AppLocalizations { @override String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Ustawienia'; @@ -2246,6 +2252,23 @@ class AppLocalizationsPl extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Ścieżka pakietu'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 403c4d6..803f7e7 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1564,6 +1564,12 @@ class AppLocalizationsPt extends AppLocalizations { @override String get repeater_cliSubtitle => 'Enviar comandos ao repetidor'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Configurações'; @@ -2248,6 +2254,23 @@ class AppLocalizationsPt extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Rótulo de Caminho de Pacote'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index e9eb10c..ec71b6e 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1560,6 +1560,12 @@ class AppLocalizationsSk extends AppLocalizations { @override String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Nastavenia'; @@ -2237,6 +2243,23 @@ class AppLocalizationsSk extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Cesta balíka'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index f95797b..eb73d01 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1561,6 +1561,12 @@ class AppLocalizationsSl extends AppLocalizations { String get repeater_cliSubtitle => 'Pošlji ukazne povelje na ponovitveno enoto.'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Nastavitve'; @@ -2242,6 +2248,23 @@ class AppLocalizationsSl extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Pot do paketa'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 6f97659..88679d7 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1548,6 +1548,12 @@ class AppLocalizationsSv extends AppLocalizations { @override String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => 'Inställningar'; @@ -2225,6 +2231,23 @@ class AppLocalizationsSv extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => 'Paketväg'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index e5b5a9f..9778d8f 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1495,6 +1495,12 @@ class AppLocalizationsZh extends AppLocalizations { @override String get repeater_cliSubtitle => '发送命令到重复器'; + @override + String get repeater_neighbours => 'Neighbors'; + + @override + String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + @override String get repeater_settings => '设置'; @@ -2125,6 +2131,23 @@ class AppLocalizationsZh extends AppLocalizations { return '$celsius°C / $fahrenheit°F'; } + @override + String get neighbors_ReceivedData => 'Received Neighbours Data'; + + @override + String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + + @override + String neighbors_errorLoading(String error) { + return 'Error loading neighbors: $error'; + } + + @override + String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + + @override + String get neighbors_noData => 'No neighbours data available.'; + @override String get channelPath_title => '数据包路径'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index da33f11..375ca35 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Herhalingen", "listFilter_roomServers": "Kamervirtualisatie", "listFilter_unreadOnly": "Alleen ongelezen", - "listFilter_newGroup": "Nieuwe groep" + "listFilter_newGroup": "Nieuwe groep", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Buren", + "repeater_neighboursSubtitle": "Bekijk nul hops buren.", + "neighbors_ReceivedData": "Ontvangen Buurdata", + "neighbors_RequestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.", + "neighbors_errorLoading": "Fout bij het laden van buren: {error}", + "neighbors_repeatersNeighbours": "Herhalingen Buren", + "neighbors_noData": "Geen gegevens van buren beschikbaar." } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index bab67b5..b900ee9 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Powtarzacze", "listFilter_roomServers": "Serwery pokoju", "listFilter_unreadOnly": "Tylko nieprzeczytane", - "listFilter_newGroup": "Nowa grupa" + "listFilter_newGroup": "Nowa grupa", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Sąsiedzi", + "repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.", + "neighbors_ReceivedData": "Otrzymano dane sąsiedztwa", + "neighbors_RequestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.", + "neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}", + "neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi", + "neighbors_noData": "Brak danych dotyczących sąsiadów." } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 59f4a47..c25cedd 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Repetidores", "listFilter_roomServers": "Servidores de sala", "listFilter_unreadOnly": "Apenas não lido", - "listFilter_newGroup": "Novo grupo" + "listFilter_newGroup": "Novo grupo", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Vizinhos", + "neighbors_ReceivedData": "Dados dos Vizinhos Recebidos", + "repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.", + "neighbors_RequestTimedOut": "Vizinhos solicitam tempo limite esgotado.", + "neighbors_errorLoading": "Erro ao carregar vizinhos: {error}", + "neighbors_repeatersNeighbours": "Repetidores Vizinhos", + "neighbors_noData": "Não estão disponíveis dados de vizinhos." } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index 67043a7..6b75982 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Opakovadlá", "listFilter_roomServers": "Servéry miestnosti", "listFilter_unreadOnly": "Nezaregistrované len", - "listFilter_newGroup": "Nová skupina" + "listFilter_newGroup": "Nová skupina", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.", + "neighbors_RequestTimedOut": "Súďia žiadajú o časové ukončenie.", + "neighbors_ReceivedData": "Obdielo dáta suseda", + "repeater_neighbours": "Súsezný", + "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", + "neighbors_repeatersNeighbours": "Opakovadlá Súsezná", + "neighbors_noData": "Nie je dostupná žiadna informácia o susedoch." } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 8a8dc59..8a6f49d 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Ponovitve", "listFilter_roomServers": "Smeti za prostore", "listFilter_unreadOnly": "Nezbrani samo", - "listFilter_newGroup": "Nova skupina" + "listFilter_newGroup": "Nova skupina", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.", + "repeater_neighbours": "Sosedi", + "neighbors_ReceivedData": "Prejeto podatke o sosedih", + "neighbors_RequestTimedOut": "Sosedi zahtevajo izklop po dogovoru.", + "neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}", + "neighbors_repeatersNeighbours": "Ponovitve Sosedi", + "neighbors_noData": "Niso na voljo podatki o sosedih." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 866e438..acf8d9c 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "Upprepare", "listFilter_roomServers": "Rumservrar", "listFilter_unreadOnly": "Endast oinlästa", - "listFilter_newGroup": "Ny grupp" + "listFilter_newGroup": "Ny grupp", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighbours": "Grannar", + "repeater_neighboursSubtitle": "Visa noll hoppgrannar.", + "neighbors_ReceivedData": "Mottagna grannars data", + "neighbors_RequestTimedOut": "Grannar begär tidsinställd utskick.", + "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", + "neighbors_repeatersNeighbours": "Upprepar grannar", + "neighbors_noData": "Inga grannuppgifter finns tillgängliga." } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f1349e8..20fb6e7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1335,5 +1335,19 @@ "listFilter_repeaters": "重复器", "listFilter_roomServers": "房间服务器", "listFilter_unreadOnly": "未读消息", - "listFilter_newGroup": "新组" + "listFilter_newGroup": "新组", + "@neighbors_errorLoading": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "repeater_neighboursSubtitle": "查看零跳邻居。", + "repeater_neighbours": "邻居", + "neighbors_ReceivedData": "收到邻居数据", + "neighbors_RequestTimedOut": "邻居请求超时处理。", + "neighbors_errorLoading": "加载邻居时出错:{error}", + "neighbors_repeatersNeighbours": "重复器邻居", + "neighbors_noData": "没有可用的邻居数据。" } diff --git a/lib/screens/neighbours_screen.dart b/lib/screens/neighbours_screen.dart new file mode 100644 index 0000000..b31295c --- /dev/null +++ b/lib/screens/neighbours_screen.dart @@ -0,0 +1,456 @@ +import 'dart:async'; +import 'dart:math'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../l10n/l10n.dart'; +import '../models/contact.dart'; +import '../models/path_selection.dart'; +import '../connector/meshcore_connector.dart'; +import '../connector/meshcore_protocol.dart'; +import '../services/repeater_command_service.dart'; +import '../widgets/path_management_dialog.dart'; +import '../widgets/snr_indicator.dart'; + +class NeighboursScreen extends StatefulWidget { + final Contact repeater; + final String password; + + const NeighboursScreen({ + super.key, + required this.repeater, + required this.password, + }); + + @override + State createState() => _NeighboursScreenState(); +} + +class _NeighboursScreenState extends State { + static const int _reqNeighboursKeyLen = 4; + static const int _statusPayloadOffset = 8; + static const int _statusStatsSize = 52; + static const int _statusResponseBytes = + _statusPayloadOffset + _statusStatsSize; + Uint8List _tagData = Uint8List(4); + //int _timeEstment = 0; + int _neighbourCount = 0; + + bool _isLoading = false; + bool _isLoaded = false; + bool _hasData = false; + Timer? _statusTimeout; + StreamSubscription? _frameSubscription; + RepeaterCommandService? _commandService; + PathSelection? _pendingStatusSelection; + List>? _parsedNeighbours; + + @override + void initState() { + super.initState(); + final connector = Provider.of(context, listen: false); + _commandService = RepeaterCommandService(connector); + _setupMessageListener(); + _loadNeighbours(); + _hasData = false; + } + + void _setupMessageListener() { + final connector = Provider.of(context, listen: false); + + // Listen for incoming text messages from the repeater + _frameSubscription = connector.receivedFrames.listen((frame) { + if (frame.isEmpty) return; + + if (frame[0] == respCodeSent) { + _tagData = frame.sublist(2, 6); + //_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little); + } + + // Check if it's a binary response + if (frame[0] == pushCodeBinaryResponse && + listEquals(frame.sublist(2, 6), _tagData)) { + _handleNeighboursResponse(context, connector, frame.sublist(6)); + } + }); + } + + String fmtDuration(double seconds) { + if (seconds < 60) { + return '${seconds.toStringAsFixed(1)}s'; + } + + final int m = (seconds ~/ 60).toInt(); + final double s = seconds - (60 * m); + + if (m < 60) { + return '${m}m ${s.toStringAsFixed(0)}s'; + } + + final int h = m ~/ 60; + final int m2 = m % 60; + + return '${h}h ${m2}m'; + } + + static List> parseNeighboursData( + BufferReader buffer, + int resultsCount, + ) { + final Map> neighbours = {}; + for (var i = 0; i < resultsCount; i++) { + final neighbourData = neighbours.putIfAbsent( + i, + () => { + 'contact': null, + 'publicKey': {}, + 'lastHeard': {}, + 'snr': {}, + }, + ); + neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen); + neighbourData['lastHeard'] = buffer.readUInt32LE(); + neighbourData['snr'] = buffer.readInt8() / 4.0; + } + + return neighbours.values.toList(); + } + + void _handleNeighboursResponse( + BuildContext context, + MeshCoreConnector connector, + Uint8List frame, + ) { + final buffer = BufferReader(frame); + final neighbourCount = buffer.readUInt16LE(); + final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE()); + connector.contacts.where((c) => c.type == advTypeRepeater).forEach(( + repeater, + ) { + parsedNeighbours.forEach((neighbourData) { + final publicKey = neighbourData['publicKey']; + if (listEquals( + repeater.publicKey.sublist(0, _reqNeighboursKeyLen), + publicKey, + )) { + neighbourData['contact'] = repeater; + } + }); + }); + + setState(() { + _parsedNeighbours = parsedNeighbours; + _neighbourCount = neighbourCount; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.neighbors_ReceivedData), + backgroundColor: Colors.green, + ), + ); + _statusTimeout?.cancel(); + if (!mounted) return; + setState(() { + _isLoading = false; + _isLoaded = true; + _hasData = true; + }); + } + + Contact _resolveRepeater(MeshCoreConnector connector) { + return connector.contacts.firstWhere( + (c) => c.publicKeyHex == widget.repeater.publicKeyHex, + orElse: () => widget.repeater, + ); + } + + Future _loadNeighbours() async { + if (_commandService == null) return; + + setState(() { + _isLoading = true; + _isLoaded = false; + }); + try { + final connector = Provider.of(context, listen: false); + final repeater = _resolveRepeater(connector); + final selection = await connector.preparePathForContactSend(repeater); + _pendingStatusSelection = selection; + + //[version][number of requested neighbours][offset_16bit][order by][len of public key] + final frame = buildSendBinaryReq( + repeater.publicKey, + payload: Uint8List.fromList([ + reqTypeGetNeighbours, + 0x00, + 0x0F, + 0x00, + 0x00, + 0x00, + _reqNeighboursKeyLen, + ]), + ); + await connector.sendFrame(frame); + + final pathLengthValue = selection.useFlood ? -1 : selection.hopCount; + final messageBytes = frame.length >= _statusResponseBytes + ? frame.length + : _statusResponseBytes; + final timeoutMs = connector.calculateTimeout( + pathLength: pathLengthValue, + messageBytes: messageBytes, + ); + _statusTimeout?.cancel(); + _statusTimeout = Timer(Duration(milliseconds: timeoutMs), () { + if (!mounted) return; + setState(() { + _isLoading = false; + _isLoaded = false; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.neighbors_RequestTimedOut), + backgroundColor: Colors.red, + ), + ); + _recordStatusResult(false); + }); + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + _isLoaded = false; + }); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.neighbors_errorLoading(e.toString())), + backgroundColor: Colors.red, + ), + ); + } + } + } + + void _recordStatusResult(bool success) { + final selection = _pendingStatusSelection; + if (selection == null) return; + final connector = Provider.of(context, listen: false); + final repeater = _resolveRepeater(connector); + connector.recordRepeaterPathResult(repeater, selection, success, null); + _pendingStatusSelection = null; + } + + @override + void dispose() { + _frameSubscription?.cancel(); + _commandService?.dispose(); + _statusTimeout?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final l10n = context.l10n; + final connector = context.watch(); + final repeater = _resolveRepeater(connector); + final isFloodMode = repeater.pathOverride == -1; + + return Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.neighbors_repeatersNeighbours, + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Text( + repeater.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + centerTitle: false, + actions: [ + PopupMenuButton( + icon: Icon(isFloodMode ? Icons.waves : Icons.route), + tooltip: l10n.repeater_routingMode, + onSelected: (mode) async { + if (mode == 'flood') { + await connector.setPathOverride(repeater, pathLen: -1); + } else { + await connector.setPathOverride(repeater, pathLen: null); + } + }, + itemBuilder: (context) => [ + PopupMenuItem( + value: 'auto', + child: Row( + children: [ + Icon( + Icons.auto_mode, + size: 20, + color: !isFloodMode + ? Theme.of(context).primaryColor + : null, + ), + const SizedBox(width: 8), + Text( + l10n.repeater_autoUseSavedPath, + style: TextStyle( + fontWeight: !isFloodMode + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'flood', + child: Row( + children: [ + Icon( + Icons.waves, + size: 20, + color: isFloodMode + ? Theme.of(context).primaryColor + : null, + ), + const SizedBox(width: 8), + Text( + l10n.repeater_forceFloodMode, + style: TextStyle( + fontWeight: isFloodMode + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + ], + ), + IconButton( + icon: const Icon(Icons.timeline), + tooltip: l10n.repeater_pathManagement, + onPressed: () => + PathManagementDialog.show(context, contact: repeater), + ), + IconButton( + icon: _isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.refresh), + onPressed: _isLoading ? null : _loadNeighbours, + tooltip: l10n.repeater_refresh, + ), + ], + ), + body: SafeArea( + top: false, + child: RefreshIndicator( + onRefresh: _loadNeighbours, + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + if (!_isLoaded && + !_hasData && + (_parsedNeighbours == null || _parsedNeighbours!.isEmpty)) + Center( + child: Text( + l10n.neighbors_noData, + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ), + if (_isLoaded || + _hasData && + !(_parsedNeighbours == null || + _parsedNeighbours!.isEmpty)) + _buildNeighboursInfoCard(l10n.repeater_neighbours), + ], + ), + ), + ), + ); + } + + Widget _buildNeighboursInfoCard(String title) { + final connector = Provider.of(context, listen: false); + return Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.info_outline, + color: Theme.of(context).textTheme.headlineSmall?.color, + ), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Divider(), + for (final entry in _parsedNeighbours!.asMap().entries) + _buildInfoRow( + entry.value['contact'] != null + ? entry.value['contact'].name + : 'Unknown (${pubKeyToHex(entry.value['publicKey'])})', + 'Heard: ${fmtDuration(entry.value['lastHeard'] + 0.0)} ago', + entry.value['snr'], + connector.currentSf!, + ), + ], + ), + ), + ); + } + + Widget _buildInfoRow( + String label, + String value, + double snr, + int spreadingFactor, + ) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: ListTile( + contentPadding: EdgeInsets.zero, + title: Text( + label, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text(value), + trailing: SNRIcon( + snr: snr, + snrLevels: getSNRfromSF(spreadingFactor), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart index 88a58fa..a919c59 100644 --- a/lib/screens/repeater_hub_screen.dart +++ b/lib/screens/repeater_hub_screen.dart @@ -5,6 +5,7 @@ import 'repeater_status_screen.dart'; import 'repeater_cli_screen.dart'; import 'repeater_settings_screen.dart'; import 'telemetry_screen.dart'; +import 'neighbours_screen.dart'; class RepeaterHubScreen extends StatelessWidget { final Contact repeater; @@ -145,12 +146,32 @@ class RepeaterHubScreen extends StatelessWidget { ), const SizedBox(height: 12), // Settings button + _buildManagementCard( + context, + icon: Icons.group, + title: l10n.repeater_neighbours, + subtitle: l10n.repeater_neighboursSubtitle, + color: Colors.orange, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => NeighboursScreen( + repeater: repeater, + password: password, + ), + ), + ); + }, + ), + const SizedBox(height: 12), + // Settings button _buildManagementCard( context, icon: Icons.settings, title: l10n.repeater_settings, subtitle: l10n.repeater_settingsSubtitle, - color: Colors.orange, + color: Colors.deepOrange, onTap: () { Navigator.push( context, diff --git a/lib/widgets/snr_indicator.dart b/lib/widgets/snr_indicator.dart new file mode 100644 index 0000000..da68a65 --- /dev/null +++ b/lib/widgets/snr_indicator.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +List getSNRfromSF(int spreadingFactor) { + switch (spreadingFactor) { + case 7: + return [4.0, -2.0, -4.0, -6.0]; + case 8: + return [4.0, -4.0, -6.0, -8.0]; + case 9: + return [4.0, -6.0, -8.0, -10.0]; + case 10: + return [4.0, -8.0, -10.0, -13.0]; + case 11: + return [4.0, -10.0, -12.5, -15.0]; + case 12: + return [4.0, -12.5, -15.0, -18.0]; + default: + return []; // Or throw Exception('Invalid SF: $spreadingFactor'); + } +} + +class SNRIcon extends StatelessWidget { + final double snr; + final List snrLevels; + + const SNRIcon({ + super.key, + required this.snr, + this.snrLevels = const [4.0, -2.0, -4.0, -6.0], + }); + + @override + Widget build(BuildContext context) { + IconData icon; + Color color; + + if (snr >= snrLevels[0]) { + icon = Icons.signal_cellular_alt; + color = Colors.green; + } else if (snr >= snrLevels[1]) { + icon = Icons.signal_cellular_alt; + color = Colors.lightGreen; + } else if (snr >= snrLevels[2]) { + icon = Icons.signal_cellular_alt; + color = Colors.yellow; + } else if (snr >= snrLevels[3]) { + icon = Icons.signal_cellular_alt_2_bar; + color = Colors.orange; + } else { + icon = Icons.signal_cellular_alt_1_bar; + color = Colors.red; + } + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: color), + Text('$snr dB', style: TextStyle(fontSize: 10, color: color)), + ], + ); + } +} From 6c8a149e1b79f878ad707b91a526895671c51611 Mon Sep 17 00:00:00 2001 From: Winston Lowe Date: Sun, 18 Jan 2026 11:54:45 -0800 Subject: [PATCH 2/4] fix a few translations and used _neighbourCount --- lib/l10n/app_bg.arb | 19 +++++++++++++++++-- lib/l10n/app_de.arb | 21 +++++++++++++++++---- lib/l10n/app_en.arb | 13 ++++++++++++- lib/l10n/app_es.arb | 21 +++++++++++++++++---- lib/l10n/app_fr.arb | 21 +++++++++++++++++---- lib/l10n/app_it.arb | 21 +++++++++++++++++---- lib/l10n/app_nl.arb | 21 +++++++++++++++++---- lib/l10n/app_pl.arb | 21 +++++++++++++++++---- lib/l10n/app_pt.arb | 21 +++++++++++++++++---- lib/l10n/app_sk.arb | 21 +++++++++++++++++---- lib/l10n/app_sl.arb | 21 +++++++++++++++++---- lib/l10n/app_sv.arb | 21 +++++++++++++++++---- lib/l10n/app_zh.arb | 21 +++++++++++++++++---- lib/screens/neighbours_screen.dart | 25 +++++++++++++------------ 14 files changed, 229 insertions(+), 59 deletions(-) diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index a49fa5a..b9a221d 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.", - "common_reload": "Презареди", "common_clear": "Изчисти", "path_currentPath": "Текущ път: {path}", @@ -1363,5 +1362,21 @@ "channels_scanQrCode": "Сканирайте QR код", "channels_scanQrCodeComingSoon": "Ще излезе скоро", "channels_enterHashtag": "Въведете хаштаг", - "channels_hashtagHint": "напр. #отбор" + "channels_hashtagHint": "напр. #отбор", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Слушано преди {time}.", + "neighbors_unknownContact": "Неизвестна {pubkey}" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index cf3e290..953d529 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.", - "common_reload": "Neu laden", "common_clear": "Löschen", "path_currentPath": "Aktiver Pfad: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Raumserver", "listFilter_unreadOnly": "Nur nicht gelesen", "listFilter_newGroup": "Neue Gruppe", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", "neighbors_repeatersNeighbours": "Wiederholer Nachbarn", "neighbors_noData": "Keine Nachbardaten verfügbar.", - "channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei", "channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.", "channels_createPrivateChannel": "Erstelle einen privaten Kanal", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Scannen Sie einen QR-Code", "channels_scanQrCodeComingSoon": "Bald verfügbar", "channels_enterHashtag": "Gib Hashtag ein", - "channels_hashtagHint": "z.B. #team" + "channels_hashtagHint": "z.B. #team", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Hörte: {time} vor her.", + "neighbors_unknownContact": "Unbekannte {pubkey}" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ad85401..a098e27 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1082,7 +1082,18 @@ }, "neighbors_repeatersNeighbours": "Repeaters Neighbours", "neighbors_noData": "No neighbours data available.", - + "neighbors_unknownContact": "Unknown {pubkey}", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": {"type": "String"} + } + }, + "neighbors_heardAgo": "Heard: {time} ago", + "@neighbors_heardAgo": { + "placeholders": { + "time": {"type": "String"} + } + }, "channelPath_title": "Packet Path", "channelPath_viewMap": "View map", "channelPath_otherObservedPaths": "Other Observed Paths", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 12325ac..0581a63 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.", - "common_reload": "Recargar", "common_clear": "Borrar", "path_currentPath": "Ruta actual: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Servidores de la sala", "listFilter_unreadOnly": "Solo sin leer", "listFilter_newGroup": "Nuevo grupo", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Error al cargar vecinos: {error}", "neighbors_repeatersNeighbours": "Repetidores Vecinos", "neighbors_noData": "No hay datos de vecinos disponibles.", - "channels_joinPrivateChannel": "Únete a un Canal Privado", "channels_createPrivateChannel": "Crear un Canal Privado", "channels_createPrivateChannelDesc": "Cifrado con una clave secreta.", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Escanear un Código QR", "channels_scanQrCodeComingSoon": "Próximamente", "channels_enterHashtag": "Introducir hashtag", - "channels_hashtagHint": "ej. #equipo" + "channels_hashtagHint": "ej. #equipo", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_unknownContact": "Clave pública desconocida {pubkey}", + "neighbors_heardAgo": "Escuchado: {time} hace atrás" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 6f21dfe..d69de69 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.", - "common_reload": "Recharger", "common_clear": "Effacer", "path_currentPath": "Chemin actuel : {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Serveurs de pièce", "listFilter_unreadOnly": "Messages non lus seulement", "listFilter_newGroup": "Nouvelle groupe", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}", "neighbors_repeatersNeighbours": "Répéteurs Voisins", "neighbors_noData": "Aucune donnée concernant les voisins disponible.", - "channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.", "channels_joinPrivateChannel": "Rejoindre un Canal Privé", "channels_createPrivateChannel": "Créer un Canal Privé", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Scanner un code QR", "channels_scanQrCodeComingSoon": "Bientôt disponible", "channels_enterHashtag": "Entrez le hashtag", - "channels_hashtagHint": "ex. #équipe" + "channels_hashtagHint": "ex. #équipe", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_unknownContact": "Clé publique inconnue {pubkey}", + "neighbors_heardAgo": "Écouté : {time} auparavant" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 5d61ff7..f34029e 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.", - "common_reload": "Ricaricare", "common_clear": "Cancella", "path_currentPath": "Percorso corrente: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Server della stanza", "listFilter_unreadOnly": "Solo non letto", "listFilter_newGroup": "Nuovo gruppo", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}", "neighbors_repeatersNeighbours": "Ripetitori Vicini", "neighbors_noData": "Nessun dato sugli vicini disponibile.", - "channels_createPrivateChannel": "Crea un Canale Privato", "channels_createPrivateChannelDesc": "Protetta con una chiave segreta.", "channels_joinPrivateChannel": "Unisciti a un Canale Privato", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Scansiona un codice QR", "channels_scanQrCodeComingSoon": "Arriverà presto", "channels_enterHashtag": "Inserisci hashtag", - "channels_hashtagHint": "es. #team" + "channels_hashtagHint": "es. #team", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Sentito: {time} fa", + "neighbors_unknownContact": "Chiave pubblica sconosciuta {pubkey}" } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 44f2f48..754e606 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.", - "common_reload": "Opnieuw laden", "common_clear": "Schoonmaken", "path_currentPath": "Huidige pad: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Roomservers", "listFilter_unreadOnly": "Alleen ongelezen", "listFilter_newGroup": "Nieuwe groep", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Fout bij het laden van buren: {error}", "neighbors_repeatersNeighbours": "Herhalingen Buren", "neighbors_noData": "Geen gegevens van buren beschikbaar.", - "channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.", "channels_createPrivateChannel": "Maak een Privé Kanaal", "channels_joinPrivateChannel": "Sluit een Privé Kanaal aan", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Scan een QR-code", "channels_scanQrCodeComingSoon": "Komt later", "channels_enterHashtag": "Voer hashtag in", - "channels_hashtagHint": "bijv. #team" + "channels_hashtagHint": "bijv. #team", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_unknownContact": "Onbekende {pubkey}", + "neighbors_heardAgo": "Horen: {time} geleden" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index ff8f058..f54644c 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.", - "common_reload": "Ponownie załadować", "common_clear": "Wyczyść", "path_currentPath": "Aktualny ścieżka: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Serwery pokoju", "listFilter_unreadOnly": "Tylko nieprzeczytane", "listFilter_newGroup": "Nowa grupa", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}", "neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi", "neighbors_noData": "Brak danych dotyczących sąsiadów.", - "channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.", "channels_createPrivateChannel": "Utwórz Prywatny Kanał", "channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Skanuj kod QR", "channels_scanQrCodeComingSoon": "Wkrótce", "channels_enterHashtag": "Wprowadź hashtag", - "channels_hashtagHint": "np. #zespół" + "channels_hashtagHint": "np. #zespół", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Usłyszano: {time} temu", + "neighbors_unknownContact": "Nieznana {pubkey}" } diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 7ecf0cb..ee9ff56 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.", - "common_reload": "Recarregar", "common_clear": "Limpar", "path_currentPath": "Caminho atual: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Servidores de sala", "listFilter_unreadOnly": "Apenas não lido", "listFilter_newGroup": "Novo grupo", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Erro ao carregar vizinhos: {error}", "neighbors_repeatersNeighbours": "Repetidores Vizinhos", "neighbors_noData": "Não estão disponíveis dados de vizinhos.", - "channels_createPrivateChannelDesc": "Protegido com uma chave secreta.", "channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.", "channels_createPrivateChannel": "Criar um Canal Privado", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Digitalizar um Código QR", "channels_scanQrCodeComingSoon": "Em breve", "channels_enterHashtag": "Insira hashtag", - "channels_hashtagHint": "ex. #equipe" + "channels_hashtagHint": "ex. #equipe", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Ouvido: {time} atrás", + "neighbors_unknownContact": "{pubkey} Desconhecido" } diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index ff602ec..2355258 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.", - "common_reload": "Načítať", "common_clear": "Zmazať", "path_currentPath": "Aktívna cesta: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Servéry miestnosti", "listFilter_unreadOnly": "Nezaregistrované len", "listFilter_newGroup": "Nová skupina", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", "neighbors_repeatersNeighbours": "Opakovadlá Súsezná", "neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.", - "channels_createPrivateChannel": "Vytvorte súkromný kanál", "channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu", "channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Skenujte QR kód", "channels_scanQrCodeComingSoon": "Čoskoro", "channels_enterHashtag": "Zadajte hashtag", - "channels_hashtagHint": "napr. #tím" + "channels_hashtagHint": "napr. #tím", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Počuli sme to: {time} dozadu", + "neighbors_unknownContact": "Neznáma {pubkey}" } diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index 9b63d01..2390bad 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.", - "common_reload": "Ponovno naloži", "common_clear": "Ponoviti", "path_currentPath": "Trenutna pot: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Smeti za prostore", "listFilter_unreadOnly": "Nezbrani samo", "listFilter_newGroup": "Nova skupina", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}", "neighbors_repeatersNeighbours": "Ponovitve Sosedi", "neighbors_noData": "Niso na voljo podatki o sosedih.", - "channels_joinPrivateChannel": "Pridružite se zasebni skupini", "channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.", "channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Skeniraj QR kodo", "channels_scanQrCodeComingSoon": "Prihajajoča", "channels_enterHashtag": "Vnesite hashtag", - "channels_hashtagHint": "npr. #ekipa" + "channels_hashtagHint": "npr. #ekipa", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_unknownContact": "Nepoznano {pubkey}", + "neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj." } diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index a91835d..58adcca 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.", - "common_reload": "Ladda om", "common_clear": "Rensa", "path_currentPath": "Nuvarande sökväg: {path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "Rumservrar", "listFilter_unreadOnly": "Endast oinlästa", "listFilter_newGroup": "Ny grupp", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", "neighbors_repeatersNeighbours": "Upprepar grannar", "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", - "channels_createPrivateChannel": "Skapa en privat kanal", "channels_joinPrivateChannel": "Gå med i en Privat Kanal", "channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "Skanna en QR-kod", "channels_scanQrCodeComingSoon": "Kommer snart", "channels_enterHashtag": "Ange hashtag", - "channels_hashtagHint": "t.ex. #team" + "channels_hashtagHint": "t.ex. #team", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "Hördes: {time} sedan", + "neighbors_unknownContact": "Okänd {pubkey}" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 1ad2c5c..b71190f 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -822,7 +822,6 @@ } }, "login_failedMessage": "登录失败。密码不正确或中继器不可达。", - "common_reload": "重新加载", "common_clear": "清除", "path_currentPath": "当前路径:{path}", @@ -1338,7 +1337,6 @@ "listFilter_roomServers": "房间服务器", "listFilter_unreadOnly": "未读消息", "listFilter_newGroup": "新组", - "@neighbors_errorLoading": { "placeholders": { "error": { @@ -1353,7 +1351,6 @@ "neighbors_errorLoading": "加载邻居时出错:{error}", "neighbors_repeatersNeighbours": "重复器邻居", "neighbors_noData": "没有可用的邻居数据。", - "channels_joinPrivateChannel": "加入私密频道", "channels_createPrivateChannelDesc": "使用密钥保护。", "channels_joinPrivateChannelDesc": "手动输入密钥。", @@ -1365,5 +1362,21 @@ "channels_scanQrCode": "扫描二维码", "channels_scanQrCodeComingSoon": "即将到来", "channels_enterHashtag": "输入标签", - "channels_hashtagHint": "例如 #团队" + "channels_hashtagHint": "例如 #团队", + "@neighbors_unknownContact": { + "placeholders": { + "pubkey": { + "type": "String" + } + } + }, + "@neighbors_heardAgo": { + "placeholders": { + "time": { + "type": "String" + } + } + }, + "neighbors_heardAgo": "听到的时间:{time}前", + "neighbors_unknownContact": "未知{pubkey}" } diff --git a/lib/screens/neighbours_screen.dart b/lib/screens/neighbours_screen.dart index b31295c..03c6062 100644 --- a/lib/screens/neighbours_screen.dart +++ b/lib/screens/neighbours_screen.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -70,7 +69,7 @@ class _NeighboursScreenState extends State { // Check if it's a binary response if (frame[0] == pushCodeBinaryResponse && listEquals(frame.sublist(2, 6), _tagData)) { - _handleNeighboursResponse(context, connector, frame.sublist(6)); + _handleNeighboursResponse(connector, frame.sublist(6)); } }); } @@ -116,18 +115,14 @@ class _NeighboursScreenState extends State { return neighbours.values.toList(); } - void _handleNeighboursResponse( - BuildContext context, - MeshCoreConnector connector, - Uint8List frame, - ) { + void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) { final buffer = BufferReader(frame); final neighbourCount = buffer.readUInt16LE(); final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE()); connector.contacts.where((c) => c.type == advTypeRepeater).forEach(( repeater, ) { - parsedNeighbours.forEach((neighbourData) { + for (var neighbourData in parsedNeighbours) { final publicKey = neighbourData['publicKey']; if (listEquals( repeater.publicKey.sublist(0, _reqNeighboursKeyLen), @@ -135,7 +130,7 @@ class _NeighboursScreenState extends State { )) { neighbourData['contact'] = repeater; } - }); + } }); setState(() { @@ -376,7 +371,9 @@ class _NeighboursScreenState extends State { _hasData && !(_parsedNeighbours == null || _parsedNeighbours!.isEmpty)) - _buildNeighboursInfoCard(l10n.repeater_neighbours), + _buildNeighboursInfoCard( + "${l10n.repeater_neighbours} - $_neighbourCount", + ), ], ), ), @@ -413,8 +410,12 @@ class _NeighboursScreenState extends State { _buildInfoRow( entry.value['contact'] != null ? entry.value['contact'].name - : 'Unknown (${pubKeyToHex(entry.value['publicKey'])})', - 'Heard: ${fmtDuration(entry.value['lastHeard'] + 0.0)} ago', + : context.l10n.neighbors_unknownContact( + "<${pubKeyToHex(entry.value['publicKey'])}>", + ), + context.l10n.neighbors_heardAgo( + fmtDuration(entry.value['lastHeard'] + 0.0), + ), entry.value['snr'], connector.currentSf!, ), From ee3b0a3126bc260ae13f2ba9ccc8a211306a70a2 Mon Sep 17 00:00:00 2001 From: zjs81 Date: Mon, 19 Jan 2026 19:13:22 -0700 Subject: [PATCH 3/4] Add untranslated messages file and update localization keys - Added `untranslated.json` to track untranslated messages. - Updated localization keys in various language files to use camelCase format for consistency. - Modified `neighbours_screen.dart` to reference updated localization keys. --- l10n.yaml | 1 + lib/l10n/app_bg.arb | 4 ++-- lib/l10n/app_de.arb | 12 +++++++--- lib/l10n/app_en.arb | 4 ++-- lib/l10n/app_es.arb | 4 ++-- lib/l10n/app_fr.arb | 4 ++-- lib/l10n/app_it.arb | 4 ++-- lib/l10n/app_localizations.dart | 20 +++++++++++++---- lib/l10n/app_localizations_bg.dart | 25 +++++++++++++++------ lib/l10n/app_localizations_de.dart | 36 +++++++++++++++++++----------- lib/l10n/app_localizations_en.dart | 14 ++++++++++-- lib/l10n/app_localizations_es.dart | 25 +++++++++++++++------ lib/l10n/app_localizations_fr.dart | 26 +++++++++++++++------ lib/l10n/app_localizations_it.dart | 25 +++++++++++++++------ lib/l10n/app_localizations_nl.dart | 25 +++++++++++++++------ lib/l10n/app_localizations_pl.dart | 26 +++++++++++++++------ lib/l10n/app_localizations_pt.dart | 26 +++++++++++++++------ lib/l10n/app_localizations_sk.dart | 25 +++++++++++++++------ lib/l10n/app_localizations_sl.dart | 25 +++++++++++++++------ lib/l10n/app_localizations_sv.dart | 24 ++++++++++++++------ lib/l10n/app_localizations_zh.dart | 24 ++++++++++++++------ lib/l10n/app_nl.arb | 4 ++-- lib/l10n/app_pl.arb | 4 ++-- lib/l10n/app_pt.arb | 4 ++-- lib/l10n/app_sk.arb | 4 ++-- lib/l10n/app_sl.arb | 4 ++-- lib/l10n/app_sv.arb | 4 ++-- lib/l10n/app_zh.arb | 4 ++-- lib/screens/neighbours_screen.dart | 5 ++--- untranslated.json | 1 + 30 files changed, 287 insertions(+), 126 deletions(-) create mode 100644 untranslated.json diff --git a/l10n.yaml b/l10n.yaml index a4d323c..b89567e 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -3,3 +3,4 @@ template-arb-file: app_en.arb output-localization-file: app_localizations.dart output-class: AppLocalizations nullable-getter: false +untranslated-messages-file: untranslated.json diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index a5571aa..ca8e338 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.", "repeater_neighbours": "Съседи", - "neighbors_ReceivedData": "Получени данни за съседи", - "neighbors_RequestTimedOut": "Съседите поискат изтичане на време.", + "neighbors_receivedData": "Получени данни за съседи", + "neighbors_requestTimedOut": "Съседите поискат изтичане на време.", "neighbors_errorLoading": "Грешка при зареждане на съседи: {error}", "neighbors_repeatersNeighbours": "Повторители Съседи", "neighbors_noData": "Няма налични данни за съседи.", diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 953d529..bd9faad 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighbours": "Nachbarn", "repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.", - "neighbors_ReceivedData": "Empfangene Nachbarendaten", - "neighbors_RequestTimedOut": "Nachbarn melden zeitweise Ausfall.", + "neighbors_receivedData": "Empfangene Nachbarendaten", + "neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.", "neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}", "neighbors_repeatersNeighbours": "Wiederholer Nachbarn", "neighbors_noData": "Keine Nachbardaten verfügbar.", @@ -1378,5 +1378,11 @@ } }, "neighbors_heardAgo": "Hörte: {time} vor her.", - "neighbors_unknownContact": "Unbekannte {pubkey}" + "neighbors_unknownContact": "Unbekannte {pubkey}", + "settings_locationGPSEnable": "GPS aktivieren", + "settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.", + "settings_locationIntervalSec": "Intervall für GPS (Sekunden)", + "settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.", + "contacts_manageRoom": "Raum-Server verwalten", + "room_management": "Raum-Server-Verwaltung" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d80b3d9..4850169 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1078,8 +1078,8 @@ } }, - "neighbors_ReceivedData": "Received Neighbours Data", - "neighbors_RequestTimedOut": "Neighbours request timed out.", + "neighbors_receivedData": "Received Neighbours Data", + "neighbors_requestTimedOut": "Neighbours request timed out.", "neighbors_errorLoading": "Error loading neighbors: {error}", "@neighbors_errorLoading": { "placeholders": { diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 472ba43..eb8474d 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighbours": "Vecinos", "repeater_neighboursSubtitle": "Ver vecinos de salto cero.", - "neighbors_ReceivedData": "Recibidas Datos de Vecinos", - "neighbors_RequestTimedOut": "Los vecinos solicitan que se desconecte.", + "neighbors_receivedData": "Recibidas Datos de Vecinos", + "neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.", "neighbors_errorLoading": "Error al cargar vecinos: {error}", "neighbors_repeatersNeighbours": "Repetidores Vecinos", "neighbors_noData": "No hay datos de vecinos disponibles.", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bf78c92..769c189 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighbours": "Voisins", "repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.", - "neighbors_ReceivedData": "Données des voisins reçues", - "neighbors_RequestTimedOut": "Les voisins demandent un délai.", + "neighbors_receivedData": "Données des voisins reçues", + "neighbors_requestTimedOut": "Les voisins demandent un délai.", "neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}", "neighbors_repeatersNeighbours": "Répéteurs Voisins", "neighbors_noData": "Aucune donnée concernant les voisins disponible.", diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 7ad31ac..0c63a16 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighbours": "Vicini", "repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.", - "neighbors_ReceivedData": "Ricevute dati vicini", - "neighbors_RequestTimedOut": "I vicini richiedono un timeout.", + "neighbors_receivedData": "Ricevute dati vicini", + "neighbors_requestTimedOut": "I vicini richiedono un timeout.", "neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}", "neighbors_repeatersNeighbours": "Ripetitori Vicini", "neighbors_noData": "Nessun dato sugli vicini disponibile.", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 96c6704..07b721b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4096,17 +4096,17 @@ abstract class AppLocalizations { /// **'{celsius}°C / {fahrenheit}°F'** String telemetry_temperatureValue(String celsius, String fahrenheit); - /// No description provided for @neighbors_ReceivedData. + /// No description provided for @neighbors_receivedData. /// /// In en, this message translates to: /// **'Received Neighbours Data'** - String get neighbors_ReceivedData; + String get neighbors_receivedData; - /// No description provided for @neighbors_RequestTimedOut. + /// No description provided for @neighbors_requestTimedOut. /// /// In en, this message translates to: /// **'Neighbours request timed out.'** - String get neighbors_RequestTimedOut; + String get neighbors_requestTimedOut; /// No description provided for @neighbors_errorLoading. /// @@ -4126,6 +4126,18 @@ abstract class AppLocalizations { /// **'No neighbours data available.'** String get neighbors_noData; + /// No description provided for @neighbors_unknownContact. + /// + /// In en, this message translates to: + /// **'Unknown {pubkey}'** + String neighbors_unknownContact(String pubkey); + + /// No description provided for @neighbors_heardAgo. + /// + /// In en, this message translates to: + /// **'Heard: {time} ago'** + String neighbors_heardAgo(String time); + /// No description provided for @channelPath_title. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 09c8e8b..4e6a190 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1631,10 +1631,11 @@ class AppLocalizationsBg extends AppLocalizations { String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Съседи'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => + 'Преглед на съседни възли с нулев скок.'; @override String get repeater_settings => 'Настройки'; @@ -2322,21 +2323,31 @@ class AppLocalizationsBg extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Получени данни за съседи'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Съседите поискат изтичане на време.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Грешка при зареждане на съседи: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Повторители Съседи'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Няма налични данни за съседи.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Неизвестна $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Слушано преди $time.'; + } @override String get channelPath_title => 'Пътеки пъзел'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index d474780..0e51676 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -201,18 +201,18 @@ class AppLocalizationsDe extends AppLocalizations { String get settings_locationInvalid => 'Ungültige Breiten- oder Längengrade.'; @override - String get settings_locationGPSEnable => 'GPS Enable'; + String get settings_locationGPSEnable => 'GPS aktivieren'; @override String get settings_locationGPSEnableSubtitle => - 'Enables GPS to automatically update location.'; + 'Aktiviert GPS zur automatischen Aktualisierung des Standorts.'; @override - String get settings_locationIntervalSec => 'Interval for GPS (Seconds)'; + String get settings_locationIntervalSec => 'Intervall für GPS (Sekunden)'; @override String get settings_locationIntervalInvalid => - 'Interval must be at least 60 seconds, and less than 86400 seconds.'; + 'Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.'; @override String get settings_latitude => 'Breitengrad'; @@ -662,7 +662,7 @@ class AppLocalizationsDe extends AppLocalizations { String get contacts_manageRepeater => 'Wiederholungen verwalten'; @override - String get contacts_manageRoom => 'Manage Room Server'; + String get contacts_manageRoom => 'Raum-Server verwalten'; @override String get contacts_roomLogin => 'Raum-Login'; @@ -1604,7 +1604,7 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_management => 'Repeater-Verwaltung'; @override - String get room_management => 'Room Server Management'; + String get room_management => 'Raum-Server-Verwaltung'; @override String get repeater_managementTools => 'Verwaltungs-Tools'; @@ -1630,10 +1630,10 @@ class AppLocalizationsDe extends AppLocalizations { String get repeater_cliSubtitle => 'Sende Befehle an den Repeater'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Nachbarn'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => 'Anzahl der Hop-Nachbarn anzeigen.'; @override String get repeater_settings => 'Einstellungen'; @@ -2325,21 +2325,31 @@ class AppLocalizationsDe extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Empfangene Nachbarendaten'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Nachbarn melden zeitweise Ausfall.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Fehler beim Laden der Nachbarn: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Wiederholer Nachbarn'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Keine Nachbardaten verfügbar.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Unbekannte $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Hörte: $time vor her.'; + } @override String get channelPath_title => 'Paketpfad'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index e9ef0ce..c87fbc6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2284,10 +2284,10 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Received Neighbours Data'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Neighbours request timed out.'; @override String neighbors_errorLoading(String error) { @@ -2300,6 +2300,16 @@ class AppLocalizationsEn extends AppLocalizations { @override String get neighbors_noData => 'No neighbours data available.'; + @override + String neighbors_unknownContact(String pubkey) { + return 'Unknown $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Heard: $time ago'; + } + @override String get channelPath_title => 'Packet Path'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 74e2c2c..404aeb0 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1629,10 +1629,10 @@ class AppLocalizationsEs extends AppLocalizations { String get repeater_cliSubtitle => 'Enviar comandos al repetidor'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Vecinos'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.'; @override String get repeater_settings => 'Configuración'; @@ -2319,21 +2319,32 @@ class AppLocalizationsEs extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Recibidas Datos de Vecinos'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => + 'Los vecinos solicitan que se desconecte.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Error al cargar vecinos: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Repetidores Vecinos'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'No hay datos de vecinos disponibles.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Clave pública desconocida $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Escuchado: $time hace atrás'; + } @override String get channelPath_title => 'Ruta del Paquete'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 1eb76d1..2e06d80 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1635,10 +1635,11 @@ class AppLocalizationsFr extends AppLocalizations { String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Voisins'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => + 'Afficher les voisins de saut nuls.'; @override String get repeater_settings => 'Paramètres'; @@ -2333,21 +2334,32 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Données des voisins reçues'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Les voisins demandent un délai.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Erreur lors du chargement des voisins : $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Répéteurs Voisins'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => + 'Aucune donnée concernant les voisins disponible.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Clé publique inconnue $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Écouté : $time auparavant'; + } @override String get channelPath_title => 'Chemin de paquet'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 61b2664..ebd3f57 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1627,10 +1627,11 @@ class AppLocalizationsIt extends AppLocalizations { String get repeater_cliSubtitle => 'Invia comandi al ripetitore'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Vicini'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => + 'Visualizza vicini di salto pari a zero.'; @override String get repeater_settings => 'Impostazioni'; @@ -2319,21 +2320,31 @@ class AppLocalizationsIt extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Ricevute dati vicini'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'I vicini richiedono un timeout.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Errore nel caricamento dei vicini: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Ripetitori Vicini'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Nessun dato sugli vicini disponibile.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Chiave pubblica sconosciuta $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Sentito: $time fa'; + } @override String get channelPath_title => 'Percorso Pacchetto'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index d69afb7..3a1afcb 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1622,10 +1622,10 @@ class AppLocalizationsNl extends AppLocalizations { String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Buren'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.'; @override String get repeater_settings => 'Instellingen'; @@ -2309,21 +2309,32 @@ class AppLocalizationsNl extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Ontvangen Buurdata'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => + 'Buren vragen om tijdelijk uitgeschakeld.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Fout bij het laden van buren: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Herhalingen Buren'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Geen gegevens van buren beschikbaar.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Onbekende $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Horen: $time geleden'; + } @override String get channelPath_title => 'Pakketpad'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 96b528e..bab42cd 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1631,10 +1631,11 @@ class AppLocalizationsPl extends AppLocalizations { String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Sąsiedzi'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => + 'Wyświetl sąsiedztwo zerowych hopów.'; @override String get repeater_settings => 'Ustawienia'; @@ -2317,21 +2318,32 @@ class AppLocalizationsPl extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => + 'Sąsiedzi proszą o wyłączenie timingu.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Błąd podczas ładowania sąsiadów: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Brak danych dotyczących sąsiadów.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Nieznana $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Usłyszano: $time temu'; + } @override String get channelPath_title => 'Ścieżka pakietu'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 66c519f..f0f4c21 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1629,10 +1629,11 @@ class AppLocalizationsPt extends AppLocalizations { String get repeater_cliSubtitle => 'Enviar comandos ao repetidor'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Vizinhos'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => + 'Visualizar vizinhos de salto zero.'; @override String get repeater_settings => 'Configurações'; @@ -2319,21 +2320,32 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Dados dos Vizinhos Recebidos'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => + 'Vizinhos solicitam tempo limite esgotado.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Erro ao carregar vizinhos: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.'; + + @override + String neighbors_unknownContact(String pubkey) { + return '$pubkey Desconhecido'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Ouvido: $time atrás'; + } @override String get channelPath_title => 'Rótulo de Caminho de Pacote'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 1d7e1b2..4fa1f0b 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1624,10 +1624,10 @@ class AppLocalizationsSk extends AppLocalizations { String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Súsezný'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.'; @override String get repeater_settings => 'Nastavenia'; @@ -2307,21 +2307,32 @@ class AppLocalizationsSk extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Obdielo dáta suseda'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Súďia žiadajú o časové ukončenie.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Chyba pri načítaní susedov: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => + 'Nie je dostupná žiadna informácia o susedoch.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Neznáma $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Počuli sme to: $time dozadu'; + } @override String get channelPath_title => 'Cesta balíka'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 778ef09..e918f4f 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1625,10 +1625,10 @@ class AppLocalizationsSl extends AppLocalizations { 'Pošlji ukazne povelje na ponovitveno enoto.'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Sosedi'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.'; @override String get repeater_settings => 'Nastavitve'; @@ -2312,21 +2312,32 @@ class AppLocalizationsSl extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Prejeto podatke o sosedih'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => + 'Sosedi zahtevajo izklop po dogovoru.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Napaka pri obnašanju sosedov: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Niso na voljo podatki o sosedih.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Nepoznano $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Udeleženec je prejel sporočilo $time nazaj.'; + } @override String get channelPath_title => 'Pot do paketa'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 1e12ada..ed11747 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1613,10 +1613,10 @@ class AppLocalizationsSv extends AppLocalizations { String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => 'Grannar'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.'; @override String get repeater_settings => 'Inställningar'; @@ -2296,21 +2296,31 @@ class AppLocalizationsSv extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => 'Mottagna grannars data'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => 'Grannar begär tidsinställd utskick.'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return 'Fel vid inläsning av grannar: $error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => 'Upprepar grannar'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.'; + + @override + String neighbors_unknownContact(String pubkey) { + return 'Okänd $pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return 'Hördes: $time sedan'; + } @override String get channelPath_title => 'Paketväg'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 5dda9a2..aa40f26 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1553,10 +1553,10 @@ class AppLocalizationsZh extends AppLocalizations { String get repeater_cliSubtitle => '发送命令到重复器'; @override - String get repeater_neighbours => 'Neighbors'; + String get repeater_neighbours => '邻居'; @override - String get repeater_neighboursSubtitle => 'View zero hop neighbors.'; + String get repeater_neighboursSubtitle => '查看零跳邻居。'; @override String get repeater_settings => '设置'; @@ -2189,21 +2189,31 @@ class AppLocalizationsZh extends AppLocalizations { } @override - String get neighbors_ReceivedData => 'Received Neighbours Data'; + String get neighbors_receivedData => '收到邻居数据'; @override - String get neighbors_RequestTimedOut => 'Neighbours request timed out.'; + String get neighbors_requestTimedOut => '邻居请求超时处理。'; @override String neighbors_errorLoading(String error) { - return 'Error loading neighbors: $error'; + return '加载邻居时出错:$error'; } @override - String get neighbors_repeatersNeighbours => 'Repeaters Neighbours'; + String get neighbors_repeatersNeighbours => '重复器邻居'; @override - String get neighbors_noData => 'No neighbours data available.'; + String get neighbors_noData => '没有可用的邻居数据。'; + + @override + String neighbors_unknownContact(String pubkey) { + return '未知$pubkey'; + } + + @override + String neighbors_heardAgo(String time) { + return '听到的时间:$time前'; + } @override String get channelPath_title => '数据包路径'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 91d5b9b..9c39f66 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighbours": "Buren", "repeater_neighboursSubtitle": "Bekijk nul hops buren.", - "neighbors_ReceivedData": "Ontvangen Buurdata", - "neighbors_RequestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.", + "neighbors_receivedData": "Ontvangen Buurdata", + "neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.", "neighbors_errorLoading": "Fout bij het laden van buren: {error}", "neighbors_repeatersNeighbours": "Herhalingen Buren", "neighbors_noData": "Geen gegevens van buren beschikbaar.", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 9f4a562..a3aebdc 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighbours": "Sąsiedzi", "repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.", - "neighbors_ReceivedData": "Otrzymano dane sąsiedztwa", - "neighbors_RequestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.", + "neighbors_receivedData": "Otrzymano dane sąsiedztwa", + "neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.", "neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}", "neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi", "neighbors_noData": "Brak danych dotyczących sąsiadów.", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index fa8de3f..e30495a 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -1345,9 +1345,9 @@ } }, "repeater_neighbours": "Vizinhos", - "neighbors_ReceivedData": "Dados dos Vizinhos Recebidos", + "neighbors_receivedData": "Dados dos Vizinhos Recebidos", "repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.", - "neighbors_RequestTimedOut": "Vizinhos solicitam tempo limite esgotado.", + "neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.", "neighbors_errorLoading": "Erro ao carregar vizinhos: {error}", "neighbors_repeatersNeighbours": "Repetidores Vizinhos", "neighbors_noData": "Não estão disponíveis dados de vizinhos.", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index be58c8b..c4d07f7 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -1345,8 +1345,8 @@ } }, "repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.", - "neighbors_RequestTimedOut": "Súďia žiadajú o časové ukončenie.", - "neighbors_ReceivedData": "Obdielo dáta suseda", + "neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.", + "neighbors_receivedData": "Obdielo dáta suseda", "repeater_neighbours": "Súsezný", "neighbors_errorLoading": "Chyba pri načítaní susedov: {error}", "neighbors_repeatersNeighbours": "Opakovadlá Súsezná", diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index e4bd604..4667eac 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.", "repeater_neighbours": "Sosedi", - "neighbors_ReceivedData": "Prejeto podatke o sosedih", - "neighbors_RequestTimedOut": "Sosedi zahtevajo izklop po dogovoru.", + "neighbors_receivedData": "Prejeto podatke o sosedih", + "neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.", "neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}", "neighbors_repeatersNeighbours": "Ponovitve Sosedi", "neighbors_noData": "Niso na voljo podatki o sosedih.", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 6972c4c..d8a294d 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighbours": "Grannar", "repeater_neighboursSubtitle": "Visa noll hoppgrannar.", - "neighbors_ReceivedData": "Mottagna grannars data", - "neighbors_RequestTimedOut": "Grannar begär tidsinställd utskick.", + "neighbors_receivedData": "Mottagna grannars data", + "neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.", "neighbors_errorLoading": "Fel vid inläsning av grannar: {error}", "neighbors_repeatersNeighbours": "Upprepar grannar", "neighbors_noData": "Inga grannuppgifter finns tillgängliga.", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 267c96a..8da91c8 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -1346,8 +1346,8 @@ }, "repeater_neighboursSubtitle": "查看零跳邻居。", "repeater_neighbours": "邻居", - "neighbors_ReceivedData": "收到邻居数据", - "neighbors_RequestTimedOut": "邻居请求超时处理。", + "neighbors_receivedData": "收到邻居数据", + "neighbors_requestTimedOut": "邻居请求超时处理。", "neighbors_errorLoading": "加载邻居时出错:{error}", "neighbors_repeatersNeighbours": "重复器邻居", "neighbors_noData": "没有可用的邻居数据。", diff --git a/lib/screens/neighbours_screen.dart b/lib/screens/neighbours_screen.dart index 03c6062..b606188 100644 --- a/lib/screens/neighbours_screen.dart +++ b/lib/screens/neighbours_screen.dart @@ -32,7 +32,6 @@ class _NeighboursScreenState extends State { static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize; Uint8List _tagData = Uint8List(4); - //int _timeEstment = 0; int _neighbourCount = 0; bool _isLoading = false; @@ -140,7 +139,7 @@ class _NeighboursScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.neighbors_ReceivedData), + content: Text(context.l10n.neighbors_receivedData), backgroundColor: Colors.green, ), ); @@ -205,7 +204,7 @@ class _NeighboursScreenState extends State { }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(context.l10n.neighbors_RequestTimedOut), + content: Text(context.l10n.neighbors_requestTimedOut), backgroundColor: Colors.red, ), ); diff --git a/untranslated.json b/untranslated.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/untranslated.json @@ -0,0 +1 @@ +{} \ No newline at end of file From 8e3b563aba6e51b5f00e9518de4c1cce2b0c56cd Mon Sep 17 00:00:00 2001 From: zjs81 Date: Mon, 19 Jan 2026 19:14:48 -0700 Subject: [PATCH 4/4] revert translate.py --- tools/translate.py | 209 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 190 insertions(+), 19 deletions(-) diff --git a/tools/translate.py b/tools/translate.py index 54dd3bc..06a95f2 100644 --- a/tools/translate.py +++ b/tools/translate.py @@ -10,6 +10,7 @@ Translates ARB/JSON localization values using a local Ollama model, while: - printing progress as it runs 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 \ @@ -17,12 +18,28 @@ Usage: --model ministral-3:latest \ --temperature 0 \ --concurrency 4 + + # 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 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 """ from __future__ import annotations import argparse import json +import os import re import sys import time @@ -448,11 +465,48 @@ 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 (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) + 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") - ap.add_argument("--out", dest="out_path", required=True, help="Output .arb/.json file path") - ap.add_argument("--to-locale", required=True, help="Target locale code, e.g. es, fr, de") + 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") @@ -504,19 +558,119 @@ def main() -> int: "vi": "Vietnamese", "id": "Indonesian", } - target_lang = args.target_lang or locale_map.get(args.to_locale, args.to_locale) + # Read source/template file try: with open(args.in_path, "r", encoding="utf-8") as f: - data = json.load(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(data, dict): + 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, + 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, @@ -540,17 +694,34 @@ def main() -> int: top_p=args.top_p, ) - out_data: Dict[str, Any] = dict(data) - out_data["@@locale"] = args.to_locale + # 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) + out_data["@@locale"] = target_locale - items: List[Tuple[str, str]] = [(k, v) for k, v in data.items() if is_translatable_entry(k, v)] + # 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 + if is_translatable_entry(k, source_data.get(k)) + ] + # Also copy over any metadata keys 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 manual_count = 0 items_to_translate: List[Tuple[str, str]] = [] for k, v in items: - if k in MANUAL_TRANSLATIONS and args.to_locale in MANUAL_TRANSLATIONS[k]: - out_data[k] = MANUAL_TRANSLATIONS[k][args.to_locale] + if k in MANUAL_TRANSLATIONS and target_locale in MANUAL_TRANSLATIONS[k]: + out_data[k] = MANUAL_TRANSLATIONS[k][target_locale] manual_count += 1 else: items_to_translate.append((k, v)) @@ -560,8 +731,8 @@ def main() -> int: total = len(items_to_translate) if total == 0 and manual_count == 0: - print("No translatable string entries found (excluding @@locale and @metadata).", file=sys.stderr) - return 1 + print("No translatable string entries found (excluding @@locale and @metadata).") + return 0 if total == 0: print("All strings handled by manual translations.") @@ -705,19 +876,19 @@ def main() -> int: if args.dry_run: print("Dry run: not writing output file.") - return 0 + return translated_ok try: - with open(args.out_path, "w", encoding="utf-8") as f: + with open(out_path, "w", encoding="utf-8") as f: json.dump(out_data, f, ensure_ascii=False, indent=2) f.write("\n") except Exception as e: print(f"Failed to write output: {e}", file=sys.stderr) - return 2 + return -1 - print(f"Wrote: {args.out_path}") - return 0 + print(f"Wrote: {out_path}") + return translated_ok if __name__ == "__main__": - raise SystemExit(main()) + raise SystemExit(main()) \ No newline at end of file