mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-04-20 22:13:48 +00:00
Add support for private and hashtag channels in localization and channel management
- Updated Polish, Portuguese, Slovak, Slovenian, Swedish, and Chinese localization files to include new strings for creating and joining private channels, as well as joining hashtag channels. - Enhanced the channel management UI to allow users to create and join private channels, join public channels, and join channels via hashtags. - Implemented PSK derivation from hashtags using SHA256 in the Channel model. - Improved the translation script to handle missing keys and translate all locales efficiently.
This commit is contained in:
parent
a14462978d
commit
14ff8250c0
30 changed files with 1250 additions and 141 deletions
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Повторители",
|
||||
"listFilter_roomServers": "Сървъри на стая",
|
||||
"listFilter_unreadOnly": "Само непрочетените",
|
||||
"listFilter_newGroup": "Нова група"
|
||||
"listFilter_newGroup": "Нова група",
|
||||
"channels_createPrivateChannel": "Създай Частен Канал",
|
||||
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
||||
"channels_createPrivateChannelDesc": "Защитено с таен ключ.",
|
||||
"channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.",
|
||||
"channels_joinPublicChannel": "Присъединете се към Публичния канал",
|
||||
"channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.",
|
||||
"channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал",
|
||||
"channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.",
|
||||
"channels_scanQrCode": "Сканирайте QR код",
|
||||
"channels_scanQrCodeComingSoon": "Ще излезе скоро",
|
||||
"channels_enterHashtag": "Въведете хаштаг",
|
||||
"channels_hashtagHint": "напр. #отбор"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Wiederholer",
|
||||
"listFilter_roomServers": "Raumserver",
|
||||
"listFilter_unreadOnly": "Nur nicht gelesen",
|
||||
"listFilter_newGroup": "Neue Gruppe"
|
||||
"listFilter_newGroup": "Neue Gruppe",
|
||||
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
||||
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
||||
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
|
||||
"channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.",
|
||||
"channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei",
|
||||
"channels_joinPublicChannelDesc": "Jeder kann diesem Kanal beitreten.",
|
||||
"channels_joinHashtagChannel": "Treten Sie einem Hashtag-Kanal bei",
|
||||
"channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.",
|
||||
"channels_scanQrCode": "Scannen Sie einen QR-Code",
|
||||
"channels_scanQrCodeComingSoon": "Bald verfügbar",
|
||||
"channels_enterHashtag": "Gib Hashtag ein",
|
||||
"channels_hashtagHint": "z.B. #team"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -361,6 +361,18 @@
|
|||
"channels_sortAZ": "A-Z",
|
||||
"channels_sortLatestMessages": "Latest messages",
|
||||
"channels_sortUnread": "Unread",
|
||||
"channels_createPrivateChannel": "Create a Private Channel",
|
||||
"channels_createPrivateChannelDesc": "Secured with a secret key.",
|
||||
"channels_joinPrivateChannel": "Join a Private Channel",
|
||||
"channels_joinPrivateChannelDesc": "Manually enter a secret key.",
|
||||
"channels_joinPublicChannel": "Join the Public Channel",
|
||||
"channels_joinPublicChannelDesc": "Anyone can join this channel.",
|
||||
"channels_joinHashtagChannel": "Join a Hashtag Channel",
|
||||
"channels_joinHashtagChannelDesc": "Anyone can join hashtag channels.",
|
||||
"channels_scanQrCode": "Scan a QR Code",
|
||||
"channels_scanQrCodeComingSoon": "Coming soon",
|
||||
"channels_enterHashtag": "Enter hashtag",
|
||||
"channels_hashtagHint": "e.g. #team",
|
||||
|
||||
"chat_noMessages": "No messages yet",
|
||||
"chat_sendMessageToStart": "Send a message to get started",
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Repetidores",
|
||||
"listFilter_roomServers": "Servidores de la sala",
|
||||
"listFilter_unreadOnly": "Solo sin leer",
|
||||
"listFilter_newGroup": "Nuevo grupo"
|
||||
"listFilter_newGroup": "Nuevo grupo",
|
||||
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
||||
"channels_createPrivateChannel": "Crear un Canal Privado",
|
||||
"channels_createPrivateChannelDesc": "Cifrado con una clave secreta.",
|
||||
"channels_joinPrivateChannelDesc": "Introducir manualmente una clave secreta.",
|
||||
"channels_joinPublicChannel": "Únete al Canal Público",
|
||||
"channels_joinPublicChannelDesc": "Cualquiera puede unirse a este canal.",
|
||||
"channels_joinHashtagChannel": "Únete a un Canal con Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "Cualquiera puede unirse a los canales de hashtag.",
|
||||
"channels_scanQrCode": "Escanear un Código QR",
|
||||
"channels_scanQrCodeComingSoon": "Próximamente",
|
||||
"channels_enterHashtag": "Introducir hashtag",
|
||||
"channels_hashtagHint": "ej. #equipo"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"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",
|
||||
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
||||
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
||||
"channels_createPrivateChannel": "Créer un Canal Privé",
|
||||
"channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.",
|
||||
"channels_joinPublicChannel": "Rejoindre le canal public",
|
||||
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
|
||||
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
|
||||
"channels_scanQrCode": "Scanner un code QR",
|
||||
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
||||
"channels_enterHashtag": "Entrez le hashtag",
|
||||
"channels_hashtagHint": "ex. #équipe"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Ripetitori",
|
||||
"listFilter_roomServers": "Server della stanza",
|
||||
"listFilter_unreadOnly": "Solo non letto",
|
||||
"listFilter_newGroup": "Nuovo gruppo"
|
||||
"listFilter_newGroup": "Nuovo gruppo",
|
||||
"channels_createPrivateChannel": "Crea un Canale Privato",
|
||||
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
||||
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
|
||||
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
|
||||
"channels_joinPublicChannel": "Unisciti al Canale Pubblico",
|
||||
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
|
||||
"channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
|
||||
"channels_scanQrCode": "Scansiona un codice QR",
|
||||
"channels_scanQrCodeComingSoon": "Arriverà presto",
|
||||
"channels_enterHashtag": "Inserisci hashtag",
|
||||
"channels_hashtagHint": "es. #team"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1596,6 +1596,78 @@ abstract class AppLocalizations {
|
|||
/// **'Unread'**
|
||||
String get channels_sortUnread;
|
||||
|
||||
/// No description provided for @channels_createPrivateChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create a Private Channel'**
|
||||
String get channels_createPrivateChannel;
|
||||
|
||||
/// No description provided for @channels_createPrivateChannelDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Secured with a secret key.'**
|
||||
String get channels_createPrivateChannelDesc;
|
||||
|
||||
/// No description provided for @channels_joinPrivateChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Join a Private Channel'**
|
||||
String get channels_joinPrivateChannel;
|
||||
|
||||
/// No description provided for @channels_joinPrivateChannelDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manually enter a secret key.'**
|
||||
String get channels_joinPrivateChannelDesc;
|
||||
|
||||
/// No description provided for @channels_joinPublicChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Join the Public Channel'**
|
||||
String get channels_joinPublicChannel;
|
||||
|
||||
/// No description provided for @channels_joinPublicChannelDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Anyone can join this channel.'**
|
||||
String get channels_joinPublicChannelDesc;
|
||||
|
||||
/// No description provided for @channels_joinHashtagChannel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Join a Hashtag Channel'**
|
||||
String get channels_joinHashtagChannel;
|
||||
|
||||
/// No description provided for @channels_joinHashtagChannelDesc.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Anyone can join hashtag channels.'**
|
||||
String get channels_joinHashtagChannelDesc;
|
||||
|
||||
/// No description provided for @channels_scanQrCode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Scan a QR Code'**
|
||||
String get channels_scanQrCode;
|
||||
|
||||
/// No description provided for @channels_scanQrCodeComingSoon.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Coming soon'**
|
||||
String get channels_scanQrCodeComingSoon;
|
||||
|
||||
/// No description provided for @channels_enterHashtag.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter hashtag'**
|
||||
String get channels_enterHashtag;
|
||||
|
||||
/// No description provided for @channels_hashtagHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'e.g. #team'**
|
||||
String get channels_hashtagHint;
|
||||
|
||||
/// No description provided for @chat_noMessages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -830,6 +830,45 @@ class AppLocalizationsBg extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Непрочетено';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Създай Частен Канал';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc => 'Защитено с таен ключ.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Присъедини се към Частен Канал';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc => 'Ръчно въведете таен ключ.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel =>
|
||||
'Присъединете се към Публичния канал';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Всеки може да се присъедини към този канал.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Присъедини се към Хаштаг Канал';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Всеки може да се присъедини към хаштаговите канали.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Сканирайте QR код';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Ще излезе скоро';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Въведете хаштаг';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'напр. #отбор';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Няма съобщения.';
|
||||
|
||||
|
|
|
|||
|
|
@ -828,6 +828,48 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Unlescht';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Erstelle einen privaten Kanal';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Verschlüsselt mit einem geheimen Schlüssel.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel =>
|
||||
'Treten Sie einem privaten Kanal bei';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Manuelle Eingabe eines geheimen Schlüssels.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Tritt dem öffentlichen Kanal bei';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Jeder kann diesem Kanal beitreten.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel =>
|
||||
'Treten Sie einem Hashtag-Kanal bei';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Jeder kann sich bei Hashtag-Kanälen beteiligen.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scannen Sie einen QR-Code';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Bald verfügbar';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Gib Hashtag ein';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'z.B. #team';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Noch keine Nachrichten.';
|
||||
|
||||
|
|
|
|||
|
|
@ -818,6 +818,43 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Unread';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Create a Private Channel';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc => 'Secured with a secret key.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Join a Private Channel';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc => 'Manually enter a secret key.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Join the Public Channel';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc => 'Anyone can join this channel.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Join a Hashtag Channel';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Anyone can join hashtag channels.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scan a QR Code';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Coming soon';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Enter hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'e.g. #team';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'No messages yet';
|
||||
|
||||
|
|
|
|||
|
|
@ -829,6 +829,46 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Sin leer';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Crear un Canal Privado';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Cifrado con una clave secreta.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Únete a un Canal Privado';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Introducir manualmente una clave secreta.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Únete al Canal Público';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Cualquiera puede unirse a este canal.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Cualquiera puede unirse a los canales de hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Escanear un Código QR';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Próximamente';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Introducir hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'ej. #equipo';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Aún no hay mensajes';
|
||||
|
||||
|
|
|
|||
|
|
@ -830,6 +830,46 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Non lu';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Créer un Canal Privé';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Sécurisé avec une clé secrète.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Entrer manuellement une clé secrète.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Rejoindre le canal public';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Tout le monde peut rejoindre ce canal.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Rejoindre un Canal Hashtag';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'N\'importe qui peut rejoindre les canaux #hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scanner un code QR';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Entrez le hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'ex. #équipe';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Aucun message pour le moment.';
|
||||
|
||||
|
|
|
|||
|
|
@ -827,6 +827,46 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Non letto';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Crea un Canale Privato';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Protetta con una chiave segreta.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Unisciti a un Canale Privato';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Inserire manualmente una chiave segreta.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Unisciti al Canale Pubblico';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Chiunque può unirsi a questo canale.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Chiunque può unirsi ai canali hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scansiona un codice QR';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Inserisci hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'es. #team';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Nessun messaggio ancora';
|
||||
|
||||
|
|
|
|||
|
|
@ -824,6 +824,46 @@ class AppLocalizationsNl extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Ongelezen';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Beveiligd met een geheime sleutel.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Handmatig een geheime sleutel invoeren.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Iedereen kan dit kanaal aanmelden.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Iedereen kan lid worden van hashtag-kanalen.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Scan een QR-code';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Komt later';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Voer hashtag in';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'bijv. #team';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Nog geen berichten.';
|
||||
|
||||
|
|
|
|||
|
|
@ -828,6 +828,46 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Niezgłoszone';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Zabezpieczone kluczem szyfrowym.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Dołącz do Prywatnego Kanału';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc => 'Ręcznie wprowadź klucz tajny.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Dołącz do kanału publicznego.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Każdy może dołączyć do tego kanału.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel =>
|
||||
'Dołącz do kanału oznaczanego hashtagiem';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Każdy może dołączyć do kanałów z hashtagami.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Skanuj kod QR';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Wkrótce';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Wprowadź hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'np. #zespół';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Brak jeszcze wiadomości';
|
||||
|
||||
|
|
|
|||
|
|
@ -829,6 +829,46 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Não lido';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Criar um Canal Privado';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Protegido com uma chave secreta.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Junte-se a um Canal Privado';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Inserir uma chave secreta manualmente.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Junte-se ao Canal Público';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Qualquer pessoa pode entrar neste canal.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Junte-se a um Canal com Hashtag';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Qualquer pessoa pode participar de canais com hashtag.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Digitalizar um Código QR';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Em breve';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Insira hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'ex. #equipe';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Ainda não existem mensagens.';
|
||||
|
||||
|
|
|
|||
|
|
@ -824,6 +824,45 @@ class AppLocalizationsSk extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Nezriadené';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Vytvorte súkromný kanál';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Zabezpečené pomocou tajného kľúča.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Pripojiť sa k súkromnému kanálu';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc => 'Ručne zadajte tajný kľúč.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Któvek sátó na tutó kanalizovát.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Ktoekolikoľvek sa môže pridať do hashtag kanálov.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Skenujte QR kód';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Čoskoro';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Zadajte hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'napr. #tím';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
||||
|
||||
|
|
|
|||
|
|
@ -824,6 +824,45 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Nerešeno';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Ustvari zasebno kanal.';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Varno zaklenjeno s skrivnim ključem.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc => 'Ročno vnesite zaporni ključ.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Kdor karkoli je, lahko se pridruži tej skupini.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Kdor karkoli, lahko se pridruži hashtag kanalom.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Skeniraj QR kodo';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Prihajajoča';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Vnesite hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 'npr. #ekipa';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Še ni sporočil.';
|
||||
|
||||
|
|
|
|||
|
|
@ -817,6 +817,46 @@ class AppLocalizationsSv extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => 'Oläst';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => 'Skapa en privat kanal';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc =>
|
||||
'Skyddat med en hemlig nyckel.';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => 'Gå med i en Privat Kanal';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc =>
|
||||
'Ange en hemlig nyckel manuellt.';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => 'Gå med i den Offentliga Kanalen';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc =>
|
||||
'Vem som helst kan gå med i denna kanal.';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => 'Gå med i en Hashtagkanal';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc =>
|
||||
'Väldigt enkelt att gå med i hashtag-kanaler.';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => 'Skanna en QR-kod';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => 'Kommer snart';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => 'Ange hashtag';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => 't.ex. #team';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Inga meddelanden ännu';
|
||||
|
||||
|
|
|
|||
|
|
@ -789,6 +789,42 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||
@override
|
||||
String get channels_sortUnread => '未读';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannel => '创建私聊频道';
|
||||
|
||||
@override
|
||||
String get channels_createPrivateChannelDesc => '使用密钥保护。';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannel => '加入私密频道';
|
||||
|
||||
@override
|
||||
String get channels_joinPrivateChannelDesc => '手动输入密钥。';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannel => '加入公共频道';
|
||||
|
||||
@override
|
||||
String get channels_joinPublicChannelDesc => '任何人都可以加入这个频道。';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannel => '加入标签频道';
|
||||
|
||||
@override
|
||||
String get channels_joinHashtagChannelDesc => '任何人都可以加入话题频道。';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCode => '扫描二维码';
|
||||
|
||||
@override
|
||||
String get channels_scanQrCodeComingSoon => '即将到来';
|
||||
|
||||
@override
|
||||
String get channels_enterHashtag => '输入标签';
|
||||
|
||||
@override
|
||||
String get channels_hashtagHint => '例如 #团队';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => '目前还没有消息';
|
||||
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Repeaters",
|
||||
"listFilter_roomServers": "Roomservers",
|
||||
"listFilter_unreadOnly": "Alleen ongelezen",
|
||||
"listFilter_newGroup": "Nieuwe groep"
|
||||
"listFilter_newGroup": "Nieuwe groep",
|
||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
|
||||
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
|
||||
"channels_joinPublicChannel": "Sluit het Open Kanaal",
|
||||
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
|
||||
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
|
||||
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
|
||||
"channels_scanQrCode": "Scan een QR-code",
|
||||
"channels_scanQrCodeComingSoon": "Komt later",
|
||||
"channels_enterHashtag": "Voer hashtag in",
|
||||
"channels_hashtagHint": "bijv. #team"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Powtarzacze",
|
||||
"listFilter_roomServers": "Serwery pokoju",
|
||||
"listFilter_unreadOnly": "Tylko nieprzeczytane",
|
||||
"listFilter_newGroup": "Nowa grupa"
|
||||
"listFilter_newGroup": "Nowa grupa",
|
||||
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
|
||||
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
|
||||
"channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.",
|
||||
"channels_joinPrivateChannel": "Dołącz do Prywatnego Kanału",
|
||||
"channels_joinPublicChannel": "Dołącz do kanału publicznego.",
|
||||
"channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanału.",
|
||||
"channels_joinHashtagChannel": "Dołącz do kanału oznaczanego hashtagiem",
|
||||
"channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.",
|
||||
"channels_scanQrCode": "Skanuj kod QR",
|
||||
"channels_scanQrCodeComingSoon": "Wkrótce",
|
||||
"channels_enterHashtag": "Wprowadź hashtag",
|
||||
"channels_hashtagHint": "np. #zespół"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Repetidores",
|
||||
"listFilter_roomServers": "Servidores de sala",
|
||||
"listFilter_unreadOnly": "Apenas não lido",
|
||||
"listFilter_newGroup": "Novo grupo"
|
||||
"listFilter_newGroup": "Novo grupo",
|
||||
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
||||
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
||||
"channels_createPrivateChannel": "Criar um Canal Privado",
|
||||
"channels_joinPrivateChannel": "Junte-se a um Canal Privado",
|
||||
"channels_joinPublicChannel": "Junte-se ao Canal Público",
|
||||
"channels_joinPublicChannelDesc": "Qualquer pessoa pode entrar neste canal.",
|
||||
"channels_joinHashtagChannel": "Junte-se a um Canal com Hashtag",
|
||||
"channels_joinHashtagChannelDesc": "Qualquer pessoa pode participar de canais com hashtag.",
|
||||
"channels_scanQrCode": "Digitalizar um Código QR",
|
||||
"channels_scanQrCodeComingSoon": "Em breve",
|
||||
"channels_enterHashtag": "Insira hashtag",
|
||||
"channels_hashtagHint": "ex. #equipe"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Opakovadlá",
|
||||
"listFilter_roomServers": "Servéry miestnosti",
|
||||
"listFilter_unreadOnly": "Nezaregistrované len",
|
||||
"listFilter_newGroup": "Nová skupina"
|
||||
"listFilter_newGroup": "Nová skupina",
|
||||
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
||||
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
||||
"channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.",
|
||||
"channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.",
|
||||
"channels_joinPublicChannel": "Pripojte sa k verejnému kanálu",
|
||||
"channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.",
|
||||
"channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu",
|
||||
"channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridať do hashtag kanálov.",
|
||||
"channels_scanQrCode": "Skenujte QR kód",
|
||||
"channels_scanQrCodeComingSoon": "Čoskoro",
|
||||
"channels_enterHashtag": "Zadajte hashtag",
|
||||
"channels_hashtagHint": "napr. #tím"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Ponovitve",
|
||||
"listFilter_roomServers": "Smeti za prostore",
|
||||
"listFilter_unreadOnly": "Nezbrani samo",
|
||||
"listFilter_newGroup": "Nova skupina"
|
||||
"listFilter_newGroup": "Nova skupina",
|
||||
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
||||
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
||||
"channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.",
|
||||
"channels_createPrivateChannel": "Ustvari zasebno kanal.",
|
||||
"channels_joinPublicChannel": "Pridružite se javnemu kanalu",
|
||||
"channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.",
|
||||
"channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom",
|
||||
"channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.",
|
||||
"channels_scanQrCode": "Skeniraj QR kodo",
|
||||
"channels_scanQrCodeComingSoon": "Prihajajoča",
|
||||
"channels_enterHashtag": "Vnesite hashtag",
|
||||
"channels_hashtagHint": "npr. #ekipa"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "Upprepare",
|
||||
"listFilter_roomServers": "Rumservrar",
|
||||
"listFilter_unreadOnly": "Endast oinlästa",
|
||||
"listFilter_newGroup": "Ny grupp"
|
||||
"listFilter_newGroup": "Ny grupp",
|
||||
"channels_createPrivateChannel": "Skapa en privat kanal",
|
||||
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
||||
"channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.",
|
||||
"channels_createPrivateChannelDesc": "Skyddat med en hemlig nyckel.",
|
||||
"channels_joinPublicChannel": "Gå med i den Offentliga Kanalen",
|
||||
"channels_joinPublicChannelDesc": "Vem som helst kan gå med i denna kanal.",
|
||||
"channels_joinHashtagChannel": "Gå med i en Hashtagkanal",
|
||||
"channels_joinHashtagChannelDesc": "Väldigt enkelt att gå med i hashtag-kanaler.",
|
||||
"channels_scanQrCode": "Skanna en QR-kod",
|
||||
"channels_scanQrCodeComingSoon": "Kommer snart",
|
||||
"channels_enterHashtag": "Ange hashtag",
|
||||
"channels_hashtagHint": "t.ex. #team"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1335,5 +1335,17 @@
|
|||
"listFilter_repeaters": "重复器",
|
||||
"listFilter_roomServers": "房间服务器",
|
||||
"listFilter_unreadOnly": "未读消息",
|
||||
"listFilter_newGroup": "新组"
|
||||
"listFilter_newGroup": "新组",
|
||||
"channels_joinPrivateChannel": "加入私密频道",
|
||||
"channels_createPrivateChannelDesc": "使用密钥保护。",
|
||||
"channels_joinPrivateChannelDesc": "手动输入密钥。",
|
||||
"channels_createPrivateChannel": "创建私聊频道",
|
||||
"channels_joinPublicChannel": "加入公共频道",
|
||||
"channels_joinPublicChannelDesc": "任何人都可以加入这个频道。",
|
||||
"channels_joinHashtagChannel": "加入标签频道",
|
||||
"channels_joinHashtagChannelDesc": "任何人都可以加入话题频道。",
|
||||
"channels_scanQrCode": "扫描二维码",
|
||||
"channels_scanQrCodeComingSoon": "即将到来",
|
||||
"channels_enterHashtag": "输入标签",
|
||||
"channels_hashtagHint": "例如 #团队"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class Channel {
|
||||
|
|
@ -61,6 +64,15 @@ class Channel {
|
|||
return bytes;
|
||||
}
|
||||
|
||||
/// Derive PSK from hashtag name using SHA256.
|
||||
/// The hashtag is normalized to include '#' prefix.
|
||||
/// Returns first 16 bytes of SHA256 hash as PSK.
|
||||
static Uint8List derivePskFromHashtag(String hashtag) {
|
||||
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
|
||||
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
|
||||
return Uint8List.fromList(hash.sublist(0, 16));
|
||||
}
|
||||
|
||||
static String formatPskHex(Uint8List psk) {
|
||||
return _bytesToHex(psk);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -515,132 +515,318 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
|||
|
||||
void _showAddChannelDialog(BuildContext context) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final nextIndex = _findNextAvailableIndex(connector.channels, connector.maxChannels);
|
||||
final hasPublicChannel = connector.channels.any((c) => c.isPublicChannel);
|
||||
int? selectedOption;
|
||||
final nameController = TextEditingController();
|
||||
final pskController = TextEditingController();
|
||||
final maxChannels = connector.maxChannels;
|
||||
int selectedIndex = _findNextAvailableIndex(connector.channels, maxChannels);
|
||||
bool usePublicPsk = false;
|
||||
final hashtagController = TextEditingController();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setDialogState) => AlertDialog(
|
||||
title: Text(dialogContext.l10n.channels_addChannel),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: selectedIndex,
|
||||
decoration: InputDecoration(
|
||||
labelText: dialogContext.l10n.channels_channelIndexLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: List.generate(maxChannels, (i) => i)
|
||||
.map((i) => DropdownMenuItem(
|
||||
value: i,
|
||||
child: Text(dialogContext.l10n.channels_channelIndex(i)),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
setDialogState(() => selectedIndex = value);
|
||||
}
|
||||
},
|
||||
builder: (dialogContext, setDialogState) {
|
||||
Widget buildOptionTile({
|
||||
required int optionIndex,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
bool enabled = true,
|
||||
}) {
|
||||
final isSelected = selectedOption == optionIndex;
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: enabled
|
||||
? (isSelected ? Theme.of(dialogContext).colorScheme.primaryContainer : null)
|
||||
: Colors.grey.withValues(alpha: 0.2),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: enabled
|
||||
? (isSelected ? Theme.of(dialogContext).colorScheme.primary : null)
|
||||
: Colors.grey,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: dialogContext.l10n.channels_channelName,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
maxLength: 31,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CheckboxListTile(
|
||||
title: Text(dialogContext.l10n.channels_usePublicChannel),
|
||||
subtitle: Text(dialogContext.l10n.channels_standardPublicPsk),
|
||||
value: usePublicPsk,
|
||||
onChanged: (value) {
|
||||
setDialogState(() {
|
||||
usePublicPsk = value ?? false;
|
||||
if (usePublicPsk) {
|
||||
nameController.text = 'Public';
|
||||
pskController.text = Channel.publicChannelPsk;
|
||||
} else {
|
||||
),
|
||||
title: Text(
|
||||
title,
|
||||
style: TextStyle(color: enabled ? null : Colors.grey),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: TextStyle(color: enabled ? null : Colors.grey),
|
||||
),
|
||||
trailing: enabled ? const Icon(Icons.chevron_right) : null,
|
||||
selected: isSelected,
|
||||
onTap: enabled
|
||||
? () {
|
||||
setDialogState(() {
|
||||
selectedOption = optionIndex;
|
||||
nameController.clear();
|
||||
pskController.clear();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
if (!usePublicPsk) ...[
|
||||
const SizedBox(height: 8),
|
||||
TextField(
|
||||
controller: pskController,
|
||||
decoration: InputDecoration(
|
||||
labelText: dialogContext.l10n.channels_pskHex,
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.casino),
|
||||
tooltip: dialogContext.l10n.channels_generateRandomPsk,
|
||||
onPressed: () {
|
||||
final random = Random.secure();
|
||||
final bytes = Uint8List(16);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
bytes[i] = random.nextInt(256);
|
||||
}
|
||||
pskController.text = Channel.formatPskHex(bytes);
|
||||
},
|
||||
hashtagController.clear();
|
||||
});
|
||||
}
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget? buildExpandedContent() {
|
||||
switch (selectedOption) {
|
||||
case 0: // Create Private Channel
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: dialogContext.l10n.channels_channelName,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
maxLength: 31,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
final name = nameController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
final random = Random.secure();
|
||||
final psk = Uint8List(16);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
psk[i] = random.nextInt(256);
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, name, psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(dialogContext.l10n.common_create),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
case 1: // Join Private Channel
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
controller: nameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: dialogContext.l10n.channels_channelName,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
maxLength: 31,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
controller: pskController,
|
||||
decoration: InputDecoration(
|
||||
labelText: dialogContext.l10n.channels_pskHex,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
final name = nameController.text.trim();
|
||||
final pskHex = pskController.text.trim();
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Uint8List psk;
|
||||
try {
|
||||
psk = Channel.parsePskHex(pskHex);
|
||||
} on FormatException {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, name, psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(dialogContext.l10n.common_add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
case 2: // Join Public Channel
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
final psk = Channel.parsePskHex(Channel.publicChannelPsk);
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, 'Public', psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.channels_publicChannelAdded)),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(dialogContext.l10n.common_add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(dialogContext.l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
final name = nameController.text.trim();
|
||||
final pskHex = usePublicPsk
|
||||
? Channel.publicChannelPsk
|
||||
: pskController.text.trim();
|
||||
);
|
||||
|
||||
if (name.isEmpty) {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
case 3: // Join Hashtag Channel
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
controller: hashtagController,
|
||||
decoration: InputDecoration(
|
||||
labelText: dialogContext.l10n.channels_enterHashtag,
|
||||
hintText: dialogContext.l10n.channels_hashtagHint,
|
||||
border: const OutlineInputBorder(),
|
||||
prefixIcon: const Icon(Icons.tag),
|
||||
),
|
||||
maxLength: 31,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
var hashtag = hashtagController.text.trim();
|
||||
if (hashtag.isEmpty) {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(content: Text(dialogContext.l10n.channels_enterChannelName)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Normalize hashtag name
|
||||
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
|
||||
final psk = Channel.derivePskFromHashtag(hashtag);
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(nextIndex, name, psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(dialogContext.l10n.common_add),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
Uint8List psk;
|
||||
try {
|
||||
psk = Channel.parsePskHex(pskHex);
|
||||
} on FormatException {
|
||||
ScaffoldMessenger.of(dialogContext).showSnackBar(
|
||||
SnackBar(content: Text(dialogContext.l10n.channels_pskMustBe32Hex)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.pop(dialogContext);
|
||||
connector.setChannel(selectedIndex, name, psk);
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.channels_channelAdded(name))),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(dialogContext.l10n.common_add),
|
||||
return AlertDialog(
|
||||
title: Text(dialogContext.l10n.channels_addChannel),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 16),
|
||||
content: SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
buildOptionTile(
|
||||
optionIndex: 0,
|
||||
icon: Icons.add,
|
||||
title: dialogContext.l10n.channels_createPrivateChannel,
|
||||
subtitle: dialogContext.l10n.channels_createPrivateChannelDesc,
|
||||
),
|
||||
if (selectedOption == 0) buildExpandedContent()!,
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
optionIndex: 1,
|
||||
icon: Icons.lock,
|
||||
title: dialogContext.l10n.channels_joinPrivateChannel,
|
||||
subtitle: dialogContext.l10n.channels_joinPrivateChannelDesc,
|
||||
),
|
||||
if (selectedOption == 1) buildExpandedContent()!,
|
||||
if (!hasPublicChannel) ...[
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
optionIndex: 2,
|
||||
icon: Icons.public,
|
||||
title: dialogContext.l10n.channels_joinPublicChannel,
|
||||
subtitle: dialogContext.l10n.channels_joinPublicChannelDesc,
|
||||
),
|
||||
if (selectedOption == 2) buildExpandedContent()!,
|
||||
],
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
optionIndex: 3,
|
||||
icon: Icons.tag,
|
||||
title: dialogContext.l10n.channels_joinHashtagChannel,
|
||||
subtitle: dialogContext.l10n.channels_joinHashtagChannelDesc,
|
||||
),
|
||||
if (selectedOption == 3) buildExpandedContent()!,
|
||||
const Divider(height: 1),
|
||||
buildOptionTile(
|
||||
optionIndex: 4,
|
||||
icon: Icons.qr_code,
|
||||
title: dialogContext.l10n.channels_scanQrCode,
|
||||
subtitle: dialogContext.l10n.channels_scanQrCodeComingSoon,
|
||||
enabled: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(dialogContext.l10n.common_close),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,18 +876,18 @@ 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__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue