diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index 6bbe2fd4..9601f38f 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -16,6 +16,7 @@ import '../models/message.dart'; import '../models/path_selection.dart'; import '../models/translation_support.dart'; import '../helpers/reaction_helper.dart'; +import '../helpers/cyr2lat.dart'; import '../helpers/smaz.dart'; import '../services/app_debug_log_service.dart'; import '../services/ble_debug_log_service.dart'; @@ -290,9 +291,13 @@ class MeshCoreConnector extends ChangeNotifier { final UnreadStore _unreadStore = UnreadStore(); List _cachedChannels = []; final Map _channelSmazEnabled = {}; + final Map _channelCyr2LatEnabled = {}; + final Map _channelCyr2LatProfileId = {}; bool _lastSentWasCliCommand = false; // Track if last sent message was a CLI command final Map _contactSmazEnabled = {}; + final Map _contactCyr2LatEnabled = {}; + final Map _contactCyr2LatProfileId = {}; final Set _knownContactKeys = {}; final Map _contactUnreadCount = {}; final Map _repeaterBatterySnapshots = {}; @@ -625,6 +630,20 @@ class MeshCoreConnector extends ChangeNotifier { _ensureContactSmazSettingLoaded(contactKeyHex); } + bool isChannelCyr2LatEnabled(int channelIndex) { + _ensureChannelCyr2LatSettingLoaded(channelIndex); + return _channelCyr2LatEnabled[channelIndex] ?? false; + } + + bool isContactCyr2LatEnabled(String contactKeyHex) { + _ensureContactCyr2LatSettingLoaded(contactKeyHex); + return _contactCyr2LatEnabled[contactKeyHex] ?? false; + } + + void ensureContactCyr2LatSettingLoaded(String contactKeyHex) { + _ensureContactCyr2LatSettingLoaded(contactKeyHex); + } + Future loadUnreadState() async { _contactUnreadCount ..clear() @@ -721,6 +740,10 @@ class MeshCoreConnector extends ChangeNotifier { Future setChannelSmazEnabled(int channelIndex, bool enabled) async { _channelSmazEnabled[channelIndex] = enabled; + if (enabled) { + _channelCyr2LatEnabled[channelIndex] = false; + await _channelSettingsStore.saveCyr2LatEnabled(channelIndex, false); + } await _channelSettingsStore.saveSmazEnabled(channelIndex, enabled); notifyListeners(); } @@ -731,6 +754,25 @@ class MeshCoreConnector extends ChangeNotifier { notifyListeners(); } + Future setChannelCyr2LatEnabled(int channelIndex, bool enabled) async { + _channelCyr2LatEnabled[channelIndex] = enabled; + if (enabled) { + _channelSmazEnabled[channelIndex] = false; + await _channelSettingsStore.saveSmazEnabled(channelIndex, false); + } + await _channelSettingsStore.saveCyr2LatEnabled(channelIndex, enabled); + notifyListeners(); + } + + Future setContactCyr2LatEnabled( + String contactKeyHex, + bool enabled, + ) async { + _contactCyr2LatEnabled[contactKeyHex] = enabled; + await _contactSettingsStore.saveCyr2LatEnabled(contactKeyHex, enabled); + notifyListeners(); + } + Future _loadChannelOrder() async { _channelOrder = await _channelOrderStore.loadChannelOrder(); _applyChannelOrder(); @@ -867,6 +909,7 @@ class MeshCoreConnector extends ChangeNotifier { ..addAll(cached); for (final contact in cached) { _ensureContactSmazSettingLoaded(contact.publicKeyHex); + _ensureContactCyr2LatSettingLoaded(contact.publicKeyHex); } } @@ -879,9 +922,12 @@ class MeshCoreConnector extends ChangeNotifier { Future loadChannelSettings({int? maxChannels}) async { _channelSmazEnabled.clear(); + _channelCyr2LatEnabled.clear(); final channelCount = maxChannels ?? _maxChannels; for (int i = 0; i < channelCount; i++) { _channelSmazEnabled[i] = await _channelSettingsStore.loadSmazEnabled(i); + _channelCyr2LatEnabled[i] = await _channelSettingsStore + .loadCyr2LatEnabled(i); } } @@ -4507,6 +4553,72 @@ class MeshCoreConnector extends ChangeNotifier { }); } + void _ensureContactCyr2LatSettingLoaded(String contactKeyHex) { + if (_contactCyr2LatEnabled.containsKey(contactKeyHex)) return; + _contactSettingsStore.loadCyr2LatEnabled(contactKeyHex).then((enabled) { + if (_contactCyr2LatEnabled[contactKeyHex] == enabled) return; + _contactCyr2LatEnabled[contactKeyHex] = enabled; + notifyListeners(); + }); + } + + void _ensureContactCyr2LatProfileLoaded(String contactKeyHex) { + if (_contactCyr2LatProfileId.containsKey(contactKeyHex)) return; + _contactSettingsStore.loadCyr2LatProfileId(contactKeyHex).then((profileId) { + if (_contactCyr2LatProfileId[contactKeyHex] == profileId) return; + _contactCyr2LatProfileId[contactKeyHex] = profileId; + notifyListeners(); + }); + } + + void _ensureChannelCyr2LatSettingLoaded(int channelIndex) { + if (_channelCyr2LatEnabled.containsKey(channelIndex)) return; + _channelSettingsStore.loadCyr2LatEnabled(channelIndex).then((enabled) { + if (_channelCyr2LatEnabled[channelIndex] == enabled) return; + _channelCyr2LatEnabled[channelIndex] = enabled; + notifyListeners(); + }); + } + + void _ensureChannelCyr2LatProfileLoaded(int channelIndex) { + if (_channelCyr2LatProfileId.containsKey(channelIndex)) return; + _channelSettingsStore.loadCyr2LatProfileId(channelIndex).then((profileId) { + if (_channelCyr2LatProfileId[channelIndex] == profileId) return; + _channelCyr2LatProfileId[channelIndex] = profileId; + notifyListeners(); + }); + } + + String? getChannelCyr2LatProfileId(int channelIndex) { + _ensureChannelCyr2LatProfileLoaded(channelIndex); + return _channelCyr2LatProfileId[channelIndex]; + } + + Future setChannelCyr2LatProfileId( + int channelIndex, + String? profileId, + ) async { + if (_channelCyr2LatProfileId[channelIndex] == profileId) return; + _channelCyr2LatProfileId[channelIndex] = profileId; + await _channelSettingsStore.saveCyr2LatProfileId(channelIndex, profileId); + notifyListeners(); + } + + String? getContactCyr2LatProfileId(String contactKeyHex) { + _ensureContactCyr2LatProfileLoaded(contactKeyHex); + return _contactCyr2LatProfileId[contactKeyHex]; + } + + Future setContactCyr2LatProfileId( + String contactKeyHex, + String? profileId, + ) async { + if (_contactCyr2LatProfileId[contactKeyHex] == profileId) return; + _contactCyr2LatProfileId[contactKeyHex] = profileId; + await _contactSettingsStore.saveCyr2LatProfileId(contactKeyHex, profileId); + notifyListeners(); + } + /// Prepares contact outbound text by applying SMAZ encoding if enabled. /// This should be used to transform text before computing ACK hashes. String prepareContactOutboundText(Contact contact, String text) { @@ -4515,8 +4627,26 @@ class MeshCoreConnector extends ChangeNotifier { trimmed.startsWith('g:') || trimmed.startsWith('m:') || trimmed.startsWith('V1|'); - if (!isStructuredPayload && isContactSmazEnabled(contact.publicKeyHex)) { - return Smaz.encodeIfSmaller(text); + if (!isStructuredPayload) { + if (isContactSmazEnabled(contact.publicKeyHex)) { + return Smaz.encodeIfSmaller(text); + } else if (isContactCyr2LatEnabled(contact.publicKeyHex)) { + final profileId = getContactCyr2LatProfileId(contact.publicKeyHex); + final profile = profileId != null && _appSettingsService != null + ? _appSettingsService!.getCyr2LatProfileById(profileId) + : null; + if (profile != null) { + Cyr2Lat.setCharMap(profile.charMap); + } else { + // Use global profile + final globalProfile = _appSettingsService + ?.getSelectedCyr2LatProfile(); + if (globalProfile != null) { + Cyr2Lat.setCharMap(globalProfile.charMap); + } + } + return Cyr2Lat.encode(text); + } } return text; } @@ -4525,8 +4655,26 @@ class MeshCoreConnector extends ChangeNotifier { final trimmed = text.trim(); final isStructuredPayload = trimmed.startsWith('g:') || trimmed.startsWith('m:'); - if (!isStructuredPayload && isChannelSmazEnabled(channelIndex)) { - return Smaz.encodeIfSmaller(text); + if (!isStructuredPayload) { + if (isChannelSmazEnabled(channelIndex)) { + return Smaz.encodeIfSmaller(text); + } else if (isChannelCyr2LatEnabled(channelIndex)) { + final profileId = getChannelCyr2LatProfileId(channelIndex); + final profile = profileId != null && _appSettingsService != null + ? _appSettingsService!.getCyr2LatProfileById(profileId) + : null; + if (profile != null) { + Cyr2Lat.setCharMap(profile.charMap); + } else { + // Use global profile + final globalProfile = _appSettingsService + ?.getSelectedCyr2LatProfile(); + if (globalProfile != null) { + Cyr2Lat.setCharMap(globalProfile.charMap); + } + } + return Cyr2Lat.encode(text); + } } return text; } diff --git a/lib/helpers/cyr2lat.dart b/lib/helpers/cyr2lat.dart new file mode 100644 index 00000000..a7ec5dde --- /dev/null +++ b/lib/helpers/cyr2lat.dart @@ -0,0 +1,63 @@ +class Cyr2Lat { + static Map _charMap = { + 'А': 'A', + 'В': 'B', + 'Е': 'E', + 'Ё': 'E', + 'З': '3', + 'К': 'K', + 'М': 'M', + 'Н': 'H', + 'О': 'O', + 'Р': 'P', + 'С': 'C', + 'Т': 'T', + 'Х': 'X', + 'Ь': 'b', + 'а': 'a', + 'е': 'e', + 'ё': 'e', + 'о': 'o', + 'р': 'p', + 'с': 'c', + 'у': 'y', + 'х': 'x', + }; + + static final RegExp _prefixRegExp = RegExp(r'\@\[[\S\s]+\] '); + + static void setCharMap(Map charMap) { + _charMap = Map.from(charMap); + } + + static String encode(String text) { + if (text.isEmpty) return text; + final buffer = StringBuffer(); + + final senderName = extractSenderName(text); + final msgText = removeSenderName(text); + + for (final rune in msgText.runes) { + final char = String.fromCharCode(rune); + buffer.write(_charMap[char] ?? char); + } + + return senderName + buffer.toString(); + } + + static String removeSenderName(String text) { + final match = _prefixRegExp.matchAsPrefix(text); + if (match != null) { + return text.substring(match.end); + } + return text; + } + + static String extractSenderName(String text) { + final match = _prefixRegExp.matchAsPrefix(text); + if (match != null) { + return match.group(0) ?? ''; + } + return ''; + } +} diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb index 50c7c18f..64c88dc5 100644 --- a/lib/l10n/app_bg.arb +++ b/lib/l10n/app_bg.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "Компресия SMAZ", + "channels_cyr2latCompression": "Компресия Cyr2Lat", + "channels_cyr2latCompressionDscr": "Заменя някои кирилични символи с латиница при изпращане.", + "channels_cyr2latSettingsHeading": "Настройки на Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Списък със замествания", + "channels_cyr2latSettingsDscr": "Редактиране на JSON конфигурацията за заместване на символи", + "channels_cyr2latSettingsDialogHint": "JSON карта за замествания", + "channels_cyr2latSettingsDialogWrongJSON": "Неправилен JSON: {error}", + "settings_cyr2latProfileAdd": "Добавяне на профил Cyr2Lat", + "settings_cyr2latProfileName": "Име на профила", + "settings_cyr2latProfileNameEmpty": "Името на профила не може да бъде празно", + "settings_cyr2latProfileAdded": "Профилът е добавен успешно", + "settings_cyr2latProfileUpdated": "Профилът е актуализиран успешно", + "settings_cyr2latProfileEdit": "Редактиране на Cyr2Lat профил", + "settings_cyr2latProfileDelete": "Изтриване на профил Cyr2Lat", + "settings_cyr2latProfileDeleted": "Профилът беше изтрит успешно", + "settings_cyr2latProfileDeleteDscr": "Сигурен ли сте, че искате да изтриете профила \"{name}\"?", "channels_channelUpdated": "Каналът \"{name}\" е актуализиран", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 8c79f435..96b7d035 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "SMAZ-Komprimierung", + "channels_cyr2latCompression": "Cyr2Lat-Komprimierung", + "channels_cyr2latCompressionDscr": "Ersetzt einige kyrillische Zeichen durch lateinische Zeichen, wenn sie gesendet werden.", + "channels_cyr2latSettingsHeading": "Cyr2Lat-Einstellungen", + "channels_cyr2latSettingsSubheading": "Ersetzungsliste", + "channels_cyr2latSettingsDscr": "JSON-Konfiguration für die Zeichenersetzung bearbeiten", + "channels_cyr2latSettingsDialogHint": "JSON-Ersetzungstabelle", + "channels_cyr2latSettingsDialogWrongJSON": "Ungültiges JSON: {error}", + "settings_cyr2latProfileAdd": "Cyr2Lat-Profil hinzufügen", + "settings_cyr2latProfileName": "Profilname", + "settings_cyr2latProfileNameEmpty": "Der Profilname darf nicht leer sein", + "settings_cyr2latProfileAdded": "Profil erfolgreich hinzugefügt", + "settings_cyr2latProfileUpdated": "Profil erfolgreich aktualisiert", + "settings_cyr2latProfileEdit": "Cyr2Lat-Profil bearbeiten", + "settings_cyr2latProfileDelete": "Cyr2Lat-Profil löschen", + "settings_cyr2latProfileDeleted": "Profil erfolgreich gelöscht", + "settings_cyr2latProfileDeleteDscr": "Möchten Sie das Profil \"{name}\" wirklich löschen?", "channels_channelUpdated": "Kanal \"{name}\" aktualisiert", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 130ca8de..456fab56 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -580,6 +580,18 @@ } }, "channels_smazCompression": "SMAZ compression", + "channels_cyr2latCompression": "Cyr2Lat compression", + "channels_cyr2latCompressionDscr": "Replaces some Cyrillic characters with Latin characters when sending.", + "channels_cyr2latSettingsHeading": "Cyr2Lat Setup", + "channels_cyr2latSettingsSubheading": "List of replacements", + "channels_cyr2latSettingsDscr": "Edit the JSON configuration of character replacement", + "channels_cyr2latSettingsDialogHint": "JSON replacement map", + "channels_cyr2latSettingsDialogWrongJSON": "Invalid JSON: {error}", + "@channels_cyr2latSettingsDialogWrongJSON": { + "placeholders": { + "error": {} + } + }, "channels_channelUpdated": "Channel \"{name}\" updated", "@channels_channelUpdated": { "placeholders": { @@ -588,6 +600,22 @@ } } }, + "settings_cyr2latProfileAdd": "Add Cyr2Lat Profile", + "settings_cyr2latProfileName": "Profile Name", + "settings_cyr2latProfileNameEmpty": "Profile name cannot be empty", + "settings_cyr2latProfileAdded": "Profile added successfully", + "settings_cyr2latProfileUpdated": "Profile updated successfully", + "settings_cyr2latProfileEdit": "Edit Cyr2Lat Profile", + "settings_cyr2latProfileDelete": "Delete Cyr2Lat Profile", + "settings_cyr2latProfileDeleted": "Profile deleted successfully", + "settings_cyr2latProfileDeleteDscr": "Are you sure you want to delete the profile \"{name}\"?", + "@settings_cyr2latProfileDeleteDscr": { + "placeholders": { + "name": { + "type": "String" + } + } + }, "channels_publicChannelAdded": "Public channel added", "channels_sortBy": "Sort by", "channels_sortManual": "Manual", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 12f8e7ce..ff1e8565 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "Compresión SMAZ", + "channels_cyr2latCompression": "Compresión Cyr2Lat", + "channels_cyr2latCompressionDscr": "Reemplaza algunos caracteres cirílicos con caracteres latinos al enviar.", + "channels_cyr2latSettingsHeading": "Configuración de Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Lista de sustituciones", + "channels_cyr2latSettingsDscr": "Editar la configuración JSON de sustitución de caracteres", + "channels_cyr2latSettingsDialogHint": "Mapa JSON de sustituciones", + "channels_cyr2latSettingsDialogWrongJSON": "JSON incorrecto: {error}", + "settings_cyr2latProfileAdd": "Añadir perfil Cyr2Lat", + "settings_cyr2latProfileName": "Nombre del perfil", + "settings_cyr2latProfileNameEmpty": "El nombre del perfil no puede estar vacío", + "settings_cyr2latProfileAdded": "Perfil añadido correctamente", + "settings_cyr2latProfileUpdated": "Perfil actualizado correctamente", + "settings_cyr2latProfileEdit": "Editar perfil Cyr2Lat", + "settings_cyr2latProfileDelete": "Eliminar perfil Cyr2Lat", + "settings_cyr2latProfileDeleted": "Perfil eliminado correctamente", + "settings_cyr2latProfileDeleteDscr": "¿Está seguro de que desea eliminar el perfil \"{name}\"?", "channels_channelUpdated": "Canal \"{name}\" actualizado", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index f5a39c07..8e006b76 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "Compression SMAZ", + "channels_cyr2latCompression": "Compression Cyr2Lat", + "channels_cyr2latCompressionDscr": "Remplace certains caractères cyrilliques par des caractères latins lors de l'envoi.", + "channels_cyr2latSettingsHeading" : "Paramètres Cyr2Lat", + "channels_cyr2latSettingsSubheading" : "Liste des remplacements", + "channels_cyr2latSettingsDscr" : "Modifier la configuration JSON des remplacements de caractères", + "channels_cyr2latSettingsDialogHint": "Tableau de remplacement JSON", + "channels_cyr2latSettingsDialogWrongJSON": "JSON incorrect : {error}", + "settings_cyr2latProfileAdd" : "Ajouter un profil Cyr2Lat", + "settings_cyr2latProfileName" : "Nom du profil", + "settings_cyr2latProfileNameEmpty" : "Le nom du profil ne peut pas être vide", + "settings_cyr2latProfileAdded": "Profil ajouté avec succès", + "settings_cyr2latProfileUpdated": "Profil mis à jour avec succès", + "settings_cyr2latProfileEdit": "Modifier le profil Cyr2Lat", + "settings_cyr2latProfileDelete" : "Supprimer le profil Cyr2Lat", + "settings_cyr2latProfileDeleted" : "Profil supprimé avec succès", + "settings_cyr2latProfileDeleteDscr" : "Êtes-vous sûr de vouloir supprimer le profil \"{name}\"?", "channels_channelUpdated": "Le canal \"{name}\" a été mis à jour", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb index ea040c7a..618f4fa2 100644 --- a/lib/l10n/app_hu.arb +++ b/lib/l10n/app_hu.arb @@ -547,6 +547,22 @@ } }, "channels_smazCompression": "SMAZ kompresszió", + "channels_cyr2latCompression": "Cyr2Lat kompresszió", + "channels_cyr2latCompressionDscr": "Néhány Cirill betűt Latin betűkkel helyettesít küldéskor.", + "channels_cyr2latSettingsHeading": "Cyr2Lat beállítások", + "channels_cyr2latSettingsSubheading": "Helyettesítési lista", + "channels_cyr2latSettingsDscr": "A karakterhelyettesítési JSON-konfiguráció szerkesztése", + "channels_cyr2latSettingsDialogHint": "JSON-csere táblázat", + "channels_cyr2latSettingsDialogWrongJSON": "Hibás JSON: {error}", + "settings_cyr2latProfileAdd": "Cyr2Lat-profil hozzáadása", + "settings_cyr2latProfileName": "Profil neve", + "settings_cyr2latProfileNameEmpty": "A profil neve nem lehet üres", + "settings_cyr2latProfileAdded": "A profil hozzáadása sikeres", + "settings_cyr2latProfileUpdated": "A profil frissítése sikeres", + "settings_cyr2latProfileEdit": "Cyr2Lat profil szerkesztése", + "settings_cyr2latProfileDelete": "Cyr2Lat profil törlése", + "settings_cyr2latProfileDeleted": "A profil törlése sikeresen megtörtént", + "settings_cyr2latProfileDeleteDscr": "Biztosan törölni szeretné a \"{name}\" profilt?", "channels_channelUpdated": "A {name} csatorna frissítve", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 459f32ad..ec75604e 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "Compressione SMAZ", + "channels_cyr2latCompression": "Compressione Cyr2Lat", + "channels_cyr2latCompressionDscr": "Sostituisce alcuni caratteri cirillici con caratteri latini durante l'invio.", + "channels_cyr2latSettingsHeading": "Impostazioni Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Elenco delle sostituzioni", + "channels_cyr2latSettingsDscr": "Modifica la configurazione JSON delle sostituzioni dei caratteri", + "channels_cyr2latSettingsDialogHint": "Mappa JSON delle sostituzioni", + "channels_cyr2latSettingsDialogWrongJSON": "JSON non corretto: {error}", + "settings_cyr2latProfileAdd": "Aggiungi profilo Cyr2Lat", + "settings_cyr2latProfileName": "Nome profilo", + "settings_cyr2latProfileNameEmpty": "Il nome del profilo non può essere vuoto", + "settings_cyr2latProfileAdded": "Profilo aggiunto con successo", + "settings_cyr2latProfileUpdated": "Profilo aggiornato con successo", + "settings_cyr2latProfileEdit": "Modifica profilo Cyr2Lat", + "settings_cyr2latProfileDelete": "Elimina profilo Cyr2Lat", + "settings_cyr2latProfileDeleted": "Profilo eliminato con successo", + "settings_cyr2latProfileDeleteDscr": "Sei sicuro di voler eliminare il profilo \"{name}\"?", "channels_channelUpdated": "Canale \"{name}\" aggiornato", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 12da5296..2512da0f 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -547,6 +547,22 @@ } }, "channels_smazCompression": "SMAZ 圧縮", + "channels_cyr2latCompression": "Cyr2Lat 圧縮", + "channels_cyr2latCompressionDscr": "送信時に一部のキリル文字をラテン文字に置き換えます。", + "channels_cyr2latSettingsHeading": "cyr2latの設定", + "channels_cyr2latSettingsSubheading": "置換リスト", + "channels_cyr2latSettingsDscr": "文字置換のJSON設定を編集する", + "channels_cyr2latSettingsDialogHint": "JSON置換マップ", + "channels_cyr2latSettingsDialogWrongJSON": "不正なJSON: {error}", + "settings_cyr2latProfileAdd": "Cyr2Latプロファイルの追加", + "settings_cyr2latProfileName": "プロファイル名", + "settings_cyr2latProfileNameEmpty": "プロファイル名は空にできません", + "settings_cyr2latProfileAdded": "プロファイルが正常に追加されました", + "settings_cyr2latProfileUpdated": "プロファイルの更新に成功しました", + "settings_cyr2latProfileEdit": "Cyr2Latプロファイルを編集", + "settings_cyr2latProfileDelete": "Cyr2Latプロファイルを削除", + "settings_cyr2latProfileDeleted": "プロファイルの削除に成功しました", + "settings_cyr2latProfileDeleteDscr": "プロファイル \"{name}\" を削除してもよろしいですか?", "channels_channelUpdated": "チャンネル「{name}」が更新されました", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index b66bfe2b..846925a3 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -547,6 +547,22 @@ } }, "channels_smazCompression": "SMAZ 압축", + "channels_cyr2latCompression": "Cyr2Lat 압축", + "channels_cyr2latCompressionDscr": "보낼 때 일부 키릴 문자를 라틴 문자로 바꿉니다.", + "channels_cyr2latSettingsHeading": "Cyr2Lat 설정", + "channels_cyr2latSettingsSubheading": "변환 목록", + "channels_cyr2latSettingsDscr": "문자 변환 JSON 구성 편집", + "channels_cyr2latSettingsDialogHint": "JSON 변환 맵", + "channels_cyr2latSettingsDialogWrongJSON": "잘못된 JSON: {error}", + "settings_cyr2latProfileAdd": "Cyr2Lat 프로필 추가", + "settings_cyr2latProfileName": "프로필 이름", + "settings_cyr2latProfileNameEmpty": "프로필 이름은 비워둘 수 없습니다", + "settings_cyr2latProfileAdded": "프로필이 성공적으로 추가되었습니다", + "settings_cyr2latProfileUpdated": "프로필이 성공적으로 업데이트되었습니다", + "settings_cyr2latProfileEdit": "Cyr2Lat 프로필 편집", + "settings_cyr2latProfileDelete": "Cyr2Lat 프로필 삭제", + "settings_cyr2latProfileDeleted": "프로필이 성공적으로 삭제되었습니다", + "settings_cyr2latProfileDeleteDscr": "\"{name}\" 프로필을 삭제하시겠습니까?", "channels_channelUpdated": "채널 \"{name}\"이 업데이트되었습니다.", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9eabc9b3..159d755c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -2182,12 +2182,108 @@ abstract class AppLocalizations { /// **'SMAZ compression'** String get channels_smazCompression; + /// No description provided for @channels_cyr2latCompression. + /// + /// In en, this message translates to: + /// **'Cyr2Lat compression'** + String get channels_cyr2latCompression; + + /// No description provided for @channels_cyr2latCompressionDscr. + /// + /// In en, this message translates to: + /// **'Replaces some Cyrillic characters with Latin characters when sending.'** + String get channels_cyr2latCompressionDscr; + + /// No description provided for @channels_cyr2latSettingsHeading. + /// + /// In en, this message translates to: + /// **'Cyr2Lat Setup'** + String get channels_cyr2latSettingsHeading; + + /// No description provided for @channels_cyr2latSettingsSubheading. + /// + /// In en, this message translates to: + /// **'List of replacements'** + String get channels_cyr2latSettingsSubheading; + + /// No description provided for @channels_cyr2latSettingsDscr. + /// + /// In en, this message translates to: + /// **'Edit the JSON configuration of character replacement'** + String get channels_cyr2latSettingsDscr; + + /// No description provided for @channels_cyr2latSettingsDialogHint. + /// + /// In en, this message translates to: + /// **'JSON replacement map'** + String get channels_cyr2latSettingsDialogHint; + + /// No description provided for @channels_cyr2latSettingsDialogWrongJSON. + /// + /// In en, this message translates to: + /// **'Invalid JSON: {error}'** + String channels_cyr2latSettingsDialogWrongJSON(Object error); + /// No description provided for @channels_channelUpdated. /// /// In en, this message translates to: /// **'Channel \"{name}\" updated'** String channels_channelUpdated(String name); + /// No description provided for @settings_cyr2latProfileAdd. + /// + /// In en, this message translates to: + /// **'Add Cyr2Lat Profile'** + String get settings_cyr2latProfileAdd; + + /// No description provided for @settings_cyr2latProfileName. + /// + /// In en, this message translates to: + /// **'Profile Name'** + String get settings_cyr2latProfileName; + + /// No description provided for @settings_cyr2latProfileNameEmpty. + /// + /// In en, this message translates to: + /// **'Profile name cannot be empty'** + String get settings_cyr2latProfileNameEmpty; + + /// No description provided for @settings_cyr2latProfileAdded. + /// + /// In en, this message translates to: + /// **'Profile added successfully'** + String get settings_cyr2latProfileAdded; + + /// No description provided for @settings_cyr2latProfileUpdated. + /// + /// In en, this message translates to: + /// **'Profile updated successfully'** + String get settings_cyr2latProfileUpdated; + + /// No description provided for @settings_cyr2latProfileEdit. + /// + /// In en, this message translates to: + /// **'Edit Cyr2Lat Profile'** + String get settings_cyr2latProfileEdit; + + /// No description provided for @settings_cyr2latProfileDelete. + /// + /// In en, this message translates to: + /// **'Delete Cyr2Lat Profile'** + String get settings_cyr2latProfileDelete; + + /// No description provided for @settings_cyr2latProfileDeleted. + /// + /// In en, this message translates to: + /// **'Profile deleted successfully'** + String get settings_cyr2latProfileDeleted; + + /// No description provided for @settings_cyr2latProfileDeleteDscr. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete the profile \"{name}\"?'** + String settings_cyr2latProfileDeleteDscr(String name); + /// No description provided for @channels_publicChannelAdded. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 81bbb0c0..6ca4ac70 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -1178,11 +1178,67 @@ class AppLocalizationsBg extends AppLocalizations { @override String get channels_smazCompression => 'Компресия SMAZ'; + @override + String get channels_cyr2latCompression => 'Компресия Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Заменя някои кирилични символи с латиница при изпращане.'; + + @override + String get channels_cyr2latSettingsHeading => 'Настройки на Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Списък със замествания'; + + @override + String get channels_cyr2latSettingsDscr => + 'Редактиране на JSON конфигурацията за заместване на символи'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON карта за замествания'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Неправилен JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Каналът \"$name\" е актуализиран'; } + @override + String get settings_cyr2latProfileAdd => 'Добавяне на профил Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Име на профила'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Името на профила не може да бъде празно'; + + @override + String get settings_cyr2latProfileAdded => 'Профилът е добавен успешно'; + + @override + String get settings_cyr2latProfileUpdated => + 'Профилът е актуализиран успешно'; + + @override + String get settings_cyr2latProfileEdit => 'Редактиране на Cyr2Lat профил'; + + @override + String get settings_cyr2latProfileDelete => 'Изтриване на профил Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Профилът беше изтрит успешно'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Сигурен ли сте, че искате да изтриете профила \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Публичен канал добавен'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 0c194929..65f562cb 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -1174,11 +1174,67 @@ class AppLocalizationsDe extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ-Komprimierung'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat-Komprimierung'; + + @override + String get channels_cyr2latCompressionDscr => + 'Ersetzt einige kyrillische Zeichen durch lateinische Zeichen, wenn sie gesendet werden.'; + + @override + String get channels_cyr2latSettingsHeading => 'Cyr2Lat-Einstellungen'; + + @override + String get channels_cyr2latSettingsSubheading => 'Ersetzungsliste'; + + @override + String get channels_cyr2latSettingsDscr => + 'JSON-Konfiguration für die Zeichenersetzung bearbeiten'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON-Ersetzungstabelle'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Ungültiges JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Kanal \"$name\" aktualisiert'; } + @override + String get settings_cyr2latProfileAdd => 'Cyr2Lat-Profil hinzufügen'; + + @override + String get settings_cyr2latProfileName => 'Profilname'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Der Profilname darf nicht leer sein'; + + @override + String get settings_cyr2latProfileAdded => 'Profil erfolgreich hinzugefügt'; + + @override + String get settings_cyr2latProfileUpdated => + 'Profil erfolgreich aktualisiert'; + + @override + String get settings_cyr2latProfileEdit => 'Cyr2Lat-Profil bearbeiten'; + + @override + String get settings_cyr2latProfileDelete => 'Cyr2Lat-Profil löschen'; + + @override + String get settings_cyr2latProfileDeleted => 'Profil erfolgreich gelöscht'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Möchten Sie das Profil \"$name\" wirklich löschen?'; + } + @override String get channels_publicChannelAdded => 'Öffentlicher Kanal hinzugefügt'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index fb176744..6def06b7 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -1154,11 +1154,65 @@ class AppLocalizationsEn extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ compression'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat compression'; + + @override + String get channels_cyr2latCompressionDscr => + 'Replaces some Cyrillic characters with Latin characters when sending.'; + + @override + String get channels_cyr2latSettingsHeading => 'Cyr2Lat Setup'; + + @override + String get channels_cyr2latSettingsSubheading => 'List of replacements'; + + @override + String get channels_cyr2latSettingsDscr => + 'Edit the JSON configuration of character replacement'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON replacement map'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Invalid JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Channel \"$name\" updated'; } + @override + String get settings_cyr2latProfileAdd => 'Add Cyr2Lat Profile'; + + @override + String get settings_cyr2latProfileName => 'Profile Name'; + + @override + String get settings_cyr2latProfileNameEmpty => 'Profile name cannot be empty'; + + @override + String get settings_cyr2latProfileAdded => 'Profile added successfully'; + + @override + String get settings_cyr2latProfileUpdated => 'Profile updated successfully'; + + @override + String get settings_cyr2latProfileEdit => 'Edit Cyr2Lat Profile'; + + @override + String get settings_cyr2latProfileDelete => 'Delete Cyr2Lat Profile'; + + @override + String get settings_cyr2latProfileDeleted => 'Profile deleted successfully'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Are you sure you want to delete the profile \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Public channel added'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 5816fd5f..c8347336 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -1176,11 +1176,67 @@ class AppLocalizationsEs extends AppLocalizations { @override String get channels_smazCompression => 'Compresión SMAZ'; + @override + String get channels_cyr2latCompression => 'Compresión Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Reemplaza algunos caracteres cirílicos con caracteres latinos al enviar.'; + + @override + String get channels_cyr2latSettingsHeading => 'Configuración de Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Lista de sustituciones'; + + @override + String get channels_cyr2latSettingsDscr => + 'Editar la configuración JSON de sustitución de caracteres'; + + @override + String get channels_cyr2latSettingsDialogHint => 'Mapa JSON de sustituciones'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'JSON incorrecto: $error'; + } + @override String channels_channelUpdated(String name) { return 'Canal \"$name\" actualizado'; } + @override + String get settings_cyr2latProfileAdd => 'Añadir perfil Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Nombre del perfil'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'El nombre del perfil no puede estar vacío'; + + @override + String get settings_cyr2latProfileAdded => 'Perfil añadido correctamente'; + + @override + String get settings_cyr2latProfileUpdated => + 'Perfil actualizado correctamente'; + + @override + String get settings_cyr2latProfileEdit => 'Editar perfil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Eliminar perfil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Perfil eliminado correctamente'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return '¿Está seguro de que desea eliminar el perfil \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Canal público añadido'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index f8c6954a..339ed85a 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -1181,11 +1181,67 @@ class AppLocalizationsFr extends AppLocalizations { @override String get channels_smazCompression => 'Compression SMAZ'; + @override + String get channels_cyr2latCompression => 'Compression Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Remplace certains caractères cyrilliques par des caractères latins lors de l\'envoi.'; + + @override + String get channels_cyr2latSettingsHeading => 'Paramètres Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Liste des remplacements'; + + @override + String get channels_cyr2latSettingsDscr => + 'Modifier la configuration JSON des remplacements de caractères'; + + @override + String get channels_cyr2latSettingsDialogHint => + 'Tableau de remplacement JSON'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'JSON incorrect : $error'; + } + @override String channels_channelUpdated(String name) { return 'Le canal \"$name\" a été mis à jour'; } + @override + String get settings_cyr2latProfileAdd => 'Ajouter un profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Nom du profil'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Le nom du profil ne peut pas être vide'; + + @override + String get settings_cyr2latProfileAdded => 'Profil ajouté avec succès'; + + @override + String get settings_cyr2latProfileUpdated => 'Profil mis à jour avec succès'; + + @override + String get settings_cyr2latProfileEdit => 'Modifier le profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Supprimer le profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Profil supprimé avec succès'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Êtes-vous sûr de vouloir supprimer le profil \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Le canal public a été ajouté'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index b9f6bdd8..b9584eca 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -1181,11 +1181,66 @@ class AppLocalizationsHu extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ kompresszió'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat kompresszió'; + + @override + String get channels_cyr2latCompressionDscr => + 'Néhány Cirill betűt Latin betűkkel helyettesít küldéskor.'; + + @override + String get channels_cyr2latSettingsHeading => 'Cyr2Lat beállítások'; + + @override + String get channels_cyr2latSettingsSubheading => 'Helyettesítési lista'; + + @override + String get channels_cyr2latSettingsDscr => + 'A karakterhelyettesítési JSON-konfiguráció szerkesztése'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON-csere táblázat'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Hibás JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'A $name csatorna frissítve'; } + @override + String get settings_cyr2latProfileAdd => 'Cyr2Lat-profil hozzáadása'; + + @override + String get settings_cyr2latProfileName => 'Profil neve'; + + @override + String get settings_cyr2latProfileNameEmpty => 'A profil neve nem lehet üres'; + + @override + String get settings_cyr2latProfileAdded => 'A profil hozzáadása sikeres'; + + @override + String get settings_cyr2latProfileUpdated => 'A profil frissítése sikeres'; + + @override + String get settings_cyr2latProfileEdit => 'Cyr2Lat profil szerkesztése'; + + @override + String get settings_cyr2latProfileDelete => 'Cyr2Lat profil törlése'; + + @override + String get settings_cyr2latProfileDeleted => + 'A profil törlése sikeresen megtörtént'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Biztosan törölni szeretné a \"$name\" profilt?'; + } + @override String get channels_publicChannelAdded => 'A nyilvános csatorna hozzáadva'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 1cb2b226..4cf29ff7 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -1177,11 +1177,68 @@ class AppLocalizationsIt extends AppLocalizations { @override String get channels_smazCompression => 'Compressione SMAZ'; + @override + String get channels_cyr2latCompression => 'Compressione Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Sostituisce alcuni caratteri cirillici con caratteri latini durante l\'invio.'; + + @override + String get channels_cyr2latSettingsHeading => 'Impostazioni Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Elenco delle sostituzioni'; + + @override + String get channels_cyr2latSettingsDscr => + 'Modifica la configurazione JSON delle sostituzioni dei caratteri'; + + @override + String get channels_cyr2latSettingsDialogHint => + 'Mappa JSON delle sostituzioni'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'JSON non corretto: $error'; + } + @override String channels_channelUpdated(String name) { return 'Canale \"$name\" aggiornato'; } + @override + String get settings_cyr2latProfileAdd => 'Aggiungi profilo Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Nome profilo'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Il nome del profilo non può essere vuoto'; + + @override + String get settings_cyr2latProfileAdded => 'Profilo aggiunto con successo'; + + @override + String get settings_cyr2latProfileUpdated => + 'Profilo aggiornato con successo'; + + @override + String get settings_cyr2latProfileEdit => 'Modifica profilo Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Elimina profilo Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Profilo eliminato con successo'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Sei sicuro di voler eliminare il profilo \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Canale pubblico aggiunto'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 254eae74..2d882c39 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -1122,11 +1122,63 @@ class AppLocalizationsJa extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ 圧縮'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat 圧縮'; + + @override + String get channels_cyr2latCompressionDscr => '送信時に一部のキリル文字をラテン文字に置き換えます。'; + + @override + String get channels_cyr2latSettingsHeading => 'cyr2latの設定'; + + @override + String get channels_cyr2latSettingsSubheading => '置換リスト'; + + @override + String get channels_cyr2latSettingsDscr => '文字置換のJSON設定を編集する'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON置換マップ'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return '不正なJSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'チャンネル「$name」が更新されました'; } + @override + String get settings_cyr2latProfileAdd => 'Cyr2Latプロファイルの追加'; + + @override + String get settings_cyr2latProfileName => 'プロファイル名'; + + @override + String get settings_cyr2latProfileNameEmpty => 'プロファイル名は空にできません'; + + @override + String get settings_cyr2latProfileAdded => 'プロファイルが正常に追加されました'; + + @override + String get settings_cyr2latProfileUpdated => 'プロファイルの更新に成功しました'; + + @override + String get settings_cyr2latProfileEdit => 'Cyr2Latプロファイルを編集'; + + @override + String get settings_cyr2latProfileDelete => 'Cyr2Latプロファイルを削除'; + + @override + String get settings_cyr2latProfileDeleted => 'プロファイルの削除に成功しました'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'プロファイル \"$name\" を削除してもよろしいですか?'; + } + @override String get channels_publicChannelAdded => 'パブリックチャンネルが追加されました'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 000cbfe8..4eadd542 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -1117,11 +1117,63 @@ class AppLocalizationsKo extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ 압축'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat 압축'; + + @override + String get channels_cyr2latCompressionDscr => '보낼 때 일부 키릴 문자를 라틴 문자로 바꿉니다.'; + + @override + String get channels_cyr2latSettingsHeading => 'Cyr2Lat 설정'; + + @override + String get channels_cyr2latSettingsSubheading => '변환 목록'; + + @override + String get channels_cyr2latSettingsDscr => '문자 변환 JSON 구성 편집'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON 변환 맵'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return '잘못된 JSON: $error'; + } + @override String channels_channelUpdated(String name) { return '채널 \"$name\"이 업데이트되었습니다.'; } + @override + String get settings_cyr2latProfileAdd => 'Cyr2Lat 프로필 추가'; + + @override + String get settings_cyr2latProfileName => '프로필 이름'; + + @override + String get settings_cyr2latProfileNameEmpty => '프로필 이름은 비워둘 수 없습니다'; + + @override + String get settings_cyr2latProfileAdded => '프로필이 성공적으로 추가되었습니다'; + + @override + String get settings_cyr2latProfileUpdated => '프로필이 성공적으로 업데이트되었습니다'; + + @override + String get settings_cyr2latProfileEdit => 'Cyr2Lat 프로필 편집'; + + @override + String get settings_cyr2latProfileDelete => 'Cyr2Lat 프로필 삭제'; + + @override + String get settings_cyr2latProfileDeleted => '프로필이 성공적으로 삭제되었습니다'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return '\"$name\" 프로필을 삭제하시겠습니까?'; + } + @override String get channels_publicChannelAdded => '공개 채널 추가'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 51e02e1f..b0c821a1 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -1165,11 +1165,66 @@ class AppLocalizationsNl extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ compressie'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat compressie'; + + @override + String get channels_cyr2latCompressionDscr => + 'Vervangt sommige Cyrillische tekens door Latijnse tekens bij het verzenden.'; + + @override + String get channels_cyr2latSettingsHeading => 'Instellingen Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Lijst met vervangingen'; + + @override + String get channels_cyr2latSettingsDscr => + 'Bewerk de JSON-configuratie voor tekenvervanging'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON-vervangingskaart'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Onjuiste JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Kanaal \"$name\" is bijgewerkt'; } + @override + String get settings_cyr2latProfileAdd => 'Cyr2Lat-profiel toevoegen'; + + @override + String get settings_cyr2latProfileName => 'Profielnaam'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Profielnaam mag niet leeg zijn'; + + @override + String get settings_cyr2latProfileAdded => 'Profiel succesvol toegevoegd'; + + @override + String get settings_cyr2latProfileUpdated => 'Profiel succesvol bijgewerkt'; + + @override + String get settings_cyr2latProfileEdit => 'Cyr2Lat-profiel bewerken'; + + @override + String get settings_cyr2latProfileDelete => 'Cyr2Lat-profiel verwijderen'; + + @override + String get settings_cyr2latProfileDeleted => 'Profiel succesvol verwijderd'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Weet u zeker dat u het profiel \"$name\" wilt verwijderen?'; + } + @override String get channels_publicChannelAdded => 'Open kanaal toegevoegd'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index aa941985..1c487a38 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -1185,11 +1185,68 @@ class AppLocalizationsPl extends AppLocalizations { @override String get channels_smazCompression => 'Kompresja SMAZ'; + @override + String get channels_cyr2latCompression => 'Kompresja Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Zastępuje niektóre znaki cyrylicy alfabetem łacińskim podczas wysyłania.'; + + @override + String get channels_cyr2latSettingsHeading => 'Ustawienia Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Lista zamian'; + + @override + String get channels_cyr2latSettingsDscr => + 'Edytuj konfigurację JSON zamiany znaków'; + + @override + String get channels_cyr2latSettingsDialogHint => 'Mapa zamian JSON'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Nieprawidłowy JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Kanał \"$name\" został zaktualizowany'; } + @override + String get settings_cyr2latProfileAdd => 'Dodaj profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Nazwa profilu'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Nazwa profilu nie może być pusta'; + + @override + String get settings_cyr2latProfileAdded => 'Profil dodano pomyślnie'; + + @override + String get settings_cyr2latProfileUpdated => + 'Profil został pomyślnie zaktualizowany'; + + @override + String get settings_cyr2latProfileEdit => 'Edytuj profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Usuń profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => + 'Profil został pomyślnie usunięty'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Czy na pewno chcesz usunąć profil \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Kanał publiczny dodany'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 5395b26f..2a0d491e 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -1176,11 +1176,66 @@ class AppLocalizationsPt extends AppLocalizations { @override String get channels_smazCompression => 'Compressão SMAZ'; + @override + String get channels_cyr2latCompression => 'Compressão Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Substitui alguns caracteres cirílicos por caracteres latinos ao enviar.'; + + @override + String get channels_cyr2latSettingsHeading => 'Configuração do Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Lista de substituições'; + + @override + String get channels_cyr2latSettingsDscr => + 'Editar a configuração JSON de substituição de caracteres'; + + @override + String get channels_cyr2latSettingsDialogHint => 'Mapa de substituições JSON'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'JSON incorreto: $error'; + } + @override String channels_channelUpdated(String name) { return 'Canal \"$name\" atualizado'; } + @override + String get settings_cyr2latProfileAdd => 'Adicionar perfil Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Nome do perfil'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'O nome do perfil não pode estar vazio'; + + @override + String get settings_cyr2latProfileAdded => 'Perfil adicionado com sucesso'; + + @override + String get settings_cyr2latProfileUpdated => 'Perfil atualizado com sucesso'; + + @override + String get settings_cyr2latProfileEdit => 'Editar perfil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Eliminar perfil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Perfil eliminado com sucesso'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Tem a certeza de que deseja eliminar o perfil \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Canal público adicionado'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4568b24f..55f5afed 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -1176,11 +1176,66 @@ class AppLocalizationsRu extends AppLocalizations { @override String get channels_smazCompression => 'Сжатие SMAZ'; + @override + String get channels_cyr2latCompression => 'Сжатие Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Заменяет некоторые кириллические символы на латиницу при отправке.'; + + @override + String get channels_cyr2latSettingsHeading => 'Настройка Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Список замен'; + + @override + String get channels_cyr2latSettingsDscr => + 'Редактировать JSON-конфигурацию замены символов'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON-карта замен'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Некорректный JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Канал \"$name\" обновлён'; } + @override + String get settings_cyr2latProfileAdd => 'Добавить профиль Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Название профиля'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Название профиля не может быть пустым'; + + @override + String get settings_cyr2latProfileAdded => 'Профиль добавлен'; + + @override + String get settings_cyr2latProfileUpdated => 'Профиль успешно обновлен'; + + @override + String get settings_cyr2latProfileEdit => 'Редактировать профиль Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Удалить профиль Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Профиль успешно удален'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Вы действительно хотите удалить профиль \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Публичный канал добавлен'; @@ -1568,10 +1623,10 @@ class AppLocalizationsRu extends AppLocalizations { } @override - String get chat_markAsUnread => 'Mark as Unread'; + String get chat_markAsUnread => 'Пометить как непрочитанные'; @override - String get chat_newMessages => 'New messages'; + String get chat_newMessages => 'Новые сообщения'; @override String get chat_openLink => 'Открыть ссылку?'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index f7e1a6a8..6ea853af 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -1165,11 +1165,67 @@ class AppLocalizationsSk extends AppLocalizations { @override String get channels_smazCompression => 'Odstránenie kompresie SMAZ'; + @override + String get channels_cyr2latCompression => 'Odstránenie kompresie Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Pri odosielaní nahradí niektoré znaky cyriliky latinskými znakmi.'; + + @override + String get channels_cyr2latSettingsHeading => 'Nastavenia Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Zoznam nahradení'; + + @override + String get channels_cyr2latSettingsDscr => + 'Upravte konfiguráciu JSON pre nahradenie znakov'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON mapa nahradení'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Nesprávny JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Kanál \"$name\" bol aktualizovaný'; } + @override + String get settings_cyr2latProfileAdd => 'Pridať profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Názov profilu'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Názov profilu nesmie byť prázdny'; + + @override + String get settings_cyr2latProfileAdded => 'Profil bol úspešne pridaný'; + + @override + String get settings_cyr2latProfileUpdated => + 'Profil bol úspešne aktualizovaný'; + + @override + String get settings_cyr2latProfileEdit => 'Upraviť profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Odstrániť profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Profil bol úspešne odstránený'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Naozaj chcete odstrániť profil \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Veľký kanál pridaný'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 68a16402..7519c5d2 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -1163,11 +1163,67 @@ class AppLocalizationsSl extends AppLocalizations { @override String get channels_smazCompression => 'Kompresija SMAZ'; + @override + String get channels_cyr2latCompression => 'Kompresija Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Pri pošiljanju nekatere cirilice nadomesti z latiničnimi.'; + + @override + String get channels_cyr2latSettingsHeading => 'Nastavitve Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Seznam zamenjav'; + + @override + String get channels_cyr2latSettingsDscr => + 'Uredi JSON-konfiguracijo zamenjav znakov'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON-tabela zamenjav'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Nepravilen JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Kanal $name je bil posodobljen'; } + @override + String get settings_cyr2latProfileAdd => 'Dodaj profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Ime profila'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Ime profila ne sme biti prazno'; + + @override + String get settings_cyr2latProfileAdded => 'Profil je bil uspešno dodan'; + + @override + String get settings_cyr2latProfileUpdated => + 'Profil je bil uspešno posodobljen'; + + @override + String get settings_cyr2latProfileEdit => 'Uredi profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Izbriši profil Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Profil je bil uspešno izbrisan'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Ali res želite izbrisati profil \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'javna skupnost dodana'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 2160b3a8..87f0f736 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -1156,11 +1156,66 @@ class AppLocalizationsSv extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ-komprimering'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat-komprimering'; + + @override + String get channels_cyr2latCompressionDscr => + 'Ersätter vissa kyrilliska tecken med latinska tecken när du skickar.'; + + @override + String get channels_cyr2latSettingsHeading => 'Inställningar för Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Ersättningslista'; + + @override + String get channels_cyr2latSettingsDscr => + 'Redigera JSON-konfigurationen för teckenersättning'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON-ersättningskarta'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Felaktig JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Kanalen \"$name\" har uppdaterats'; } + @override + String get settings_cyr2latProfileAdd => 'Lägg till Cyr2Lat-profil'; + + @override + String get settings_cyr2latProfileName => 'Profilnamn'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Profilnamnet får inte vara tomt'; + + @override + String get settings_cyr2latProfileAdded => 'Profilen har lagts till'; + + @override + String get settings_cyr2latProfileUpdated => 'Profilen har uppdaterats'; + + @override + String get settings_cyr2latProfileEdit => 'Redigera Cyr2Lat-profil'; + + @override + String get settings_cyr2latProfileDelete => 'Ta bort Cyr2Lat-profil'; + + @override + String get settings_cyr2latProfileDeleted => 'Profilen har tagits bort'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Är du säker på att du vill ta bort profilen \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Allmänt kanal tillagd'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 24936665..950430bc 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -1171,11 +1171,66 @@ class AppLocalizationsUk extends AppLocalizations { @override String get channels_smazCompression => 'Стиснення SMAZ'; + @override + String get channels_cyr2latCompression => 'Стиснення Cyr2Lat'; + + @override + String get channels_cyr2latCompressionDscr => + 'Замінює деякі кириличні символи на латиницю при відправці.'; + + @override + String get channels_cyr2latSettingsHeading => 'Налаштування Cyr2Lat'; + + @override + String get channels_cyr2latSettingsSubheading => 'Список замін'; + + @override + String get channels_cyr2latSettingsDscr => + 'Редагувати JSON-конфігурацію заміни символів'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON-карта замін'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'Некоректний JSON: $error'; + } + @override String channels_channelUpdated(String name) { return 'Канал «$name» оновлено'; } + @override + String get settings_cyr2latProfileAdd => 'Додати профіль Cyr2Lat'; + + @override + String get settings_cyr2latProfileName => 'Назва профілю'; + + @override + String get settings_cyr2latProfileNameEmpty => + 'Назва профілю не може бути порожньою'; + + @override + String get settings_cyr2latProfileAdded => 'Профіль успішно додано'; + + @override + String get settings_cyr2latProfileUpdated => 'Профіль успішно оновлено'; + + @override + String get settings_cyr2latProfileEdit => 'Редагувати профіль Cyr2Lat'; + + @override + String get settings_cyr2latProfileDelete => 'Видалити профіль Cyr2Lat'; + + @override + String get settings_cyr2latProfileDeleted => 'Профіль успішно видалено'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return 'Ви впевнені, що хочете видалити профіль \"$name\"?'; + } + @override String get channels_publicChannelAdded => 'Публічний канал додано'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index b49ba5e9..a5cdb6bd 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -1104,11 +1104,63 @@ class AppLocalizationsZh extends AppLocalizations { @override String get channels_smazCompression => 'SMAZ 压缩'; + @override + String get channels_cyr2latCompression => 'Cyr2Lat 压缩'; + + @override + String get channels_cyr2latCompressionDscr => '发送时将一些西里尔字符替换为拉丁字符。'; + + @override + String get channels_cyr2latSettingsHeading => 'Cyr2Lat 設定'; + + @override + String get channels_cyr2latSettingsSubheading => '替換清單'; + + @override + String get channels_cyr2latSettingsDscr => '編輯 JSON 字元替換設定檔'; + + @override + String get channels_cyr2latSettingsDialogHint => 'JSON 替換映射表'; + + @override + String channels_cyr2latSettingsDialogWrongJSON(Object error) { + return 'JSON 格式錯誤:$error'; + } + @override String channels_channelUpdated(String name) { return '频道 \"$name\" 已更新'; } + @override + String get settings_cyr2latProfileAdd => '新增 Cyr2Lat 設定檔'; + + @override + String get settings_cyr2latProfileName => '設定檔名稱'; + + @override + String get settings_cyr2latProfileNameEmpty => '設定檔名稱不能為空'; + + @override + String get settings_cyr2latProfileAdded => '設定檔已成功新增'; + + @override + String get settings_cyr2latProfileUpdated => '設定檔已成功更新'; + + @override + String get settings_cyr2latProfileEdit => '編輯 Cyr2Lat 設定檔'; + + @override + String get settings_cyr2latProfileDelete => '刪除 Cyr2Lat 設定檔'; + + @override + String get settings_cyr2latProfileDeleted => '設定檔已成功刪除'; + + @override + String settings_cyr2latProfileDeleteDscr(String name) { + return '您確定要刪除設定檔 \"$name\" 嗎?'; + } + @override String get channels_publicChannelAdded => '已添加公共频道'; diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index e185bd88..ba9cc18b 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "SMAZ compressie", + "channels_cyr2latCompression": "Cyr2Lat compressie", + "channels_cyr2latCompressionDscr": "Vervangt sommige Cyrillische tekens door Latijnse tekens bij het verzenden.", + "channels_cyr2latSettingsHeading": "Instellingen Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Lijst met vervangingen", + "channels_cyr2latSettingsDscr": "Bewerk de JSON-configuratie voor tekenvervanging", + "channels_cyr2latSettingsDialogHint": "JSON-vervangingskaart", + "channels_cyr2latSettingsDialogWrongJSON": "Onjuiste JSON: {error}", + "settings_cyr2latProfileAdd": "Cyr2Lat-profiel toevoegen", + "settings_cyr2latProfileName": "Profielnaam", + "settings_cyr2latProfileNameEmpty": "Profielnaam mag niet leeg zijn", + "settings_cyr2latProfileAdded": "Profiel succesvol toegevoegd", + "settings_cyr2latProfileUpdated": "Profiel succesvol bijgewerkt", + "settings_cyr2latProfileEdit": "Cyr2Lat-profiel bewerken", + "settings_cyr2latProfileDelete": "Cyr2Lat-profiel verwijderen", + "settings_cyr2latProfileDeleted": "Profiel succesvol verwijderd", + "settings_cyr2latProfileDeleteDscr": "Weet u zeker dat u het profiel \"{name}\" wilt verwijderen?", "channels_channelUpdated": "Kanaal \"{name}\" is bijgewerkt", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index a3937a9b..9cb9344d 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -397,6 +397,22 @@ } }, "channels_smazCompression": "Kompresja SMAZ", + "channels_cyr2latCompression": "Kompresja Cyr2Lat", + "channels_cyr2latCompressionDscr": "Zastępuje niektóre znaki cyrylicy alfabetem łacińskim podczas wysyłania.", + "channels_cyr2latSettingsHeading": "Ustawienia Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Lista zamian", + "channels_cyr2latSettingsDscr": "Edytuj konfigurację JSON zamiany znaków", + "channels_cyr2latSettingsDialogHint": "Mapa zamian JSON", + "channels_cyr2latSettingsDialogWrongJSON": "Nieprawidłowy JSON: {error}", + "settings_cyr2latProfileAdd": "Dodaj profil Cyr2Lat", + "settings_cyr2latProfileName": "Nazwa profilu", + "settings_cyr2latProfileNameEmpty": "Nazwa profilu nie może być pusta", + "settings_cyr2latProfileAdded": "Profil dodano pomyślnie", + "settings_cyr2latProfileUpdated": "Profil został pomyślnie zaktualizowany", + "settings_cyr2latProfileEdit": "Edytuj profil Cyr2Lat", + "settings_cyr2latProfileDelete": "Usuń profil Cyr2Lat", + "settings_cyr2latProfileDeleted": "Profil został pomyślnie usunięty", + "settings_cyr2latProfileDeleteDscr": "Czy na pewno chcesz usunąć profil \"{name}\"?", "channels_channelUpdated": "Kanał \"{name}\" został zaktualizowany", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 2a096fb3..2a668dff 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "Compressão SMAZ", + "channels_cyr2latCompression": "Compressão Cyr2Lat", + "channels_cyr2latCompressionDscr": "Substitui alguns caracteres cirílicos por caracteres latinos ao enviar.", + "channels_cyr2latSettingsHeading": "Configuração do Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Lista de substituições", + "channels_cyr2latSettingsDscr": "Editar a configuração JSON de substituição de caracteres", + "channels_cyr2latSettingsDialogHint": "Mapa de substituições JSON", + "channels_cyr2latSettingsDialogWrongJSON": "JSON incorreto: {error}", + "settings_cyr2latProfileAdd": "Adicionar perfil Cyr2Lat", + "settings_cyr2latProfileName": "Nome do perfil", + "settings_cyr2latProfileNameEmpty": "O nome do perfil não pode estar vazio", + "settings_cyr2latProfileAdded": "Perfil adicionado com sucesso", + "settings_cyr2latProfileUpdated": "Perfil atualizado com sucesso", + "settings_cyr2latProfileEdit": "Editar perfil Cyr2Lat", + "settings_cyr2latProfileDelete": "Eliminar perfil Cyr2Lat", + "settings_cyr2latProfileDeleted": "Perfil eliminado com sucesso", + "settings_cyr2latProfileDeleteDscr": "Tem a certeza de que deseja eliminar o perfil \"{name}\"?", "channels_channelUpdated": "Canal \"{name}\" atualizado", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index a556ba09..cdf92d03 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -251,6 +251,22 @@ "channels_channelAdded": "Канал \"{name}\" добавлен", "channels_editChannelTitle": "Изменить канал {index}", "channels_smazCompression": "Сжатие SMAZ", + "channels_cyr2latCompression": "Сжатие Cyr2Lat", + "channels_cyr2latCompressionDscr": "Заменяет некоторые кириллические символы на латиницу при отправке.", + "channels_cyr2latSettingsHeading": "Настройка Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Список замен", + "channels_cyr2latSettingsDscr": "Редактировать JSON-конфигурацию замены символов", + "channels_cyr2latSettingsDialogHint": "JSON-карта замен", + "channels_cyr2latSettingsDialogWrongJSON": "Некорректный JSON: {error}", + "settings_cyr2latProfileAdd": "Добавить профиль Cyr2Lat", + "settings_cyr2latProfileName": "Название профиля", + "settings_cyr2latProfileNameEmpty": "Название профиля не может быть пустым", + "settings_cyr2latProfileAdded": "Профиль добавлен", + "settings_cyr2latProfileUpdated": "Профиль успешно обновлен", + "settings_cyr2latProfileEdit": "Редактировать профиль Cyr2Lat", + "settings_cyr2latProfileDelete": "Удалить профиль Cyr2Lat", + "settings_cyr2latProfileDeleted": "Профиль успешно удален", + "settings_cyr2latProfileDeleteDscr": "Вы действительно хотите удалить профиль \"{name}\"?", "channels_channelUpdated": "Канал \"{name}\" обновлён", "channels_publicChannelAdded": "Публичный канал добавлен", "channels_sortBy": "Сортировка", @@ -357,6 +373,8 @@ "chat_direct": "Прямой", "chat_poiShared": "Точка интереса отправлена", "chat_unread": "Непрочитанных: {count}", + "chat_markAsUnread": "Пометить как непрочитанные", + "chat_newMessages": "Новые сообщения", "map_title": "Карта нод", "map_noNodesWithLocation": "Нет нод с данными о местоположении", "map_nodesNeedGps": "Ноды должны передавать свои GPS-координаты, чтобы отображаться на карте", diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb index aebeb217..7c0b8d8a 100644 --- a/lib/l10n/app_sk.arb +++ b/lib/l10n/app_sk.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "Odstránenie kompresie SMAZ", + "channels_cyr2latCompression": "Odstránenie kompresie Cyr2Lat", + "channels_cyr2latCompressionDscr": "Pri odosielaní nahradí niektoré znaky cyriliky latinskými znakmi.", + "channels_cyr2latSettingsHeading": "Nastavenia Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Zoznam nahradení", + "channels_cyr2latSettingsDscr": "Upravte konfiguráciu JSON pre nahradenie znakov", + "channels_cyr2latSettingsDialogHint": "JSON mapa nahradení", + "channels_cyr2latSettingsDialogWrongJSON": "Nesprávny JSON: {error}", + "settings_cyr2latProfileAdd": "Pridať profil Cyr2Lat", + "settings_cyr2latProfileName": "Názov profilu", + "settings_cyr2latProfileNameEmpty": "Názov profilu nesmie byť prázdny", + "settings_cyr2latProfileAdded": "Profil bol úspešne pridaný", + "settings_cyr2latProfileUpdated": "Profil bol úspešne aktualizovaný", + "settings_cyr2latProfileEdit": "Upraviť profil Cyr2Lat", + "settings_cyr2latProfileDelete": "Odstrániť profil Cyr2Lat", + "settings_cyr2latProfileDeleted": "Profil bol úspešne odstránený", + "settings_cyr2latProfileDeleteDscr": "Naozaj chcete odstrániť profil \"{name}\"?", "channels_channelUpdated": "Kanál \"{name}\" bol aktualizovaný", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb index aa07eead..e074506d 100644 --- a/lib/l10n/app_sl.arb +++ b/lib/l10n/app_sl.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "Kompresija SMAZ", + "channels_cyr2latCompression": "Kompresija Cyr2Lat", + "channels_cyr2latCompressionDscr": "Pri pošiljanju nekatere cirilice nadomesti z latiničnimi.", + "channels_cyr2latSettingsHeading": "Nastavitve Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Seznam zamenjav", + "channels_cyr2latSettingsDscr": "Uredi JSON-konfiguracijo zamenjav znakov", + "channels_cyr2latSettingsDialogHint": "JSON-tabela zamenjav", + "channels_cyr2latSettingsDialogWrongJSON": "Nepravilen JSON: {error}", + "settings_cyr2latProfileAdd": "Dodaj profil Cyr2Lat", + "settings_cyr2latProfileName": "Ime profila", + "settings_cyr2latProfileNameEmpty": "Ime profila ne sme biti prazno", + "settings_cyr2latProfileAdded": "Profil je bil uspešno dodan", + "settings_cyr2latProfileUpdated": "Profil je bil uspešno posodobljen", + "settings_cyr2latProfileEdit": "Uredi profil Cyr2Lat", + "settings_cyr2latProfileDelete": "Izbriši profil Cyr2Lat", + "settings_cyr2latProfileDeleted": "Profil je bil uspešno izbrisan", + "settings_cyr2latProfileDeleteDscr": "Ali res želite izbrisati profil \"{name}\"?", "channels_channelUpdated": "Kanal {name} je bil posodobljen", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 599f7d67..f5a85b21 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -387,6 +387,22 @@ } }, "channels_smazCompression": "SMAZ-komprimering", + "channels_cyr2latCompression": "Cyr2Lat-komprimering", + "channels_cyr2latCompressionDscr": "Ersätter vissa kyrilliska tecken med latinska tecken när du skickar.", + "channels_cyr2latSettingsHeading": "Inställningar för Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Ersättningslista", + "channels_cyr2latSettingsDscr": "Redigera JSON-konfigurationen för teckenersättning", + "channels_cyr2latSettingsDialogHint": "JSON-ersättningskarta", + "channels_cyr2latSettingsDialogWrongJSON": "Felaktig JSON: {error}", + "settings_cyr2latProfileAdd": "Lägg till Cyr2Lat-profil", + "settings_cyr2latProfileName": "Profilnamn", + "settings_cyr2latProfileNameEmpty": "Profilnamnet får inte vara tomt", + "settings_cyr2latProfileAdded": "Profilen har lagts till", + "settings_cyr2latProfileUpdated": "Profilen har uppdaterats", + "settings_cyr2latProfileEdit": "Redigera Cyr2Lat-profil", + "settings_cyr2latProfileDelete": "Ta bort Cyr2Lat-profil", + "settings_cyr2latProfileDeleted": "Profilen har tagits bort", + "settings_cyr2latProfileDeleteDscr": "Är du säker på att du vill ta bort profilen \"{name}\"?", "channels_channelUpdated": "Kanalen \"{name}\" har uppdaterats", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index acd4df72..da5579e1 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -390,6 +390,22 @@ } }, "channels_smazCompression": "Стиснення SMAZ", + "channels_cyr2latCompression": "Стиснення Cyr2Lat", + "channels_cyr2latCompressionDscr": "Замінює деякі кириличні символи на латиницю при відправці.", + "channels_cyr2latSettingsHeading": "Налаштування Cyr2Lat", + "channels_cyr2latSettingsSubheading": "Список замін", + "channels_cyr2latSettingsDscr": "Редагувати JSON-конфігурацію заміни символів", + "channels_cyr2latSettingsDialogHint": "JSON-карта замін", + "channels_cyr2latSettingsDialogWrongJSON": "Некоректний JSON: {error}", + "settings_cyr2latProfileAdd": "Додати профіль Cyr2Lat", + "settings_cyr2latProfileName": "Назва профілю", + "settings_cyr2latProfileNameEmpty": "Назва профілю не може бути порожньою", + "settings_cyr2latProfileAdded": "Профіль успішно додано", + "settings_cyr2latProfileUpdated": "Профіль успішно оновлено", + "settings_cyr2latProfileEdit": "Редагувати профіль Cyr2Lat", + "settings_cyr2latProfileDelete": "Видалити профіль Cyr2Lat", + "settings_cyr2latProfileDeleted": "Профіль успішно видалено", + "settings_cyr2latProfileDeleteDscr": "Ви впевнені, що хочете видалити профіль \"{name}\"?", "channels_channelUpdated": "Канал «{name}» оновлено", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index f6c469f1..6b074c93 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -402,6 +402,22 @@ } }, "channels_smazCompression": "SMAZ 压缩", + "channels_cyr2latCompression": "Cyr2Lat 压缩", + "channels_cyr2latCompressionDscr": "发送时将一些西里尔字符替换为拉丁字符。", + "channels_cyr2latSettingsHeading": "Cyr2Lat 設定", + "channels_cyr2latSettingsSubheading": "替換清單", + "channels_cyr2latSettingsDscr": "編輯 JSON 字元替換設定檔", + "channels_cyr2latSettingsDialogHint": "JSON 替換映射表", + "channels_cyr2latSettingsDialogWrongJSON": "JSON 格式錯誤:{error}", + "settings_cyr2latProfileAdd": "新增 Cyr2Lat 設定檔", + "settings_cyr2latProfileName": "設定檔名稱", + "settings_cyr2latProfileNameEmpty": "設定檔名稱不能為空", + "settings_cyr2latProfileAdded": "設定檔已成功新增", + "settings_cyr2latProfileUpdated": "設定檔已成功更新", + "settings_cyr2latProfileEdit": "編輯 Cyr2Lat 設定檔", + "settings_cyr2latProfileDelete": "刪除 Cyr2Lat 設定檔", + "settings_cyr2latProfileDeleted": "設定檔已成功刪除", + "settings_cyr2latProfileDeleteDscr": "您確定要刪除設定檔 \"{name}\" 嗎?", "channels_channelUpdated": "频道 \"{name}\" 已更新", "@channels_channelUpdated": { "placeholders": { diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart index d3d3421d..4e95311f 100644 --- a/lib/models/app_settings.dart +++ b/lib/models/app_settings.dart @@ -13,6 +13,67 @@ extension UnitSystemValue on UnitSystem { } } +const Map defaultCyr2LatCharMap = { + 'А': 'A', + 'В': 'B', + 'Е': 'E', + 'Ё': 'E', + 'З': '3', + 'К': 'K', + 'М': 'M', + 'Н': 'H', + 'О': 'O', + 'Р': 'P', + 'С': 'C', + 'Т': 'T', + 'Х': 'X', + 'Ь': 'b', + 'а': 'a', + 'е': 'e', + 'ё': 'e', + 'о': 'o', + 'р': 'p', + 'с': 'c', + 'у': 'y', + 'х': 'x', +}; + +class Cyr2LatProfile { + final String id; + final String name; + final Map charMap; + + Cyr2LatProfile({required this.id, required this.name, required this.charMap}); + + Map toJson() { + return {'id': id, 'name': name, 'char_map': charMap}; + } + + factory Cyr2LatProfile.fromJson(Map json) { + return Cyr2LatProfile( + id: json['id'] as String, + name: json['name'] as String, + charMap: + (json['char_map'] as Map?)?.map( + (key, value) => MapEntry(key.toString(), value.toString()), + ) ?? + {}, + ); + } + + Cyr2LatProfile copyWith({ + String? id, + String? name, + Map? charMap, + }) { + return Cyr2LatProfile( + id: id ?? this.id, + name: name ?? this.name, + charMap: charMap ?? this.charMap, + ); + } +} + class AppSettings { static const Object _unset = Object(); @@ -57,6 +118,16 @@ class AppSettings { final String? translationModelSourceUrl; final String? translationSelectedModelId; final List translationDownloadedModels; + final List cyr2latProfiles; + final String selectedCyr2latProfileId; + + Map get cyr2latCharMap { + final profile = cyr2latProfiles.firstWhere( + (p) => p.id == selectedCyr2latProfileId, + orElse: () => cyr2latProfiles.first, + ); + return profile.charMap; + } AppSettings({ this.clearPathOnMaxRetry = false, @@ -100,10 +171,22 @@ class AppSettings { this.translationModelSourceUrl, this.translationSelectedModelId, List? translationDownloadedModels, + List? cyr2latProfiles, + String? selectedCyr2latProfileId, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, mutedChannels = mutedChannels ?? {}, - translationDownloadedModels = translationDownloadedModels ?? const []; + translationDownloadedModels = translationDownloadedModels ?? const [], + cyr2latProfiles = + cyr2latProfiles ?? + [ + Cyr2LatProfile( + id: 'default', + name: 'Default', + charMap: defaultCyr2LatCharMap, + ), + ], + selectedCyr2latProfileId = selectedCyr2latProfileId ?? 'default'; Map toJson() { return { @@ -150,6 +233,10 @@ class AppSettings { 'translation_downloaded_models': translationDownloadedModels .map((model) => model.toJson()) .toList(), + 'cyr2lat_profiles': cyr2latProfiles + .map((profile) => profile.toJson()) + .toList(), + 'selected_cyr2lat_profile_id': selectedCyr2latProfileId, }; } @@ -237,6 +324,38 @@ class AppSettings { ) .toList() ?? const [], + cyr2latProfiles: + (json['cyr2lat_profiles'] as List?) + ?.map( + (entry) => Cyr2LatProfile.fromJson( + Map.from(entry as Map), + ), + ) + .toList() ?? + // Backward compatibility: if old cyr2lat_char_map exists, create a profile from it + (json['cyr2lat_char_map'] != null + ? [ + Cyr2LatProfile( + id: 'migrated', + name: 'Migrated Profile', + charMap: + (json['cyr2lat_char_map'] as Map?)?.map( + (key, value) => + MapEntry(key.toString(), value.toString()), + ) ?? + defaultCyr2LatCharMap, + ), + ] + : [ + Cyr2LatProfile( + id: 'default', + name: 'Default', + charMap: defaultCyr2LatCharMap, + ), + ]), + selectedCyr2latProfileId: + json['selected_cyr2lat_profile_id'] as String? ?? + (json['cyr2lat_char_map'] != null ? 'migrated' : 'default'), ); } @@ -282,6 +401,8 @@ class AppSettings { Object? translationModelSourceUrl = _unset, Object? translationSelectedModelId = _unset, List? translationDownloadedModels, + List? cyr2latProfiles, + String? selectedCyr2latProfileId, }) { return AppSettings( clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry, @@ -345,6 +466,9 @@ class AppSettings { : translationSelectedModelId as String?, translationDownloadedModels: translationDownloadedModels ?? this.translationDownloadedModels, + cyr2latProfiles: cyr2latProfiles ?? this.cyr2latProfiles, + selectedCyr2latProfileId: + selectedCyr2latProfileId ?? this.selectedCyr2latProfileId, ); } } diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart index 5d81e1ed..94b3efe8 100644 --- a/lib/screens/app_settings_screen.dart +++ b/lib/screens/app_settings_screen.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -60,6 +61,8 @@ class AppSettingsScreen extends StatelessWidget { const SizedBox(height: 16), _buildMapSettingsCard(context, settingsService), const SizedBox(height: 16), + _buildCyr2LatCard(context, settingsService), + const SizedBox(height: 16), _buildDebugCard(context, settingsService), ], ); @@ -1260,6 +1263,298 @@ class AppSettingsScreen extends StatelessWidget { return '${sizeMb.toStringAsFixed(1)} MB • $source'; } + Widget _buildCyr2LatCard( + BuildContext context, + AppSettingsService settingsService, + ) { + final selectedProfile = settingsService.getSelectedCyr2LatProfile(); + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + context.l10n.channels_cyr2latSettingsHeading, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: DropdownButtonFormField( + initialValue: settingsService.settings.selectedCyr2latProfileId, + decoration: InputDecoration( + labelText: context.l10n.channels_cyr2latSettingsSubheading, + border: const OutlineInputBorder(), + ), + items: settingsService.settings.cyr2latProfiles.map((profile) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }).toList(), + onChanged: (value) { + if (value != null) { + settingsService.setSelectedCyr2LatProfile(value); + } + }, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () => + _showAddCyr2LatProfileDialog(context, settingsService), + icon: const Icon(Icons.add), + label: Text(context.l10n.common_add), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: () => _showEditCyr2LatProfileDialog( + context, + settingsService, + selectedProfile, + ), + icon: const Icon(Icons.edit), + label: Text(context.l10n.common_edit), + ), + ), + const SizedBox(width: 8), + Expanded( + child: OutlinedButton.icon( + onPressed: + settingsService.settings.cyr2latProfiles.length > 1 + ? () => _showDeleteCyr2LatProfileDialog( + context, + settingsService, + selectedProfile, + ) + : null, + icon: const Icon(Icons.delete), + label: Text(context.l10n.common_delete), + ), + ), + ], + ), + ), + ], + ), + ); + } + + void _showAddCyr2LatProfileDialog( + BuildContext context, + AppSettingsService settingsService, + ) { + final nameController = TextEditingController(); + final jsonController = TextEditingController( + text: const JsonEncoder.withIndent(' ').convert(defaultCyr2LatCharMap), + ); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.settings_cyr2latProfileAdd), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: InputDecoration( + labelText: context.l10n.settings_cyr2latProfileName, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextField( + controller: jsonController, + maxLines: 15, + decoration: InputDecoration( + labelText: context.l10n.channels_cyr2latSettingsDialogHint, + border: const OutlineInputBorder(), + hintText: context.l10n.channels_cyr2latSettingsDscr, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + if (nameController.text.isEmpty) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_cyr2latProfileNameEmpty), + ); + return; + } + try { + final json = + jsonDecode(jsonController.text) as Map; + final map = json.map( + (key, value) => MapEntry(key, value.toString()), + ); + final profile = Cyr2LatProfile( + id: DateTime.now().millisecondsSinceEpoch.toString(), + name: nameController.text, + charMap: map, + ); + await settingsService.addCyr2LatProfile(profile); + if (!context.mounted) return; + Navigator.pop(context); + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_cyr2latProfileAdded), + ); + } catch (e) { + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_cyr2latSettingsDialogWrongJSON( + e.toString(), + ), + ), + ); + } + }, + child: Text(context.l10n.common_save), + ), + ], + ), + ); + } + + void _showEditCyr2LatProfileDialog( + BuildContext context, + AppSettingsService settingsService, + Cyr2LatProfile profile, + ) { + final nameController = TextEditingController(text: profile.name); + final jsonController = TextEditingController( + text: const JsonEncoder.withIndent(' ').convert(profile.charMap), + ); + + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.settings_cyr2latProfileEdit), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: nameController, + decoration: InputDecoration( + labelText: context.l10n.settings_cyr2latProfileName, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextField( + controller: jsonController, + maxLines: 15, + decoration: InputDecoration( + labelText: context.l10n.channels_cyr2latSettingsDialogHint, + border: const OutlineInputBorder(), + hintText: context.l10n.channels_cyr2latSettingsDscr, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + if (nameController.text.isEmpty) { + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_cyr2latProfileNameEmpty), + ); + return; + } + try { + final json = + jsonDecode(jsonController.text) as Map; + final map = json.map( + (key, value) => MapEntry(key, value.toString()), + ); + final updatedProfile = profile.copyWith( + name: nameController.text, + charMap: map, + ); + await settingsService.updateCyr2LatProfile(updatedProfile); + if (!context.mounted) return; + Navigator.pop(context); + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_cyr2latProfileUpdated), + ); + } catch (e) { + showDismissibleSnackBar( + context, + content: Text( + context.l10n.channels_cyr2latSettingsDialogWrongJSON( + e.toString(), + ), + ), + ); + } + }, + child: Text(context.l10n.common_save), + ), + ], + ), + ); + } + + void _showDeleteCyr2LatProfileDialog( + BuildContext context, + AppSettingsService settingsService, + Cyr2LatProfile profile, + ) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.l10n.settings_cyr2latProfileDelete), + content: Text( + context.l10n.settings_cyr2latProfileDeleteDscr(profile.name), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.l10n.common_cancel), + ), + TextButton( + onPressed: () async { + await settingsService.removeCyr2LatProfile(profile.id); + if (!context.mounted) return; + Navigator.pop(context); + showDismissibleSnackBar( + context, + content: Text(context.l10n.settings_cyr2latProfileDeleted), + ); + }, + child: Text(context.l10n.common_delete), + ), + ], + ), + ); + } + Widget _buildDebugCard( BuildContext context, AppSettingsService settingsService, diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart index bfcc301b..fcd50861 100644 --- a/lib/screens/channel_chat_screen.dart +++ b/lib/screens/channel_chat_screen.dart @@ -11,6 +11,7 @@ import '../connector/meshcore_connector.dart'; import '../utils/platform_info.dart'; import '../helpers/chat_scroll_controller.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/cyr2lat.dart'; import '../helpers/gif_helper.dart'; import '../helpers/reaction_helper.dart'; import '../helpers/snack_bar_builder.dart'; @@ -1151,7 +1152,12 @@ class _ChannelChatScreenState extends State { hintText: context.l10n.chat_typeMessage, onSubmitted: (_) => _sendMessage(), encoder: - connector.isChannelSmazEnabled(widget.channel.index) + (connector.isChannelSmazEnabled( + widget.channel.index, + ) || + connector.isChannelCyr2LatEnabled( + widget.channel.index, + )) ? (text) => connector.prepareChannelOutboundText( widget.channel.index, text, @@ -1264,6 +1270,15 @@ class _ChannelChatScreenState extends State { return; } + // When messageText is transformed with cyr2lat, it (generally) hasn't visual differences, + // but we getting messages doubles in chat screen (source text and transformed). + // To prevent, we'll perform transform of source before pass to main sender logic. + // We can pass whole text, senderName will be kept intact + if (connector.isChannelCyr2LatEnabled(widget.channel.index)) { + messageText = Cyr2Lat.encode(messageText); + } + // end transform + _textController.clear(); _cancelReply(); _textFieldFocusNode.requestFocus(); diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart index 155939bd..4613a8ee 100644 --- a/lib/screens/channels_screen.dart +++ b/lib/screens/channels_screen.dart @@ -1397,9 +1397,17 @@ class _ChannelsScreenState extends State MeshCoreConnector connector, Channel channel, ) { + final appSettingsService = Provider.of( + context, + listen: false, + ); final nameController = TextEditingController(text: channel.name); final pskController = TextEditingController(text: channel.pskHex); bool smazEnabled = connector.isChannelSmazEnabled(channel.index); + bool cyr2latEnabled = connector.isChannelCyr2LatEnabled(channel.index); + String? selectedCyr2LatProfileId = connector.getChannelCyr2LatProfileId( + channel.index, + ); showDialog( context: context, @@ -1445,8 +1453,52 @@ class _ChannelsScreenState extends State contentPadding: EdgeInsets.zero, title: Text(dialogContext.l10n.channels_smazCompression), value: smazEnabled, - onChanged: (value) => setState(() => smazEnabled = value), + onChanged: (value) => setState(() { + smazEnabled = value; + if (smazEnabled) { + cyr2latEnabled = false; + } + }), ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(dialogContext.l10n.channels_cyr2latCompression), + subtitle: Text( + dialogContext.l10n.channels_cyr2latCompressionDscr, + ), + value: cyr2latEnabled, + onChanged: (value) => setState(() { + cyr2latEnabled = value; + if (cyr2latEnabled) { + smazEnabled = false; + } + }), + ), + if (cyr2latEnabled) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: DropdownButtonFormField( + initialValue: selectedCyr2LatProfileId, + decoration: InputDecoration( + labelText: dialogContext + .l10n + .channels_cyr2latSettingsSubheading, + border: const OutlineInputBorder(), + ), + items: appSettingsService.settings.cyr2latProfiles.map(( + profile, + ) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }).toList(), + onChanged: (value) => setState(() { + selectedCyr2LatProfileId = value; + }), + ), + ), + ], ], ), ), @@ -1478,6 +1530,14 @@ class _ChannelsScreenState extends State channel.index, smazEnabled, ); + await connector.setChannelCyr2LatEnabled( + channel.index, + cyr2latEnabled, + ); + await connector.setChannelCyr2LatProfileId( + channel.index, + selectedCyr2LatProfileId, + ); if (!context.mounted) return; showDismissibleSnackBar( context, diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart index 54e1ecbe..8d3bc66c 100644 --- a/lib/screens/chat_screen.dart +++ b/lib/screens/chat_screen.dart @@ -12,6 +12,7 @@ import '../utils/platform_info.dart'; import '../connector/meshcore_connector.dart'; import '../connector/meshcore_protocol.dart'; +import '../helpers/cyr2lat.dart'; import '../helpers/reaction_helper.dart'; import '../widgets/message_status_icon.dart'; import '../helpers/chat_scroll_controller.dart'; @@ -624,9 +625,12 @@ class _ChatScreenState extends State { hintText: context.l10n.chat_typeMessage, onSubmitted: (_) => _sendMessage(connector), encoder: - connector.isContactSmazEnabled( - widget.contact.publicKeyHex, - ) + (connector.isContactSmazEnabled( + widget.contact.publicKeyHex, + ) || + connector.isContactCyr2LatEnabled( + widget.contact.publicKeyHex, + )) ? (text) => connector.prepareContactOutboundText( widget.contact, text, @@ -745,6 +749,18 @@ class _ChatScreenState extends State { return; } + // This is only for cyr2lat compression - to see the message being sent in the same format as the other person will receive + try { + if (connector.isContactCyr2LatEnabled( + _resolveContact(connector).publicKeyHex, + )) { + outgoingText = Cyr2Lat.encode(outgoingText); + } + } catch (_) { + // TODO maybe log + } + // end transform + _textController.clear(); _textFieldFocusNode.requestFocus(); connector.sendMessage( @@ -1251,9 +1267,20 @@ class _ChatScreenState extends State { void _showContactSettings(BuildContext context) { final connector = Provider.of(context, listen: false); + final appSettingsService = Provider.of( + context, + listen: false, + ); connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex); + connector.ensureContactCyr2LatSettingLoaded(widget.contact.publicKeyHex); final contact = widget.contact; bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex); + bool cyr2latEnabled = connector.isContactCyr2LatEnabled( + contact.publicKeyHex, + ); + String? selectedCyr2LatProfileId = connector.getContactCyr2LatProfileId( + contact.publicKeyHex, + ); bool teleBaseEnabled = contact.teleBaseEnabled; bool teleLocEnabled = contact.teleLocEnabled; bool teleEnvEnabled = contact.teleEnvEnabled; @@ -1284,10 +1311,72 @@ class _ChatScreenState extends State { contact.publicKeyHex, value, ); - setDialogState(() => smazEnabled = value); + connector.setContactCyr2LatEnabled( + contact.publicKeyHex, + false, + ); + setDialogState(() { + smazEnabled = value; + if (smazEnabled) { + cyr2latEnabled = false; + } + }); }, ), const Divider(height: 8), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(context.l10n.channels_cyr2latCompression), + subtitle: Text(context.l10n.channels_cyr2latCompressionDscr), + value: cyr2latEnabled, + onChanged: (value) { + connector.setContactCyr2LatEnabled( + contact.publicKeyHex, + value, + ); + connector.setContactSmazEnabled( + contact.publicKeyHex, + false, + ); + setDialogState(() { + cyr2latEnabled = value; + if (cyr2latEnabled) { + smazEnabled = false; + } + }); + }, + ), + if (cyr2latEnabled) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(0, 8, 0, 8), + child: DropdownButtonFormField( + initialValue: selectedCyr2LatProfileId, + decoration: InputDecoration( + labelText: + context.l10n.channels_cyr2latSettingsSubheading, + border: const OutlineInputBorder(), + ), + items: appSettingsService.settings.cyr2latProfiles.map(( + profile, + ) { + return DropdownMenuItem( + value: profile.id, + child: Text(profile.name), + ); + }).toList(), + onChanged: (value) { + connector.setContactCyr2LatProfileId( + contact.publicKeyHex, + value, + ); + setDialogState(() { + selectedCyr2LatProfileId = value; + }); + }, + ), + ), + ], + const Divider(height: 8), SwitchListTile( contentPadding: EdgeInsets.zero, title: Text(context.l10n.contact_teleBase), diff --git a/lib/services/app_settings_service.dart b/lib/services/app_settings_service.dart index 283ce726..7b3d5848 100644 --- a/lib/services/app_settings_service.dart +++ b/lib/services/app_settings_service.dart @@ -4,6 +4,7 @@ import '../models/app_settings.dart'; import '../models/translation_support.dart'; import '../storage/prefs_manager.dart'; import '../utils/app_logger.dart'; +import '../helpers/cyr2lat.dart'; class AppSettingsService extends ChangeNotifier { static const String _settingsKey = 'app_settings'; @@ -32,16 +33,22 @@ class AppSettingsService extends ChangeNotifier { try { final json = jsonDecode(jsonStr) as Map; _settings = AppSettings.fromJson(json); + Cyr2Lat.setCharMap(_settings.cyr2latCharMap); notifyListeners(); } catch (e) { // If parsing fails, use defaults _settings = AppSettings(); + Cyr2Lat.setCharMap(_settings.cyr2latCharMap); } + } else { + _settings = AppSettings(); + Cyr2Lat.setCharMap(_settings.cyr2latCharMap); } } Future updateSettings(AppSettings newSettings) async { _settings = newSettings; + Cyr2Lat.setCharMap(_settings.cyr2latCharMap); notifyListeners(); final prefs = PrefsManager.instance; @@ -253,4 +260,56 @@ class AppSettingsService extends ChangeNotifier { _settings.copyWith(translationDownloadedModels: value), ); } + + Cyr2LatProfile getSelectedCyr2LatProfile() { + return _settings.cyr2latProfiles.firstWhere( + (p) => p.id == _settings.selectedCyr2latProfileId, + orElse: () => _settings.cyr2latProfiles.first, + ); + } + + Cyr2LatProfile? getCyr2LatProfileById(String profileId) { + return _settings.cyr2latProfiles.cast().firstWhere( + (p) => p?.id == profileId, + orElse: () => null, + ); + } + + Future setSelectedCyr2LatProfile(String profileId) async { + await updateSettings( + _settings.copyWith(selectedCyr2latProfileId: profileId), + ); + } + + Future addCyr2LatProfile(Cyr2LatProfile profile) async { + final updated = List.from(_settings.cyr2latProfiles) + ..add(profile); + await updateSettings(_settings.copyWith(cyr2latProfiles: updated)); + } + + Future updateCyr2LatProfile(Cyr2LatProfile updatedProfile) async { + final updated = _settings.cyr2latProfiles + .map((p) => p.id == updatedProfile.id ? updatedProfile : p) + .toList(); + await updateSettings(_settings.copyWith(cyr2latProfiles: updated)); + } + + Future removeCyr2LatProfile(String profileId) async { + if (_settings.cyr2latProfiles.length <= 1) { + return; // Don't remove the last profile + } + final updated = _settings.cyr2latProfiles + .where((p) => p.id != profileId) + .toList(); + var newSelectedId = _settings.selectedCyr2latProfileId; + if (newSelectedId == profileId) { + newSelectedId = updated.first.id; + } + await updateSettings( + _settings.copyWith( + cyr2latProfiles: updated, + selectedCyr2latProfileId: newSelectedId, + ), + ); + } } diff --git a/lib/storage/channel_settings_store.dart b/lib/storage/channel_settings_store.dart index 276826d7..e0b390f8 100644 --- a/lib/storage/channel_settings_store.dart +++ b/lib/storage/channel_settings_store.dart @@ -3,12 +3,14 @@ import 'prefs_manager.dart'; class ChannelSettingsStore { static const String _keyPrefix = 'channel_smaz_'; + static const String _cyr2latKeyPrefix = 'channel_cyr2lat_'; String publicKeyHex = ''; set setPublicKeyHex(String value) => publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; String get keyFor => '$_keyPrefix$publicKeyHex'; + String get keyForCyr2Lat => '$_cyr2latKeyPrefix$publicKeyHex'; Future loadSmazEnabled(int channelIndex) async { if (publicKeyHex.isEmpty) { @@ -20,7 +22,7 @@ class ChannelSettingsStore { final prefs = PrefsManager.instance; final key = '$keyFor$channelIndex'; final oldKey = '$_keyPrefix$channelIndex'; - bool? enabled = prefs.getBool(oldKey); + bool? enabled = prefs.getBool(key); if (enabled == null) { // Attempt migration from legacy unscoped key on first load enabled = prefs.getBool(oldKey); @@ -46,4 +48,56 @@ class ChannelSettingsStore { final key = '$keyFor$channelIndex'; await prefs.setBool(key, enabled); } + + Future loadCyr2LatEnabled(int channelIndex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load channel Cyr2Lat settings.', + ); + return false; + } + final prefs = PrefsManager.instance; + final key = '$keyForCyr2Lat$channelIndex'; + return prefs.getBool(key) ?? false; + } + + Future saveCyr2LatEnabled(int channelIndex, bool enabled) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save channel Cyr2Lat settings.', + ); + return; + } + final prefs = PrefsManager.instance; + final key = '$keyForCyr2Lat$channelIndex'; + await prefs.setBool(key, enabled); + } + + Future loadCyr2LatProfileId(int channelIndex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load channel settings.', + ); + return null; + } + final prefs = PrefsManager.instance; + final key = '${keyForCyr2Lat}profile_$channelIndex'; + return prefs.getString(key); + } + + Future saveCyr2LatProfileId(int channelIndex, String? profileId) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save channel settings.', + ); + return; + } + final prefs = PrefsManager.instance; + final key = '${keyForCyr2Lat}profile_$channelIndex'; + if (profileId == null) { + await prefs.remove(key); + } else { + await prefs.setString(key, profileId); + } + } } diff --git a/lib/storage/contact_settings_store.dart b/lib/storage/contact_settings_store.dart index 94c6430e..682f1af6 100644 --- a/lib/storage/contact_settings_store.dart +++ b/lib/storage/contact_settings_store.dart @@ -3,12 +3,14 @@ import 'prefs_manager.dart'; class ContactSettingsStore { static const String _keyPrefix = 'contact_smaz_'; + static const String _cyr2latKeyPrefix = 'contact_cyr2lat_'; String publicKeyHex = ''; set setPublicKeyHex(String value) => publicKeyHex = value.length > 10 ? value.substring(0, 10) : ''; String get keyFor => '$_keyPrefix$publicKeyHex'; + String get keyForCyr2Lat => '$_cyr2latKeyPrefix$publicKeyHex'; Future loadSmazEnabled(String contactKeyHex) async { if (publicKeyHex.isEmpty) { @@ -46,4 +48,59 @@ class ContactSettingsStore { final key = '$keyFor$contactKeyHex'; await prefs.setBool(key, enabled); } + + Future loadCyr2LatEnabled(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load contact Cyr2Lat settings.', + ); + return false; + } + final prefs = PrefsManager.instance; + final key = '$keyForCyr2Lat$contactKeyHex'; + return prefs.getBool(key) ?? false; + } + + Future saveCyr2LatEnabled(String contactKeyHex, bool enabled) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save contact Cyr2Lat settings.', + ); + return; + } + final prefs = PrefsManager.instance; + final key = '$keyForCyr2Lat$contactKeyHex'; + await prefs.setBool(key, enabled); + } + + Future loadCyr2LatProfileId(String contactKeyHex) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot load contact settings.', + ); + return null; + } + final prefs = PrefsManager.instance; + final key = '${keyForCyr2Lat}profile_$contactKeyHex'; + return prefs.getString(key); + } + + Future saveCyr2LatProfileId( + String contactKeyHex, + String? profileId, + ) async { + if (publicKeyHex.isEmpty) { + appLogger.warn( + 'Public key hex is not set. Cannot save contact settings.', + ); + return; + } + final prefs = PrefsManager.instance; + final key = '${keyForCyr2Lat}profile_$contactKeyHex'; + if (profileId == null) { + await prefs.remove(key); + } else { + await prefs.setString(key, profileId); + } + } }