Compare commits

...

57 Commits

Author SHA1 Message Date
Enot (ded) Skelly bdd7fc0cdd remove unused macos path_provider_foundation
added in #299 but appears not needed, flutter removes when building
2026-04-08 14:56:34 -07:00
Ded 5ea044af10 Merge pull request #358 from zjs81/followup-#275
small clean up from PR #275
2026-04-08 10:31:17 -07:00
Enot (ded) Skelly 9d20be1c06 small clean up from PR #275
just removes extraneous assignment to _lastNonRepeatSnapshot and moves
the Navigator pop to after all uses of the context in _RadioSettingsDialog
2026-04-08 10:23:57 -07:00
Ded 9436c2d45a Merge pull request #275 from just-stuff-tm/enhancement/preset-offgrid-repeat-toggle
Enhancement/preset offgrid repeat toggle
2026-04-08 10:10:08 -07:00
Ded 17e55e96bb Merge pull request #357 from zjs81/discovered-text
use l10n strings for discovered menu item
2026-04-08 10:03:10 -07:00
Enot (ded) Skelly e4cfbb57b4 use l10n strings for discovered menu item 2026-04-08 10:01:45 -07:00
Ded d9f9ff58b4 Merge pull request #299 from ericszimmermann/ez_location-snr
reimplement location aware snr-indikator after alpha7
2026-04-08 09:07:02 -07:00
Ded a059f1be45 Merge pull request #356 from zjs81/gen-plug-jni
add jni to generated plugins
2026-04-08 08:38:35 -07:00
Enot (ded) Skelly 9e46f8b44c add jni to generated plugins
linux and windows were missing jni which was being added on fresh builds from dev
2026-04-08 08:37:50 -07:00
Ded a934781009 Merge pull request #355 from zjs81/ignore-fvm
add fvm directory and rc file to gitignore
2026-04-08 08:36:31 -07:00
Enot (ded) Skelly 5fe6738f25 add fvm directory and rc file to gitignore 2026-04-08 08:35:20 -07:00
Ded c1bcf261d7 Merge pull request #353 from zjs81/more-tooltips
add tooltip to send message buttons
2026-04-08 08:32:17 -07:00
Enot (ded) Skelly b570539a2d add tooltip to send message buttons 2026-04-08 08:22:13 -07:00
Winston Lowe a14833494e Merge branch 'dev' of github.com:zjs81/meshcore-open into dev 2026-04-05 12:27:38 -07:00
n-kam 457b44de3a make unread badge max out at 9999+ not 99+ 2026-04-05 12:17:16 -07:00
Winston Lowe 36d4a10396 Update ML timeout handling and adjust distance threshold for path hops 2026-04-05 12:17:15 -07:00
Winston Lowe 77566b0fe1 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-04-05 12:17:15 -07:00
zjs81 10b63e0df2 Merge pull request #334 from zjs81/Local-LLM-Translator
Local llm translator
2026-04-02 22:57:17 -07:00
zjs81 ba6d751346 #256 finalize translation service 2026-04-02 22:52:52 -07:00
zjs81 96d222a580 fix: update translation model ID retrieval and improve file name extraction in translation service 2026-04-02 22:38:31 -07:00
zjs81 01ad8471cc fix: improve message sending logic and handle range download errors in translation service 2026-04-02 19:52:43 -07:00
zjs81 2b826757cb feat: add translation strings for message translation feature 2026-04-02 19:18:19 -07:00
zjs81 9bf649e2c6 feat: add message translation support
- Introduced translation functionality in chat screen, allowing users to translate messages before sending.
- Added MessageTranslationButton to the input bar for enabling/disabling translation.
- Implemented translation service to handle incoming and outgoing text translations using llama models.
- Enhanced message storage to include original and translated text, language codes, and translation status.
- Created UI components for displaying translated messages and managing translation options.
- Added translation model management, including downloading and storing models locally.
- Updated app settings to manage translation preferences and model selections.
2026-04-02 19:09:17 -07:00
zjs81 c7a2bf9a95 Merge pull request #316 from n-kam/unread-badge-max-value
Make unread badge max out at 9999+ instead of 99+
2026-04-01 22:47:31 -07:00
zjs81 82adbd761b Merge pull request #313 from thesebas/pl-lang
new labels fixed polish translations
2026-04-01 22:46:39 -07:00
zjs81 9a8bdf00dc Merge pull request #326 from spfmoby/better-french-translations
Better french translations
2026-04-01 22:45:12 -07:00
zjs81 8b30342113 Merge pull request #329 from dennis1248/main
Update Dutch translations
2026-04-01 16:51:11 -07:00
Winston Lowe 817c60a155 Update ML timeout handling and adjust distance threshold for path hops 2026-03-31 19:02:29 -07:00
Dennis ten Hoove f08e86cf97 Update Dutch translations 2026-03-31 20:09:26 +02:00
spfmoby a6bb9490a1 Better french translations 2026-03-30 09:17:28 +02:00
ericz d1e45fc2ba moved _getRepeaterPrefixMatchNearLocation since I don't need the function anywhere else anymore. 2026-03-28 17:08:59 +01:00
ericz 32fa96431e Merge branch 'main' into ez_location-snr 2026-03-28 17:01:38 +01:00
just-stuff-tm 1e9508d401 fix(settings): use integer Hz comparison, unify snapshot conversion, gate debug logging
- Replace floating-point epsilon frequency comparison with integer Hz
- Add frequencyHz getter and fromMeshCoreSnapshot/toMeshCoreSnapshot
  conversion methods on _RadioSettingsSnapshot
- Move _toUiCodingRate/_toDeviceCodingRate to documented top-level functions
- Gate _logRadioSettingsState behind kDebugMode
- Use integer Hz in == and hashCode for _RadioSettingsSnapshot

Addresses code review findings on preset/off-grid repeat toggle PR.
2026-03-27 11:49:59 -04:00
just-stuff-tm 36697c6e61 fix(settings): scope repeat preset memory to saved state 2026-03-27 11:49:59 -04:00
just-stuff-tm c9145c99d3 fix(settings): preserve preset across off-grid repeat 2026-03-27 11:49:59 -04:00
just-stuff-tm 6b6d9caeeb Reapply "Fixed Preset on offgrid repeat toggle enhancemet #183"
This reverts commit 758619bbaa6ce5895c7146bbfc3b89054e759527.
2026-03-27 11:49:59 -04:00
Winston Lowe d0e3767db6 Refactor contact handling and other improvments (#317)
* Refactor contact filtering and improve localization strings; enhance path trace handling

* Add localization for new CLI commands and update existing strings

* Enhance contact handling and UI updates across multiple screens
add unfiltered contact access and improve last seen resolution

* Add polling interval configuration and improve contact handling

* Reorder command constants for better organization and clarity

* Refactor contact handling by removing unnecessary mapping and improving clarity across multiple screens

* Moved RadioStatsIconButton in chat screen for improved UI consistency

* Added indicators to AppBar for channels

* Ignore contacts with self public key in contact handling

* Simplify path removal logic and clean up unused imports in path management dialog

* Enhance path hop resolution by adding distance checks to improve candidate selection accuracy

* Remove unnecessary reset of radio stats poll reference count in polling interval setter
2026-03-26 22:28:01 -07:00
n-kam f9cb0c80a5 make unread badge max out at 9999+ not 99+ 2026-03-27 01:39:52 +03:00
thesebas a26d14bd46 new labels fixed polish translations 2026-03-25 08:36:09 +01:00
zjs81 411cd3f8d2 Merge pull request #270 from just-stuff-tm/fix/linux-ble-pairing-flow
Fix/linux ble pairing flow
2026-03-24 17:48:07 -07:00
just_stuff_tm 38f4de80b6 Refactor Bluetooth pairing localization strings across multiple languages
- Reintroduced Bluetooth pairing PIN title, prompt, show, and hide strings in English, Spanish, French, Hungarian, Italian, Japanese, Korean, Dutch, Polish, Portuguese, Russian, Slovak, Slovenian, Swedish, Ukrainian, and Chinese.
- Updated localization files to ensure consistency and clarity in user prompts related to Bluetooth pairing.
2026-03-24 22:21:23 +00:00
just_stuff_tm 7de07c023f Merge branch 'main' into fix/linux-ble-pairing-flow 2026-03-24 02:24:28 -04:00
zjs81 c272c60f9a Formatted file 2026-03-23 22:37:05 -07:00
zjs81 eca78453d6 Remove debug print statements from MeshCoreConnector, MessageRetryService, and UsbSerialService and fix wrong retry being credited 2026-03-23 22:26:51 -07:00
zjs81 3754cf14ea Bump version to 7.0.0+9 in pubspec.yaml 2026-03-23 19:50:52 -07:00
zjs81 834850fb51 Add companion radio stats, adaptive backoff, path hash width, and UI improvements
- Companion radio stats: poll and display noise floor, RSSI, SNR, airtime
  with dedicated ValueNotifier and ref-counted polling
- Adaptive RF-aware TX backoff based on radio conditions instead of fixed 5s
- Variable-width path hash support (1-3 bytes per hop)
- Air activity dot indicator in app bar with tap to open stats screen
- Jump to oldest unread setting for chat screens
- 1s send cooldown on DM and channel messages
- Link style: theme-aware orange, added EmailLinkifier
- New languages: Hungarian, Japanese, Korean
- Remove dead DeviceScreen and BatteryIndicatorChip
- Remove wakelock_plus dependency
- TX power fields now read as signed int8
2026-03-23 19:26:05 -07:00
zjs81 e7e2bb91b8 Add radio statistics and localization updates
- Implemented radio statistics features in multiple screens including chat, channels, and settings.
- Added localization for new strings in Swedish, Ukrainian, and Chinese.
- Introduced a setting to jump to the oldest unread message in chat and channels.
- Enhanced path management and display for contacts and messages.
- Updated app settings to include new boolean for jumping to the oldest unread message.
- Improved battery indicator and radio stats display in the app bar.
- Removed unused wakelock_plus dependency and updated plugin registrations.
2026-03-23 19:24:27 -07:00
zjs81 4c492f69ef Merge pull request #218 from zjs81/dev-mapOverlap
Show overlaps in public keys of repeaters
2026-03-23 18:51:14 -07:00
zjs81 50f2a8b439 Merge pull request #311 from zjs81/dev
Merge pull request #310 from zjs81/main
2026-03-23 18:50:02 -07:00
zjs81 ebbc367fec Merge pull request #310 from zjs81/main
merge dev
2026-03-23 18:46:40 -07:00
just-stuff-tm 14f3429eb5 fix: correct casing of "WisCore-" in deviceNamePrefixes list 2026-03-21 21:07:56 -04:00
just-stuff-tm e49e80d330 style: format deviceNamePrefixes list for better readability 2026-03-21 20:59:54 -04:00
just-stuff-tm d07372c7e0 feat: add MeshCoreUuids class for UUID constants and device name prefixes 2026-03-21 20:59:54 -04:00
just-stuff-tm 990f2bd33d addressed copilot issues still need pr #301 for smoke tests to pass 2026-03-21 20:59:54 -04:00
just-stuff-tm 29660d520e feat: Linux BLE pairing support via bluetoothctl
Add Linux BLE pairing helper that drives bluetoothctl for pair/trust/PIN
entry, with Completer-based flow control, explicit retry loop, and named
timeout constants.

- LinuxBlePairingService: pair-and-trust with up to 2 retries
- LinuxBleErrorClassifier: map bluetoothctl stderr to user-facing errors
- Conditional import stub for web builds (dart.library.io gate)
- Scanner screen: PIN dialog integration for Linux pairing flow
- MeshCoreConnector: Linux pairing/recovery/reconnect wiring
- l10n: 4 new pairing keys across all 14 locales
- 12 unit tests (pairing service + error classifier)
2026-03-21 20:59:53 -04:00
ericszimmermann 0ef2194fb0 codex suggested fix: explicit check if contact location is not null
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-03-15 12:10:47 +01:00
ericz 3664ae34cd reimplement location aware snr-indikator after alpha7 2026-03-15 11:42:46 +01:00
94 changed files with 28240 additions and 805 deletions
+3
View File
@@ -33,6 +33,9 @@ migrate_working_dir/
pubspec.lock
/build/
/coverage/
# fvm project files
.fvm/
.fvmrc
# Symbolication related
app.*.symbols
File diff suppressed because it is too large Load Diff
+19 -1
View File
@@ -202,13 +202,15 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdSendAnonReq = 57;
const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdGetStats = 56;
const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
const int cmdSetPathHashMode = 61;
// Text message types
const int txtTypePlain = 0;
@@ -245,6 +247,11 @@ const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeCustomVars = 21;
const int respCodeAutoAddConfig = 25;
const int respCodeStats = 24;
const int statsTypeCore = 0;
const int statsTypeRadio = 1;
const int statsTypePackets = 2;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -554,6 +561,17 @@ Uint8List buildGetBattAndStorageFrame() {
return Uint8List.fromList([cmdGetBattAndStorage]);
}
/// Companion radio stats: [56][statsType] where statsType is statsTypeCore/Radio/Packets.
Uint8List buildGetStatsFrame(int statsType) {
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
}
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
Uint8List buildSetPathHashModeFrame(int mode) {
final m = mode.clamp(0, 2);
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
}
// Build CMD_SET_DEVICE_TIME frame
Uint8List buildSetDeviceTimeFrame(int timestamp) {
final writer = BufferWriter();
+12
View File
@@ -0,0 +1,12 @@
class MeshCoreUuids {
static const String service = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
static const String rxCharacteristic = "6e400002-b5a3-f393-e0a9-e50e24dcca9e";
static const String txCharacteristic = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
static const List<String> deviceNamePrefixes = [
"MeshCore-",
"Whisper-",
"WisCore-",
"HT-",
];
}
+10 -7
View File
@@ -5,6 +5,14 @@ import '../l10n/l10n.dart';
import '../utils/platform_info.dart';
class LinkHandler {
static TextStyle defaultLinkStyle(BuildContext context, TextStyle base) {
final brightness = Theme.of(context).brightness;
final orange = brightness == Brightness.dark
? const Color(0xFFFFB74D)
: const Color(0xFFE65100);
return base.copyWith(color: orange, decoration: TextDecoration.underline);
}
/// Returns a [SelectableLinkify] on desktop or a [Linkify] on mobile.
static Widget buildLinkifyText({
required BuildContext context,
@@ -12,14 +20,9 @@ class LinkHandler {
required TextStyle style,
TextStyle? linkStyle,
}) {
final effectiveLinkStyle =
linkStyle ??
style.copyWith(
color: Colors.green,
decoration: TextDecoration.underline,
);
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
const linkifiers = [UrlLinkifier()];
const linkifiers = [UrlLinkifier(), EmailLinkifier()];
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) {
+119 -1
View File
@@ -1943,5 +1943,123 @@
"settings_multiAck": "Мулти-потвърди: {value}",
"settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
"map_showOverlaps": "Покриване на ключа на повтаряча",
"map_runTraceWithReturnPath": "Върни се по същия път."
"map_runTraceWithReturnPath": "Върни се по същия път.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Моля, изчакайте малко, преди да изпратите отново.",
"appSettings_languageHu": "Унгарски",
"appSettings_jumpToOldestUnread": "Преминете към най-старата непочетена статия",
"appSettings_jumpToOldestUnreadSubtitle": "Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.",
"appSettings_languageJa": "Японски",
"appSettings_languageKo": "Корейски",
"radioStats_tooltip": "Статистика за радио и мрежа",
"radioStats_screenTitle": "Статистически данни за радиопредаванията",
"radioStats_notConnected": "Свържете се с устройство, за да видите статистически данни за радиопредаване.",
"radioStats_firmwareTooOld": "Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.",
"radioStats_waiting": "Изчакване на данни…",
"radioStats_noiseFloor": "Ниво на шума: {noiseDbm} dBm",
"radioStats_lastRssi": "Последен RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Последна стойност на SNR: {snr} dB",
"radioStats_txAir": "Време на въздух (общо): {seconds} секунди",
"radioStats_rxAir": "Общо време на използване на RX (в секунди): {seconds} с",
"radioStats_chartCaption": "Ниво на шума (dBm) за последните измервания.",
"radioStats_stripNoise": "Ниво на шума: {noiseDbm} dBm",
"radioStats_stripWaiting": "Извличане на данни за радиото…",
"radioStats_settingsTile": "Статистически данни за радиостанции",
"radioStats_settingsSubtitle": "Ниво на шума, RSSI, SNR и време на пренос",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableTitle": "Активирайте превода",
"translation_title": "Превод",
"translation_composerTitle": "Преведете преди да изпратите",
"translation_enableSubtitle": "Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.",
"translation_composerSubtitle": "Контролира началния статус на иконата за превод, създадена от композитора.",
"translation_targetLanguage": "Целеви език",
"translation_useAppLanguage": "Използвайте езика на приложението",
"translation_downloadedModelLabel": "Изтегнат модел",
"translation_presetModelLabel": "Предварително конфигуриран модел от Hugging Face",
"translation_manualUrlLabel": "URL на ръководството",
"translation_downloadModel": "Изтеглете модела",
"translation_downloading": "Изтегляне...",
"translation_working": "Работа...",
"translation_stop": "Спрете",
"translation_mergingChunks": "Съединяване на изтеглените части в един файл...",
"translation_downloadedModels": "Изтеглени модели",
"translation_deleteModel": "Изтриване на модела",
"translation_modelDownloaded": "Моделът за превод е изтеглен.",
"translation_downloadStopped": "Изтеглянето беше прекъснато.",
"translation_downloadFailed": "Не успях да изтегля: {error}",
"translation_enterUrlFirst": "Въведете първо URL адрес на модела.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Изпращайте съобщения на оригиналния въведен език.",
"translation_translateBeforeSending": "Преведете преди да изпратите",
"translation_messageTranslation": "Превод на съобщението",
"translation_composerEnabledHint": "Съобщенията ще бъдат преведени, преди да бъдат изпратени.",
"translation_translateTo": "Превеждане на {language}",
"translation_translationOptions": "Опции за превод",
"translation_systemLanguage": "Език на системата",
"scanner_linuxPairingPinTitle": "PIN за съвпадение чрез Bluetooth",
"scanner_linuxPairingPinPrompt": "Въведете PIN кода за {deviceName} (оставете празно, ако няма такъв).",
"scanner_linuxPairingHidePin": "Скриване на PIN кода",
"scanner_linuxPairingShowPin": "Покажи PIN",
"repeater_cliQuickClockSync": "Синхронизация на часовника",
"repeater_cliQuickDiscovery": "Открий Съседи"
}
+119 -1
View File
@@ -1971,5 +1971,123 @@
"settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
"settings_multiAck": "Mehrfach-Bestätigungen: {value}",
"map_showOverlaps": "Überlappungen der Repeater-Taste",
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren."
"map_runTraceWithReturnPath": "Auf dem gleichen Pfad zurückkehren.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Bitte warten Sie einen Moment, bevor Sie erneut senden.",
"appSettings_jumpToOldestUnread": "Zum ältesten, nicht gelesenen Eintrag springen",
"appSettings_languageHu": "Ungarisch",
"appSettings_jumpToOldestUnreadSubtitle": "Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.",
"appSettings_languageJa": "Japanisch",
"appSettings_languageKo": "Koreanisch",
"radioStats_tooltip": "Daten zu Radio- und Mesh-Netzwerken",
"radioStats_screenTitle": "Senderinformationen",
"radioStats_notConnected": "Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.",
"radioStats_firmwareTooOld": "Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.",
"radioStats_waiting": "Warte auf Daten…",
"radioStats_noiseFloor": "Rauschpegel: {noiseDbm} dBm",
"radioStats_lastRssi": "Letzter RSSI-Wert: {rssiDbm} dBm",
"radioStats_lastSnr": "Letzter SNR: {snr} dB",
"radioStats_txAir": "Gesamt-TX-Zeit: {seconds} s",
"radioStats_rxAir": "Gesamt-RX-Zeit: {seconds} s",
"radioStats_chartCaption": "Rauschpegel (dBm) basierend auf den letzten Messwerten.",
"radioStats_stripNoise": "Rauschpegel: {noiseDbm} dBm",
"radioStats_stripWaiting": "Abrufen von Radiostatus…",
"radioStats_settingsTile": "Senderinformationen",
"radioStats_settingsSubtitle": "Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_title": "Übersetzung",
"translation_composerTitle": "Übersetzen Sie vor dem Versenden",
"translation_enableSubtitle": "Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.",
"translation_enableTitle": "Aktivieren Sie die Übersetzung",
"translation_composerSubtitle": "Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.",
"translation_targetLanguage": "Zielsprache",
"translation_useAppLanguage": "Verwenden Sie die App-Sprache",
"translation_downloadedModelLabel": "Heruntergeladenes Modell",
"translation_presetModelLabel": "Vordefinierter Hugging Face-Modell",
"translation_manualUrlLabel": "URL für das manuelle Modell",
"translation_downloadModel": "Modell herunterladen",
"translation_downloading": "Herunterladen...",
"translation_working": "Arbeiten...",
"translation_stop": "Stopp",
"translation_mergingChunks": "Zusammenführen der heruntergeladenen Teile in die finale Datei...",
"translation_downloadedModels": "Heruntergeladene Modelle",
"translation_deleteModel": "Modell löschen",
"translation_modelDownloaded": "Übersetzungsmotor heruntergeladen.",
"translation_downloadStopped": "Herunterladen abgebrochen.",
"translation_downloadFailed": "Download fehlgeschlagen: {error}",
"translation_enterUrlFirst": "Geben Sie zunächst die URL eines Modells ein.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_messageTranslation": "Nachricht übersetzen",
"translation_composerEnabledHint": "Die Nachrichten werden vor dem Versenden übersetzt.",
"translation_translateBeforeSending": "Übersetzen Sie vor dem Versenden",
"translation_composerDisabledHint": "Nachrichten in der ursprünglichen, getippten Sprache senden.",
"translation_translateTo": "Übersetzen Sie auf {language}",
"translation_translationOptions": "Übersetzungsmöglichkeiten",
"translation_systemLanguage": "Sprache des Systems",
"scanner_linuxPairingShowPin": "PIN anzeigen",
"scanner_linuxPairingHidePin": "PIN ausblenden",
"scanner_linuxPairingPinTitle": "Bluetooth-Paarungs-PIN",
"scanner_linuxPairingPinPrompt": "Geben Sie die PIN für {deviceName} ein (leer lassen, falls keine).",
"repeater_cliQuickClockSync": "Uhr Synchronisieren",
"repeater_cliQuickDiscovery": "Entdecke Nachbarn"
}
+137 -4
View File
@@ -127,6 +127,7 @@
}
}
},
"scanner_stop": "Stop",
"scanner_scan": "Scan",
"scanner_bluetoothOff": "Bluetooth is off",
@@ -302,8 +303,12 @@
"path_routeWeight": "{weight}/{max}",
"@path_routeWeight": {
"placeholders": {
"weight": { "type": "String" },
"max": { "type": "String" }
"weight": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"appSettings_battery": "Battery",
@@ -602,6 +607,15 @@
"channels_enterHashtag": "Enter hashtag",
"channels_hashtagHint": "e.g. #team",
"chat_noMessages": "No messages yet",
"chat_sendMessage": "Send message",
"chat_sendMessageTo": "Send message to {name}",
"@chat_sendMessageTo": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"chat_sendMessageToStart": "Send a message to get started",
"chat_originalMessageNotFound": "Original message not found",
"chat_replyingTo": "Replying to {name}",
@@ -1332,6 +1346,8 @@
"repeater_cliQuickVersion": "Version",
"repeater_cliQuickAdvertise": "Advertise",
"repeater_cliQuickClock": "Clock",
"repeater_cliQuickClockSync": "Clock Sync",
"repeater_cliQuickDiscovery": "Discover Neighbors",
"repeater_cliHelpAdvert": "Sends an advertisement packet",
"repeater_cliHelpReboot": "Reboots the device. (note, you'll prob get 'Timeout' which is normal)",
"repeater_cliHelpClock": "Displays current time per device's clock.",
@@ -1977,5 +1993,122 @@
"discoveredContacts_copyContact": "Copy Contact to clipboard",
"discoveredContacts_deleteContact": "Delete Discovered Contact",
"discoveredContacts_deleteContactAll": "Delete All Discovered Contacts",
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?"
}
"discoveredContacts_deleteContactAllContent": "Are you sure you want to delete all discovered contacts?",
"chat_sendCooldown": "Please wait a moment before sending again.",
"appSettings_jumpToOldestUnread": "Jump to oldest unread",
"appSettings_jumpToOldestUnreadSubtitle": "When opening a chat with unread messages, scroll to the first unread instead of the latest.",
"appSettings_languageHu": "Hungarian",
"appSettings_languageJa": "Japanese",
"appSettings_languageKo": "Korean",
"radioStats_tooltip": "Radio & mesh stats",
"radioStats_screenTitle": "Radio stats",
"radioStats_notConnected": "Connect to a device to view radio statistics.",
"radioStats_firmwareTooOld": "Radio statistics require companion firmware v8 or newer.",
"radioStats_waiting": "Waiting for data…",
"radioStats_noiseFloor": "Noise floor: {noiseDbm} dBm",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"radioStats_lastRssi": "Last RSSI: {rssiDbm} dBm",
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"radioStats_lastSnr": "Last SNR: {snr} dB",
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"radioStats_txAir": "TX airtime (total): {seconds} s",
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"radioStats_rxAir": "RX airtime (total): {seconds} s",
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"radioStats_chartCaption": "Noise floor (dBm) over recent samples.",
"radioStats_stripNoise": "Noise floor: {noiseDbm} dBm",
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"radioStats_stripWaiting": "Fetching radio stats…",
"radioStats_settingsTile": "Radio stats",
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
"translation_title": "Translation",
"translation_enableTitle": "Enable translation",
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
"translation_composerTitle": "Translate before sending",
"translation_composerSubtitle": "Controls the default state of the composer translation icon.",
"translation_targetLanguage": "Target language",
"translation_useAppLanguage": "Use app language",
"translation_downloadedModelLabel": "Downloaded model",
"translation_presetModelLabel": "Preset Hugging Face model",
"translation_manualUrlLabel": "Manual model URL",
"translation_downloadModel": "Download model",
"translation_downloading": "Downloading...",
"translation_working": "Working...",
"translation_stop": "Stop",
"translation_mergingChunks": "Merging downloaded chunks into final file...",
"translation_downloadedModels": "Downloaded models",
"translation_deleteModel": "Delete model",
"translation_modelDownloaded": "Translation model downloaded.",
"translation_downloadStopped": "Download stopped.",
"translation_downloadFailed": "Download failed: {error}",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enterUrlFirst": "Enter a model URL first.",
"scanner_linuxPairingShowPin": "Show PIN",
"scanner_linuxPairingHidePin": "Hide PIN",
"scanner_linuxPairingPinTitle": "Bluetooth Pairing PIN",
"scanner_linuxPairingPinPrompt": "Enter PIN for {deviceName} (leave blank if none).",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"translation_messageTranslation": "Message translation",
"translation_translateBeforeSending": "Translate before sending",
"translation_composerEnabledHint": "Messages will be translated before send.",
"translation_composerDisabledHint": "Send messages in the original typed language.",
"translation_translateTo": "Translate to {language}",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translationOptions": "Translation options",
"translation_systemLanguage": "System language"
}
+119 -1
View File
@@ -1971,5 +1971,123 @@
"settings_telemetryModeUpdated": "Modo de telemetría actualizado",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Superposiciones de tecla repetidora",
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino."
"map_runTraceWithReturnPath": "Volver atrás por el mismo camino.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Salta a los mensajes más antiguos sin leer",
"chat_sendCooldown": "Por favor, espere un momento antes de reenviar.",
"appSettings_languageHu": "Húngaro",
"appSettings_jumpToOldestUnreadSubtitle": "Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.",
"appSettings_languageJa": "Japonés",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Estadísticas de radio y malla",
"radioStats_screenTitle": "Estadísticas de radio",
"radioStats_notConnected": "Conéctese a un dispositivo para visualizar estadísticas de radio.",
"radioStats_firmwareTooOld": "Las estadísticas de radio requieren un firmware compatible v8 o posterior.",
"radioStats_waiting": "Esperando datos…",
"radioStats_noiseFloor": "Nivel de ruido: {noiseDbm} dBm",
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Último SNR: {snr} dB",
"radioStats_txAir": "Tiempo de emisión en Texas (total): {seconds} s",
"radioStats_rxAir": "Tiempo de transmisión de RX (total): {seconds} s",
"radioStats_chartCaption": "Nivel de ruido (dBm) en muestras recientes.",
"radioStats_stripNoise": "Nivel de ruido: {noiseDbm} dBm",
"radioStats_stripWaiting": "Obteniendo estadísticas de la radio…",
"radioStats_settingsTile": "Estadísticas de radio",
"radioStats_settingsSubtitle": "Nivel de ruido, RSSI, SNR y tiempo de transmisión",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_title": "Traducción",
"translation_enableSubtitle": "Traducir los mensajes entrantes y permitir la traducción previa al envío.",
"translation_enableTitle": "Habilitar la traducción",
"translation_composerTitle": "Traducir antes de enviar",
"translation_composerSubtitle": "Controla el estado predeterminado del icono de traducción del compositor.",
"translation_targetLanguage": "Idioma de destino",
"translation_useAppLanguage": "Utilizar el idioma de la aplicación",
"translation_downloadedModelLabel": "Modelo descargado",
"translation_presetModelLabel": "Modelo predefinido de Hugging Face",
"translation_manualUrlLabel": "URL del modelo manual",
"translation_downloadModel": "Descargar el modelo",
"translation_downloading": "Descargando...",
"translation_working": "Trabajando...",
"translation_stop": "¡Detente!",
"translation_mergingChunks": "Combinando los fragmentos descargados en el archivo final...",
"translation_downloadedModels": "Modelos descargados",
"translation_deleteModel": "Eliminar modelo",
"translation_modelDownloaded": "Modelo de traducción descargado.",
"translation_downloadStopped": "La descarga se ha detenido.",
"translation_downloadFailed": "No se pudo descargar: {error}",
"translation_enterUrlFirst": "Primero, introduzca la URL del modelo.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinPrompt": "Introduzca el código PIN para {deviceName} (deje en blanco si no hay ninguno).",
"scanner_linuxPairingShowPin": "Mostrar código PIN",
"scanner_linuxPairingPinTitle": "PIN para emparejar dispositivos Bluetooth",
"scanner_linuxPairingHidePin": "Ocultar PIN",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Envía mensajes utilizando el lenguaje escrito original.",
"translation_composerEnabledHint": "Los mensajes serán traducidos antes de ser enviados.",
"translation_messageTranslation": "Traducción del mensaje",
"translation_translateBeforeSending": "Traducir antes de enviar",
"translation_translateTo": "Traducir a {language}",
"translation_translationOptions": "Opciones de traducción",
"translation_systemLanguage": "Idioma del sistema",
"repeater_cliQuickDiscovery": "Descubrir Vecinos",
"repeater_cliQuickClockSync": "Sincronización del reloj"
}
+142 -24
View File
@@ -143,8 +143,8 @@
"settings_frequencyHelper": "300,0 - 2 500,0",
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
"settings_bandwidth": "Bande passante",
"settings_spreadingFactor": "Facteur de répartition",
"settings_codingRate": "Taux de codage",
"settings_spreadingFactor": "Facteur de répartition (SF)",
"settings_codingRate": "Taux de codage (CR)",
"settings_txPower": "TX Puissance (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
@@ -567,7 +567,7 @@
"chat_clearPath": "Effacer le chemin",
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
"chat_floodModeSubtitle": "Désactive l'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d'application pour rebasculer en mode auto par la suite.",
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
"chat_fullPath": "Chemin complet",
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
@@ -643,7 +643,7 @@
},
"map_chat": "Chat",
"map_repeater": "Répéteur",
"map_room": "Salle",
"map_room": "Room Server",
"map_sensor": "Capteur",
"map_pinDm": "Clé (DM)",
"map_pinPrivate": "Verrouiller (Privé)",
@@ -682,7 +682,7 @@
"map_showSharedMarkers": "Afficher les marqueurs partagés",
"map_lastSeenTime": "Dernière fois vu",
"map_sharedPin": "Clé partagée",
"map_joinRoom": "Rejoindre la salle",
"map_joinRoom": "Rejoindre le room server",
"map_manageRepeater": "Gérer le répéteur",
"mapCache_title": "Cache de Carte Hors Ligne",
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
@@ -865,7 +865,7 @@
"path_labelHexPrefixes": "Préfixes hexadécimaux",
"path_helperMaxHops": "Max 64 sauts. Chaque préfixe fait 2 caractères hexadécimaux (1 octet)",
"path_selectFromContacts": "Sélectionner à partir des contacts :",
"path_noRepeatersFound": "Aucun répéteur ou serveur de salle n'a été trouvé.",
"path_noRepeatersFound": "Aucun répéteur ou room server n'a été trouvé.",
"path_customPathsRequire": "Les chemins personnalisés nécessitent des sauts intermédiaires qui peuvent transmettre des messages.",
"path_invalidHexPrefixes": "Préfixes hexadécimaux invalides : {prefixes}",
"@path_invalidHexPrefixes": {
@@ -996,15 +996,15 @@
"repeater_txPower": "TX Puissance",
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Bande passante",
"repeater_spreadingFactor": "Facteur de répartition",
"repeater_codingRate": "Taux de codage",
"repeater_spreadingFactor": "Facteur de répartition (SF)",
"repeater_codingRate": "Taux de codage (CR)",
"repeater_locationSettings": "Paramètres de localisation",
"repeater_latitude": "Latitude",
"repeater_latitudeHelper": "Degrés décimaux (par exemple, 37.7749)",
"repeater_longitude": "Longitude",
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
"repeater_features": "Fonctionnalités",
"repeater_packetForwarding": "Transfert de paquets",
"repeater_packetForwarding": "Mode répéteur",
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
"repeater_guestAccess": "Accès Invité",
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
@@ -1377,7 +1377,7 @@
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCode": "Scanner un QR code",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #equipe",
@@ -1466,8 +1466,8 @@
"community_join": "Rejoindre",
"community_joinTitle": "Rejoindre la communauté",
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
"community_scanQr": "Scanner la communauté QR",
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
"community_scanQr": "Scanner un QR code de communauté",
"community_scanInstructions": "Pointez l'appareil photo vers un QR code de communauté.",
"community_showQr": "Afficher le QR Code",
"community_publicChannel": "Communauté Publique",
"community_hashtagChannel": "Hashtag Communauté",
@@ -1478,13 +1478,13 @@
"community_qrTitle": "Partager Communauté",
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
"community_invalidQrCode": "Code QR de communauté non valide",
"community_invalidQrCode": "QR code de communauté non valide",
"community_alreadyMember": "Déjà membre",
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
"community_scanOrCreate": "Scanner un QR code ou créer une communauté pour commencer",
"community_manageCommunities": "Gérer les Communautés",
"community_delete": "Quitter la communauté",
"community_deleteConfirm": "Quitter \"{name}\" ?",
@@ -1534,10 +1534,10 @@
}
},
"community_regenerateSecret": "Régénérer le secret",
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.",
"community_regenerateSecretConfirm": "Régénérer la clé secrète pour \"{name}\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.",
"community_regenerate": "Régénérer",
"community_secretRegenerated": "Mot de passe secret régénéré pour \"{name}\"",
"community_scanToUpdateSecret": "Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"{name}\"",
"community_scanToUpdateSecret": "Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"{name}\"",
"community_updateSecret": "Mettre à jour le secret",
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
"@contacts_pathTraceTo": {
@@ -1554,11 +1554,11 @@
"contacts_pathTrace": "Traçage de chemin",
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
"contacts_repeaterPing": "Pinguer le répéteur",
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
"contacts_roomPathTrace": "Traçage du chemin vers le room server",
"contacts_chatTraceRoute": "Tracer le chemin",
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
"contacts_ping": "Ping",
"contacts_roomPing": "Pinguer le serveur de la salle",
"contacts_roomPing": "Pinguer le room server",
"contacts_invalidAdvertFormat": "Données de contact non valides",
"appSettings_languageUk": "Ukrainien",
"appSettings_languageRu": "Russe",
@@ -1583,12 +1583,12 @@
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
"notification_receivedNewMessage": "Nouveau message reçu",
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
"settings_gpxExportRepeaters": "Exporter les répéteurs / room servers au format GPX",
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
"settings_gpxExportNotAvailable": "Non pris en charge sur votre appareil/Système d'exploitation",
"settings_gpxExportError": "Une erreur s'est produite lors de l'exportation.",
"settings_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
"settings_gpxExportRepeatersRoom": "Emplacements des répéteurs et room servers",
"settings_gpxExportContacts": "Exporter les compagnons au format GPX",
"settings_gpxExportAll": "Exporter tous les contacts au format GPX",
"settings_gpxExportAllSubtitle": "Exporte tous les contacts avec une localisation vers un fichier GPX.",
@@ -1800,15 +1800,15 @@
"contacts_unread": "Non lu",
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
"contacts_searchRoomServers": "Rechercher {number}{str} room server...",
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
"settings_contactSettings": "Paramètres de contact",
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les room servers",
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les room servers découverts",
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
"contactsSettings_title": "Paramètres des contacts",
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
@@ -1943,5 +1943,123 @@
"settings_multiAck": "Multi-ACKs : {value}",
"settings_telemetryModeUpdated": "Le mode télémétrie a été mis à jour",
"map_showOverlaps": "Chevauchement de la touche répétitive",
"map_runTraceWithReturnPath": "Revenir sur le même chemin."
"map_runTraceWithReturnPath": "Revenir sur le même chemin.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Veuillez patienter un instant avant de réessayer.",
"appSettings_jumpToOldestUnread": "Accéder au message le plus ancien non lu",
"appSettings_languageHu": "Hongrois",
"appSettings_jumpToOldestUnreadSubtitle": "Lorsque vous ouvrez une conversation contenant des messages non lus, faites défiler la page jusqu'au premier message non lu, plutôt que jusqu'au dernier.",
"appSettings_languageJa": "Japonais",
"appSettings_languageKo": "Coréen",
"radioStats_tooltip": "Statistiques des radios et des réseaux sans fil",
"radioStats_screenTitle": "Statistiques de radio",
"radioStats_notConnected": "Connectez-vous à un appareil pour visualiser les statistiques de la radio.",
"radioStats_firmwareTooOld": "Les statistiques radio nécessitent un firmware compatible v8 ou une version ultérieure.",
"radioStats_waiting": "En attente des données…",
"radioStats_noiseFloor": "Niveau de bruit : {noiseDbm} dBm",
"radioStats_lastRssi": "Dernier RSSI : {rssiDbm} dBm",
"radioStats_lastSnr": "Dernier SNR : {snr} dB",
"radioStats_txAir": "Temps d'antenne à la télévision du Texas (total) : {seconds} s",
"radioStats_rxAir": "Temps d'utilisation de l'appareil RX (total) : {seconds} s",
"radioStats_chartCaption": "Niveau de bruit (dBm) sur les échantillons récents.",
"radioStats_stripNoise": "Niveau de bruit : {noiseDbm} dBm",
"radioStats_stripWaiting": "Récupération des statistiques de la radio…",
"radioStats_settingsTile": "Statistiques de radio",
"radioStats_settingsSubtitle": "Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d'antenne",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Traduire avant d'envoyer",
"translation_enableTitle": "Activer la traduction",
"translation_title": "Traduction",
"translation_enableSubtitle": "Traduire les messages entrants et permettre la traduction avant l'envoi.",
"translation_composerSubtitle": "Contrôle l'état par défaut de l'icône de traduction du composant.",
"translation_targetLanguage": "Langue cible",
"translation_useAppLanguage": "Utiliser la langue de l'application",
"translation_downloadedModelLabel": "Modèle téléchargé",
"translation_presetModelLabel": "Modèle Hugging Face préconfiguré",
"translation_manualUrlLabel": "URL du modèle manuel",
"translation_downloadModel": "Télécharger le modèle",
"translation_downloading": "Téléchargement...",
"translation_working": "Au travail...",
"translation_stop": "Arrêtez",
"translation_mergingChunks": "Fusion des fragments téléchargés dans le fichier final...",
"translation_downloadedModels": "Modèles téléchargés",
"translation_deleteModel": "Supprimer le modèle",
"translation_modelDownloaded": "Modèle de traduction téléchargé.",
"translation_downloadStopped": "Le téléchargement a été interrompu.",
"translation_downloadFailed": "Échec du téléchargement : {error}",
"translation_enterUrlFirst": "Entrez d'abord l'URL du modèle.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Les messages seront traduits avant d'être envoyés.",
"translation_translateBeforeSending": "Traduire avant d'envoyer",
"translation_composerDisabledHint": "Envoyez des messages dans la langue originale, telle que vous l'avez tapée.",
"translation_messageTranslation": "Traduction du message",
"translation_translateTo": "Traduire en {language}",
"translation_translationOptions": "Options de traduction",
"translation_systemLanguage": "Langue du système",
"scanner_linuxPairingPinTitle": "Code PIN pour la connexion Bluetooth",
"scanner_linuxPairingHidePin": "Masquer le code PIN",
"scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si nécessaire).",
"scanner_linuxPairingShowPin": "Afficher le code PIN",
"repeater_cliQuickClockSync": "Synchronisation de l'horloge",
"repeater_cliQuickDiscovery": "Découvrir les voisins"
}
+2103
View File
File diff suppressed because it is too large Load Diff
+119 -1
View File
@@ -1943,5 +1943,123 @@
"settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso"
"map_runTraceWithReturnPath": "Tornare indietro sullo stesso percorso",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnreadSubtitle": "Quando si apre una chat con messaggi non letti, scorrete verso l'alto fino al primo messaggio non letto, invece che al più recente.",
"chat_sendCooldown": "Si prega di attendere un momento prima di inviare nuovamente.",
"appSettings_jumpToOldestUnread": "Vai al messaggio più vecchio non letto",
"appSettings_languageHu": "Ungherese",
"appSettings_languageJa": "Giapponese",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Statistiche per radio e reti",
"radioStats_screenTitle": "Statistiche radio",
"radioStats_notConnected": "Connettiti a un dispositivo per visualizzare le statistiche radio.",
"radioStats_firmwareTooOld": "Le statistiche radio richiedono il firmware versione 8 o successiva.",
"radioStats_noiseFloor": "Livello di rumore: {noiseDbm} dBm",
"radioStats_waiting": "In attesa dei dati…",
"radioStats_lastRssi": "Ultimo valore RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Ultimo SNR: {snr} dB",
"radioStats_txAir": "Tempo di trasmissione in diretta (totale): {seconds} s",
"radioStats_rxAir": "Tempo di trasmissione RX (totale): {seconds} s",
"radioStats_chartCaption": "Livello di rumore (dBm) misurato su campioni recenti.",
"radioStats_stripNoise": "Livello di rumore: {noiseDbm} dBm",
"radioStats_stripWaiting": "Recupero delle statistiche radio…",
"radioStats_settingsTile": "Statistiche radio",
"radioStats_settingsSubtitle": "Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Tradurre prima di inviare",
"translation_enableSubtitle": "Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell'invio.",
"translation_enableTitle": "Abilitare la traduzione",
"translation_title": "Traduzione",
"translation_composerSubtitle": "Controlla lo stato predefinito dell'icona di traduzione del compositore.",
"translation_targetLanguage": "Lingua di destinazione",
"translation_useAppLanguage": "Utilizza la lingua dell'app",
"translation_downloadedModelLabel": "Modello scaricato",
"translation_presetModelLabel": "Modello predefinito di Hugging Face",
"translation_manualUrlLabel": "URL del modello manuale",
"translation_downloadModel": "Scarica il modello",
"translation_downloading": "Inizio download...",
"translation_working": "Lavoro...",
"translation_stop": "Smetta",
"translation_downloadedModels": "Modelli scaricati",
"translation_mergingChunks": "Unione dei frammenti scaricati in un unico file...",
"translation_deleteModel": "Elimina modello",
"translation_modelDownloaded": "Modello di traduzione scaricato.",
"translation_downloadStopped": "Il download è stato interrotto.",
"translation_downloadFailed": "Download fallito: {error}",
"translation_enterUrlFirst": "Inserite innanzitutto l'URL del modello.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_messageTranslation": "Traduzione del messaggio",
"translation_translateBeforeSending": "Tradurre prima di inviare",
"translation_composerDisabledHint": "Invia messaggi utilizzando la lingua originale, scritta.",
"translation_composerEnabledHint": "I messaggi verranno tradotti prima di essere inviati.",
"translation_translateTo": "Tradurre in {language}",
"translation_translationOptions": "Opzioni di traduzione",
"translation_systemLanguage": "Lingua del sistema",
"scanner_linuxPairingPinPrompt": "Inserire il codice PIN per {deviceName} (lasciare vuoto se non presente).",
"scanner_linuxPairingShowPin": "Mostra PIN",
"scanner_linuxPairingPinTitle": "PIN per l'accoppiamento Bluetooth",
"scanner_linuxPairingHidePin": "Nascondi il PIN",
"repeater_cliQuickClockSync": "Sincronizzazione dell'orologio",
"repeater_cliQuickDiscovery": "Scopri i Vicini"
}
+2103
View File
File diff suppressed because it is too large Load Diff
+2103
View File
File diff suppressed because it is too large Load Diff
+357 -6
View File
@@ -10,7 +10,10 @@ import 'app_localizations_de.dart';
import 'app_localizations_en.dart';
import 'app_localizations_es.dart';
import 'app_localizations_fr.dart';
import 'app_localizations_hu.dart';
import 'app_localizations_it.dart';
import 'app_localizations_ja.dart';
import 'app_localizations_ko.dart';
import 'app_localizations_nl.dart';
import 'app_localizations_pl.dart';
import 'app_localizations_pt.dart';
@@ -112,7 +115,10 @@ abstract class AppLocalizations {
Locale('en'),
Locale('es'),
Locale('fr'),
Locale('hu'),
Locale('it'),
Locale('ja'),
Locale('ko'),
Locale('nl'),
Locale('pl'),
Locale('pt'),
@@ -2290,6 +2296,18 @@ abstract class AppLocalizations {
/// **'No messages yet'**
String get chat_noMessages;
/// No description provided for @chat_sendMessage.
///
/// In en, this message translates to:
/// **'Send message'**
String get chat_sendMessage;
/// No description provided for @chat_sendMessageTo.
///
/// In en, this message translates to:
/// **'Send a message to {contactName}'**
String chat_sendMessageTo(String contactName);
/// No description provided for @chat_sendMessageToStart.
///
/// In en, this message translates to:
@@ -2320,12 +2338,6 @@ abstract class AppLocalizations {
/// **'Location'**
String get chat_location;
/// No description provided for @chat_sendMessageTo.
///
/// In en, this message translates to:
/// **'Send a message to {contactName}'**
String chat_sendMessageTo(String contactName);
/// No description provided for @chat_typeMessage.
///
/// In en, this message translates to:
@@ -4316,6 +4328,18 @@ abstract class AppLocalizations {
/// **'Clock'**
String get repeater_cliQuickClock;
/// No description provided for @repeater_cliQuickClockSync.
///
/// In en, this message translates to:
/// **'Clock Sync'**
String get repeater_cliQuickClockSync;
/// No description provided for @repeater_cliQuickDiscovery.
///
/// In en, this message translates to:
/// **'Discover Neighbors'**
String get repeater_cliQuickDiscovery;
/// No description provided for @repeater_cliHelpAdvert.
///
/// In en, this message translates to:
@@ -6016,6 +6040,324 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'Are you sure you want to delete all discovered contacts?'**
String get discoveredContacts_deleteContactAllContent;
/// No description provided for @chat_sendCooldown.
///
/// In en, this message translates to:
/// **'Please wait a moment before sending again.'**
String get chat_sendCooldown;
/// No description provided for @appSettings_jumpToOldestUnread.
///
/// In en, this message translates to:
/// **'Jump to oldest unread'**
String get appSettings_jumpToOldestUnread;
/// No description provided for @appSettings_jumpToOldestUnreadSubtitle.
///
/// In en, this message translates to:
/// **'When opening a chat with unread messages, scroll to the first unread instead of the latest.'**
String get appSettings_jumpToOldestUnreadSubtitle;
/// No description provided for @appSettings_languageHu.
///
/// In en, this message translates to:
/// **'Hungarian'**
String get appSettings_languageHu;
/// No description provided for @appSettings_languageJa.
///
/// In en, this message translates to:
/// **'Japanese'**
String get appSettings_languageJa;
/// No description provided for @appSettings_languageKo.
///
/// In en, this message translates to:
/// **'Korean'**
String get appSettings_languageKo;
/// No description provided for @radioStats_tooltip.
///
/// In en, this message translates to:
/// **'Radio & mesh stats'**
String get radioStats_tooltip;
/// No description provided for @radioStats_screenTitle.
///
/// In en, this message translates to:
/// **'Radio stats'**
String get radioStats_screenTitle;
/// No description provided for @radioStats_notConnected.
///
/// In en, this message translates to:
/// **'Connect to a device to view radio statistics.'**
String get radioStats_notConnected;
/// No description provided for @radioStats_firmwareTooOld.
///
/// In en, this message translates to:
/// **'Radio statistics require companion firmware v8 or newer.'**
String get radioStats_firmwareTooOld;
/// No description provided for @radioStats_waiting.
///
/// In en, this message translates to:
/// **'Waiting for data…'**
String get radioStats_waiting;
/// No description provided for @radioStats_noiseFloor.
///
/// In en, this message translates to:
/// **'Noise floor: {noiseDbm} dBm'**
String radioStats_noiseFloor(int noiseDbm);
/// No description provided for @radioStats_lastRssi.
///
/// In en, this message translates to:
/// **'Last RSSI: {rssiDbm} dBm'**
String radioStats_lastRssi(int rssiDbm);
/// No description provided for @radioStats_lastSnr.
///
/// In en, this message translates to:
/// **'Last SNR: {snr} dB'**
String radioStats_lastSnr(String snr);
/// No description provided for @radioStats_txAir.
///
/// In en, this message translates to:
/// **'TX airtime (total): {seconds} s'**
String radioStats_txAir(int seconds);
/// No description provided for @radioStats_rxAir.
///
/// In en, this message translates to:
/// **'RX airtime (total): {seconds} s'**
String radioStats_rxAir(int seconds);
/// No description provided for @radioStats_chartCaption.
///
/// In en, this message translates to:
/// **'Noise floor (dBm) over recent samples.'**
String get radioStats_chartCaption;
/// No description provided for @radioStats_stripNoise.
///
/// In en, this message translates to:
/// **'Noise floor: {noiseDbm} dBm'**
String radioStats_stripNoise(int noiseDbm);
/// No description provided for @radioStats_stripWaiting.
///
/// In en, this message translates to:
/// **'Fetching radio stats…'**
String get radioStats_stripWaiting;
/// No description provided for @radioStats_settingsTile.
///
/// In en, this message translates to:
/// **'Radio stats'**
String get radioStats_settingsTile;
/// No description provided for @radioStats_settingsSubtitle.
///
/// In en, this message translates to:
/// **'Noise floor, RSSI, SNR, and airtime'**
String get radioStats_settingsSubtitle;
/// No description provided for @translation_title.
///
/// In en, this message translates to:
/// **'Translation'**
String get translation_title;
/// No description provided for @translation_enableTitle.
///
/// In en, this message translates to:
/// **'Enable translation'**
String get translation_enableTitle;
/// No description provided for @translation_enableSubtitle.
///
/// In en, this message translates to:
/// **'Translate incoming messages and allow pre-send translation.'**
String get translation_enableSubtitle;
/// No description provided for @translation_composerTitle.
///
/// In en, this message translates to:
/// **'Translate before sending'**
String get translation_composerTitle;
/// No description provided for @translation_composerSubtitle.
///
/// In en, this message translates to:
/// **'Controls the default state of the composer translation icon.'**
String get translation_composerSubtitle;
/// No description provided for @translation_targetLanguage.
///
/// In en, this message translates to:
/// **'Target language'**
String get translation_targetLanguage;
/// No description provided for @translation_useAppLanguage.
///
/// In en, this message translates to:
/// **'Use app language'**
String get translation_useAppLanguage;
/// No description provided for @translation_downloadedModelLabel.
///
/// In en, this message translates to:
/// **'Downloaded model'**
String get translation_downloadedModelLabel;
/// No description provided for @translation_presetModelLabel.
///
/// In en, this message translates to:
/// **'Preset Hugging Face model'**
String get translation_presetModelLabel;
/// No description provided for @translation_manualUrlLabel.
///
/// In en, this message translates to:
/// **'Manual model URL'**
String get translation_manualUrlLabel;
/// No description provided for @translation_downloadModel.
///
/// In en, this message translates to:
/// **'Download model'**
String get translation_downloadModel;
/// No description provided for @translation_downloading.
///
/// In en, this message translates to:
/// **'Downloading...'**
String get translation_downloading;
/// No description provided for @translation_working.
///
/// In en, this message translates to:
/// **'Working...'**
String get translation_working;
/// No description provided for @translation_stop.
///
/// In en, this message translates to:
/// **'Stop'**
String get translation_stop;
/// No description provided for @translation_mergingChunks.
///
/// In en, this message translates to:
/// **'Merging downloaded chunks into final file...'**
String get translation_mergingChunks;
/// No description provided for @translation_downloadedModels.
///
/// In en, this message translates to:
/// **'Downloaded models'**
String get translation_downloadedModels;
/// No description provided for @translation_deleteModel.
///
/// In en, this message translates to:
/// **'Delete model'**
String get translation_deleteModel;
/// No description provided for @translation_modelDownloaded.
///
/// In en, this message translates to:
/// **'Translation model downloaded.'**
String get translation_modelDownloaded;
/// No description provided for @translation_downloadStopped.
///
/// In en, this message translates to:
/// **'Download stopped.'**
String get translation_downloadStopped;
/// No description provided for @translation_downloadFailed.
///
/// In en, this message translates to:
/// **'Download failed: {error}'**
String translation_downloadFailed(String error);
/// No description provided for @translation_enterUrlFirst.
///
/// In en, this message translates to:
/// **'Enter a model URL first.'**
String get translation_enterUrlFirst;
/// No description provided for @scanner_linuxPairingShowPin.
///
/// In en, this message translates to:
/// **'Show PIN'**
String get scanner_linuxPairingShowPin;
/// No description provided for @scanner_linuxPairingHidePin.
///
/// In en, this message translates to:
/// **'Hide PIN'**
String get scanner_linuxPairingHidePin;
/// No description provided for @scanner_linuxPairingPinTitle.
///
/// In en, this message translates to:
/// **'Bluetooth Pairing PIN'**
String get scanner_linuxPairingPinTitle;
/// No description provided for @scanner_linuxPairingPinPrompt.
///
/// In en, this message translates to:
/// **'Enter PIN for {deviceName} (leave blank if none).'**
String scanner_linuxPairingPinPrompt(String deviceName);
/// No description provided for @translation_messageTranslation.
///
/// In en, this message translates to:
/// **'Message translation'**
String get translation_messageTranslation;
/// No description provided for @translation_translateBeforeSending.
///
/// In en, this message translates to:
/// **'Translate before sending'**
String get translation_translateBeforeSending;
/// No description provided for @translation_composerEnabledHint.
///
/// In en, this message translates to:
/// **'Messages will be translated before send.'**
String get translation_composerEnabledHint;
/// No description provided for @translation_composerDisabledHint.
///
/// In en, this message translates to:
/// **'Send messages in the original typed language.'**
String get translation_composerDisabledHint;
/// No description provided for @translation_translateTo.
///
/// In en, this message translates to:
/// **'Translate to {language}'**
String translation_translateTo(String language);
/// No description provided for @translation_translationOptions.
///
/// In en, this message translates to:
/// **'Translation options'**
String get translation_translationOptions;
/// No description provided for @translation_systemLanguage.
///
/// In en, this message translates to:
/// **'System language'**
String get translation_systemLanguage;
}
class _AppLocalizationsDelegate
@@ -6034,7 +6376,10 @@ class _AppLocalizationsDelegate
'en',
'es',
'fr',
'hu',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
@@ -6063,8 +6408,14 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
return AppLocalizationsEs();
case 'fr':
return AppLocalizationsFr();
case 'hu':
return AppLocalizationsHu();
case 'it':
return AppLocalizationsIt();
case 'ja':
return AppLocalizationsJa();
case 'ko':
return AppLocalizationsKo();
case 'nl':
return AppLocalizationsNl();
case 'pl':
+206 -5
View File
@@ -1239,6 +1239,14 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_noMessages => 'Няма съобщения.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Изпрати съобщение на $contactName';
}
@override
String get chat_sendMessageToStart => 'Изпрати съобщение, за да започнеш.';
@@ -1258,11 +1266,6 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get chat_location => 'Местоположение';
@override
String chat_sendMessageTo(String contactName) {
return 'Изпрати съобщение на $contactName';
}
@override
String get chat_typeMessage => 'Въведете съобщение...';
@@ -2429,6 +2432,12 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Часовник';
@override
String get repeater_cliQuickClockSync => 'Синхронизация на часовника';
@override
String get repeater_cliQuickDiscovery => 'Открий Съседи';
@override
String get repeater_cliHelpAdvert => 'Изпраща рекламен пакет';
@@ -3484,4 +3493,196 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
@override
String get chat_sendCooldown =>
'Моля, изчакайте малко, преди да изпратите отново.';
@override
String get appSettings_jumpToOldestUnread =>
'Преминете към най-старата непочетена статия';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Когато отворите чат с непрочетени съобщения, плъзнете надолу, за да видите първото непрочетено съобщение, вместо най-новото.';
@override
String get appSettings_languageHu => 'Унгарски';
@override
String get appSettings_languageJa => 'Японски';
@override
String get appSettings_languageKo => 'Корейски';
@override
String get radioStats_tooltip => 'Статистика за радио и мрежа';
@override
String get radioStats_screenTitle =>
'Статистически данни за радиопредаванията';
@override
String get radioStats_notConnected =>
'Свържете се с устройство, за да видите статистически данни за радиопредаване.';
@override
String get radioStats_firmwareTooOld =>
'Статистиката на радиостанцията изисква съвместимо софтуерно решение версия 8 или по-нова.';
@override
String get radioStats_waiting => 'Изчакване на данни…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Ниво на шума: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Последен RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Последна стойност на SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Време на въздух (общо): $seconds секунди';
}
@override
String radioStats_rxAir(int seconds) {
return 'Общо време на използване на RX (в секунди): $seconds с';
}
@override
String get radioStats_chartCaption =>
'Ниво на шума (dBm) за последните измервания.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Ниво на шума: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Извличане на данни за радиото…';
@override
String get radioStats_settingsTile => 'Статистически данни за радиостанции';
@override
String get radioStats_settingsSubtitle =>
'Ниво на шума, RSSI, SNR и време на пренос';
@override
String get translation_title => 'Превод';
@override
String get translation_enableTitle => 'Активирайте превода';
@override
String get translation_enableSubtitle =>
'Превеждайте входящите съобщения и позволявайте предварително превеждане преди изпращане.';
@override
String get translation_composerTitle => 'Преведете преди да изпратите';
@override
String get translation_composerSubtitle =>
'Контролира началния статус на иконата за превод, създадена от композитора.';
@override
String get translation_targetLanguage => 'Целеви език';
@override
String get translation_useAppLanguage => 'Използвайте езика на приложението';
@override
String get translation_downloadedModelLabel => 'Изтегнат модел';
@override
String get translation_presetModelLabel =>
'Предварително конфигуриран модел от Hugging Face';
@override
String get translation_manualUrlLabel => 'URL на ръководството';
@override
String get translation_downloadModel => 'Изтеглете модела';
@override
String get translation_downloading => 'Изтегляне...';
@override
String get translation_working => 'Работа...';
@override
String get translation_stop => 'Спрете';
@override
String get translation_mergingChunks =>
'Съединяване на изтеглените части в един файл...';
@override
String get translation_downloadedModels => 'Изтеглени модели';
@override
String get translation_deleteModel => 'Изтриване на модела';
@override
String get translation_modelDownloaded => 'Моделът за превод е изтеглен.';
@override
String get translation_downloadStopped => 'Изтеглянето беше прекъснато.';
@override
String translation_downloadFailed(String error) {
return 'Не успях да изтегля: $error';
}
@override
String get translation_enterUrlFirst => 'Въведете първо URL адрес на модела.';
@override
String get scanner_linuxPairingShowPin => 'Покажи PIN';
@override
String get scanner_linuxPairingHidePin => 'Скриване на PIN кода';
@override
String get scanner_linuxPairingPinTitle => 'PIN за съвпадение чрез Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Въведете PIN кода за $deviceName (оставете празно, ако няма такъв).';
}
@override
String get translation_messageTranslation => 'Превод на съобщението';
@override
String get translation_translateBeforeSending =>
'Преведете преди да изпратите';
@override
String get translation_composerEnabledHint =>
'Съобщенията ще бъдат преведени, преди да бъдат изпратени.';
@override
String get translation_composerDisabledHint =>
'Изпращайте съобщения на оригиналния въведен език.';
@override
String translation_translateTo(String language) {
return 'Превеждане на $language';
}
@override
String get translation_translationOptions => 'Опции за превод';
@override
String get translation_systemLanguage => 'Език на системата';
}
+207 -5
View File
@@ -1238,6 +1238,14 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_noMessages => 'Noch keine Nachrichten.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Sende eine Nachricht an $contactName';
}
@override
String get chat_sendMessageToStart => 'Eine Nachricht senden, um anzufangen.';
@@ -1257,11 +1265,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get chat_location => 'Ort';
@override
String chat_sendMessageTo(String contactName) {
return 'Sende eine Nachricht an $contactName';
}
@override
String get chat_typeMessage => 'Eine Nachricht eingeben...';
@@ -2429,6 +2432,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Uhr';
@override
String get repeater_cliQuickClockSync => 'Uhr Synchronisieren';
@override
String get repeater_cliQuickDiscovery => 'Entdecke Nachbarn';
@override
String get repeater_cliHelpAdvert => 'Sendet eine Ankündigung';
@@ -3494,4 +3503,197 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Sind Sie sicher, dass Sie alle gefundenen Kontakte löschen möchten?';
@override
String get chat_sendCooldown =>
'Bitte warten Sie einen Moment, bevor Sie erneut senden.';
@override
String get appSettings_jumpToOldestUnread =>
'Zum ältesten, nicht gelesenen Eintrag springen';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Wenn Sie ein Chatfenster öffnen, in dem Nachrichten vorhanden sind, die noch nicht gelesen wurden, scrollen Sie zu der ersten unlesenen Nachricht, anstatt zur neuesten.';
@override
String get appSettings_languageHu => 'Ungarisch';
@override
String get appSettings_languageJa => 'Japanisch';
@override
String get appSettings_languageKo => 'Koreanisch';
@override
String get radioStats_tooltip => 'Daten zu Radio- und Mesh-Netzwerken';
@override
String get radioStats_screenTitle => 'Senderinformationen';
@override
String get radioStats_notConnected =>
'Verbinden Sie ein Gerät, um Radiostatisiken anzuzeigen.';
@override
String get radioStats_firmwareTooOld =>
'Für die Verwendung der Funkstatistiken ist die Firmware-Version 8 oder höher erforderlich.';
@override
String get radioStats_waiting => 'Warte auf Daten…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Rauschpegel: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Letzter RSSI-Wert: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Letzter SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Gesamt-TX-Zeit: $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Gesamt-RX-Zeit: $seconds s';
}
@override
String get radioStats_chartCaption =>
'Rauschpegel (dBm) basierend auf den letzten Messwerten.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Rauschpegel: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Abrufen von Radiostatus…';
@override
String get radioStats_settingsTile => 'Senderinformationen';
@override
String get radioStats_settingsSubtitle =>
'Rauschpegel, RSSI, Signal-Rausch-Verhältnis (SNR) und Nutzzeit';
@override
String get translation_title => 'Übersetzung';
@override
String get translation_enableTitle => 'Aktivieren Sie die Übersetzung';
@override
String get translation_enableSubtitle =>
'Nachrichten empfangen und übersetzen sowie die Möglichkeit bieten, Nachrichten vor dem Versenden zu übersetzen.';
@override
String get translation_composerTitle => 'Übersetzen Sie vor dem Versenden';
@override
String get translation_composerSubtitle =>
'Steuert den Standardzustand des Icons für die Übersetzung des Komponisten.';
@override
String get translation_targetLanguage => 'Zielsprache';
@override
String get translation_useAppLanguage => 'Verwenden Sie die App-Sprache';
@override
String get translation_downloadedModelLabel => 'Heruntergeladenes Modell';
@override
String get translation_presetModelLabel =>
'Vordefinierter Hugging Face-Modell';
@override
String get translation_manualUrlLabel => 'URL für das manuelle Modell';
@override
String get translation_downloadModel => 'Modell herunterladen';
@override
String get translation_downloading => 'Herunterladen...';
@override
String get translation_working => 'Arbeiten...';
@override
String get translation_stop => 'Stopp';
@override
String get translation_mergingChunks =>
'Zusammenführen der heruntergeladenen Teile in die finale Datei...';
@override
String get translation_downloadedModels => 'Heruntergeladene Modelle';
@override
String get translation_deleteModel => 'Modell löschen';
@override
String get translation_modelDownloaded =>
'Übersetzungsmotor heruntergeladen.';
@override
String get translation_downloadStopped => 'Herunterladen abgebrochen.';
@override
String translation_downloadFailed(String error) {
return 'Download fehlgeschlagen: $error';
}
@override
String get translation_enterUrlFirst =>
'Geben Sie zunächst die URL eines Modells ein.';
@override
String get scanner_linuxPairingShowPin => 'PIN anzeigen';
@override
String get scanner_linuxPairingHidePin => 'PIN ausblenden';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth-Paarungs-PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Geben Sie die PIN für $deviceName ein (leer lassen, falls keine).';
}
@override
String get translation_messageTranslation => 'Nachricht übersetzen';
@override
String get translation_translateBeforeSending =>
'Übersetzen Sie vor dem Versenden';
@override
String get translation_composerEnabledHint =>
'Die Nachrichten werden vor dem Versenden übersetzt.';
@override
String get translation_composerDisabledHint =>
'Nachrichten in der ursprünglichen, getippten Sprache senden.';
@override
String translation_translateTo(String language) {
return 'Übersetzen Sie auf $language';
}
@override
String get translation_translationOptions => 'Übersetzungsmöglichkeiten';
@override
String get translation_systemLanguage => 'Sprache des Systems';
}
+201 -5
View File
@@ -1213,6 +1213,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get chat_noMessages => 'No messages yet';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Send a message to $contactName';
}
@override
String get chat_sendMessageToStart => 'Send a message to get started';
@@ -1232,11 +1240,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get chat_location => 'Location';
@override
String chat_sendMessageTo(String contactName) {
return 'Send a message to $contactName';
}
@override
String get chat_typeMessage => 'Type a message...';
@@ -2379,6 +2382,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Clock';
@override
String get repeater_cliQuickClockSync => 'Clock Sync';
@override
String get repeater_cliQuickDiscovery => 'Discover Neighbors';
@override
String get repeater_cliHelpAdvert => 'Sends an advertisement packet';
@@ -3421,4 +3430,191 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Are you sure you want to delete all discovered contacts?';
@override
String get chat_sendCooldown => 'Please wait a moment before sending again.';
@override
String get appSettings_jumpToOldestUnread => 'Jump to oldest unread';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'When opening a chat with unread messages, scroll to the first unread instead of the latest.';
@override
String get appSettings_languageHu => 'Hungarian';
@override
String get appSettings_languageJa => 'Japanese';
@override
String get appSettings_languageKo => 'Korean';
@override
String get radioStats_tooltip => 'Radio & mesh stats';
@override
String get radioStats_screenTitle => 'Radio stats';
@override
String get radioStats_notConnected =>
'Connect to a device to view radio statistics.';
@override
String get radioStats_firmwareTooOld =>
'Radio statistics require companion firmware v8 or newer.';
@override
String get radioStats_waiting => 'Waiting for data…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Noise floor: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Last RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Last SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX airtime (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX airtime (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Noise floor (dBm) over recent samples.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Noise floor: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Fetching radio stats…';
@override
String get radioStats_settingsTile => 'Radio stats';
@override
String get radioStats_settingsSubtitle =>
'Noise floor, RSSI, SNR, and airtime';
@override
String get translation_title => 'Translation';
@override
String get translation_enableTitle => 'Enable translation';
@override
String get translation_enableSubtitle =>
'Translate incoming messages and allow pre-send translation.';
@override
String get translation_composerTitle => 'Translate before sending';
@override
String get translation_composerSubtitle =>
'Controls the default state of the composer translation icon.';
@override
String get translation_targetLanguage => 'Target language';
@override
String get translation_useAppLanguage => 'Use app language';
@override
String get translation_downloadedModelLabel => 'Downloaded model';
@override
String get translation_presetModelLabel => 'Preset Hugging Face model';
@override
String get translation_manualUrlLabel => 'Manual model URL';
@override
String get translation_downloadModel => 'Download model';
@override
String get translation_downloading => 'Downloading...';
@override
String get translation_working => 'Working...';
@override
String get translation_stop => 'Stop';
@override
String get translation_mergingChunks =>
'Merging downloaded chunks into final file...';
@override
String get translation_downloadedModels => 'Downloaded models';
@override
String get translation_deleteModel => 'Delete model';
@override
String get translation_modelDownloaded => 'Translation model downloaded.';
@override
String get translation_downloadStopped => 'Download stopped.';
@override
String translation_downloadFailed(String error) {
return 'Download failed: $error';
}
@override
String get translation_enterUrlFirst => 'Enter a model URL first.';
@override
String get scanner_linuxPairingShowPin => 'Show PIN';
@override
String get scanner_linuxPairingHidePin => 'Hide PIN';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth Pairing PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Enter PIN for $deviceName (leave blank if none).';
}
@override
String get translation_messageTranslation => 'Message translation';
@override
String get translation_translateBeforeSending => 'Translate before sending';
@override
String get translation_composerEnabledHint =>
'Messages will be translated before send.';
@override
String get translation_composerDisabledHint =>
'Send messages in the original typed language.';
@override
String translation_translateTo(String language) {
return 'Translate to $language';
}
@override
String get translation_translationOptions => 'Translation options';
@override
String get translation_systemLanguage => 'System language';
}
+207 -5
View File
@@ -1238,6 +1238,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get chat_noMessages => 'Aún no hay mensajes';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Enviar un mensaje a $contactName';
}
@override
String get chat_sendMessageToStart => 'Enviar un mensaje para comenzar';
@@ -1257,11 +1265,6 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get chat_location => 'Ubicación';
@override
String chat_sendMessageTo(String contactName) {
return 'Enviar un mensaje a $contactName';
}
@override
String get chat_typeMessage => 'Escribe un mensaje...';
@@ -2423,6 +2426,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Reloj';
@override
String get repeater_cliQuickClockSync => 'Sincronización del reloj';
@override
String get repeater_cliQuickDiscovery => 'Descubrir Vecinos';
@override
String get repeater_cliHelpAdvert => 'Envía un paquete de publicidad';
@@ -3487,4 +3496,197 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
@override
String get chat_sendCooldown =>
'Por favor, espere un momento antes de reenviar.';
@override
String get appSettings_jumpToOldestUnread =>
'Salta a los mensajes más antiguos sin leer';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Cuando abras una conversación con mensajes sin leer, desplázate hacia el primer mensaje sin leer en lugar del más reciente.';
@override
String get appSettings_languageHu => 'Húngaro';
@override
String get appSettings_languageJa => 'Japonés';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Estadísticas de radio y malla';
@override
String get radioStats_screenTitle => 'Estadísticas de radio';
@override
String get radioStats_notConnected =>
'Conéctese a un dispositivo para visualizar estadísticas de radio.';
@override
String get radioStats_firmwareTooOld =>
'Las estadísticas de radio requieren un firmware compatible v8 o posterior.';
@override
String get radioStats_waiting => 'Esperando datos…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Nivel de ruido: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Último RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Último SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tiempo de emisión en Texas (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tiempo de transmisión de RX (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Nivel de ruido (dBm) en muestras recientes.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Nivel de ruido: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Obteniendo estadísticas de la radio…';
@override
String get radioStats_settingsTile => 'Estadísticas de radio';
@override
String get radioStats_settingsSubtitle =>
'Nivel de ruido, RSSI, SNR y tiempo de transmisión';
@override
String get translation_title => 'Traducción';
@override
String get translation_enableTitle => 'Habilitar la traducción';
@override
String get translation_enableSubtitle =>
'Traducir los mensajes entrantes y permitir la traducción previa al envío.';
@override
String get translation_composerTitle => 'Traducir antes de enviar';
@override
String get translation_composerSubtitle =>
'Controla el estado predeterminado del icono de traducción del compositor.';
@override
String get translation_targetLanguage => 'Idioma de destino';
@override
String get translation_useAppLanguage =>
'Utilizar el idioma de la aplicación';
@override
String get translation_downloadedModelLabel => 'Modelo descargado';
@override
String get translation_presetModelLabel =>
'Modelo predefinido de Hugging Face';
@override
String get translation_manualUrlLabel => 'URL del modelo manual';
@override
String get translation_downloadModel => 'Descargar el modelo';
@override
String get translation_downloading => 'Descargando...';
@override
String get translation_working => 'Trabajando...';
@override
String get translation_stop => '¡Detente!';
@override
String get translation_mergingChunks =>
'Combinando los fragmentos descargados en el archivo final...';
@override
String get translation_downloadedModels => 'Modelos descargados';
@override
String get translation_deleteModel => 'Eliminar modelo';
@override
String get translation_modelDownloaded => 'Modelo de traducción descargado.';
@override
String get translation_downloadStopped => 'La descarga se ha detenido.';
@override
String translation_downloadFailed(String error) {
return 'No se pudo descargar: $error';
}
@override
String get translation_enterUrlFirst =>
'Primero, introduzca la URL del modelo.';
@override
String get scanner_linuxPairingShowPin => 'Mostrar código PIN';
@override
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
@override
String get scanner_linuxPairingPinTitle =>
'PIN para emparejar dispositivos Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Introduzca el código PIN para $deviceName (deje en blanco si no hay ninguno).';
}
@override
String get translation_messageTranslation => 'Traducción del mensaje';
@override
String get translation_translateBeforeSending => 'Traducir antes de enviar';
@override
String get translation_composerEnabledHint =>
'Los mensajes serán traducidos antes de ser enviados.';
@override
String get translation_composerDisabledHint =>
'Envía mensajes utilizando el lenguaje escrito original.';
@override
String translation_translateTo(String language) {
return 'Traducir a $language';
}
@override
String get translation_translationOptions => 'Opciones de traducción';
@override
String get translation_systemLanguage => 'Idioma del sistema';
}
+231 -29
View File
@@ -559,10 +559,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_bandwidth => 'Bande passante';
@override
String get settings_spreadingFactor => 'Facteur de répartition';
String get settings_spreadingFactor => 'Facteur de répartition (SF)';
@override
String get settings_codingRate => 'Taux de codage';
String get settings_codingRate => 'Taux de codage (CR)';
@override
String get settings_txPower => 'TX Puissance (dBm)';
@@ -946,7 +946,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String contacts_searchRoomServers(int number, String str) {
return 'Rechercher $number$str serveurs de salle...';
return 'Rechercher $number$str room server...';
}
@override
@@ -1229,7 +1229,7 @@ class AppLocalizationsFr extends AppLocalizations {
'N\'importe qui peut rejoindre les canaux #hashtag.';
@override
String get channels_scanQrCode => 'Scanner un code QR';
String get channels_scanQrCode => 'Scanner un QR code';
@override
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
@@ -1243,6 +1243,14 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_noMessages => 'Aucun message pour le moment.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Envoyer un message à $contactName';
}
@override
String get chat_sendMessageToStart => 'Envoyer un message pour commencer';
@@ -1262,11 +1270,6 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_location => 'Emplacement';
@override
String chat_sendMessageTo(String contactName) {
return 'Envoyer un message à $contactName';
}
@override
String get chat_typeMessage => 'Saisir un message...';
@@ -1489,7 +1492,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_floodModeSubtitle =>
'Utiliser le commutateur de routage dans la barre d\'application';
'Désactive l\'apprentissage du chemin (à éviter). Utiliser le commutateur de routage dans la barre d\'application pour rebasculer en mode auto par la suite.';
@override
String get chat_floodModeEnabled =>
@@ -1614,7 +1617,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_repeater => 'Répéteur';
@override
String get map_room => 'Salle';
String get map_room => 'Room Server';
@override
String get map_sensor => 'Capteur';
@@ -1730,7 +1733,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get map_sharedPin => 'Clé partagée';
@override
String get map_joinRoom => 'Rejoindre la salle';
String get map_joinRoom => 'Rejoindre le room server';
@override
String get map_manageRepeater => 'Gérer le répéteur';
@@ -1999,7 +2002,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get path_noRepeatersFound =>
'Aucun répéteur ou serveur de salle n\'a été trouvé.';
'Aucun répéteur ou room server n\'a été trouvé.';
@override
String get path_customPathsRequire =>
@@ -2209,10 +2212,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_bandwidth => 'Bande passante';
@override
String get repeater_spreadingFactor => 'Facteur de répartition';
String get repeater_spreadingFactor => 'Facteur de répartition (SF)';
@override
String get repeater_codingRate => 'Taux de codage';
String get repeater_codingRate => 'Taux de codage (CR)';
@override
String get repeater_locationSettings => 'Paramètres de localisation';
@@ -2235,7 +2238,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_features => 'Fonctionnalités';
@override
String get repeater_packetForwarding => 'Transfert de paquets';
String get repeater_packetForwarding => 'Mode répéteur';
@override
String get repeater_packetForwardingSubtitle =>
@@ -2442,6 +2445,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Horloge';
@override
String get repeater_cliQuickClockSync => 'Synchronisation de l\'horloge';
@override
String get repeater_cliQuickDiscovery => 'Découvrir les voisins';
@override
String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce';
@@ -2896,11 +2905,11 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get community_scanQr => 'Scanner la communauté QR';
String get community_scanQr => 'Scanner un QR code de communauté';
@override
String get community_scanInstructions =>
'Pointez l\'appareil photo vers un code QR communautaire.';
'Pointez l\'appareil photo vers un QR code de communauté.';
@override
String get community_showQr => 'Afficher le QR Code';
@@ -2940,7 +2949,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
@override
String get community_invalidQrCode => 'Code QR de communauté non valide';
String get community_invalidQrCode => 'QR code de communauté non valide';
@override
String get community_alreadyMember => 'Déjà membre';
@@ -2964,7 +2973,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get community_scanOrCreate =>
'Scanner un code QR ou créer une communauté pour commencer';
'Scanner un QR code ou créer une communauté pour commencer';
@override
String get community_manageCommunities => 'Gérer les Communautés';
@@ -2992,7 +3001,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String community_regenerateSecretConfirm(String name) {
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau code QR pour continuer à communiquer.';
return 'Régénérer la clé secrète pour \"$name\" ? Tous les membres devront scanner le nouveau QR code pour continuer à communiquer.';
}
@override
@@ -3013,7 +3022,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String community_scanToUpdateSecret(String name) {
return 'Scanner le nouveau code QR pour mettre à jour le mot de passe pour \"$name\"';
return 'Scanner le nouveau QR code pour mettre à jour le mot de passe pour \"$name\"';
}
@override
@@ -3261,11 +3270,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get contacts_repeaterPing => 'Pinguer le répéteur';
@override
String get contacts_roomPathTrace =>
'Traçage du chemin vers le serveur de la salle';
String get contacts_roomPathTrace => 'Traçage du chemin vers le room server';
@override
String get contacts_roomPing => 'Pinguer le serveur de la salle';
String get contacts_roomPing => 'Pinguer le room server';
@override
String get contacts_chatTraceRoute => 'Tracer le chemin';
@@ -3371,7 +3379,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_gpxExportRepeaters =>
'Exporter les répéteurs / serveur de salle au format GPX';
'Exporter les répéteurs / room servers au format GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
@@ -3409,7 +3417,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_gpxExportRepeatersRoom =>
'Emplacements des serveurs de répéteur et de salle';
'Emplacements des répéteurs et room servers';
@override
String get settings_gpxExportChat => 'Emplacements des compagnons';
@@ -3460,11 +3468,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Ajouter automatiquement les serveurs de salle';
'Ajouter automatiquement les room servers';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts';
'Autoriser le compagnon à ajouter automatiquement les room servers découverts';
@override
String get contactsSettings_autoAddSensorsTitle =>
@@ -3511,4 +3519,198 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?';
@override
String get chat_sendCooldown =>
'Veuillez patienter un instant avant de réessayer.';
@override
String get appSettings_jumpToOldestUnread =>
'Accéder au message le plus ancien non lu';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Lorsque vous ouvrez une conversation contenant des messages non lus, faites défiler la page jusqu\'au premier message non lu, plutôt que jusqu\'au dernier.';
@override
String get appSettings_languageHu => 'Hongrois';
@override
String get appSettings_languageJa => 'Japonais';
@override
String get appSettings_languageKo => 'Coréen';
@override
String get radioStats_tooltip =>
'Statistiques des radios et des réseaux sans fil';
@override
String get radioStats_screenTitle => 'Statistiques de radio';
@override
String get radioStats_notConnected =>
'Connectez-vous à un appareil pour visualiser les statistiques de la radio.';
@override
String get radioStats_firmwareTooOld =>
'Les statistiques radio nécessitent un firmware compatible v8 ou une version ultérieure.';
@override
String get radioStats_waiting => 'En attente des données…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Niveau de bruit : $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Dernier RSSI : $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Dernier SNR : $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Temps d\'antenne à la télévision du Texas (total) : $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Temps d\'utilisation de l\'appareil RX (total) : $seconds s';
}
@override
String get radioStats_chartCaption =>
'Niveau de bruit (dBm) sur les échantillons récents.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Niveau de bruit : $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting =>
'Récupération des statistiques de la radio…';
@override
String get radioStats_settingsTile => 'Statistiques de radio';
@override
String get radioStats_settingsSubtitle =>
'Niveau de bruit, RSSI, rapport signal/bruit (SNR) et temps d\'antenne';
@override
String get translation_title => 'Traduction';
@override
String get translation_enableTitle => 'Activer la traduction';
@override
String get translation_enableSubtitle =>
'Traduire les messages entrants et permettre la traduction avant l\'envoi.';
@override
String get translation_composerTitle => 'Traduire avant d\'envoyer';
@override
String get translation_composerSubtitle =>
'Contrôle l\'état par défaut de l\'icône de traduction du composant.';
@override
String get translation_targetLanguage => 'Langue cible';
@override
String get translation_useAppLanguage =>
'Utiliser la langue de l\'application';
@override
String get translation_downloadedModelLabel => 'Modèle téléchargé';
@override
String get translation_presetModelLabel => 'Modèle Hugging Face préconfiguré';
@override
String get translation_manualUrlLabel => 'URL du modèle manuel';
@override
String get translation_downloadModel => 'Télécharger le modèle';
@override
String get translation_downloading => 'Téléchargement...';
@override
String get translation_working => 'Au travail...';
@override
String get translation_stop => 'Arrêtez';
@override
String get translation_mergingChunks =>
'Fusion des fragments téléchargés dans le fichier final...';
@override
String get translation_downloadedModels => 'Modèles téléchargés';
@override
String get translation_deleteModel => 'Supprimer le modèle';
@override
String get translation_modelDownloaded => 'Modèle de traduction téléchargé.';
@override
String get translation_downloadStopped =>
'Le téléchargement a été interrompu.';
@override
String translation_downloadFailed(String error) {
return 'Échec du téléchargement : $error';
}
@override
String get translation_enterUrlFirst => 'Entrez d\'abord l\'URL du modèle.';
@override
String get scanner_linuxPairingShowPin => 'Afficher le code PIN';
@override
String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
@override
String get scanner_linuxPairingPinTitle =>
'Code PIN pour la connexion Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Entrez le code PIN pour $deviceName (laissez vide si nécessaire).';
}
@override
String get translation_messageTranslation => 'Traduction du message';
@override
String get translation_translateBeforeSending => 'Traduire avant d\'envoyer';
@override
String get translation_composerEnabledHint =>
'Les messages seront traduits avant d\'être envoyés.';
@override
String get translation_composerDisabledHint =>
'Envoyez des messages dans la langue originale, telle que vous l\'avez tapée.';
@override
String translation_translateTo(String language) {
return 'Traduire en $language';
}
@override
String get translation_translationOptions => 'Options de traduction';
@override
String get translation_systemLanguage => 'Langue du système';
}
File diff suppressed because it is too large Load Diff
+206 -5
View File
@@ -1239,6 +1239,14 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get chat_noMessages => 'Nessun messaggio ancora';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Invia un messaggio a $contactName';
}
@override
String get chat_sendMessageToStart => 'Invia un messaggio per iniziare';
@@ -1258,11 +1266,6 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get chat_location => 'Posizione';
@override
String chat_sendMessageTo(String contactName) {
return 'Invia un messaggio a $contactName';
}
@override
String get chat_typeMessage => 'Digita un messaggio...';
@@ -2426,6 +2429,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Orologio';
@override
String get repeater_cliQuickClockSync => 'Sincronizzazione dell\'orologio';
@override
String get repeater_cliQuickDiscovery => 'Scopri i Vicini';
@override
String get repeater_cliHelpAdvert => 'Invia un pacchetto pubblicitario';
@@ -3491,4 +3500,196 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
@override
String get chat_sendCooldown =>
'Si prega di attendere un momento prima di inviare nuovamente.';
@override
String get appSettings_jumpToOldestUnread =>
'Vai al messaggio più vecchio non letto';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Quando si apre una chat con messaggi non letti, scorrete verso l\'alto fino al primo messaggio non letto, invece che al più recente.';
@override
String get appSettings_languageHu => 'Ungherese';
@override
String get appSettings_languageJa => 'Giapponese';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Statistiche per radio e reti';
@override
String get radioStats_screenTitle => 'Statistiche radio';
@override
String get radioStats_notConnected =>
'Connettiti a un dispositivo per visualizzare le statistiche radio.';
@override
String get radioStats_firmwareTooOld =>
'Le statistiche radio richiedono il firmware versione 8 o successiva.';
@override
String get radioStats_waiting => 'In attesa dei dati…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Livello di rumore: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Ultimo valore RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Ultimo SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tempo di trasmissione in diretta (totale): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tempo di trasmissione RX (totale): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Livello di rumore (dBm) misurato su campioni recenti.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Livello di rumore: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Recupero delle statistiche radio…';
@override
String get radioStats_settingsTile => 'Statistiche radio';
@override
String get radioStats_settingsSubtitle =>
'Livello di rumore, RSSI, rapporto segnale/rumore (SNR) e tempo di trasmissione';
@override
String get translation_title => 'Traduzione';
@override
String get translation_enableTitle => 'Abilitare la traduzione';
@override
String get translation_enableSubtitle =>
'Tradurre i messaggi in arrivo e consentire la traduzione preventiva prima dell\'invio.';
@override
String get translation_composerTitle => 'Tradurre prima di inviare';
@override
String get translation_composerSubtitle =>
'Controlla lo stato predefinito dell\'icona di traduzione del compositore.';
@override
String get translation_targetLanguage => 'Lingua di destinazione';
@override
String get translation_useAppLanguage => 'Utilizza la lingua dell\'app';
@override
String get translation_downloadedModelLabel => 'Modello scaricato';
@override
String get translation_presetModelLabel =>
'Modello predefinito di Hugging Face';
@override
String get translation_manualUrlLabel => 'URL del modello manuale';
@override
String get translation_downloadModel => 'Scarica il modello';
@override
String get translation_downloading => 'Inizio download...';
@override
String get translation_working => 'Lavoro...';
@override
String get translation_stop => 'Smetta';
@override
String get translation_mergingChunks =>
'Unione dei frammenti scaricati in un unico file...';
@override
String get translation_downloadedModels => 'Modelli scaricati';
@override
String get translation_deleteModel => 'Elimina modello';
@override
String get translation_modelDownloaded => 'Modello di traduzione scaricato.';
@override
String get translation_downloadStopped => 'Il download è stato interrotto.';
@override
String translation_downloadFailed(String error) {
return 'Download fallito: $error';
}
@override
String get translation_enterUrlFirst =>
'Inserite innanzitutto l\'URL del modello.';
@override
String get scanner_linuxPairingShowPin => 'Mostra PIN';
@override
String get scanner_linuxPairingHidePin => 'Nascondi il PIN';
@override
String get scanner_linuxPairingPinTitle =>
'PIN per l\'accoppiamento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Inserire il codice PIN per $deviceName (lasciare vuoto se non presente).';
}
@override
String get translation_messageTranslation => 'Traduzione del messaggio';
@override
String get translation_translateBeforeSending => 'Tradurre prima di inviare';
@override
String get translation_composerEnabledHint =>
'I messaggi verranno tradotti prima di essere inviati.';
@override
String get translation_composerDisabledHint =>
'Invia messaggi utilizzando la lingua originale, scritta.';
@override
String translation_translateTo(String language) {
return 'Tradurre in $language';
}
@override
String get translation_translationOptions => 'Opzioni di traduzione';
@override
String get translation_systemLanguage => 'Lingua del sistema';
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+251 -51
View File
@@ -313,7 +313,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_nodeSettings => 'Node Instellingen';
@override
String get settings_nodeName => 'Node Naam';
String get settings_nodeName => 'Nodenaam';
@override
String get settings_nodeNameNotSet => 'Niet ingesteld';
@@ -452,7 +452,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_advertisementSent => 'Advertentie verzonden';
@override
String get settings_syncTime => 'Synchronisatie Tijd';
String get settings_syncTime => 'Tijd Synchroniseren';
@override
String get settings_syncTimeSubtitle =>
@@ -472,7 +472,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_rebootDevice => 'Apparaat opnieuw opstarten';
@override
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore apparaat';
String get settings_rebootDeviceSubtitle => 'Herstart het MeshCore-apparaat';
@override
String get settings_rebootDeviceConfirm =>
@@ -556,7 +556,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_codingRate => 'Codeertarief';
@override
String get settings_txPower => 'TX Vermogen (dBm)';
String get settings_txPower => 'TX-Vermogen (dBm)';
@override
String get settings_txPowerHelper => '0 - 22';
@@ -565,11 +565,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
@override
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
String get settings_clientRepeat => 'Off-Grid Herhalen';
@override
String get settings_clientRepeatSubtitle =>
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
'Laat dit apparaat de berichten van andere apparaten doorsturen.';
@override
String get settings_clientRepeatFreqWarning =>
@@ -846,19 +846,19 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_allTime => 'Altijd';
@override
String get appSettings_lastHour => 'Laat uur';
String get appSettings_lastHour => 'Afgelopen uur';
@override
String get appSettings_last6Hours => 'laatste 6 uur';
String get appSettings_last6Hours => 'Afgelopen 6 uur';
@override
String get appSettings_last24Hours => 'De laatste 24 uur';
String get appSettings_last24Hours => 'Afgelopen 24 uur';
@override
String get appSettings_lastWeek => 'Laatste week';
String get appSettings_lastWeek => 'Afgelopen week';
@override
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
String get appSettings_offlineMapCache => 'Offline Kaartcache';
@override
String get appSettings_unitsTitle => 'Eenheden';
@@ -1185,32 +1185,32 @@ class AppLocalizationsNl extends AppLocalizations {
String get channels_sortUnread => 'Ongelezen';
@override
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
String get channels_createPrivateChannel => 'PrivéKanaal Aanmaken';
@override
String get channels_createPrivateChannelDesc =>
'Beveiligd met een geheime sleutel.';
@override
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
String get channels_joinPrivateChannel => 'PrivéKanaal Toetreden';
@override
String get channels_joinPrivateChannelDesc =>
'Handmatig een geheime sleutel invoeren.';
'Voer handmatig een geheime sleutel in.';
@override
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
String get channels_joinPublicChannel => 'Publiek Kanaal Toetreden';
@override
String get channels_joinPublicChannelDesc =>
'Iedereen kan dit kanaal aanmelden.';
'Iedereen kan toetreden tot dit kanaal.';
@override
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
String get channels_joinHashtagChannel => 'Hashtag-kanaal Aanmaken';
@override
String get channels_joinHashtagChannelDesc =>
'Iedereen kan lid worden van hashtag-kanalen.';
'Iedereen kan toetreden tot hashtag-kanalen.';
@override
String get channels_scanQrCode => 'Scan een QR-code';
@@ -1227,6 +1227,14 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_noMessages => 'Nog geen berichten.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Verstuur een bericht naar $contactName';
}
@override
String get chat_sendMessageToStart => 'Een bericht sturen om te beginnen';
@@ -1246,11 +1254,6 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_location => 'Locatie';
@override
String chat_sendMessageTo(String contactName) {
return 'Verstuur een bericht naar $contactName';
}
@override
String get chat_typeMessage => 'Type een bericht...';
@@ -1709,7 +1712,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_sharedPin => 'Gedeelde pin';
@override
String get map_joinRoom => 'Sluit Kamer';
String get map_joinRoom => 'Kamer Toetreden';
@override
String get map_manageRepeater => 'Beheer Repeater';
@@ -2001,7 +2004,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get room_management => 'Beheer Server Kamer';
@override
String get repeater_managementTools => 'Beheerinstrumenten';
String get repeater_managementTools => 'Beheerfuncties';
@override
String get repeater_status => 'Status';
@@ -2027,7 +2030,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_neighbors => 'Buren';
@override
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
String get repeater_neighborsSubtitle => 'Bekijk nul-hopsburen.';
@override
String get repeater_settings => 'Instellingen';
@@ -2093,10 +2096,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_noiseFloor => 'Ruisvloer';
@override
String get repeater_txAirtime => 'TX Airtime';
String get repeater_txAirtime => 'TX-zendtijd';
@override
String get repeater_rxAirtime => 'RX Airtime';
String get repeater_rxAirtime => 'RX-zendtijd';
@override
String get repeater_packetStatistics => 'Pakketstatistieken';
@@ -2141,7 +2144,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_settingsTitle => 'Repeater Instellingen';
String get repeater_settingsTitle => 'Repeaterinstellingen';
@override
String get repeater_basicSettings => 'Basisinstellingen';
@@ -2150,19 +2153,19 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_repeaterName => 'Repeaternaam';
@override
String get repeater_repeaterNameHelper => 'Weergave naam voor deze repeater';
String get repeater_repeaterNameHelper => 'Weergavenaam voor deze repeater';
@override
String get repeater_adminPassword => 'Admin wachtwoord';
@override
String get repeater_adminPasswordHelper => 'Volledige toegangspaswoord';
String get repeater_adminPasswordHelper => 'Wachtwoord administratortoegang';
@override
String get repeater_guestPassword => 'Wachtwoord Gast';
String get repeater_guestPassword => 'Gast wachtwoord';
@override
String get repeater_guestPasswordHelper => 'Leesbeheer wachtwoord';
String get repeater_guestPasswordHelper => 'Wachtwoord gasttoegen';
@override
String get repeater_radioSettings => 'Radio Instellingen';
@@ -2189,7 +2192,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_codingRate => 'Codeertarief';
@override
String get repeater_locationSettings => 'Locatie Instellingen';
String get repeater_locationSettings => 'Locatie-instellingen';
@override
String get repeater_latitude => 'Breedtegraad';
@@ -2221,14 +2224,14 @@ class AppLocalizationsNl extends AppLocalizations {
'Toegestane leesbeheer toegang voor gasten.';
@override
String get repeater_privacyMode => 'Privacy Modus';
String get repeater_privacyMode => 'Privacymodus';
@override
String get repeater_privacyModeSubtitle =>
'Naam/locatie verbergen in advertenties';
@override
String get repeater_advertisementSettings => 'Advertentie Instellingen';
String get repeater_advertisementSettings => 'Advertentie-instellingen';
@override
String get repeater_localAdvertInterval => 'Lokale Advertentie Interval';
@@ -2333,7 +2336,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_refreshGuestAccess => 'Toegang Gast Vernieuwen';
@override
String get repeater_refreshPrivacyMode => 'Privacy Mode vernieuwen';
String get repeater_refreshPrivacyMode => 'Privacymode vernieuwen';
@override
String get repeater_refreshAdvertisementSettings =>
@@ -2359,10 +2362,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_commandHelp => 'Help';
@override
String get repeater_clearHistory => 'Verwijder Geschiedenis';
String get repeater_clearHistory => 'Geschiedenis Verwijderen';
@override
String get repeater_noCommandsSent => 'Geen commando\'s verzonden nog.';
String get repeater_noCommandsSent => 'Nog geen commando\'s verzonden.';
@override
String get repeater_typeCommandOrUseQuick =>
@@ -2389,28 +2392,34 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_cliQuickGetName => 'Haal Naam op';
String get repeater_cliQuickGetName => 'Naam opvragen';
@override
String get repeater_cliQuickGetRadio => 'Radio ontvangen';
String get repeater_cliQuickGetRadio => 'Radio-instellingen opvragen';
@override
String get repeater_cliQuickGetTx => 'Krijg TX';
String get repeater_cliQuickGetTx => 'TX opvragen';
@override
String get repeater_cliQuickNeighbors => 'Buren';
String get repeater_cliQuickNeighbors => 'Buren opvragen';
@override
String get repeater_cliQuickVersion => 'Versie';
String get repeater_cliQuickVersion => 'Versie opvragen';
@override
String get repeater_cliQuickAdvertise => 'Advertenties';
String get repeater_cliQuickAdvertise => 'Advertenties opvragen';
@override
String get repeater_cliQuickClock => 'Tijd';
String get repeater_cliQuickClock => 'Tijd opvragen';
@override
String get repeater_cliHelpAdvert => 'Verstuurt een advertentiepakket';
String get repeater_cliQuickClockSync => 'Kloksynchronisatie';
@override
String get repeater_cliQuickDiscovery => 'Ontdek Buren';
@override
String get repeater_cliHelpAdvert => 'Advertentie uitzenden';
@override
String get repeater_cliHelpReboot =>
@@ -2682,7 +2691,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get telemetry_voltageLabel => 'Spanning';
@override
String get telemetry_mcuTemperatureLabel => 'MCU Temperatuur';
String get telemetry_mcuTemperatureLabel => 'MCU-temperatuur';
@override
String get telemetry_temperatureLabel => 'Temperatuur';
@@ -2723,7 +2732,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
String get neighbors_repeatersNeighbors => 'Repeatbburen';
@override
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
@@ -3022,7 +3031,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get listFilter_latestMessages => 'Recente berichten';
@override
String get listFilter_heardRecently => 'Hoor je onlangs';
String get listFilter_heardRecently => 'Recent gezien';
@override
String get listFilter_az => 'A-Z';
@@ -3268,7 +3277,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Contact uit klembord toevoegen';
@override
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
String get contacts_ShareContact => 'Contact naar Klembord kopiëren';
@override
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
@@ -3469,4 +3478,195 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?';
@override
String get chat_sendCooldown =>
'Gelieve even te wachten voordat u opnieuw verzendt.';
@override
String get appSettings_jumpToOldestUnread =>
'Ga naar het oudste ongelezen bericht';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Bij het openen van een chat met ongelezen berichten, scroll dan naar het eerste ongelezen bericht, in plaats van naar het meest recente.';
@override
String get appSettings_languageHu => 'Hongaars';
@override
String get appSettings_languageJa => 'Japanisch';
@override
String get appSettings_languageKo => 'Koreaans';
@override
String get radioStats_tooltip => 'Statistieken voor radio en mesh-netwerken';
@override
String get radioStats_screenTitle => 'Statistieken over radio';
@override
String get radioStats_notConnected =>
'Verbind met een apparaat om radio-statistieken te bekijken.';
@override
String get radioStats_firmwareTooOld =>
'Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.';
@override
String get radioStats_waiting => 'Wacht op gegevens…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Ruisfrequentie: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Laatste RSSI-waarde: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Laatste SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX-tijd (totaal): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tijd besteed met RX (totaal): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ruisfrequentie (dBm) over recente metingen.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Ruisfrequentie: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Radio-statistieken ophalen…';
@override
String get radioStats_settingsTile => 'Statistieken over radio';
@override
String get radioStats_settingsSubtitle =>
'Ruimtelijke ruis, RSSI, SNR en beschikbare tijd';
@override
String get translation_title => 'Vertaling';
@override
String get translation_enableTitle => 'Activeer vertaling';
@override
String get translation_enableSubtitle =>
'Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.';
@override
String get translation_composerTitle => 'Vertaal voor verzending';
@override
String get translation_composerSubtitle =>
'Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.';
@override
String get translation_targetLanguage => 'Doeltaal';
@override
String get translation_useAppLanguage => 'Gebruik de taal van de app';
@override
String get translation_downloadedModelLabel => 'Gedownloade model';
@override
String get translation_presetModelLabel =>
'Voorgeprogrammeerd Hugging Face-model';
@override
String get translation_manualUrlLabel => 'URL van de handleiding';
@override
String get translation_downloadModel => 'Download het model';
@override
String get translation_downloading => 'Downloaden...';
@override
String get translation_working => 'Werken...';
@override
String get translation_stop => 'Stoppen';
@override
String get translation_mergingChunks =>
'Het samenvoegen van de gedownloade stukken tot één eindbestand...';
@override
String get translation_downloadedModels => 'Gedownloade modellen';
@override
String get translation_deleteModel => 'Model verwijderen';
@override
String get translation_modelDownloaded => 'Vertalingmodel gedownload.';
@override
String get translation_downloadStopped => 'Download is afgebroken.';
@override
String translation_downloadFailed(String error) {
return 'Download mislukt: $error';
}
@override
String get translation_enterUrlFirst =>
'Voer eerst een URL van een model in.';
@override
String get scanner_linuxPairingShowPin => 'Toon PIN';
@override
String get scanner_linuxPairingHidePin => 'PIN verbergen';
@override
String get scanner_linuxPairingPinTitle => 'BluetoothkoppelingsPIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Voer PIN in voor $deviceName (laat leeg als er geen is).';
}
@override
String get translation_messageTranslation => 'Berichtvertaling';
@override
String get translation_translateBeforeSending => 'Vertaal voor verzending';
@override
String get translation_composerEnabledHint =>
'De berichten worden vertaald voordat ze verzonden worden.';
@override
String get translation_composerDisabledHint =>
'Stuur berichten in de oorspronkelijke, getypte taal.';
@override
String translation_translateTo(String language) {
return 'Vertalen naar $language';
}
@override
String get translation_translationOptions => 'Opties voor vertaling';
@override
String get translation_systemLanguage => 'Taal van het systeem';
}
+225 -28
View File
@@ -440,7 +440,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String settings_multiAck(String value) {
return 'Wiele potwierdzeń: $value';
return 'Wielokrotne ACK: $value';
}
@override
@@ -611,7 +611,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_language => 'Język';
@override
String get appSettings_languageSystem => 'Domyślny systemu';
String get appSettings_languageSystem => 'Domyślny systemowy';
@override
String get appSettings_languageEn => 'English';
@@ -744,42 +744,42 @@ class AppLocalizationsPl extends AppLocalizations {
'Automatyczne obracanie tras wyłączone';
@override
String get appSettings_maxRouteWeight => 'Maksymalna waga ścieżki';
String get appSettings_maxRouteWeight =>
'Maksymalny dopuszczalny ciężar pojazdu';
@override
String get appSettings_maxRouteWeightSubtitle =>
'Maksymalna waga, jaką ścieżka może osiągnąć dzięki udanym dostarczeniom';
'Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.';
@override
String get appSettings_initialRouteWeight => 'Początkowa waga ścieżki';
String get appSettings_initialRouteWeight => 'Początkowa waga trasy';
@override
String get appSettings_initialRouteWeightSubtitle =>
'Waga początkowa dla nowo odkrytych ścieżek';
'Początkowa waga dla nowych, odkrytych ścieżek';
@override
String get appSettings_routeWeightSuccessIncrement =>
'Przyrost wagi po sukcesie';
String get appSettings_routeWeightSuccessIncrement => 'Wzrost wagi sukcesu';
@override
String get appSettings_routeWeightSuccessIncrementSubtitle =>
'Waga dodawana do ścieżki po udanym dostarczeniu';
'Waga dodana do ścieżki po pomyślnym dostarczeniu';
@override
String get appSettings_routeWeightFailureDecrement =>
'Spadek wagi po niepowodzeniu';
'Zmniejszenie wagi kary';
@override
String get appSettings_routeWeightFailureDecrementSubtitle =>
'Waga odejmowana od ścieżki po nieudanym dostarczeniu';
'Waga usunięta z trasy po nieudanej dostawie';
@override
String get appSettings_maxMessageRetries =>
'Maksymalna liczba ponowień wiadomości';
'Maksymalna liczba prób wysłania wiadomości';
@override
String get appSettings_maxMessageRetriesSubtitle =>
'Liczba prób ponowienia przed oznaczeniem wiadomości jako nieudanej';
'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej';
@override
String path_routeWeight(String weight, String max) {
@@ -1247,6 +1247,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_noMessages => 'Brak jeszcze wiadomości';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Wyślij wiadomość do $contactName';
}
@override
String get chat_sendMessageToStart => 'Wyślij wiadomość, aby rozpocząć.';
@@ -1267,11 +1275,6 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_location => 'Lokalizacja';
@override
String chat_sendMessageTo(String contactName) {
return 'Wyślij wiadomość do $contactName';
}
@override
String get chat_typeMessage => 'Wpisz wiadomość...';
@@ -1699,7 +1702,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_otherNodes => 'Inne węzły';
@override
String get map_showOverlaps => 'Nakładające się klucze powtarzalne';
String get map_showOverlaps => 'Nakładające się klucze przekaźników';
@override
String get map_keyPrefix => 'Prefiks klucza';
@@ -1745,7 +1748,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_runTrace => 'Uruchom ślad ścieżki';
@override
String get map_runTraceWithReturnPath => 'Wróć z powrotem tą samą ścieżką';
String get map_runTraceWithReturnPath => 'Wróć tą samą ścieżką';
@override
String get map_removeLast => 'Usuń ostatni';
@@ -2055,8 +2058,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_neighbors => 'Sąsiedzi';
@override
String get repeater_neighborsSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
String get repeater_neighborsSubtitle => 'Wyświetl sąsiadów zero-hop.';
@override
String get repeater_settings => 'Ustawienia';
@@ -2436,6 +2438,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Godzina';
@override
String get repeater_cliQuickClockSync => 'Synchronizacja zegara';
@override
String get repeater_cliQuickDiscovery => 'Odkryj Sąsiadów';
@override
String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy';
@@ -3368,11 +3376,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportRepeaters =>
'Eksportuj przekaźniki / serwer pokojowy do GPX';
'Eksportuj przekaźniki / roomservery do GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
'Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.';
'Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.';
@override
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
@@ -3404,7 +3412,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportRepeatersRoom =>
'Lokalizacje przekaźników i serwerów pokojowych';
'Lokalizacje przekaźników i roomserverów';
@override
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
@@ -3421,7 +3429,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Eksport danych mapy GPX meshcore-open';
@override
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
String get snrIndicator_nearByRepeaters => 'Pobliskie przekaźniki';
@override
String get snrIndicator_lastSeen => 'Ostatnio widziany';
@@ -3454,11 +3462,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contactsSettings_autoAddRoomServersTitle =>
'Automatycznie dodaj serwery pokojowe';
'Automatycznie dodaj roomservery';
@override
String get contactsSettings_autoAddRoomServersSubtitle =>
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.';
@override
String get contactsSettings_autoAddSensorsTitle =>
@@ -3503,4 +3511,193 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?';
@override
String get chat_sendCooldown =>
'Prosimy o chwilowe oczekiwanie przed ponownym wysłaniem.';
@override
String get appSettings_jumpToOldestUnread =>
'Przejdź do najstarszego nieodczytanej wiadomości';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Przy otwieraniu czatu z nieodczytanymi wiadomościami, przewijaj, aby przejść do pierwszej nieodczytanej wiadomości, zamiast do najnowszej.';
@override
String get appSettings_languageHu => 'Węgierski';
@override
String get appSettings_languageJa => 'Japoński';
@override
String get appSettings_languageKo => 'Koreański';
@override
String get radioStats_tooltip => 'Statystyki dotyczące radia i siatki';
@override
String get radioStats_screenTitle => 'Statystyki radiowe';
@override
String get radioStats_notConnected =>
'Połącz się z urządzeniem, aby wyświetlić statystyki radiowe.';
@override
String get radioStats_firmwareTooOld =>
'Statystyki radiowe wymagają towarzyszącej oprogramowania w wersji 8 lub nowszej.';
@override
String get radioStats_waiting => 'Czekam na dane…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Poziom szumów: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Ostatni poziom RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Ostatni poziom SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Czas emisji w stacji TX (całkowity): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Czas wykorzystania kanału RX (całkowity): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Poziom szumów (dBm) w ostatnich próbkach.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Poziom szumów: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Pobieranie danych dotyczących radia…';
@override
String get radioStats_settingsTile => 'Statystyki radiowe';
@override
String get radioStats_settingsSubtitle =>
'Szum tła, RSSI, SNR oraz czas dostępny';
@override
String get translation_title => 'Tłumaczenie';
@override
String get translation_enableTitle => 'Włącz tłumaczenie';
@override
String get translation_enableSubtitle =>
'Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.';
@override
String get translation_composerTitle => 'Przekład przed wysłaniem';
@override
String get translation_composerSubtitle =>
'Kontroluje domyślny stan ikony tłumaczenia w edytorze.';
@override
String get translation_targetLanguage => 'Język docelowy';
@override
String get translation_useAppLanguage => 'Użyj języka aplikacji';
@override
String get translation_downloadedModelLabel => 'Pobudowany model';
@override
String get translation_presetModelLabel => 'Wspólny model Hugging Face';
@override
String get translation_manualUrlLabel => 'Adres URL do wersji manualnej';
@override
String get translation_downloadModel => 'Pobierz model';
@override
String get translation_downloading => 'Pobieranie...';
@override
String get translation_working => 'Praca...';
@override
String get translation_stop => 'Zatrzymaj się';
@override
String get translation_mergingChunks =>
'Scalanie pobranych fragmentów w jeden plik końcowy...';
@override
String get translation_downloadedModels => 'Pobrane modele';
@override
String get translation_deleteModel => 'Usuń model';
@override
String get translation_modelDownloaded => 'Model tłumaczenia został pobrany.';
@override
String get translation_downloadStopped => 'Pobieranie zakończone.';
@override
String translation_downloadFailed(String error) {
return 'Nie udało się pobrać: $error';
}
@override
String get translation_enterUrlFirst => 'Najpierw wprowadź adres URL modelu.';
@override
String get scanner_linuxPairingShowPin => 'Pokaż PIN';
@override
String get scanner_linuxPairingHidePin => 'Ukryj PIN';
@override
String get scanner_linuxPairingPinTitle => 'Kod PIN parowania Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Wprowadź kod PIN dla $deviceName (pozostaw puste, jeśli brak).';
}
@override
String get translation_messageTranslation => 'Tłumaczenie wiadomości';
@override
String get translation_translateBeforeSending => 'Przekład przed wysłaniem';
@override
String get translation_composerEnabledHint =>
'Komunikaty zostaną przetłumaczone przed wysłaniem.';
@override
String get translation_composerDisabledHint =>
'Wysyłaj wiadomości w oryginalnym, wpisanym formacie.';
@override
String translation_translateTo(String language) {
return 'Tłumacz na $language';
}
@override
String get translation_translationOptions => 'Opcje tłumaczenia';
@override
String get translation_systemLanguage => 'Język systemu';
}
+204 -5
View File
@@ -1238,6 +1238,14 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get chat_noMessages => 'Ainda não existem mensagens.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Enviar uma mensagem para $contactName';
}
@override
String get chat_sendMessageToStart => 'Enviar uma mensagem para começar';
@@ -1257,11 +1265,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get chat_location => 'Localização';
@override
String chat_sendMessageTo(String contactName) {
return 'Enviar uma mensagem para $contactName';
}
@override
String get chat_typeMessage => 'Digite uma mensagem...';
@@ -2423,6 +2426,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Relógio';
@override
String get repeater_cliQuickClockSync => 'Sincronização do Relógio';
@override
String get repeater_cliQuickDiscovery => 'Descobrir Vizinhos';
@override
String get repeater_cliHelpAdvert => 'Envia um pacote de anúncios';
@@ -3484,4 +3493,194 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Tem certeza de que deseja excluir todos os contatos descobertos?';
@override
String get chat_sendCooldown =>
'Por favor, aguarde um momento antes de reenviar.';
@override
String get appSettings_jumpToOldestUnread =>
'Vá para a mensagem mais antiga não lida';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.';
@override
String get appSettings_languageHu => 'Húngaro';
@override
String get appSettings_languageJa => 'Japonês';
@override
String get appSettings_languageKo => 'Coreano';
@override
String get radioStats_tooltip => 'Estatísticas de rádio e malha';
@override
String get radioStats_screenTitle => 'Estatísticas de rádio';
@override
String get radioStats_notConnected =>
'Conecte-se a um dispositivo para visualizar estatísticas de rádio.';
@override
String get radioStats_firmwareTooOld =>
'As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.';
@override
String get radioStats_waiting => 'Aguardando dados…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Nível de ruído: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Último RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Último SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Tempo de transmissão da TX (total): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Tempo de uso do RX (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Nível de ruído (dBm) em amostras recentes.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Nível de ruído: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Obtendo estatísticas de rádio…';
@override
String get radioStats_settingsTile => 'Estatísticas de rádio';
@override
String get radioStats_settingsSubtitle =>
'Nível de ruído, RSSI, SNR e tempo de transmissão';
@override
String get translation_title => 'Tradução';
@override
String get translation_enableTitle => 'Ativar a tradução';
@override
String get translation_enableSubtitle =>
'Traduzir mensagens recebidas e permitir a tradução antes do envio.';
@override
String get translation_composerTitle => 'Traduza antes de enviar';
@override
String get translation_composerSubtitle =>
'Controla o estado padrão do ícone de tradução do compositor.';
@override
String get translation_targetLanguage => 'Língua-alvo';
@override
String get translation_useAppLanguage => 'Utilize o idioma da aplicação';
@override
String get translation_downloadedModelLabel => 'Modelo baixado';
@override
String get translation_presetModelLabel =>
'Modelo pré-definido da Hugging Face';
@override
String get translation_manualUrlLabel => 'URL do modelo manual';
@override
String get translation_downloadModel => 'Baixar modelo';
@override
String get translation_downloading => 'Baixando...';
@override
String get translation_working => 'Trabalhando...';
@override
String get translation_stop => 'Pare';
@override
String get translation_mergingChunks =>
'Combinando os fragmentos baixados em um único arquivo...';
@override
String get translation_downloadedModels => 'Modelos baixados';
@override
String get translation_deleteModel => 'Excluir modelo';
@override
String get translation_modelDownloaded => 'Modelo de tradução baixado.';
@override
String get translation_downloadStopped => 'Download interrompido.';
@override
String translation_downloadFailed(String error) {
return 'Falha na descarga: $error';
}
@override
String get translation_enterUrlFirst => 'Insira primeiro a URL do modelo.';
@override
String get scanner_linuxPairingShowPin => 'Mostrar PIN';
@override
String get scanner_linuxPairingHidePin => 'Ocultar PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN de emparelhamento Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Insira o PIN para $deviceName (deixe em branco se não houver).';
}
@override
String get translation_messageTranslation => 'Tradução da mensagem';
@override
String get translation_translateBeforeSending => 'Traduzir antes de enviar';
@override
String get translation_composerEnabledHint =>
'As mensagens serão traduzidas antes de serem enviadas.';
@override
String get translation_composerDisabledHint =>
'Envie mensagens no idioma original, conforme digitado.';
@override
String translation_translateTo(String language) {
return 'Traduzir para $language';
}
@override
String get translation_translationOptions => 'Opções de tradução';
@override
String get translation_systemLanguage => 'Idioma do sistema';
}
+204 -5
View File
@@ -1238,6 +1238,14 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_noMessages => 'Сообщений пока нет';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Отправить сообщение $contactName';
}
@override
String get chat_sendMessageToStart => 'Отправьте сообщение, чтобы начать';
@@ -1257,11 +1265,6 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get chat_location => 'Местоположение';
@override
String chat_sendMessageTo(String contactName) {
return 'Отправить сообщение $contactName';
}
@override
String get chat_typeMessage => 'Напишите сообщение...';
@@ -2427,6 +2430,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Время';
@override
String get repeater_cliQuickClockSync => 'Синхронизация часов';
@override
String get repeater_cliQuickDiscovery => 'Обнаружить Соседей';
@override
String get repeater_cliHelpAdvert => 'Отправляет пакет анонсирования';
@@ -3498,4 +3507,194 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Вы уверены, что хотите удалить все обнаруженные контакты?';
@override
String get chat_sendCooldown =>
'Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.';
@override
String get appSettings_jumpToOldestUnread =>
'Перейти к самому старому непрочитанному сообщению';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.';
@override
String get appSettings_languageHu => 'Венгерский';
@override
String get appSettings_languageJa => 'Японский';
@override
String get appSettings_languageKo => 'Корейский';
@override
String get radioStats_tooltip => 'Статистика радио и беспроводной сети';
@override
String get radioStats_screenTitle => 'Статистика радиовещания';
@override
String get radioStats_notConnected =>
'Подключитесь к устройству, чтобы просмотреть статистику радио.';
@override
String get radioStats_firmwareTooOld =>
'Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.';
@override
String get radioStats_waiting => 'Ожидаем данных…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Уровень шума: $noiseDbm дБм';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Последнее значение RSSI: $rssiDbm дБм';
}
@override
String radioStats_lastSnr(String snr) {
return 'Последнее значение SNR: $snr дБ';
}
@override
String radioStats_txAir(int seconds) {
return 'Время эфира на телеканале TX (общее): $seconds секунд';
}
@override
String radioStats_rxAir(int seconds) {
return 'Общее время использования RX (в секундах): $seconds с';
}
@override
String get radioStats_chartCaption =>
'Уровень шума (дБм) на основе последних измерений.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Уровень шума: $noiseDbm дБм';
}
@override
String get radioStats_stripWaiting => 'Получение данных о радио…';
@override
String get radioStats_settingsTile => 'Статистика радиовещания';
@override
String get radioStats_settingsSubtitle =>
'Уровень шума, RSSI, SNR и время передачи';
@override
String get translation_title => 'Перевод';
@override
String get translation_enableTitle => 'Включить перевод';
@override
String get translation_enableSubtitle =>
'Переводить входящие сообщения и позволять предварительный перевод перед отправкой.';
@override
String get translation_composerTitle => 'Переводить перед отправкой';
@override
String get translation_composerSubtitle =>
'Управляет исходным состоянием значка перевода, предоставляемого редактором.';
@override
String get translation_targetLanguage => 'Целевой язык';
@override
String get translation_useAppLanguage => 'Используйте язык приложения';
@override
String get translation_downloadedModelLabel => 'Загруженная модель';
@override
String get translation_presetModelLabel =>
'Предопределенная модель от Hugging Face';
@override
String get translation_manualUrlLabel => 'Ссылка на руководство';
@override
String get translation_downloadModel => 'Скачать модель';
@override
String get translation_downloading => 'Загрузка...';
@override
String get translation_working => 'Работа...';
@override
String get translation_stop => 'Прекратите';
@override
String get translation_mergingChunks =>
'Объединение скачанных фрагментов в один финальный файл...';
@override
String get translation_downloadedModels => 'Загруженные модели';
@override
String get translation_deleteModel => 'Удалить модель';
@override
String get translation_modelDownloaded => 'Модель перевода загружена.';
@override
String get translation_downloadStopped => 'Процесс загрузки был прерван.';
@override
String translation_downloadFailed(String error) {
return 'Не удалось скачать: $error';
}
@override
String get translation_enterUrlFirst => 'Сначала введите URL модели.';
@override
String get scanner_linuxPairingShowPin => 'Показать PIN';
@override
String get scanner_linuxPairingHidePin => 'Скрыть PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN‑код сопряжения Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Введите PIN‑код для $deviceName (оставьте пустым, если нет).';
}
@override
String get translation_messageTranslation => 'Перевод сообщения';
@override
String get translation_translateBeforeSending => 'Перевести перед отправкой';
@override
String get translation_composerEnabledHint =>
'Сообщения будут переведены перед отправкой.';
@override
String get translation_composerDisabledHint =>
'Отправляйте сообщения на языке, в котором они были изначально набраны.';
@override
String translation_translateTo(String language) {
return 'Перевести на $language';
}
@override
String get translation_translationOptions => 'Варианты перевода';
@override
String get translation_systemLanguage => 'Язык системы';
}
+204 -5
View File
@@ -1226,6 +1226,14 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_noMessages => 'Zatiaľ žiadne správy.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Pošli správu $contactName';
}
@override
String get chat_sendMessageToStart => 'Pošlite správu na začiatok';
@@ -1245,11 +1253,6 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_location => 'Lokalita';
@override
String chat_sendMessageTo(String contactName) {
return 'Pošli správu $contactName';
}
@override
String get chat_typeMessage => 'Napište správu...';
@@ -2406,6 +2409,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Hodiny';
@override
String get repeater_cliQuickClockSync => 'Synchronizácia hodin';
@override
String get repeater_cliQuickDiscovery => 'Objaviť susedov';
@override
String get repeater_cliHelpAdvert => 'Odosiela reklamnú balíček.';
@@ -3464,4 +3473,194 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste si istí, že chcete zmazať všetky objavené kontakty?';
@override
String get chat_sendCooldown => 'Prosím, počkajte chvíľu, než zašlete znova.';
@override
String get appSettings_jumpToOldestUnread => 'Presk oceň';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.';
@override
String get appSettings_languageHu => 'Maďarský';
@override
String get appSettings_languageJa => 'Japonský';
@override
String get appSettings_languageKo => 'Kórejský';
@override
String get radioStats_tooltip => 'Statistiky rádiových a sieťových kanálov';
@override
String get radioStats_screenTitle => 'Štatistiky rádiových vysielaní';
@override
String get radioStats_notConnected =>
'Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.';
@override
String get radioStats_firmwareTooOld =>
'Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.';
@override
String get radioStats_waiting => 'Čakám na údaje…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Úroveň hluku: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Posledný údaj RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Posledná hodnota SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Čas vysielania na TX (celkový): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Čas RX (celkový): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Úroveň šumu (dBm) pre posledné vzorky.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Úroveň hluku: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Získavanie údajov o rádiu…';
@override
String get radioStats_settingsTile => 'Štatistiky rádiových vysielaní';
@override
String get radioStats_settingsSubtitle =>
'Úroveň hluku, RSSI, SNR a časové rozloženie';
@override
String get translation_title => 'Preklad';
@override
String get translation_enableTitle => 'Aktivovať preklad';
@override
String get translation_enableSubtitle =>
'Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.';
@override
String get translation_composerTitle => 'Preložte pred odeslaním';
@override
String get translation_composerSubtitle =>
'Riadi výchoce stav ikony pre preklad, ktorú používa program.';
@override
String get translation_targetLanguage => 'Cieľový jazyk';
@override
String get translation_useAppLanguage => 'Použite jazyk aplikácie';
@override
String get translation_downloadedModelLabel => 'Stiahnutý model';
@override
String get translation_presetModelLabel =>
'Prednastavený model od Hugging Face';
@override
String get translation_manualUrlLabel =>
'Odkaz na manuál (v elektronickej forme)';
@override
String get translation_downloadModel => 'Stiahnuť model';
@override
String get translation_downloading => 'Stiahnutie...';
@override
String get translation_working => 'Práca...';
@override
String get translation_stop => 'Zastavte';
@override
String get translation_mergingChunks =>
'Sliečenie stiahnutých častí do konečného súboru...';
@override
String get translation_downloadedModels => 'Stiahnuté modely';
@override
String get translation_deleteModel => 'Odstrániť model';
@override
String get translation_modelDownloaded => 'Model pre preklad bol stiahnutý.';
@override
String get translation_downloadStopped => 'Stiahnutie bolo prerušené.';
@override
String translation_downloadFailed(String error) {
return 'Neúspešné stiahnutie: $error';
}
@override
String get translation_enterUrlFirst =>
'Najprv zadajte URL pre konkrétny model.';
@override
String get scanner_linuxPairingShowPin => 'Zobraziť PIN';
@override
String get scanner_linuxPairingHidePin => 'Skryť PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN pre párovanie cez Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Zadajte PIN pre $deviceName (ak neexistuje, nechajte prázdne).';
}
@override
String get translation_messageTranslation => 'Preklad textu';
@override
String get translation_translateBeforeSending => 'Preložte pred odeslaním';
@override
String get translation_composerEnabledHint =>
'Správy budú preložené, než budú odoslané.';
@override
String get translation_composerDisabledHint =>
'Posielajte správy v pôvodnej písanom jazyku.';
@override
String translation_translateTo(String language) {
return 'Preložte do $language';
}
@override
String get translation_translationOptions => 'Možnosti prekladania';
@override
String get translation_systemLanguage => 'Jazyk systému';
}
+206 -5
View File
@@ -1224,6 +1224,14 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get chat_noMessages => 'Še ni sporočil.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Pošlji sporočilo $contactName';
}
@override
String get chat_sendMessageToStart => 'Pošlji sporočilo za začetek.';
@@ -1244,11 +1252,6 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get chat_location => 'Lokacija';
@override
String chat_sendMessageTo(String contactName) {
return 'Pošlji sporočilo $contactName';
}
@override
String get chat_typeMessage => 'Vnesi sporočilo...';
@@ -2409,6 +2412,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Ura';
@override
String get repeater_cliQuickClockSync => 'Usklajevanje ure';
@override
String get repeater_cliQuickDiscovery => 'Odkrijte sosede';
@override
String get repeater_cliHelpAdvert => 'Pošlje paket oglasov';
@@ -3467,4 +3476,196 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Ste prepričani, da želite izbrisati vse odkrite kontakte?';
@override
String get chat_sendCooldown =>
'Prosimo, počakajte trenutek, preden pošljete ponovno.';
@override
String get appSettings_jumpToOldestUnread =>
'Pritisnite za najstarejše nepročitano sporočilo';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.';
@override
String get appSettings_languageHu => 'Madžarski';
@override
String get appSettings_languageJa => 'Japonski';
@override
String get appSettings_languageKo => 'Korejski';
@override
String get radioStats_tooltip => 'Statistike za radio in mrežo';
@override
String get radioStats_screenTitle => 'Radijske statistike';
@override
String get radioStats_notConnected =>
'Povežite se z napravo, da si ogledate statistiko o radiju.';
@override
String get radioStats_firmwareTooOld =>
'Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.';
@override
String get radioStats_waiting => 'Čakam na podatke…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Število šuma: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Najkasnejše vrednost RSSI: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Najkasnejše vrednost SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'Čas na TX (skupno): $seconds s';
}
@override
String radioStats_rxAir(int seconds) {
return 'Čas, namenjen RX-ju (skupno): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ravnovredna raven šuma (dBm) za nedavne vzorce.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Število šuma: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Prejemanje statistike o radiju…';
@override
String get radioStats_settingsTile => 'Radijske statistike';
@override
String get radioStats_settingsSubtitle =>
'Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema';
@override
String get translation_title => 'Prevod';
@override
String get translation_enableTitle => 'Omogočite prevod';
@override
String get translation_enableSubtitle =>
'Prevedite vstopne sporočila in omogočite predhodno prevajanje.';
@override
String get translation_composerTitle => 'Preprištejte, preden pošljete';
@override
String get translation_composerSubtitle =>
'Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.';
@override
String get translation_targetLanguage => 'Ciljna jezika';
@override
String get translation_useAppLanguage => 'Uporabite jezik aplikacije';
@override
String get translation_downloadedModelLabel => 'Naložen model';
@override
String get translation_presetModelLabel =>
'Prednastavljeni model Hugging Face';
@override
String get translation_manualUrlLabel => 'URL za ročni model';
@override
String get translation_downloadModel => 'Prenesite model';
@override
String get translation_downloading => 'Izvajanje...';
@override
String get translation_working => 'Delo...';
@override
String get translation_stop => 'Prekliji';
@override
String get translation_mergingChunks =>
'Sklapljanje prenesenih delov v končni datoteko...';
@override
String get translation_downloadedModels => 'Naloženi modeli';
@override
String get translation_deleteModel => 'Izbrisati model';
@override
String get translation_modelDownloaded =>
'Model za prevajanje je bil naložen.';
@override
String get translation_downloadStopped => 'Prenos je bil prekinjen.';
@override
String translation_downloadFailed(String error) {
return 'Izgovoritev ni bila uspešna: $error';
}
@override
String get translation_enterUrlFirst => 'Najprej vnesite URL model.';
@override
String get scanner_linuxPairingShowPin => 'Prikaži PIN';
@override
String get scanner_linuxPairingHidePin => 'Skrij PIN';
@override
String get scanner_linuxPairingPinTitle => 'Bluetooth PIN za seznanjanje';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Vnesite PIN za $deviceName (pustite prazno, če ga ni).';
}
@override
String get translation_messageTranslation => 'Prevod sporočila';
@override
String get translation_translateBeforeSending =>
'Preprištejte, preden pošljete';
@override
String get translation_composerEnabledHint =>
'Vsebina sporočil bo prevedena, preden jih pošljemo.';
@override
String get translation_composerDisabledHint =>
'Pošljite sporočila v originalnem tipkanem jeziku.';
@override
String translation_translateTo(String language) {
return 'Prevesti v $language';
}
@override
String get translation_translationOptions => 'Možnosti prevoda';
@override
String get translation_systemLanguage => 'Jezik sistema';
}
+206 -5
View File
@@ -1217,6 +1217,14 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get chat_noMessages => 'Inga meddelanden ännu';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Skicka ett meddelande till $contactName';
}
@override
String get chat_sendMessageToStart =>
'Skicka ett meddelande för att komma igång';
@@ -1238,11 +1246,6 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get chat_location => 'Plats';
@override
String chat_sendMessageTo(String contactName) {
return 'Skicka ett meddelande till $contactName';
}
@override
String get chat_typeMessage => 'Skriv ett meddelande...';
@@ -2394,6 +2397,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Klocka';
@override
String get repeater_cliQuickClockSync => 'Synkronisera klocka';
@override
String get repeater_cliQuickDiscovery => 'Upptäck grannar';
@override
String get repeater_cliHelpAdvert => 'Skickar ett annonspaket';
@@ -3444,4 +3453,196 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Är du säker på att du vill ta bort alla upptäckta kontakter?';
@override
String get chat_sendCooldown =>
'Vänligen vänta en stund innan du skickar igen.';
@override
String get appSettings_jumpToOldestUnread =>
'Gå direkt till det äldsta, obesvarade meddelandet';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.';
@override
String get appSettings_languageHu => 'Ungerskt';
@override
String get appSettings_languageJa => 'Japanska';
@override
String get appSettings_languageKo => 'Koreanska';
@override
String get radioStats_tooltip => 'Radio- och mesh-statistik';
@override
String get radioStats_screenTitle => 'Radiostation';
@override
String get radioStats_notConnected =>
'Anslut till en enhet för att visa radiostatistik.';
@override
String get radioStats_firmwareTooOld =>
'Radio statistik kräver kompatibel firmware version 8 eller senare.';
@override
String get radioStats_waiting => 'Väntar på data…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Bakgrundsnivå: $noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Senaste RSSI-värde: $rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return 'Senaste SNR: $snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX-tid (total): $seconds sekunder';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX-tid (total): $seconds s';
}
@override
String get radioStats_chartCaption =>
'Ljudnivå (dBm) baserat på de senaste mätningarna.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Bakgrundsnivå: $noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => 'Hämtar radiostatistik…';
@override
String get radioStats_settingsTile => 'Radiostation';
@override
String get radioStats_settingsSubtitle =>
'Bakgrundsnivå, RSSI, SNR och tillgänglig tid';
@override
String get translation_title => 'Översättning';
@override
String get translation_enableTitle => 'Aktivera översättning';
@override
String get translation_enableSubtitle =>
'Översätt inkommande meddelanden och möjliggör översättning före avsändning.';
@override
String get translation_composerTitle => 'Översätt innan du skickar';
@override
String get translation_composerSubtitle =>
'Styr standardtillståndet för kompositorns översättningsikon.';
@override
String get translation_targetLanguage => 'Målmedvetet språk';
@override
String get translation_useAppLanguage => 'Använd appens språk';
@override
String get translation_downloadedModelLabel => 'Nedladdad modell';
@override
String get translation_presetModelLabel =>
'Fördefinierat Hugging Face-modell';
@override
String get translation_manualUrlLabel => 'Manualens URL';
@override
String get translation_downloadModel => 'Ladda ner modellen';
@override
String get translation_downloading => 'Nedladdning...';
@override
String get translation_working => 'Arbeta...';
@override
String get translation_stop => 'Stopp';
@override
String get translation_mergingChunks =>
'Slå samman de nedladdade delarna till en slutlig fil...';
@override
String get translation_downloadedModels => 'Nedladdade modeller';
@override
String get translation_deleteModel => 'Ta bort modell';
@override
String get translation_modelDownloaded =>
'Översättningsmodellen har laddats ner.';
@override
String get translation_downloadStopped => 'Nedladdningen avbruten.';
@override
String translation_downloadFailed(String error) {
return 'Nedladdning misslyckades: $error';
}
@override
String get translation_enterUrlFirst =>
'Ange först en URL för en specifik modell.';
@override
String get scanner_linuxPairingShowPin => 'Visa PIN';
@override
String get scanner_linuxPairingHidePin => 'Dölj PIN';
@override
String get scanner_linuxPairingPinTitle => 'BluetoothparningsPIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Ange PIN för $deviceName (lämna tomt om ingen).';
}
@override
String get translation_messageTranslation => 'Meddelandets översättning';
@override
String get translation_translateBeforeSending => 'Översätt innan du skickar';
@override
String get translation_composerEnabledHint =>
'Meddelandena kommer att översättas innan de skickas.';
@override
String get translation_composerDisabledHint =>
'Skicka meddelanden på det ursprungliga, stavade språket.';
@override
String translation_translateTo(String language) {
return 'Översätt till $language';
}
@override
String get translation_translationOptions => 'Översättningsalternativ';
@override
String get translation_systemLanguage => 'Språk för systemet';
}
+206 -5
View File
@@ -1230,6 +1230,14 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get chat_noMessages => 'Поки немає повідомлень.';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return 'Надіслати повідомлення $contactName';
}
@override
String get chat_sendMessageToStart => 'Надішліть повідомлення, щоб почати';
@@ -1250,11 +1258,6 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get chat_location => 'Розташування';
@override
String chat_sendMessageTo(String contactName) {
return 'Надіслати повідомлення $contactName';
}
@override
String get chat_typeMessage => 'Введіть повідомлення...';
@@ -2427,6 +2430,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get repeater_cliQuickClock => 'Годинник';
@override
String get repeater_cliQuickClockSync => 'Синхронізація годинника';
@override
String get repeater_cliQuickDiscovery => 'Відкрити сусідів';
@override
String get repeater_cliHelpAdvert => 'Надсилає пакет оголошення';
@@ -3501,4 +3510,196 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent =>
'Ви впевнені, що хочете видалити всі виявлені контакти?';
@override
String get chat_sendCooldown =>
'Будь ласка, зачекайте трохи, перш ніж відправляти знову.';
@override
String get appSettings_jumpToOldestUnread =>
'Перейти до найстарішого непрочитаного повідомлення';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.';
@override
String get appSettings_languageHu => 'Угорський';
@override
String get appSettings_languageJa => 'Японська';
@override
String get appSettings_languageKo => 'Кореєська';
@override
String get radioStats_tooltip => 'Статистика радіо та мережі';
@override
String get radioStats_screenTitle => 'Дані про радіостанції';
@override
String get radioStats_notConnected =>
'Підключіться до пристрою, щоб переглядати статистику радіопередач.';
@override
String get radioStats_firmwareTooOld =>
'Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.';
@override
String get radioStats_waiting => 'Очікую на отримання даних…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return 'Рівень шуму: $noiseDbm дБм';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return 'Останній показник RSSI: $rssiDbm дБм';
}
@override
String radioStats_lastSnr(String snr) {
return 'Останній показник SNR: $snr дБ';
}
@override
String radioStats_txAir(int seconds) {
return 'Час трансляції на телеканалі TX (загальний): $seconds секунд';
}
@override
String radioStats_rxAir(int seconds) {
return 'Загальний час використання RX: $seconds секунд';
}
@override
String get radioStats_chartCaption =>
'Рівень шуму (дБм) на основі останніх вимірювань.';
@override
String radioStats_stripNoise(int noiseDbm) {
return 'Рівень шуму: $noiseDbm дБм';
}
@override
String get radioStats_stripWaiting => 'Отримано статистику радіо…';
@override
String get radioStats_settingsTile => 'Дані про радіостанції';
@override
String get radioStats_settingsSubtitle =>
'Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.';
@override
String get translation_title => 'Переклад';
@override
String get translation_enableTitle => 'Увімкнути переклад';
@override
String get translation_enableSubtitle =>
'Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.';
@override
String get translation_composerTitle => 'Перекладіть перед відправкою';
@override
String get translation_composerSubtitle =>
'Контролює стан ікон перекладу, який використовується за замовчуванням.';
@override
String get translation_targetLanguage => 'Цільова мова';
@override
String get translation_useAppLanguage => 'Використовуйте мову додатку';
@override
String get translation_downloadedModelLabel => 'Завантажений шаблон';
@override
String get translation_presetModelLabel =>
'Заздалегідь налаштований модель від Hugging Face';
@override
String get translation_manualUrlLabel =>
'Посилання на веб-сторінку з інструкцією';
@override
String get translation_downloadModel => 'Завантажити модель';
@override
String get translation_downloading => 'Завантаження...';
@override
String get translation_working => 'Працюю...';
@override
String get translation_stop => 'Припинити';
@override
String get translation_mergingChunks =>
'Об\'єднання завантажених фрагментів у кінцевий файл...';
@override
String get translation_downloadedModels => 'Завантажені моделі';
@override
String get translation_deleteModel => 'Видалити модель';
@override
String get translation_modelDownloaded => 'Модель перекладу завантажена.';
@override
String get translation_downloadStopped => 'Завантаження призупинено.';
@override
String translation_downloadFailed(String error) {
return 'Не вдалося завантажити: $error';
}
@override
String get translation_enterUrlFirst => 'Спочатку введіть URL моделі.';
@override
String get scanner_linuxPairingShowPin => 'Показати PIN';
@override
String get scanner_linuxPairingHidePin => 'Приховати PIN';
@override
String get scanner_linuxPairingPinTitle => 'PIN‑код спарювання Bluetooth';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return 'Введіть PIN для $deviceName (залиште порожнім, якщо його немає).';
}
@override
String get translation_messageTranslation => 'Переклад повідомлення';
@override
String get translation_translateBeforeSending =>
'Перекладіть перед відправкою';
@override
String get translation_composerEnabledHint =>
'Повідомлення будуть перекладені перед відправленням.';
@override
String get translation_composerDisabledHint =>
'Надсилайте повідомлення, використовуючи оригінальний текстовий формат.';
@override
String translation_translateTo(String language) {
return 'Перекласти на $language';
}
@override
String get translation_translationOptions => 'Варіанти перекладу';
@override
String get translation_systemLanguage => 'Мова системи';
}
+192 -5
View File
@@ -1161,6 +1161,14 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get chat_noMessages => '暂无消息';
@override
String get chat_sendMessage => 'Send message';
@override
String chat_sendMessageTo(String contactName) {
return '发送消息给 $contactName';
}
@override
String get chat_sendMessageToStart => '发送消息开始对话';
@@ -1180,11 +1188,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get chat_location => '位置';
@override
String chat_sendMessageTo(String contactName) {
return '发送消息给 $contactName';
}
@override
String get chat_typeMessage => '输入消息...';
@@ -2277,6 +2280,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_cliQuickClock => '时钟';
@override
String get repeater_cliQuickClockSync => '同步时钟';
@override
String get repeater_cliQuickDiscovery => '发现邻居';
@override
String get repeater_cliHelpAdvert => '发送广播包';
@@ -3222,4 +3231,182 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get discoveredContacts_deleteContactAllContent => '您确定要删除所有发现的联系人吗?';
@override
String get chat_sendCooldown => '请稍等片刻后再尝试发送。';
@override
String get appSettings_jumpToOldestUnread => '跳转到最旧、未读的文章';
@override
String get appSettings_jumpToOldestUnreadSubtitle =>
'在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。';
@override
String get appSettings_languageHu => '匈牙利';
@override
String get appSettings_languageJa => '日语';
@override
String get appSettings_languageKo => '韩语';
@override
String get radioStats_tooltip => '无线电和网状结构统计数据';
@override
String get radioStats_screenTitle => '广播统计数据';
@override
String get radioStats_notConnected => '连接到设备以查看收音机统计信息。';
@override
String get radioStats_firmwareTooOld => '使用无线电统计功能需要配合使用 v8 或更高版本的固件。';
@override
String get radioStats_waiting => '正在等待数据…';
@override
String radioStats_noiseFloor(int noiseDbm) {
return '噪声水平:$noiseDbm dBm';
}
@override
String radioStats_lastRssi(int rssiDbm) {
return '上次 RSSI 值:$rssiDbm dBm';
}
@override
String radioStats_lastSnr(String snr) {
return '上次 SNR$snr dB';
}
@override
String radioStats_txAir(int seconds) {
return 'TX 频道播出时间(总时长):$seconds';
}
@override
String radioStats_rxAir(int seconds) {
return 'RX 使用时长(总时长):$seconds';
}
@override
String get radioStats_chartCaption => '近期的噪声水平(dBm)。';
@override
String radioStats_stripNoise(int noiseDbm) {
return '噪声水平:$noiseDbm dBm';
}
@override
String get radioStats_stripWaiting => '正在获取收音机数据…';
@override
String get radioStats_settingsTile => '广播统计数据';
@override
String get radioStats_settingsSubtitle => '噪声水平、RSSI、信噪比和空中时间';
@override
String get translation_title => '翻译';
@override
String get translation_enableTitle => '启用翻译功能';
@override
String get translation_enableSubtitle => '翻译收到的消息,并允许在发送前进行翻译。';
@override
String get translation_composerTitle => '在发送之前进行翻译';
@override
String get translation_composerSubtitle => '控制作曲家翻译图标的默认状态。';
@override
String get translation_targetLanguage => '目标语言';
@override
String get translation_useAppLanguage => '使用应用程序语言';
@override
String get translation_downloadedModelLabel => '下载的模型';
@override
String get translation_presetModelLabel => '预设的 Hugging Face 模型';
@override
String get translation_manualUrlLabel => '手动模型网址';
@override
String get translation_downloadModel => '下载模型';
@override
String get translation_downloading => '正在下载...';
@override
String get translation_working => '工作中...';
@override
String get translation_stop => '停止';
@override
String get translation_mergingChunks => '将下载的片段合并成最终文件...';
@override
String get translation_downloadedModels => '下载的模型';
@override
String get translation_deleteModel => '删除模型';
@override
String get translation_modelDownloaded => '翻译模型已下载。';
@override
String get translation_downloadStopped => '下载已停止。';
@override
String translation_downloadFailed(String error) {
return '下载失败:$error';
}
@override
String get translation_enterUrlFirst => '首先,请输入模型的 URL。';
@override
String get scanner_linuxPairingShowPin => '显示PIN码';
@override
String get scanner_linuxPairingHidePin => '隐藏 PIN';
@override
String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN';
@override
String scanner_linuxPairingPinPrompt(String deviceName) {
return '输入 $deviceName 的 PIN 码(如果为空,则留空)。';
}
@override
String get translation_messageTranslation => '消息翻译';
@override
String get translation_translateBeforeSending => '在发送前进行翻译';
@override
String get translation_composerEnabledHint => '消息将在发送前进行翻译。';
@override
String get translation_composerDisabledHint => '使用原始的打字方式发送消息。';
@override
String translation_translateTo(String language) {
return '翻译成 $language';
}
@override
String get translation_translationOptions => '翻译选项';
@override
String get translation_systemLanguage => '系统语言';
}
+165 -47
View File
@@ -84,7 +84,7 @@
"settings_appSettings": "App Instellingen",
"settings_appSettingsSubtitle": "Notificaties, berichten en kaartinstellingen",
"settings_nodeSettings": "Node Instellingen",
"settings_nodeName": "Node Naam",
"settings_nodeName": "Nodenaam",
"settings_nodeNameNotSet": "Niet ingesteld",
"settings_nodeNameHint": "Voer nodenaam in",
"settings_nodeNameUpdated": "Naam bijgewerkt",
@@ -107,13 +107,13 @@
"settings_sendAdvertisement": "Verzend Advertentie",
"settings_sendAdvertisementSubtitle": "Nu aanwezigheid uitzenden",
"settings_advertisementSent": "Advertentie verzonden",
"settings_syncTime": "Synchronisatie Tijd",
"settings_syncTime": "Tijd Synchroniseren",
"settings_syncTimeSubtitle": "Stel de apparaatklok in op de tijd van de telefoon.",
"settings_timeSynchronized": "Tijdsynchronisatie",
"settings_refreshContacts": "Contacten vernieuwen",
"settings_refreshContactsSubtitle": "Contactlijst opnieuw laden van het apparaat",
"settings_rebootDevice": "Apparaat opnieuw opstarten",
"settings_rebootDeviceSubtitle": "Herstart het MeshCore apparaat",
"settings_rebootDeviceSubtitle": "Herstart het MeshCore-apparaat",
"settings_rebootDeviceConfirm": "Ben je er zeker van dat je het apparaat opnieuw wilt opstarten? Je wordt losgekoppeld.",
"settings_debug": "Debug",
"settings_bleDebugLog": "BLE Debug Log",
@@ -145,7 +145,7 @@
"settings_bandwidth": "Bandbreedte",
"settings_spreadingFactor": "Spreadsnelheid",
"settings_codingRate": "Codeertarief",
"settings_txPower": "TX Vermogen (dBm)",
"settings_txPower": "TX-Vermogen (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
"settings_error": "Fout: {message}",
@@ -232,11 +232,11 @@
"appSettings_mapTimeFilter": "Filter tijd op kaart",
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
"appSettings_allTime": "Altijd",
"appSettings_lastHour": "Laat uur",
"appSettings_last6Hours": "laatste 6 uur",
"appSettings_last24Hours": "De laatste 24 uur",
"appSettings_lastWeek": "Laatste week",
"appSettings_offlineMapCache": "Offline Kaarten Cache",
"appSettings_lastHour": "Afgelopen uur",
"appSettings_last6Hours": "Afgelopen 6 uur",
"appSettings_last24Hours": "Afgelopen 24 uur",
"appSettings_lastWeek": "Afgelopen week",
"appSettings_offlineMapCache": "Offline Kaartcache",
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
"@appSettings_areaSelectedZoom": {
@@ -682,7 +682,7 @@
"map_showSharedMarkers": "Toon gedeelde markeringen",
"map_lastSeenTime": "Laatste Bekeken Tijd",
"map_sharedPin": "Gedeelde pin",
"map_joinRoom": "Sluit Kamer",
"map_joinRoom": "Kamer Toetreden",
"map_manageRepeater": "Beheer Repeater",
"mapCache_title": "Offline Kaarten Cache",
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
@@ -878,7 +878,7 @@
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
"path_setPath": "Stel Pad in",
"repeater_management": "Beheer Repeaters",
"repeater_managementTools": "Beheerinstrumenten",
"repeater_managementTools": "Beheerfuncties",
"repeater_status": "Status",
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
"repeater_telemetry": "Telemetry",
@@ -912,8 +912,8 @@
"repeater_lastRssi": "Laatste RSSI",
"repeater_lastSnr": "Laatste SNR",
"repeater_noiseFloor": "Ruisvloer",
"repeater_txAirtime": "TX Airtime",
"repeater_rxAirtime": "RX Airtime",
"repeater_txAirtime": "TX-zendtijd",
"repeater_rxAirtime": "RX-zendtijd",
"repeater_packetStatistics": "Pakketstatistieken",
"repeater_sent": "Verzonden",
"repeater_received": "Ontvangen",
@@ -982,14 +982,14 @@
}
}
},
"repeater_settingsTitle": "Repeater Instellingen",
"repeater_settingsTitle": "Repeaterinstellingen",
"repeater_basicSettings": "Basisinstellingen",
"repeater_repeaterName": "Repeaternaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
"repeater_repeaterNameHelper": "Weergavenaam voor deze repeater",
"repeater_adminPassword": "Admin wachtwoord",
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
"repeater_guestPassword": "Wachtwoord Gast",
"repeater_guestPasswordHelper": "Leesbeheer wachtwoord",
"repeater_adminPasswordHelper": "Wachtwoord administratortoegang",
"repeater_guestPassword": "Gast wachtwoord",
"repeater_guestPasswordHelper": "Wachtwoord gasttoegen",
"repeater_radioSettings": "Radio Instellingen",
"repeater_frequencyMhz": "Frequentie (MHz)",
"repeater_frequencyHelper": "300-2500 MHz",
@@ -998,7 +998,7 @@
"repeater_bandwidth": "Bandbreedte",
"repeater_spreadingFactor": "Spreidingsfactor",
"repeater_codingRate": "Codeertarief",
"repeater_locationSettings": "Locatie Instellingen",
"repeater_locationSettings": "Locatie-instellingen",
"repeater_latitude": "Breedtegraad",
"repeater_latitudeHelper": "Graadseconden (bijv. 37.7749)",
"repeater_longitude": "Lengtegraad",
@@ -1008,9 +1008,9 @@
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
"repeater_guestAccess": "Toegang voor Gasten",
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
"repeater_privacyMode": "Privacy Modus",
"repeater_privacyMode": "Privacymodus",
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"repeater_advertisementSettings": "Advertentie Instellingen",
"repeater_advertisementSettings": "Advertentie-instellingen",
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
"repeater_localAdvertIntervalMinutes": "{minutes} minuten",
"@repeater_localAdvertIntervalMinutes": {
@@ -1073,7 +1073,7 @@
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
"repeater_refreshGuestAccess": "Toegang Gast Vernieuwen",
"repeater_refreshPrivacyMode": "Privacy Mode vernieuwen",
"repeater_refreshPrivacyMode": "Privacymode vernieuwen",
"repeater_refreshAdvertisementSettings": "Instellingen Advertentie Bijwerken",
"repeater_refreshed": "{label} is vernieuwd",
"@repeater_refreshed": {
@@ -1094,8 +1094,8 @@
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Debug Volgende Commando",
"repeater_commandHelp": "Help",
"repeater_clearHistory": "Verwijder Geschiedenis",
"repeater_noCommandsSent": "Geen commando's verzonden nog.",
"repeater_clearHistory": "Geschiedenis Verwijderen",
"repeater_noCommandsSent": "Nog geen commando's verzonden.",
"repeater_typeCommandOrUseQuick": "Typ een opdracht hieronder of gebruik snelle commando's",
"repeater_enterCommandHint": "Voer bevel in...",
"repeater_previousCommand": "Vorige opdracht",
@@ -1110,14 +1110,14 @@
}
}
},
"repeater_cliQuickGetName": "Haal Naam op",
"repeater_cliQuickGetRadio": "Radio ontvangen",
"repeater_cliQuickGetTx": "Krijg TX",
"repeater_cliQuickNeighbors": "Buren",
"repeater_cliQuickVersion": "Versie",
"repeater_cliQuickAdvertise": "Advertenties",
"repeater_cliQuickClock": "Tijd",
"repeater_cliHelpAdvert": "Verstuurt een advertentiepakket",
"repeater_cliQuickGetName": "Naam opvragen",
"repeater_cliQuickGetRadio": "Radio-instellingen opvragen",
"repeater_cliQuickGetTx": "TX opvragen",
"repeater_cliQuickNeighbors": "Buren opvragen",
"repeater_cliQuickVersion": "Versie opvragen",
"repeater_cliQuickAdvertise": "Advertenties opvragen",
"repeater_cliQuickClock": "Tijd opvragen",
"repeater_cliHelpAdvert": "Advertentie uitzenden",
"repeater_cliHelpReboot": "Herstart het apparaat. (let op, je krijgt mogelijk een 'Timeout', wat normaal is)",
"repeater_cliHelpClock": "Toont de huidige tijd per apparaat's klok.",
"repeater_cliHelpPassword": "Stelt een nieuw beheerderswachtwoord in voor het apparaat.",
@@ -1203,7 +1203,7 @@
},
"telemetry_batteryLabel": "Batterij",
"telemetry_voltageLabel": "Spanning",
"telemetry_mcuTemperatureLabel": "MCU Temperatuur",
"telemetry_mcuTemperatureLabel": "MCU-temperatuur",
"telemetry_temperatureLabel": "Temperatuur",
"telemetry_currentLabel": "Huidig",
"telemetry_batteryValue": "{percent}% / {volts}V",
@@ -1346,7 +1346,7 @@
"listFilter_tooltip": "Filteren en sorteren",
"listFilter_sortBy": "Sorteren door",
"listFilter_latestMessages": "Recente berichten",
"listFilter_heardRecently": "Hoor je onlangs",
"listFilter_heardRecently": "Recent gezien",
"listFilter_az": "A-Z",
"listFilter_filters": "Filters",
"listFilter_all": "Alles",
@@ -1363,20 +1363,20 @@
}
},
"repeater_neighbors": "Buren",
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
"repeater_neighborsSubtitle": "Bekijk nul-hopsburen.",
"neighbors_receivedData": "Ontvangen Buurdata",
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
"neighbors_repeatersNeighbors": "Herhalingen Buren",
"neighbors_repeatersNeighbors": "Repeatbburen",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
"channels_createPrivateChannel": "PrivéKanaal Aanmaken",
"channels_joinPrivateChannel": "PrivéKanaal Toetreden",
"channels_joinPrivateChannelDesc": "Voer handmatig een geheime sleutel in.",
"channels_joinPublicChannel": "Publiek Kanaal Toetreden",
"channels_joinPublicChannelDesc": "Iedereen kan toetreden tot dit kanaal.",
"channels_joinHashtagChannel": "Hashtag-kanaal Aanmaken",
"channels_joinHashtagChannelDesc": "Iedereen kan toetreden tot hashtag-kanalen.",
"channels_scanQrCode": "Scan een QR-code",
"channels_scanQrCodeComingSoon": "Komt later",
"channels_enterHashtag": "Voer hashtag in",
@@ -1574,7 +1574,7 @@
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
"contacts_ShareContact": "Contact naar Klembord kopiëren",
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
"notification_activityTitle": "MeshCore Activiteit",
@@ -1612,8 +1612,8 @@
"snrIndicator_lastSeen": "Laatst gezien",
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
"chat_ShowAllPaths": "Toon alle paden",
"settings_clientRepeat": "Herhalen: Afgekoppeld",
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
"settings_clientRepeat": "Off-Grid Herhalen",
"settings_clientRepeatSubtitle": "Laat dit apparaat de berichten van andere apparaten doorsturen.",
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
"settings_aboutOpenMeteoAttribution": "LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)",
"appSettings_unitsTitle": "Eenheden",
@@ -1943,5 +1943,123 @@
"settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Herhalingssleutel overlapt",
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad."
"map_runTraceWithReturnPath": "Terugkeren op hetzelfde pad.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Ga naar het oudste ongelezen bericht",
"appSettings_jumpToOldestUnreadSubtitle": "Bij het openen van een chat met ongelezen berichten, scroll dan naar het eerste ongelezen bericht, in plaats van naar het meest recente.",
"chat_sendCooldown": "Gelieve even te wachten voordat u opnieuw verzendt.",
"appSettings_languageHu": "Hongaars",
"appSettings_languageJa": "Japanisch",
"appSettings_languageKo": "Koreaans",
"radioStats_tooltip": "Statistieken voor radio en mesh-netwerken",
"radioStats_screenTitle": "Statistieken over radio",
"radioStats_notConnected": "Verbind met een apparaat om radio-statistieken te bekijken.",
"radioStats_firmwareTooOld": "Om de statistieken via radio te kunnen gebruiken, is firmware versie 8 of een nieuwere vereist.",
"radioStats_waiting": "Wacht op gegevens…",
"radioStats_noiseFloor": "Ruisfrequentie: {noiseDbm} dBm",
"radioStats_lastRssi": "Laatste RSSI-waarde: {rssiDbm} dBm",
"radioStats_lastSnr": "Laatste SNR: {snr} dB",
"radioStats_txAir": "TX-tijd (totaal): {seconds} s",
"radioStats_rxAir": "Tijd besteed met RX (totaal): {seconds} s",
"radioStats_chartCaption": "Ruisfrequentie (dBm) over recente metingen.",
"radioStats_stripNoise": "Ruisfrequentie: {noiseDbm} dBm",
"radioStats_stripWaiting": "Radio-statistieken ophalen…",
"radioStats_settingsTile": "Statistieken over radio",
"radioStats_settingsSubtitle": "Ruimtelijke ruis, RSSI, SNR en beschikbare tijd",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Vertaal inkomende berichten en maak het mogelijk om berichten vooraf te vertalen.",
"translation_enableTitle": "Activeer vertaling",
"translation_title": "Vertaling",
"translation_composerTitle": "Vertaal voor verzending",
"translation_composerSubtitle": "Stelt de standaardstatus van het pictogram voor de vertaling van de componist in.",
"translation_useAppLanguage": "Gebruik de taal van de app",
"translation_targetLanguage": "Doeltaal",
"translation_downloadedModelLabel": "Gedownloade model",
"translation_presetModelLabel": "Voorgeprogrammeerd Hugging Face-model",
"translation_manualUrlLabel": "URL van de handleiding",
"translation_downloadModel": "Download het model",
"translation_downloading": "Downloaden...",
"translation_working": "Werken...",
"translation_mergingChunks": "Het samenvoegen van de gedownloade stukken tot één eindbestand...",
"translation_stop": "Stoppen",
"translation_downloadedModels": "Gedownloade modellen",
"translation_deleteModel": "Model verwijderen",
"translation_modelDownloaded": "Vertalingmodel gedownload.",
"translation_downloadStopped": "Download is afgebroken.",
"translation_downloadFailed": "Download mislukt: {error}",
"translation_enterUrlFirst": "Voer eerst een URL van een model in.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Stuur berichten in de oorspronkelijke, getypte taal.",
"translation_translateBeforeSending": "Vertaal voor verzending",
"translation_composerEnabledHint": "De berichten worden vertaald voordat ze verzonden worden.",
"translation_messageTranslation": "Berichtvertaling",
"translation_translationOptions": "Opties voor vertaling",
"translation_systemLanguage": "Taal van het systeem",
"translation_translateTo": "Vertalen naar {language}",
"scanner_linuxPairingShowPin": "Toon PIN",
"scanner_linuxPairingHidePin": "PIN verbergen",
"scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
"scanner_linuxPairingPinTitle": "BluetoothkoppelingsPIN",
"repeater_cliQuickDiscovery": "Ontdek Buren",
"repeater_cliQuickClockSync": "Kloksynchronisatie"
}
+130 -12
View File
@@ -163,7 +163,7 @@
"appSettings_themeLight": "Jasne",
"appSettings_themeDark": "Ciemny",
"appSettings_language": "Język",
"appSettings_languageSystem": "Domyślny systemu",
"appSettings_languageSystem": "Domyślny systemowy",
"appSettings_languageEn": "English",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
@@ -1373,7 +1373,7 @@
}
},
"repeater_neighbors": "Sąsiedzi",
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"repeater_neighborsSubtitle": "Wyświetl sąsiadów zero-hop.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
@@ -1622,12 +1622,12 @@
},
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / serwer pokojowy do GPX",
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.",
"settings_gpxExportRepeaters": "Eksportuj przekaźniki / roomservery do GPX",
"settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomservery z lokalizacją do pliku GPX.",
"settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.",
"settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym",
"settings_gpxExportError": "Wystąpił błąd podczas eksportowania.",
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i serwerów pokojowych",
"settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i roomserverów",
"settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.",
"settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX",
"settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.",
@@ -1648,7 +1648,7 @@
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
"scanner_enableBluetooth": "Włącz Bluetooth",
"snrIndicator_lastSeen": "Ostatnio widziany",
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
"snrIndicator_nearByRepeaters": "Pobliskie przekaźniki",
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
@@ -1846,12 +1846,12 @@
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie przekaźników",
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie odkrytych przekaźników.",
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj roomservery",
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
"settings_contactSettings": "Ustawienia kontaktów",
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych roomserverów.",
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
"discoveredContacts_contactAdded": "Kontakt dodany",
@@ -1979,7 +1979,125 @@
"appSettings_maxMessageRetriesSubtitle": "Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej",
"path_routeWeight": "{weight}/{max}",
"settings_telemetryModeUpdated": "Tryb telemetryczny zaktualizowany",
"settings_multiAck": "Wiele potwierdzeń: {value}",
"map_showOverlaps": "Nakładające się klucze powtarzalne",
"map_runTraceWithReturnPath": "Wróć z powrotem tą samą ścieżką"
}
"settings_multiAck": "Wielokrotne ACK: {value}",
"map_showOverlaps": "Nakładające się klucze przekaźników",
"map_runTraceWithReturnPath": "Wróć tą samą ścieżką",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_languageHu": "Węgierski",
"appSettings_jumpToOldestUnreadSubtitle": "Przy otwieraniu czatu z nieodczytanymi wiadomościami, przewijaj, aby przejść do pierwszej nieodczytanej wiadomości, zamiast do najnowszej.",
"appSettings_jumpToOldestUnread": "Przejdź do najstarszego nieodczytanej wiadomości",
"chat_sendCooldown": "Prosimy o chwilowe oczekiwanie przed ponownym wysłaniem.",
"appSettings_languageJa": "Japoński",
"appSettings_languageKo": "Koreański",
"radioStats_tooltip": "Statystyki dotyczące radia i siatki",
"radioStats_screenTitle": "Statystyki radiowe",
"radioStats_notConnected": "Połącz się z urządzeniem, aby wyświetlić statystyki radiowe.",
"radioStats_firmwareTooOld": "Statystyki radiowe wymagają towarzyszącej oprogramowania w wersji 8 lub nowszej.",
"radioStats_waiting": "Czekam na dane…",
"radioStats_noiseFloor": "Poziom szumów: {noiseDbm} dBm",
"radioStats_lastRssi": "Ostatni poziom RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Ostatni poziom SNR: {snr} dB",
"radioStats_txAir": "Czas emisji w stacji TX (całkowity): {seconds} s",
"radioStats_rxAir": "Czas wykorzystania kanału RX (całkowity): {seconds} s",
"radioStats_chartCaption": "Poziom szumów (dBm) w ostatnich próbkach.",
"radioStats_stripNoise": "Poziom szumów: {noiseDbm} dBm",
"radioStats_stripWaiting": "Pobieranie danych dotyczących radia…",
"radioStats_settingsTile": "Statystyki radiowe",
"radioStats_settingsSubtitle": "Szum tła, RSSI, SNR oraz czas dostępny",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Przekład przed wysłaniem",
"translation_title": "Tłumaczenie",
"translation_enableTitle": "Włącz tłumaczenie",
"translation_enableSubtitle": "Tłumaczenie otrzymywanych wiadomości oraz umożliwienie tłumaczenia przed wysłaniem.",
"translation_composerSubtitle": "Kontroluje domyślny stan ikony tłumaczenia w edytorze.",
"translation_targetLanguage": "Język docelowy",
"translation_useAppLanguage": "Użyj języka aplikacji",
"translation_downloadedModelLabel": "Pobudowany model",
"translation_presetModelLabel": "Wspólny model Hugging Face",
"translation_manualUrlLabel": "Adres URL do wersji manualnej",
"translation_downloadModel": "Pobierz model",
"translation_downloading": "Pobieranie...",
"translation_working": "Praca...",
"translation_stop": "Zatrzymaj się",
"translation_mergingChunks": "Scalanie pobranych fragmentów w jeden plik końcowy...",
"translation_downloadedModels": "Pobrane modele",
"translation_deleteModel": "Usuń model",
"translation_modelDownloaded": "Model tłumaczenia został pobrany.",
"translation_downloadStopped": "Pobieranie zakończone.",
"translation_downloadFailed": "Nie udało się pobrać: {error}",
"translation_enterUrlFirst": "Najpierw wprowadź adres URL modelu.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Komunikaty zostaną przetłumaczone przed wysłaniem.",
"translation_translateBeforeSending": "Przekład przed wysłaniem",
"translation_composerDisabledHint": "Wysyłaj wiadomości w oryginalnym, wpisanym formacie.",
"translation_messageTranslation": "Tłumaczenie wiadomości",
"translation_translationOptions": "Opcje tłumaczenia",
"translation_systemLanguage": "Język systemu",
"translation_translateTo": "Tłumacz na {language}",
"scanner_linuxPairingShowPin": "Pokaż PIN",
"scanner_linuxPairingHidePin": "Ukryj PIN",
"scanner_linuxPairingPinPrompt": "Wprowadź kod PIN dla {deviceName} (pozostaw puste, jeśli brak).",
"scanner_linuxPairingPinTitle": "Kod PIN parowania Bluetooth",
"repeater_cliQuickClockSync": "Synchronizacja zegara",
"repeater_cliQuickDiscovery": "Odkryj Sąsiadów"
}
+119 -1
View File
@@ -1943,5 +1943,123 @@
"settings_telemetryModeUpdated": "Modo de telemetria atualizado",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Sobreposições da Chave Repeater",
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho."
"map_runTraceWithReturnPath": "Retornar ao mesmo caminho.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnread": "Vá para a mensagem mais antiga não lida",
"chat_sendCooldown": "Por favor, aguarde um momento antes de reenviar.",
"appSettings_languageHu": "Húngaro",
"appSettings_jumpToOldestUnreadSubtitle": "Ao abrir uma conversa com mensagens não lidas, role para a primeira mensagem não lida, em vez da mais recente.",
"appSettings_languageJa": "Japonês",
"appSettings_languageKo": "Coreano",
"radioStats_tooltip": "Estatísticas de rádio e malha",
"radioStats_screenTitle": "Estatísticas de rádio",
"radioStats_notConnected": "Conecte-se a um dispositivo para visualizar estatísticas de rádio.",
"radioStats_firmwareTooOld": "As estatísticas de rádio exigem o firmware v8 ou uma versão mais recente.",
"radioStats_waiting": "Aguardando dados…",
"radioStats_noiseFloor": "Nível de ruído: {noiseDbm} dBm",
"radioStats_lastRssi": "Último RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Último SNR: {snr} dB",
"radioStats_txAir": "Tempo de transmissão da TX (total): {seconds} s",
"radioStats_rxAir": "Tempo de uso do RX (total): {seconds} s",
"radioStats_chartCaption": "Nível de ruído (dBm) em amostras recentes.",
"radioStats_stripNoise": "Nível de ruído: {noiseDbm} dBm",
"radioStats_stripWaiting": "Obtendo estatísticas de rádio…",
"radioStats_settingsTile": "Estatísticas de rádio",
"radioStats_settingsSubtitle": "Nível de ruído, RSSI, SNR e tempo de transmissão",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Traduza antes de enviar",
"translation_enableSubtitle": "Traduzir mensagens recebidas e permitir a tradução antes do envio.",
"translation_enableTitle": "Ativar a tradução",
"translation_title": "Tradução",
"translation_composerSubtitle": "Controla o estado padrão do ícone de tradução do compositor.",
"translation_targetLanguage": "Língua-alvo",
"translation_useAppLanguage": "Utilize o idioma da aplicação",
"translation_downloadedModelLabel": "Modelo baixado",
"translation_presetModelLabel": "Modelo pré-definido da Hugging Face",
"translation_manualUrlLabel": "URL do modelo manual",
"translation_downloading": "Baixando...",
"translation_downloadModel": "Baixar modelo",
"translation_working": "Trabalhando...",
"translation_stop": "Pare",
"translation_mergingChunks": "Combinando os fragmentos baixados em um único arquivo...",
"translation_downloadedModels": "Modelos baixados",
"translation_deleteModel": "Excluir modelo",
"translation_modelDownloaded": "Modelo de tradução baixado.",
"translation_downloadStopped": "Download interrompido.",
"translation_downloadFailed": "Falha na descarga: {error}",
"translation_enterUrlFirst": "Insira primeiro a URL do modelo.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_messageTranslation": "Tradução da mensagem",
"translation_translateBeforeSending": "Traduzir antes de enviar",
"translation_composerEnabledHint": "As mensagens serão traduzidas antes de serem enviadas.",
"translation_composerDisabledHint": "Envie mensagens no idioma original, conforme digitado.",
"translation_translateTo": "Traduzir para {language}",
"translation_translationOptions": "Opções de tradução",
"translation_systemLanguage": "Idioma do sistema",
"scanner_linuxPairingShowPin": "Mostrar PIN",
"scanner_linuxPairingHidePin": "Ocultar PIN",
"scanner_linuxPairingPinPrompt": "Insira o PIN para {deviceName} (deixe em branco se não houver).",
"scanner_linuxPairingPinTitle": "PIN de emparelhamento Bluetooth",
"repeater_cliQuickClockSync": "Sincronização do Relógio",
"repeater_cliQuickDiscovery": "Descobrir Vizinhos"
}
+119 -1
View File
@@ -1183,5 +1183,123 @@
"settings_telemetryModeUpdated": "Режим телеметрии обновлен",
"settings_multiAck": "Мульти-ACK: {value}",
"map_showOverlaps": "Перекрытия ключа повтора",
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути"
"map_runTraceWithReturnPath": "Вернуться обратно по тому же пути",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Пожалуйста, подождите немного, прежде чем отправлять сообщение снова.",
"appSettings_jumpToOldestUnread": "Перейти к самому старому непрочитанному сообщению",
"appSettings_languageHu": "Венгерский",
"appSettings_jumpToOldestUnreadSubtitle": "При открытии чата с непрочитанными сообщениями, прокрутите страницу, чтобы увидеть первое непрочитанное сообщение, а не последнее.",
"appSettings_languageJa": "Японский",
"appSettings_languageKo": "Корейский",
"radioStats_tooltip": "Статистика радио и беспроводной сети",
"radioStats_screenTitle": "Статистика радиовещания",
"radioStats_notConnected": "Подключитесь к устройству, чтобы просмотреть статистику радио.",
"radioStats_firmwareTooOld": "Для работы радиостатистики требуется установленная версия прошивки v8 или более новая.",
"radioStats_waiting": "Ожидаем данных…",
"radioStats_noiseFloor": "Уровень шума: {noiseDbm} дБм",
"radioStats_lastRssi": "Последнее значение RSSI: {rssiDbm} дБм",
"radioStats_lastSnr": "Последнее значение SNR: {snr} дБ",
"radioStats_txAir": "Время эфира на телеканале TX (общее): {seconds} секунд",
"radioStats_rxAir": "Общее время использования RX (в секундах): {seconds} с",
"radioStats_chartCaption": "Уровень шума (дБм) на основе последних измерений.",
"radioStats_stripNoise": "Уровень шума: {noiseDbm} дБм",
"radioStats_stripWaiting": "Получение данных о радио…",
"radioStats_settingsTile": "Статистика радиовещания",
"radioStats_settingsSubtitle": "Уровень шума, RSSI, SNR и время передачи",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Переводить входящие сообщения и позволять предварительный перевод перед отправкой.",
"translation_composerTitle": "Переводить перед отправкой",
"translation_title": "Перевод",
"translation_enableTitle": "Включить перевод",
"translation_composerSubtitle": "Управляет исходным состоянием значка перевода, предоставляемого редактором.",
"translation_targetLanguage": "Целевой язык",
"translation_useAppLanguage": "Используйте язык приложения",
"translation_downloadedModelLabel": "Загруженная модель",
"translation_presetModelLabel": "Предопределенная модель от Hugging Face",
"translation_manualUrlLabel": "Ссылка на руководство",
"translation_downloadModel": "Скачать модель",
"translation_downloading": "Загрузка...",
"translation_stop": "Прекратите",
"translation_working": "Работа...",
"translation_mergingChunks": "Объединение скачанных фрагментов в один финальный файл...",
"translation_downloadedModels": "Загруженные модели",
"translation_deleteModel": "Удалить модель",
"translation_modelDownloaded": "Модель перевода загружена.",
"translation_downloadStopped": "Процесс загрузки был прерван.",
"translation_downloadFailed": "Не удалось скачать: {error}",
"translation_enterUrlFirst": "Сначала введите URL модели.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translateBeforeSending": "Перевести перед отправкой",
"translation_composerEnabledHint": "Сообщения будут переведены перед отправкой.",
"translation_messageTranslation": "Перевод сообщения",
"translation_composerDisabledHint": "Отправляйте сообщения на языке, в котором они были изначально набраны.",
"translation_translateTo": "Перевести на {language}",
"translation_translationOptions": "Варианты перевода",
"translation_systemLanguage": "Язык системы",
"scanner_linuxPairingShowPin": "Показать PIN",
"scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
"scanner_linuxPairingHidePin": "Скрыть PIN",
"scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth",
"repeater_cliQuickDiscovery": "Обнаружить Соседей",
"repeater_cliQuickClockSync": "Синхронизация часов"
}
+119 -1
View File
@@ -1943,5 +1943,123 @@
"settings_telemetryModeUpdated": "Režim telemetrie bol aktualizovaný",
"settings_multiAck": "Viaceré ACK: {value}",
"map_showOverlaps": "Prekrývanie opakovača kľúča",
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste."
"map_runTraceWithReturnPath": "Vráťte sa späť po tej istej ceste.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Prosím, počkajte chvíľu, než zašlete znova.",
"appSettings_jumpToOldestUnread": "Presk oceň",
"appSettings_jumpToOldestUnreadSubtitle": "Pri otvorení chatu s neprečítanými správami, prejdite do prvého neprečítaného, namiesto poslednej.",
"appSettings_languageHu": "Maďarský",
"appSettings_languageJa": "Japonský",
"appSettings_languageKo": "Kórejský",
"radioStats_tooltip": "Statistiky rádiových a sieťových kanálov",
"radioStats_screenTitle": "Štatistiky rádiových vysielaní",
"radioStats_notConnected": "Pripojte sa k zariadeniu, aby ste mohli sledovať štatistiky rádiového vysielania.",
"radioStats_firmwareTooOld": "Statistické údaje z rádia vyžadujú sprievodný softvér verzie v8 alebo novšej.",
"radioStats_waiting": "Čakám na údaje…",
"radioStats_noiseFloor": "Úroveň hluku: {noiseDbm} dBm",
"radioStats_lastRssi": "Posledný údaj RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Posledná hodnota SNR: {snr} dB",
"radioStats_txAir": "Čas vysielania na TX (celkový): {seconds} s",
"radioStats_rxAir": "Čas RX (celkový): {seconds} s",
"radioStats_chartCaption": "Úroveň šumu (dBm) pre posledné vzorky.",
"radioStats_stripNoise": "Úroveň hluku: {noiseDbm} dBm",
"radioStats_stripWaiting": "Získavanie údajov o rádiu…",
"radioStats_settingsTile": "Štatistiky rádiových vysielaní",
"radioStats_settingsSubtitle": "Úroveň hluku, RSSI, SNR a časové rozloženie",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Prekladajte prichádzajúce správy a umožnite ich preklad pred odoslaním.",
"translation_enableTitle": "Aktivovať preklad",
"translation_composerTitle": "Preložte pred odeslaním",
"translation_title": "Preklad",
"translation_composerSubtitle": "Riadi výchoce stav ikony pre preklad, ktorú používa program.",
"translation_targetLanguage": "Cieľový jazyk",
"translation_useAppLanguage": "Použite jazyk aplikácie",
"translation_downloadedModelLabel": "Stiahnutý model",
"translation_presetModelLabel": "Prednastavený model od Hugging Face",
"translation_manualUrlLabel": "Odkaz na manuál (v elektronickej forme)",
"translation_downloadModel": "Stiahnuť model",
"translation_downloading": "Stiahnutie...",
"translation_working": "Práca...",
"translation_stop": "Zastavte",
"translation_mergingChunks": "Sliečenie stiahnutých častí do konečného súboru...",
"translation_downloadedModels": "Stiahnuté modely",
"translation_deleteModel": "Odstrániť model",
"translation_modelDownloaded": "Model pre preklad bol stiahnutý.",
"translation_downloadStopped": "Stiahnutie bolo prerušené.",
"translation_downloadFailed": "Neúspešné stiahnutie: {error}",
"translation_enterUrlFirst": "Najprv zadajte URL pre konkrétny model.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingHidePin": "Skryť PIN",
"scanner_linuxPairingShowPin": "Zobraziť PIN",
"scanner_linuxPairingPinTitle": "PIN pre párovanie cez Bluetooth",
"scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak neexistuje, nechajte prázdne).",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Posielajte správy v pôvodnej písanom jazyku.",
"translation_composerEnabledHint": "Správy budú preložené, než budú odoslané.",
"translation_translateBeforeSending": "Preložte pred odeslaním",
"translation_messageTranslation": "Preklad textu",
"translation_translateTo": "Preložte do {language}",
"translation_translationOptions": "Možnosti prekladania",
"translation_systemLanguage": "Jazyk systému",
"repeater_cliQuickClockSync": "Synchronizácia hodin",
"repeater_cliQuickDiscovery": "Objaviť susedov"
}
+119 -1
View File
@@ -1943,5 +1943,123 @@
"settings_multiAck": "Večkratni potrditvi: {value}",
"settings_telemetryModeUpdated": "Način telemetrije posodobljen",
"map_showOverlaps": "Prekrivanje ključa ponovnega predvajanja",
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti."
"map_runTraceWithReturnPath": "Vrni se nazaj po isti poti.",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_languageHu": "Madžarski",
"appSettings_jumpToOldestUnreadSubtitle": "Ko odpirate klepet z neprebranimi sporočili, se premaknite na prvo neprebrano sporočilo, namesto najnovejšega.",
"chat_sendCooldown": "Prosimo, počakajte trenutek, preden pošljete ponovno.",
"appSettings_jumpToOldestUnread": "Pritisnite za najstarejše nepročitano sporočilo",
"appSettings_languageJa": "Japonski",
"appSettings_languageKo": "Korejski",
"radioStats_tooltip": "Statistike za radio in mrežo",
"radioStats_notConnected": "Povežite se z napravo, da si ogledate statistiko o radiju.",
"radioStats_screenTitle": "Radijske statistike",
"radioStats_firmwareTooOld": "Statistika za radio zahteva združljivo programsko opremo v8 ali kasnejše.",
"radioStats_waiting": "Čakam na podatke…",
"radioStats_noiseFloor": "Število šuma: {noiseDbm} dBm",
"radioStats_lastRssi": "Najkasnejše vrednost RSSI: {rssiDbm} dBm",
"radioStats_lastSnr": "Najkasnejše vrednost SNR: {snr} dB",
"radioStats_txAir": "Čas na TX (skupno): {seconds} s",
"radioStats_rxAir": "Čas, namenjen RX-ju (skupno): {seconds} s",
"radioStats_chartCaption": "Ravnovredna raven šuma (dBm) za nedavne vzorce.",
"radioStats_stripNoise": "Število šuma: {noiseDbm} dBm",
"radioStats_stripWaiting": "Prejemanje statistike o radiju…",
"radioStats_settingsTile": "Radijske statistike",
"radioStats_settingsSubtitle": "Število šumov, RSSI, SNR in čas, ki ga je napolnila oprema",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Preprištejte, preden pošljete",
"translation_title": "Prevod",
"translation_enableSubtitle": "Prevedite vstopne sporočila in omogočite predhodno prevajanje.",
"translation_enableTitle": "Omogočite prevod",
"translation_composerSubtitle": "Ureja privzeto stanje ikone za prevod, ki jo uporablja avtor.",
"translation_targetLanguage": "Ciljna jezika",
"translation_useAppLanguage": "Uporabite jezik aplikacije",
"translation_downloadedModelLabel": "Naložen model",
"translation_presetModelLabel": "Prednastavljeni model Hugging Face",
"translation_manualUrlLabel": "URL za ročni model",
"translation_downloadModel": "Prenesite model",
"translation_downloading": "Izvajanje...",
"translation_working": "Delo...",
"translation_stop": "Prekliji",
"translation_mergingChunks": "Sklapljanje prenesenih delov v končni datoteko...",
"translation_downloadedModels": "Naloženi modeli",
"translation_deleteModel": "Izbrisati model",
"translation_modelDownloaded": "Model za prevajanje je bil naložen.",
"translation_downloadStopped": "Prenos je bil prekinjen.",
"translation_downloadFailed": "Izgovoritev ni bila uspešna: {error}",
"translation_enterUrlFirst": "Najprej vnesite URL model.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_translateBeforeSending": "Preprištejte, preden pošljete",
"translation_composerDisabledHint": "Pošljite sporočila v originalnem tipkanem jeziku.",
"translation_composerEnabledHint": "Vsebina sporočil bo prevedena, preden jih pošljemo.",
"translation_messageTranslation": "Prevod sporočila",
"translation_translateTo": "Prevesti v {language}",
"translation_translationOptions": "Možnosti prevoda",
"translation_systemLanguage": "Jezik sistema",
"scanner_linuxPairingShowPin": "Prikaži PIN",
"scanner_linuxPairingHidePin": "Skrij PIN",
"scanner_linuxPairingPinPrompt": "Vnesite PIN za {deviceName} (pustite prazno, če ga ni).",
"scanner_linuxPairingPinTitle": "Bluetooth PIN za seznanjanje",
"repeater_cliQuickDiscovery": "Odkrijte sosede",
"repeater_cliQuickClockSync": "Usklajevanje ure"
}
+119 -1
View File
@@ -1943,5 +1943,123 @@
"settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
"settings_multiAck": "Multi-ACKs: {value}",
"map_showOverlaps": "Repeater-nyckelöverlappningar",
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg"
"map_runTraceWithReturnPath": "Gå tillbaka på samma väg",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"appSettings_jumpToOldestUnreadSubtitle": "När du öppnar en chatt med oinlästa meddelanden, scrolla till det första oinlästa meddelandet istället för det senaste.",
"chat_sendCooldown": "Vänligen vänta en stund innan du skickar igen.",
"appSettings_jumpToOldestUnread": "Gå direkt till det äldsta, obesvarade meddelandet",
"appSettings_languageHu": "Ungerskt",
"appSettings_languageJa": "Japanska",
"appSettings_languageKo": "Koreanska",
"radioStats_tooltip": "Radio- och mesh-statistik",
"radioStats_screenTitle": "Radiostation",
"radioStats_notConnected": "Anslut till en enhet för att visa radiostatistik.",
"radioStats_firmwareTooOld": "Radio statistik kräver kompatibel firmware version 8 eller senare.",
"radioStats_waiting": "Väntar på data…",
"radioStats_noiseFloor": "Bakgrundsnivå: {noiseDbm} dBm",
"radioStats_lastRssi": "Senaste RSSI-värde: {rssiDbm} dBm",
"radioStats_lastSnr": "Senaste SNR: {snr} dB",
"radioStats_txAir": "TX-tid (total): {seconds} sekunder",
"radioStats_rxAir": "RX-tid (total): {seconds} s",
"radioStats_chartCaption": "Ljudnivå (dBm) baserat på de senaste mätningarna.",
"radioStats_stripNoise": "Bakgrundsnivå: {noiseDbm} dBm",
"radioStats_stripWaiting": "Hämtar radiostatistik…",
"radioStats_settingsTile": "Radiostation",
"radioStats_settingsSubtitle": "Bakgrundsnivå, RSSI, SNR och tillgänglig tid",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_enableSubtitle": "Översätt inkommande meddelanden och möjliggör översättning före avsändning.",
"translation_enableTitle": "Aktivera översättning",
"translation_title": "Översättning",
"translation_composerTitle": "Översätt innan du skickar",
"translation_composerSubtitle": "Styr standardtillståndet för kompositorns översättningsikon.",
"translation_targetLanguage": "Målmedvetet språk",
"translation_useAppLanguage": "Använd appens språk",
"translation_downloadedModelLabel": "Nedladdad modell",
"translation_presetModelLabel": "Fördefinierat Hugging Face-modell",
"translation_manualUrlLabel": "Manualens URL",
"translation_downloadModel": "Ladda ner modellen",
"translation_downloading": "Nedladdning...",
"translation_working": "Arbeta...",
"translation_stop": "Stopp",
"translation_mergingChunks": "Slå samman de nedladdade delarna till en slutlig fil...",
"translation_downloadedModels": "Nedladdade modeller",
"translation_deleteModel": "Ta bort modell",
"translation_modelDownloaded": "Översättningsmodellen har laddats ner.",
"translation_downloadStopped": "Nedladdningen avbruten.",
"translation_downloadFailed": "Nedladdning misslyckades: {error}",
"translation_enterUrlFirst": "Ange först en URL för en specifik modell.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "Skicka meddelanden på det ursprungliga, stavade språket.",
"translation_translateBeforeSending": "Översätt innan du skickar",
"translation_composerEnabledHint": "Meddelandena kommer att översättas innan de skickas.",
"translation_messageTranslation": "Meddelandets översättning",
"translation_translateTo": "Översätt till {language}",
"translation_translationOptions": "Översättningsalternativ",
"translation_systemLanguage": "Språk för systemet",
"scanner_linuxPairingShowPin": "Visa PIN",
"scanner_linuxPairingPinTitle": "BluetoothparningsPIN",
"scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
"scanner_linuxPairingHidePin": "Dölj PIN",
"repeater_cliQuickDiscovery": "Upptäck grannar",
"repeater_cliQuickClockSync": "Synkronisera klocka"
}
+119 -1
View File
@@ -1943,5 +1943,123 @@
"settings_telemetryModeUpdated": "Режим телеметрії оновлено",
"settings_multiAck": "Багатократне підтвердження: {value}",
"map_showOverlaps": "Перекриття ключа повторювача",
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом"
"map_runTraceWithReturnPath": "Повернутися назад тим же шляхом",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "Будь ласка, зачекайте трохи, перш ніж відправляти знову.",
"appSettings_languageHu": "Угорський",
"appSettings_jumpToOldestUnreadSubtitle": "При відкритті чату з не прочитаними повідомленнями, прокрутіть до першого не прочитаного повідомлення, а не до останнього.",
"appSettings_jumpToOldestUnread": "Перейти до найстарішого непрочитаного повідомлення",
"appSettings_languageJa": "Японська",
"appSettings_languageKo": "Кореєська",
"radioStats_tooltip": "Статистика радіо та мережі",
"radioStats_screenTitle": "Дані про радіостанції",
"radioStats_notConnected": "Підключіться до пристрою, щоб переглядати статистику радіопередач.",
"radioStats_firmwareTooOld": "Статистика радіо приймача вимагає супутнього програмного забезпечення версії 8 або новішої.",
"radioStats_waiting": "Очікую на отримання даних…",
"radioStats_noiseFloor": "Рівень шуму: {noiseDbm} дБм",
"radioStats_lastRssi": "Останній показник RSSI: {rssiDbm} дБм",
"radioStats_lastSnr": "Останній показник SNR: {snr} дБ",
"radioStats_txAir": "Час трансляції на телеканалі TX (загальний): {seconds} секунд",
"radioStats_rxAir": "Загальний час використання RX: {seconds} секунд",
"radioStats_chartCaption": "Рівень шуму (дБм) на основі останніх вимірювань.",
"radioStats_stripNoise": "Рівень шуму: {noiseDbm} дБм",
"radioStats_stripWaiting": "Отримано статистику радіо…",
"radioStats_settingsTile": "Дані про радіостанції",
"radioStats_settingsSubtitle": "Рівень шуму, RSSI, SNR та час, протягом якого пристрій використовує радіоканал.",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_composerTitle": "Перекладіть перед відправкою",
"translation_title": "Переклад",
"translation_enableTitle": "Увімкнути переклад",
"translation_enableSubtitle": "Перекладати отримані повідомлення та дозволяти попередній переклад перед відправкою.",
"translation_composerSubtitle": "Контролює стан ікон перекладу, який використовується за замовчуванням.",
"translation_targetLanguage": "Цільова мова",
"translation_useAppLanguage": "Використовуйте мову додатку",
"translation_downloadedModelLabel": "Завантажений шаблон",
"translation_presetModelLabel": "Заздалегідь налаштований модель від Hugging Face",
"translation_manualUrlLabel": "Посилання на веб-сторінку з інструкцією",
"translation_downloadModel": "Завантажити модель",
"translation_downloading": "Завантаження...",
"translation_working": "Працюю...",
"translation_stop": "Припинити",
"translation_mergingChunks": "Об'єднання завантажених фрагментів у кінцевий файл...",
"translation_downloadedModels": "Завантажені моделі",
"translation_deleteModel": "Видалити модель",
"translation_modelDownloaded": "Модель перекладу завантажена.",
"translation_downloadStopped": "Завантаження призупинено.",
"translation_downloadFailed": "Не вдалося завантажити: {error}",
"translation_enterUrlFirst": "Спочатку введіть URL моделі.",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerEnabledHint": "Повідомлення будуть перекладені перед відправленням.",
"translation_messageTranslation": "Переклад повідомлення",
"translation_composerDisabledHint": "Надсилайте повідомлення, використовуючи оригінальний текстовий формат.",
"translation_translateBeforeSending": "Перекладіть перед відправкою",
"translation_translateTo": "Перекласти на {language}",
"translation_translationOptions": "Варіанти перекладу",
"translation_systemLanguage": "Мова системи",
"scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
"scanner_linuxPairingShowPin": "Показати PIN",
"scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
"scanner_linuxPairingHidePin": "Приховати PIN",
"repeater_cliQuickClockSync": "Синхронізація годинника",
"repeater_cliQuickDiscovery": "Відкрити сусідів"
}
+119 -1
View File
@@ -1948,5 +1948,123 @@
"settings_multiAck": "多重ACK{value}",
"settings_telemetryModeUpdated": "遥测模式已更新",
"map_showOverlaps": "重复键重叠",
"map_runTraceWithReturnPath": "沿着相同的路径返回"
"map_runTraceWithReturnPath": "沿着相同的路径返回",
"@radioStats_noiseFloor": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"@radioStats_lastRssi": {
"placeholders": {
"rssiDbm": {
"type": "int"
}
}
},
"@radioStats_lastSnr": {
"placeholders": {
"snr": {
"type": "String"
}
}
},
"@radioStats_txAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_rxAir": {
"placeholders": {
"seconds": {
"type": "int"
}
}
},
"@radioStats_stripNoise": {
"placeholders": {
"noiseDbm": {
"type": "int"
}
}
},
"chat_sendCooldown": "请稍等片刻后再尝试发送。",
"appSettings_jumpToOldestUnreadSubtitle": "在打开包含未读消息的聊天时,请滚动到第一个未读消息,而不是最新的消息。",
"appSettings_jumpToOldestUnread": "跳转到最旧、未读的文章",
"appSettings_languageHu": "匈牙利",
"appSettings_languageJa": "日语",
"appSettings_languageKo": "韩语",
"radioStats_tooltip": "无线电和网状结构统计数据",
"radioStats_screenTitle": "广播统计数据",
"radioStats_notConnected": "连接到设备以查看收音机统计信息。",
"radioStats_firmwareTooOld": "使用无线电统计功能需要配合使用 v8 或更高版本的固件。",
"radioStats_waiting": "正在等待数据…",
"radioStats_noiseFloor": "噪声水平:{noiseDbm} dBm",
"radioStats_lastRssi": "上次 RSSI 值:{rssiDbm} dBm",
"radioStats_lastSnr": "上次 SNR{snr} dB",
"radioStats_txAir": "TX 频道播出时间(总时长):{seconds} 秒",
"radioStats_rxAir": "RX 使用时长(总时长):{seconds} 秒",
"radioStats_chartCaption": "近期的噪声水平(dBm)。",
"radioStats_stripNoise": "噪声水平:{noiseDbm} dBm",
"radioStats_stripWaiting": "正在获取收音机数据…",
"radioStats_settingsTile": "广播统计数据",
"radioStats_settingsSubtitle": "噪声水平、RSSI、信噪比和空中时间",
"@translation_downloadFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"translation_title": "翻译",
"translation_enableSubtitle": "翻译收到的消息,并允许在发送前进行翻译。",
"translation_composerTitle": "在发送之前进行翻译",
"translation_enableTitle": "启用翻译功能",
"translation_composerSubtitle": "控制作曲家翻译图标的默认状态。",
"translation_targetLanguage": "目标语言",
"translation_useAppLanguage": "使用应用程序语言",
"translation_downloadedModelLabel": "下载的模型",
"translation_presetModelLabel": "预设的 Hugging Face 模型",
"translation_downloadModel": "下载模型",
"translation_manualUrlLabel": "手动模型网址",
"translation_downloading": "正在下载...",
"translation_working": "工作中...",
"translation_stop": "停止",
"translation_mergingChunks": "将下载的片段合并成最终文件...",
"translation_downloadedModels": "下载的模型",
"translation_deleteModel": "删除模型",
"translation_modelDownloaded": "翻译模型已下载。",
"translation_downloadStopped": "下载已停止。",
"translation_downloadFailed": "下载失败:{error}",
"translation_enterUrlFirst": "首先,请输入模型的 URL。",
"@scanner_linuxPairingPinPrompt": {
"placeholders": {
"deviceName": {
"type": "String"
}
}
},
"scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
"scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN 码(如果为空,则留空)。",
"scanner_linuxPairingHidePin": "隐藏 PIN",
"scanner_linuxPairingShowPin": "显示PIN码",
"@translation_translateTo": {
"placeholders": {
"language": {
"type": "String"
}
}
},
"translation_composerDisabledHint": "使用原始的打字方式发送消息。",
"translation_messageTranslation": "消息翻译",
"translation_composerEnabledHint": "消息将在发送前进行翻译。",
"translation_translateBeforeSending": "在发送前进行翻译",
"translation_translateTo": "翻译成 {language}",
"translation_translationOptions": "翻译选项",
"translation_systemLanguage": "系统语言",
"repeater_cliQuickDiscovery": "发现邻居",
"repeater_cliQuickClockSync": "同步时钟"
}
+8
View File
@@ -19,6 +19,7 @@ import 'services/app_debug_log_service.dart';
import 'services/background_service.dart';
import 'services/map_tile_cache_service.dart';
import 'services/chat_text_scale_service.dart';
import 'services/translation_service.dart';
import 'services/ui_view_state_service.dart';
import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart';
@@ -41,6 +42,7 @@ void main() async {
final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService();
final chatTextScaleService = ChatTextScaleService();
final translationService = TranslationService(appSettingsService);
final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage);
@@ -60,6 +62,7 @@ void main() async {
_registerThirdPartyLicenses();
await chatTextScaleService.initialize();
await translationService.refreshDownloadedModels();
await uiViewStateService.initialize();
await timeoutPredictionService.initialize();
@@ -68,6 +71,7 @@ void main() async {
retryService: retryService,
pathHistoryService: pathHistoryService,
appSettingsService: appSettingsService,
translationService: translationService,
bleDebugLogService: bleDebugLogService,
appDebugLogService: appDebugLogService,
backgroundService: backgroundService,
@@ -93,6 +97,7 @@ void main() async {
appDebugLogService: appDebugLogService,
mapTileCacheService: mapTileCacheService,
chatTextScaleService: chatTextScaleService,
translationService: translationService,
uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService,
),
@@ -130,6 +135,7 @@ class MeshCoreApp extends StatelessWidget {
final AppDebugLogService appDebugLogService;
final MapTileCacheService mapTileCacheService;
final ChatTextScaleService chatTextScaleService;
final TranslationService translationService;
final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService;
@@ -144,6 +150,7 @@ class MeshCoreApp extends StatelessWidget {
required this.appDebugLogService,
required this.mapTileCacheService,
required this.chatTextScaleService,
required this.translationService,
required this.uiViewStateService,
required this.timeoutPredictionService,
});
@@ -159,6 +166,7 @@ class MeshCoreApp extends StatelessWidget {
ChangeNotifierProvider.value(value: bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService),
ChangeNotifierProvider.value(value: translationService),
ChangeNotifierProvider.value(value: uiViewStateService),
Provider.value(value: storage),
Provider.value(value: mapTileCacheService),
+68 -1
View File
@@ -1,3 +1,5 @@
import 'translation_support.dart';
enum UnitSystem { metric, imperial }
extension UnitSystemValue on UnitSystem {
@@ -48,6 +50,13 @@ class AppSettings {
final bool mapShowDiscoveryContacts;
final String tcpServerAddress;
final int tcpServerPort;
final bool jumpToOldestUnread;
final bool translationEnabled;
final String? translationTargetLanguageCode;
final bool composerTranslationEnabled;
final String? translationModelSourceUrl;
final String? translationSelectedModelId;
final List<TranslationModelRecord> translationDownloadedModels;
AppSettings({
this.clearPathOnMaxRetry = false,
@@ -84,9 +93,17 @@ class AppSettings {
this.mapShowDiscoveryContacts = true,
this.tcpServerAddress = '',
this.tcpServerPort = 0,
this.jumpToOldestUnread = false,
this.translationEnabled = false,
this.translationTargetLanguageCode,
this.composerTranslationEnabled = false,
this.translationModelSourceUrl,
this.translationSelectedModelId,
List<TranslationModelRecord>? translationDownloadedModels,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {};
mutedChannels = mutedChannels ?? {},
translationDownloadedModels = translationDownloadedModels ?? const [];
Map<String, dynamic> toJson() {
return {
@@ -124,6 +141,15 @@ class AppSettings {
'map_show_discovery_contacts': mapShowDiscoveryContacts,
'tcp_server_address': tcpServerAddress,
'tcp_server_port': tcpServerPort,
'jump_to_oldest_unread': jumpToOldestUnread,
'translation_enabled': translationEnabled,
'translation_target_language_code': translationTargetLanguageCode,
'composer_translation_enabled': composerTranslationEnabled,
'translation_model_source_url': translationModelSourceUrl,
'translation_selected_model_id': translationSelectedModelId,
'translation_downloaded_models': translationDownloadedModels
.map((model) => model.toJson())
.toList(),
};
}
@@ -192,6 +218,25 @@ class AppSettings {
json['map_show_discovery_contacts'] as bool? ?? true,
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
translationEnabled: json['translation_enabled'] as bool? ?? false,
translationTargetLanguageCode:
json['translation_target_language_code'] as String?,
composerTranslationEnabled:
json['composer_translation_enabled'] as bool? ?? false,
translationModelSourceUrl:
json['translation_model_source_url'] as String?,
translationSelectedModelId:
json['translation_selected_model_id'] as String?,
translationDownloadedModels:
(json['translation_downloaded_models'] as List<dynamic>?)
?.map(
(entry) => TranslationModelRecord.fromJson(
Map<String, dynamic>.from(entry as Map),
),
)
.toList() ??
const [],
);
}
@@ -230,6 +275,13 @@ class AppSettings {
bool? mapShowDiscoveryContacts,
String? tcpServerAddress,
int? tcpServerPort,
bool? jumpToOldestUnread,
bool? translationEnabled,
Object? translationTargetLanguageCode = _unset,
bool? composerTranslationEnabled,
Object? translationModelSourceUrl = _unset,
Object? translationSelectedModelId = _unset,
List<TranslationModelRecord>? translationDownloadedModels,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
@@ -278,6 +330,21 @@ class AppSettings {
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
translationEnabled: translationEnabled ?? this.translationEnabled,
translationTargetLanguageCode: translationTargetLanguageCode == _unset
? this.translationTargetLanguageCode
: translationTargetLanguageCode as String?,
composerTranslationEnabled:
composerTranslationEnabled ?? this.composerTranslationEnabled,
translationModelSourceUrl: translationModelSourceUrl == _unset
? this.translationModelSourceUrl
: translationModelSourceUrl as String?,
translationSelectedModelId: translationSelectedModelId == _unset
? this.translationSelectedModelId
: translationSelectedModelId as String?,
translationDownloadedModels:
translationDownloadedModels ?? this.translationDownloadedModels,
);
}
}
+39 -2
View File
@@ -2,6 +2,7 @@ import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart';
import 'translation_support.dart';
import '../utils/app_logger.dart';
enum ChannelMessageStatus { pending, sent, failed }
@@ -24,9 +25,16 @@ class Repeat {
}
class ChannelMessage {
static const Object _unset = Object();
final Uint8List? senderKey;
final String senderName;
final String text;
final String? originalText;
final String? translatedText;
final String? translatedLanguageCode;
final MessageTranslationStatus translationStatus;
final String? translationModelId;
final DateTime timestamp;
final bool isOutgoing;
final ChannelMessageStatus status;
@@ -47,6 +55,11 @@ class ChannelMessage {
this.senderKey,
required this.senderName,
required this.text,
this.originalText,
this.translatedText,
this.translatedLanguageCode,
this.translationStatus = MessageTranslationStatus.none,
this.translationModelId,
required this.timestamp,
required this.isOutgoing,
this.status = ChannelMessageStatus.pending,
@@ -86,12 +99,30 @@ class ChannelMessage {
String? replyToMessageId,
String? replyToSenderName,
String? replyToText,
Object? originalText = _unset,
Object? translatedText = _unset,
Object? translatedLanguageCode = _unset,
MessageTranslationStatus? translationStatus,
Object? translationModelId = _unset,
Map<String, int>? reactions,
}) {
return ChannelMessage(
senderKey: senderKey,
senderName: senderName,
text: text,
originalText: originalText == _unset
? this.originalText
: originalText as String?,
translatedText: translatedText == _unset
? this.translatedText
: translatedText as String?,
translatedLanguageCode: translatedLanguageCode == _unset
? this.translatedLanguageCode
: translatedLanguageCode as String?,
translationStatus: translationStatus ?? this.translationStatus,
translationModelId: translationModelId == _unset
? this.translationModelId
: translationModelId as String?,
timestamp: timestamp,
isOutgoing: isOutgoing,
status: status ?? this.status,
@@ -191,12 +222,18 @@ class ChannelMessage {
static ChannelMessage outgoing(
String text,
String senderName,
int channelIndex,
) {
int channelIndex, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
}) {
return ChannelMessage(
senderKey: null,
senderName: senderName,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
status: ChannelMessageStatus.pending,
+48
View File
@@ -0,0 +1,48 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../utils/app_logger.dart';
/// Parsed `RESP_CODE_STATS` + `STATS_TYPE_RADIO` (14 bytes total).
class CompanionRadioStats {
final int noiseFloorDbm;
final int lastRssiDbm;
final double lastSnrDb;
final int txAirSecs;
final int rxAirSecs;
final DateTime receivedAt;
const CompanionRadioStats({
required this.noiseFloorDbm,
required this.lastRssiDbm,
required this.lastSnrDb,
required this.txAirSecs,
required this.rxAirSecs,
required this.receivedAt,
});
static CompanionRadioStats? tryParse(Uint8List frame) {
if (frame.length < 14) return null;
if (frame[0] != respCodeStats || frame[1] != statsTypeRadio) return null;
try {
final reader = BufferReader(frame);
reader.skipBytes(2);
final noise = reader.readInt16LE();
final rssi = reader.readInt8();
final snrRaw = reader.readInt8();
final txAir = reader.readUInt32LE();
final rxAir = reader.readUInt32LE();
return CompanionRadioStats(
noiseFloorDbm: noise,
lastRssiDbm: rssi,
lastSnrDb: snrRaw / 4.0,
txAirSecs: txAir,
rxAirSecs: rxAir,
receivedAt: DateTime.now(),
);
} catch (e) {
appLogger.warn('CompanionRadioStats parse error: $e');
return null;
}
}
}
+8 -6
View File
@@ -119,15 +119,14 @@ class Contact {
);
}
String get pathIdList {
/// Formats path bytes into comma-separated hex groups of [hashByteWidth] bytes.
String pathFormattedIdList(int hashByteWidth) {
final pathBytes = pathBytesForDisplay;
if (pathBytes.isEmpty) return '';
final w = hashByteWidth.clamp(1, 8);
final parts = <String>[];
final groupSize = pathHashSize;
for (int i = 0; i < pathBytes.length; i += groupSize) {
final end = (i + groupSize) <= pathBytes.length
? (i + groupSize)
: pathBytes.length;
for (int i = 0; i < pathBytes.length; i += w) {
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
final chunk = pathBytes.sublist(i, end);
parts.add(
chunk
@@ -138,6 +137,9 @@ class Contact {
return parts.join(',');
}
/// Default grouping uses legacy single-byte hop hash width.
String get pathIdList => pathFormattedIdList(pathHashSize);
String get shortPubKeyHex {
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
}
+43 -3
View File
@@ -1,19 +1,27 @@
import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import 'translation_support.dart';
enum MessageStatus { pending, sent, delivered, failed }
class Message {
static const Object _unset = Object();
final Uint8List senderKey;
final String text;
final DateTime timestamp;
final bool isOutgoing;
final bool isCli;
final MessageStatus status;
final String? originalText;
final String? translatedText;
final String? translatedLanguageCode;
final MessageTranslationStatus translationStatus;
final String? translationModelId;
// NEW: Retry logic fields
final String? messageId;
final String messageId;
final int retryCount;
final int? estimatedTimeoutMs;
final int? expectedAckHash;
@@ -33,7 +41,12 @@ class Message {
required this.isOutgoing,
this.isCli = false,
this.status = MessageStatus.pending,
this.messageId,
String? messageId,
this.originalText,
this.translatedText,
this.translatedLanguageCode,
this.translationStatus = MessageTranslationStatus.none,
this.translationModelId,
this.retryCount = 0,
this.estimatedTimeoutMs,
this.expectedAckHash,
@@ -45,7 +58,10 @@ class Message {
Uint8List? fourByteRoomContactKey,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
}) : pathBytes = pathBytes ?? Uint8List(0),
}) : messageId =
messageId ??
'${timestamp.millisecondsSinceEpoch}_${pubKeyToHex(senderKey)}_${text.hashCode}',
pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
reactions = reactions ?? {},
reactionStatuses = reactionStatuses ?? {};
@@ -63,6 +79,11 @@ class Message {
int? pathLength,
Uint8List? pathBytes,
bool? isCli,
Object? originalText = _unset,
Object? translatedText = _unset,
Object? translatedLanguageCode = _unset,
MessageTranslationStatus? translationStatus,
Object? translationModelId = _unset,
Map<String, int>? reactions,
Map<String, MessageStatus>? reactionStatuses,
Uint8List? fourByteRoomContactKey,
@@ -75,6 +96,19 @@ class Message {
isCli: isCli ?? this.isCli,
status: status ?? this.status,
messageId: messageId,
originalText: originalText == _unset
? this.originalText
: originalText as String?,
translatedText: translatedText == _unset
? this.translatedText
: translatedText as String?,
translatedLanguageCode: translatedLanguageCode == _unset
? this.translatedLanguageCode
: translatedLanguageCode as String?,
translationStatus: translationStatus ?? this.translationStatus,
translationModelId: translationModelId == _unset
? this.translationModelId
: translationModelId as String?,
retryCount: retryCount ?? this.retryCount,
estimatedTimeoutMs: estimatedTimeoutMs ?? this.estimatedTimeoutMs,
expectedAckHash: expectedAckHash ?? this.expectedAckHash,
@@ -124,12 +158,18 @@ class Message {
static Message outgoing(
Uint8List recipientKey,
String text, {
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
int? pathLength,
Uint8List? pathBytes,
}) {
return Message(
senderKey: recipientKey,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
isCli: false,
+136
View File
@@ -0,0 +1,136 @@
enum MessageTranslationStatus { none, pending, completed, failed, skipped }
extension MessageTranslationStatusValue on MessageTranslationStatus {
String get value {
switch (this) {
case MessageTranslationStatus.pending:
return 'pending';
case MessageTranslationStatus.completed:
return 'completed';
case MessageTranslationStatus.failed:
return 'failed';
case MessageTranslationStatus.skipped:
return 'skipped';
case MessageTranslationStatus.none:
return 'none';
}
}
}
MessageTranslationStatus parseMessageTranslationStatus(dynamic value) {
if (value is! String) {
return MessageTranslationStatus.none;
}
for (final status in MessageTranslationStatus.values) {
if (status.value == value) {
return status;
}
}
return MessageTranslationStatus.none;
}
class TranslationModelRecord {
final String id;
final String name;
final String sourceUrl;
final String localPath;
final DateTime downloadedAt;
final int fileSizeBytes;
const TranslationModelRecord({
required this.id,
required this.name,
required this.sourceUrl,
required this.localPath,
required this.downloadedAt,
required this.fileSizeBytes,
});
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'source_url': sourceUrl,
'local_path': localPath,
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
'file_size_bytes': fileSizeBytes,
};
}
factory TranslationModelRecord.fromJson(Map<String, dynamic> json) {
return TranslationModelRecord(
id: json['id'] as String? ?? '',
name: json['name'] as String? ?? '',
sourceUrl: json['source_url'] as String? ?? '',
localPath: json['local_path'] as String? ?? '',
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
json['downloaded_at'] as int? ?? 0,
),
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
);
}
}
String translationModelFriendlyName(TranslationModelRecord model) {
switch (model.id) {
case 'hy-mt1.5-1.8b-q4_k_m':
return 'Tencent HY-MT 1.5 1.8B Q4_K_M';
case 'hy-mt1.5-1.8b-q6_k':
return 'Tencent HY-MT 1.5 1.8B Q6_K';
default:
final trimmed = model.name.trim();
if (trimmed.endsWith('.gguf')) {
return trimmed.substring(0, trimmed.length - 5);
}
return trimmed.isEmpty ? model.id : trimmed;
}
}
class TranslationLanguageOption {
final String code;
final String label;
const TranslationLanguageOption({required this.code, required this.label});
}
const List<TranslationLanguageOption> supportedTranslationLanguages = [
TranslationLanguageOption(code: 'bg', label: 'Bulgarian'),
TranslationLanguageOption(code: 'de', label: 'German'),
TranslationLanguageOption(code: 'en', label: 'English'),
TranslationLanguageOption(code: 'es', label: 'Spanish'),
TranslationLanguageOption(code: 'fr', label: 'French'),
TranslationLanguageOption(code: 'hu', label: 'Hungarian'),
TranslationLanguageOption(code: 'it', label: 'Italian'),
TranslationLanguageOption(code: 'ja', label: 'Japanese'),
TranslationLanguageOption(code: 'ko', label: 'Korean'),
TranslationLanguageOption(code: 'nl', label: 'Dutch'),
TranslationLanguageOption(code: 'pl', label: 'Polish'),
TranslationLanguageOption(code: 'pt', label: 'Portuguese'),
TranslationLanguageOption(code: 'ru', label: 'Russian'),
TranslationLanguageOption(code: 'sk', label: 'Slovak'),
TranslationLanguageOption(code: 'sl', label: 'Slovenian'),
TranslationLanguageOption(code: 'sv', label: 'Swedish'),
TranslationLanguageOption(code: 'uk', label: 'Ukrainian'),
TranslationLanguageOption(code: 'zh', label: 'Chinese'),
];
final List<TranslationModelRecord> translationPresetModels = [
TranslationModelRecord(
id: 'hy-mt1.5-1.8b-q4_k_m',
name: 'HY-MT1.5-1.8B-Q4_K_M.gguf',
sourceUrl:
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q4_K_M.gguf?download=true',
localPath: '',
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
fileSizeBytes: 0,
),
TranslationModelRecord(
id: 'hy-mt1.5-1.8b-q6_k',
name: 'HY-MT1.5-1.8B-Q6_K.gguf',
sourceUrl:
'https://huggingface.co/tencent/HY-MT1.5-1.8B-GGUF/resolve/main/HY-MT1.5-1.8B-Q6_K.gguf?download=true',
localPath: '',
downloadedAt: DateTime.fromMillisecondsSinceEpoch(0),
fileSizeBytes: 0,
),
];
+568 -20
View File
@@ -1,11 +1,14 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/notification_service.dart';
import '../services/translation_service.dart';
import '../widgets/adaptive_app_bar_title.dart';
import 'map_cache_screen.dart';
@@ -21,26 +24,46 @@ class AppSettingsScreen extends StatelessWidget {
),
body: SafeArea(
top: false,
child: Consumer2<AppSettingsService, MeshCoreConnector>(
builder: (context, settingsService, connector, child) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_buildAppearanceCard(context, settingsService),
const SizedBox(height: 16),
_buildNotificationsCard(context, settingsService),
const SizedBox(height: 16),
_buildMessagingCard(context, settingsService),
const SizedBox(height: 16),
_buildBatteryCard(context, settingsService, connector),
const SizedBox(height: 16),
_buildMapSettingsCard(context, settingsService),
const SizedBox(height: 16),
_buildDebugCard(context, settingsService),
],
);
},
),
child:
Consumer3<
AppSettingsService,
MeshCoreConnector,
TranslationService
>(
builder:
(
context,
settingsService,
connector,
translationService,
child,
) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
_buildAppearanceCard(context, settingsService),
const SizedBox(height: 16),
_buildNotificationsCard(context, settingsService),
const SizedBox(height: 16),
_buildMessagingCard(context, settingsService),
const SizedBox(height: 16),
if (!kIsWeb) ...[
_buildTranslationCard(
context,
settingsService,
translationService,
),
const SizedBox(height: 16),
],
_buildBatteryCard(context, settingsService, connector),
const SizedBox(height: 16),
_buildMapSettingsCard(context, settingsService),
const SizedBox(height: 16),
_buildDebugCard(context, settingsService),
],
);
},
),
),
);
}
@@ -291,6 +314,14 @@ class AppSettingsScreen extends StatelessWidget {
},
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.vertical_align_top),
title: Text(context.l10n.appSettings_jumpToOldestUnread),
subtitle: Text(context.l10n.appSettings_jumpToOldestUnreadSubtitle),
value: settingsService.settings.jumpToOldestUnread,
onChanged: settingsService.setJumpToOldestUnread,
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.alt_route),
title: Text(context.l10n.appSettings_autoRouteRotation),
@@ -522,6 +553,211 @@ class AppSettingsScreen extends StatelessWidget {
);
}
Widget _buildTranslationCard(
BuildContext context,
AppSettingsService settingsService,
TranslationService translationService,
) {
final settings = settingsService.settings;
return Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
context.l10n.translation_title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
SwitchListTile(
secondary: const Icon(Icons.translate),
title: Text(context.l10n.translation_enableTitle),
subtitle: Text(context.l10n.translation_enableSubtitle),
value: settings.translationEnabled,
onChanged: settingsService.setTranslationEnabled,
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.outgoing_mail),
title: Text(context.l10n.translation_composerTitle),
subtitle: Text(context.l10n.translation_composerSubtitle),
value: settings.composerTranslationEnabled,
onChanged: settingsService.setComposerTranslationEnabled,
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.language),
title: Text(context.l10n.translation_targetLanguage),
subtitle: Text(
_translationLanguageLabel(
context,
settings.translationTargetLanguageCode,
),
),
trailing: const Icon(Icons.chevron_right),
onTap: () =>
_showTranslationLanguageDialog(context, settingsService),
),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: DropdownButtonFormField<String>(
initialValue: settings.translationSelectedModelId,
isExpanded: true,
decoration: InputDecoration(
labelText: context.l10n.translation_downloadedModelLabel,
border: const OutlineInputBorder(),
),
items: [
for (final model in settings.translationDownloadedModels)
DropdownMenuItem(
value: model.id,
child: Text(translationModelFriendlyName(model)),
),
],
onChanged: settings.translationDownloadedModels.isEmpty
? null
: (value) {
settingsService.setTranslationSelectedModelId(value);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: DropdownButtonFormField<String>(
initialValue: null,
isExpanded: true,
decoration: InputDecoration(
labelText: context.l10n.translation_presetModelLabel,
border: const OutlineInputBorder(),
),
items: [
for (final preset in translationPresetModels)
DropdownMenuItem(
value: preset.sourceUrl,
child: Text(translationModelFriendlyName(preset)),
),
],
onChanged: translationService.isBusy
? null
: (value) async {
if (value == null) return;
final preset = translationPresetModels.firstWhere(
(entry) => entry.sourceUrl == value,
);
await _downloadTranslationModel(
context,
translationService,
settingsService,
sourceUrl: preset.sourceUrl,
fileName: preset.name,
id: preset.id,
);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
child: Column(
children: [
_TranslationUrlField(
initialValue: settings.translationModelSourceUrl ?? '',
onChanged: settingsService.setTranslationModelSourceUrl,
onDownload: translationService.isBusy
? null
: (url) => _downloadTranslationModel(
context,
translationService,
settingsService,
sourceUrl: url,
),
downloadLabel: translationService.isDownloading
? context.l10n.translation_downloading
: translationService.isBusy
? context.l10n.translation_working
: context.l10n.translation_downloadModel,
isDownloading: translationService.isDownloading,
onCancel: translationService.cancelDownload,
labelText: context.l10n.translation_manualUrlLabel,
stopLabel: context.l10n.translation_stop,
),
if (translationService.isDownloading) ...[
const SizedBox(height: 12),
LinearProgressIndicator(
value:
translationService.downloadFileName ==
'Merging chunks...'
? null
: translationService.downloadProgress,
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerLeft,
child: Text(
_downloadProgressLabel(context, translationService),
style: Theme.of(context).textTheme.bodySmall,
),
),
],
if (settings.translationDownloadedModels.isNotEmpty) ...[
const SizedBox(height: 16),
Align(
alignment: Alignment.centerLeft,
child: Text(
context.l10n.translation_downloadedModels,
style: Theme.of(context).textTheme.titleSmall,
),
),
const SizedBox(height: 8),
for (final model in settings.translationDownloadedModels)
Card.outlined(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 4,
),
leading: Icon(
model.id == settings.translationSelectedModelId
? Icons.check_circle
: Icons.memory_outlined,
),
title: Text(translationModelFriendlyName(model)),
subtitle: Text(_downloadedModelLabel(model)),
trailing: IconButton(
tooltip: context.l10n.translation_deleteModel,
onPressed: translationService.isBusy
? null
: () => _deleteTranslationModel(
context,
translationService,
model,
),
icon: const Icon(Icons.delete_outline),
),
onTap: () => settingsService
.setTranslationSelectedModelId(model.id),
),
),
],
if (translationService.lastError != null) ...[
const SizedBox(height: 8),
Text(
translationService.lastError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
],
],
),
),
],
),
);
}
// Fixed rendering issues
Widget _buildBatteryCard(
BuildContext context,
@@ -689,6 +925,12 @@ class AppSettingsScreen extends StatelessWidget {
return context.l10n.appSettings_languageRu;
case 'uk':
return context.l10n.appSettings_languageUk;
case 'hu':
return context.l10n.appSettings_languageHu;
case 'ja':
return context.l10n.appSettings_languageJa;
case 'ko':
return context.l10n.appSettings_languageKo;
default:
return context.l10n.appSettings_languageSystem;
}
@@ -776,6 +1018,18 @@ class AppSettingsScreen extends StatelessWidget {
title: Text(context.l10n.appSettings_languageUk),
value: 'uk',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageHu),
value: 'hu',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageJa),
value: 'ja',
),
RadioListTile<String?>(
title: Text(context.l10n.appSettings_languageKo),
value: 'ko',
),
],
),
),
@@ -884,6 +1138,124 @@ class AppSettingsScreen extends StatelessWidget {
);
}
void _showTranslationLanguageDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (context) => _TranslationLanguageDialogContent(
currentLanguageCode:
settingsService.settings.translationTargetLanguageCode,
onLanguageSelected: (value) {
settingsService.setTranslationTargetLanguageCode(value);
Navigator.pop(context);
},
),
);
}
Future<void> _downloadTranslationModel(
BuildContext context,
TranslationService translationService,
AppSettingsService settingsService, {
required String sourceUrl,
String? fileName,
String? id,
}) async {
if (sourceUrl.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.translation_enterUrlFirst)),
);
return;
}
try {
await translationService.downloadModel(
sourceUrl: sourceUrl,
fileName: fileName,
id: id,
);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.translation_modelDownloaded)),
);
await settingsService.setTranslationEnabled(true);
} on TranslationDownloadCancelled {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.translation_downloadStopped)),
);
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.translation_downloadFailed(error.toString()),
),
),
);
}
}
String _translationLanguageLabel(BuildContext context, String? languageCode) {
if (languageCode == null || languageCode.isEmpty) {
return context.l10n.translation_useAppLanguage;
}
for (final option in supportedTranslationLanguages) {
if (option.code == languageCode) {
return option.label;
}
}
return languageCode.toUpperCase();
}
String _downloadProgressLabel(
BuildContext context,
TranslationService translationService,
) {
final fileName = translationService.downloadFileName ?? 'Model';
if (fileName == 'Merging chunks...') {
return context.l10n.translation_mergingChunks;
}
final currentMb = translationService.downloadedBytes / (1024 * 1024);
final totalBytes = translationService.downloadTotalBytes;
if (totalBytes == null || totalBytes <= 0) {
return '$fileName: ${currentMb.toStringAsFixed(1)} MB';
}
final totalMb = totalBytes / (1024 * 1024);
final percent = ((translationService.downloadProgress ?? 0) * 100)
.toStringAsFixed(0);
return '$fileName: ${currentMb.toStringAsFixed(1)} / ${totalMb.toStringAsFixed(1)} MB ($percent%)';
}
Future<void> _deleteTranslationModel(
BuildContext context,
TranslationService translationService,
TranslationModelRecord model,
) async {
try {
await translationService.removeModel(model);
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
// TODO: l10n
content: Text('Deleted ${translationModelFriendlyName(model)}.'),
),
);
} catch (error) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Delete failed: $error')),
); // TODO: l10n
}
}
String _downloadedModelLabel(TranslationModelRecord model) {
final sizeMb = model.fileSizeBytes / (1024 * 1024);
final source = model.sourceUrl.isEmpty ? model.name : model.sourceUrl;
return '${sizeMb.toStringAsFixed(1)} MB • $source';
}
Widget _buildDebugCard(
BuildContext context,
AppSettingsService settingsService,
@@ -924,3 +1296,179 @@ class AppSettingsScreen extends StatelessWidget {
);
}
}
/// Owns the [TextEditingController] for the manual model URL field so it
/// survives rebuilds of the parent [Consumer3].
class _TranslationUrlField extends StatefulWidget {
const _TranslationUrlField({
required this.initialValue,
required this.onChanged,
required this.onDownload,
required this.downloadLabel,
required this.isDownloading,
required this.onCancel,
required this.labelText,
required this.stopLabel,
});
final String initialValue;
final ValueChanged<String> onChanged;
final void Function(String url)? onDownload;
final String downloadLabel;
final bool isDownloading;
final VoidCallback onCancel;
final String labelText;
final String stopLabel;
@override
State<_TranslationUrlField> createState() => _TranslationUrlFieldState();
}
class _TranslationUrlFieldState extends State<_TranslationUrlField> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialValue);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _controller,
decoration: InputDecoration(
labelText: widget.labelText,
border: const OutlineInputBorder(),
),
onChanged: widget.onChanged,
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: FilledButton.icon(
onPressed: widget.onDownload == null
? null
: () => widget.onDownload!(_controller.text.trim()),
icon: const Icon(Icons.download),
label: Text(widget.downloadLabel),
),
),
if (widget.isDownloading) ...[
const SizedBox(width: 8),
OutlinedButton.icon(
onPressed: widget.onCancel,
icon: const Icon(Icons.stop_circle_outlined),
label: Text(widget.stopLabel),
),
],
],
),
],
);
}
}
/// Dialog content for choosing the translation target language.
/// Owns the search [TextEditingController] so it is properly disposed.
class _TranslationLanguageDialogContent extends StatefulWidget {
const _TranslationLanguageDialogContent({
required this.currentLanguageCode,
required this.onLanguageSelected,
});
final String? currentLanguageCode;
final ValueChanged<String?> onLanguageSelected;
@override
State<_TranslationLanguageDialogContent> createState() =>
_TranslationLanguageDialogContentState();
}
class _TranslationLanguageDialogContentState
extends State<_TranslationLanguageDialogContent> {
late final TextEditingController _searchController;
List<TranslationLanguageOption> _filtered = supportedTranslationLanguages;
@override
void initState() {
super.initState();
_searchController = TextEditingController();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(context.l10n.translation_targetLanguage),
content: SizedBox(
width: 360,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _searchController,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onChanged: (value) {
final normalized = value.trim().toLowerCase();
setState(() {
_filtered = supportedTranslationLanguages.where((option) {
return option.label.toLowerCase().contains(normalized) ||
option.code.toLowerCase().contains(normalized);
}).toList();
});
},
),
const SizedBox(height: 12),
Flexible(
child: RadioGroup<String?>(
groupValue: widget.currentLanguageCode,
onChanged: (value) {
widget.onLanguageSelected(value);
},
child: ListView(
shrinkWrap: true,
children: [
RadioListTile<String?>(
value: null,
title: Text(context.l10n.translation_useAppLanguage),
),
for (final option in _filtered)
RadioListTile<String?>(
value: option.code,
title: Text(option.label),
subtitle: Text(option.code.toUpperCase()),
),
],
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.l10n.common_close),
),
],
);
}
}
+130 -12
View File
@@ -11,21 +11,25 @@ import '../connector/meshcore_connector.dart';
import '../utils/platform_info.dart';
import '../helpers/chat_scroll_controller.dart';
import '../connector/meshcore_protocol.dart';
import '../helpers/link_handler.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../l10n/l10n.dart';
import '../models/channel.dart';
import '../models/channel_message.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/translation_service.dart';
import '../utils/emoji_utils.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_translation_button.dart';
import '../widgets/message_status_icon.dart';
import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -47,6 +51,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
bool _isLoadingOlder = false;
MeshCoreConnector? _connector;
DateTime? _lastChannelSendAt;
bool _channelSkipNextBottomSnap = false;
@override
void initState() {
@@ -55,11 +61,45 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_connector = context.read<MeshCoreConnector>();
_connector?.setActiveChannel(widget.channel.index);
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final idx = widget.channel.index;
final unread = connector.getUnreadCountForChannelIndex(idx);
ChannelMessage? anchor;
if (settings.jumpToOldestUnread && unread > 0) {
anchor = _findOldestUnreadChannelAnchor(
connector.getChannelMessages(widget.channel),
unread,
);
}
connector.setActiveChannel(idx);
_connector = connector;
if (anchor != null) {
_channelSkipNextBottomSnap = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_scrollToMessage(anchor!.messageId);
});
}
});
}
ChannelMessage? _findOldestUnreadChannelAnchor(
List<ChannelMessage> messages,
int unreadCount,
) {
if (unreadCount <= 0 || messages.isEmpty) return null;
var n = 0;
ChannelMessage? oldest;
for (final m in messages.reversed) {
if (m.isOutgoing) continue;
n++;
oldest = m;
if (n >= unreadCount) break;
}
return oldest;
}
void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
@@ -167,6 +207,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
),
centerTitle: false,
actions: [
const RadioStatsIconButton(),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: (value) {
@@ -243,6 +284,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_channelSkipNextBottomSnap) {
_channelSkipNextBottomSnap = false;
return;
}
_scrollController.scrollToBottomIfAtBottom();
});
@@ -312,6 +357,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
final isOutgoing = message.isOutgoing;
final gifId = _parseGifId(message.text);
final poi = _parsePoiMessage(message.text);
final translatedDisplayText =
message.translatedText != null &&
message.translatedText!.trim().isNotEmpty
? message.translatedText!.trim()
: message.text;
final originalDisplayText = message.isOutgoing
? message.originalText
: (translatedDisplayText != message.text ? message.text : null);
final displayPath = message.pathBytes.isNotEmpty
? message.pathBytes
: (message.pathVariants.isNotEmpty
@@ -462,16 +515,17 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: LinkHandler.buildLinkifyText(
context: context,
text: message.text,
child: TranslatedMessageContent(
displayText: translatedDisplayText,
originalText: originalDisplayText,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
originalStyle: TextStyle(
fontSize: bodyFontSize * textScale,
color: Colors.green,
decoration: TextDecoration.underline,
fontStyle: FontStyle.italic,
color: Theme.of(context).colorScheme.onSurface
.withValues(alpha: 0.72),
),
),
),
@@ -957,6 +1011,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
Widget _buildMessageComposer() {
final connector = context.watch<MeshCoreConnector>();
final maxBytes = maxChannelMessageBytes(connector.selfName);
final settings = context.watch<AppSettingsService>().settings;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -988,6 +1043,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
onPressed: () => _showGifPicker(context),
tooltip: context.l10n.chat_sendGif,
),
if (settings.translationEnabled)
MessageTranslationButton(
enabled: settings.composerTranslationEnabled,
languageCode: settings.translationTargetLanguageCode,
onPressed: _showTranslationOptions,
),
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
@@ -1065,6 +1126,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.send),
tooltip: context.l10n.chat_sendMessage,
onPressed: _sendMessage,
color: Theme.of(context).colorScheme.primary,
),
@@ -1075,15 +1137,65 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
);
}
void _sendMessage() {
Future<void> _showTranslationOptions() async {
final settingsService = context.read<AppSettingsService>();
final settings = settingsService.settings;
await showMessageTranslationSheet(
context: context,
enabled: settings.composerTranslationEnabled,
selectedLanguageCode: settings.translationTargetLanguageCode,
onEnabledChanged: settingsService.setComposerTranslationEnabled,
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
);
}
Future<void> _sendMessage() async {
final text = _textController.text.trim();
if (text.isEmpty) return;
final now = DateTime.now();
if (_lastChannelSendAt != null &&
now.difference(_lastChannelSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
return;
}
_lastChannelSendAt = now;
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final translationService = context.read<TranslationService>();
String messageText = text;
String? originalText;
String? translatedLanguageCode;
String? translationModelId;
if (settings.translationEnabled) {
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
Localizations.localeOf(context).languageCode,
);
if (translationService.shouldTranslateOutgoing(
text: text,
targetLanguageCode: targetLanguageCode,
)) {
final result = await translationService.translateOutgoingText(
text: text,
targetLanguageCode: targetLanguageCode,
);
if (!mounted) return;
if (result != null &&
result.status == MessageTranslationStatus.completed &&
result.translatedText.isNotEmpty) {
messageText = result.translatedText;
originalText = text;
translatedLanguageCode = result.targetLanguageCode;
translationModelId = result.modelId;
}
}
}
if (_replyingToMessage != null) {
messageText = '@[${_replyingToMessage!.senderName}] $text';
messageText = '@[${_replyingToMessage!.senderName}] $messageText';
}
final maxBytes = maxChannelMessageBytes(connector.selfName);
@@ -1094,10 +1206,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
return;
}
connector.sendChannelMessage(widget.channel, messageText);
_textController.clear();
_cancelReply();
_textFieldFocusNode.requestFocus();
connector.sendChannelMessage(
widget.channel,
messageText,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
}
String _formatTime(DateTime time) {
+18 -3
View File
@@ -64,6 +64,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
flipPathAround: true,
reversePathAround:
!(!channelMessage && !message.isOutgoing),
pathHashByteWidth: context
.read<MeshCoreConnector>()
.pathHashByteWidth,
),
),
),
@@ -819,7 +822,8 @@ List<_PathHop> _buildPathHops(
) {
if (pathBytes.isEmpty) return const [];
final candidatesByPrefix = <int, List<Contact>>{};
for (final contact in connector.allContacts) {
final allContacts = connector.allContacts;
for (final contact in allContacts) {
if (contact.publicKey.isEmpty) continue;
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
continue;
@@ -836,7 +840,8 @@ List<_PathHop> _buildPathHops(
: null;
var previousPosition = startPoint;
final distance = Distance();
var lastDistance = 0.0;
var bestDistance = 0.0;
final hops = <_PathHop>[];
for (var i = 0; i < pathBytes.length; i++) {
final searchPoint = i == 0 ? startPoint : previousPosition;
@@ -845,7 +850,7 @@ List<_PathHop> _buildPathHops(
if (candidates != null && candidates.isNotEmpty) {
var bestIndex = 0;
if (searchPoint != null) {
var bestDistance = double.infinity;
bestDistance = double.infinity;
for (var j = 0; j < candidates.length; j++) {
final candidate = candidates[j];
if (!candidate.hasLocation ||
@@ -873,6 +878,16 @@ List<_PathHop> _buildPathHops(
if (resolvedPosition != null) {
previousPosition = resolvedPosition;
}
// If the best candidate is much farther than the previous hop, it's likely not the correct match.
if (lastDistance + bestDistance > 50000 &&
candidates != null &&
candidates.isNotEmpty) {
i--;
lastDistance = bestDistance;
continue;
}
lastDistance = bestDistance;
hops.add(
_PathHop(
index: i + 1,
+147 -15
View File
@@ -16,16 +16,17 @@ import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart';
import '../helpers/chat_scroll_controller.dart';
import '../helpers/link_handler.dart';
import '../helpers/path_helper.dart';
import '../helpers/utf8_length_limiter.dart';
import '../models/channel_message.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_history.dart';
import '../models/translation_support.dart';
import '../services/app_settings_service.dart';
import '../services/chat_text_scale_service.dart';
import '../services/path_history_service.dart';
import '../services/translation_service.dart';
import '../widgets/chat_zoom_wrapper.dart';
import '../widgets/elements_ui.dart';
import 'channel_message_path_screen.dart';
@@ -35,7 +36,10 @@ import '../widgets/emoji_picker.dart';
import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_translation_button.dart';
import '../widgets/path_selection_dialog.dart';
import '../widgets/radio_stats_entry.dart';
import '../widgets/translated_message_content.dart';
import '../utils/app_logger.dart';
import '../l10n/l10n.dart';
import 'telemetry_screen.dart';
@@ -53,8 +57,11 @@ class _ChatScreenState extends State<ChatScreen> {
final _textController = TextEditingController();
final _scrollController = ChatScrollController();
final _textFieldFocusNode = FocusNode();
final GlobalKey _unreadScrollKey = GlobalKey();
bool _isLoadingOlder = false;
MeshCoreConnector? _connector;
Message? _pendingUnreadScrollTarget;
DateTime? _lastTextSendAt;
@override
void initState() {
@@ -63,11 +70,50 @@ class _ChatScreenState extends State<ChatScreen> {
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_connector = context.read<MeshCoreConnector>();
_connector?.setActiveContact(widget.contact.publicKeyHex);
final connector = context.read<MeshCoreConnector>();
final settings = context.read<AppSettingsService>().settings;
final keyHex = widget.contact.publicKeyHex;
final unread = connector.getUnreadCountForContactKey(keyHex);
Message? anchor;
if (settings.jumpToOldestUnread && unread > 0) {
anchor = _findOldestUnreadAnchor(
connector.getMessages(widget.contact),
unread,
);
}
connector.setActiveContact(keyHex);
_connector = connector;
if (anchor != null) {
setState(() => _pendingUnreadScrollTarget = anchor);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final ctx = _unreadScrollKey.currentContext;
if (ctx != null) {
Scrollable.ensureVisible(
ctx,
duration: const Duration(milliseconds: 350),
alignment: 0.15,
);
}
setState(() => _pendingUnreadScrollTarget = null);
});
}
});
}
Message? _findOldestUnreadAnchor(List<Message> messages, int unreadCount) {
if (unreadCount <= 0 || messages.isEmpty) return null;
var n = 0;
Message? oldest;
for (final m in messages.reversed) {
if (m.isOutgoing || m.isCli) continue;
n++;
oldest = m;
if (n >= unreadCount) break;
}
return oldest;
}
void _onTextFieldFocusChange() {
if (_textFieldFocusNode.hasFocus && mounted) {
_scrollController.handleKeyboardOpen();
@@ -247,6 +293,7 @@ class _ChatScreenState extends State<ChatScreen> {
tooltip: context.l10n.chat_pathManagement,
onPressed: () => _showPathHistory(context),
),
const RadioStatsIconButton(),
Consumer<MeshCoreConnector>(
builder: (context, connector, _) {
return PopupMenuButton<String>(
@@ -378,6 +425,7 @@ class _ChatScreenState extends State<ChatScreen> {
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (_pendingUnreadScrollTarget != null) return;
_scrollController.scrollToBottomIfAtBottom();
});
@@ -424,7 +472,7 @@ class _ChatScreenState extends State<ChatScreen> {
(service) => service.scale,
);
final resolvedContact = _resolveContact(connector);
return _MessageBubble(
final bubble = _MessageBubble(
message: message,
senderName: resolvedContact.type == advTypeRoom
? "${contact.name} [$fourByteHex]"
@@ -436,6 +484,10 @@ class _ChatScreenState extends State<ChatScreen> {
onRetryReaction: (msg, emoji) =>
_sendReaction(msg, contact, emoji),
);
if (identical(message, _pendingUnreadScrollTarget)) {
return KeyedSubtree(key: _unreadScrollKey, child: bubble);
}
return bubble;
},
);
},
@@ -446,6 +498,7 @@ class _ChatScreenState extends State<ChatScreen> {
Widget _buildInputBar(MeshCoreConnector connector) {
final maxBytes = maxContactMessageBytes();
final colorScheme = Theme.of(context).colorScheme;
final settings = context.watch<AppSettingsService>().settings;
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
@@ -460,6 +513,12 @@ class _ChatScreenState extends State<ChatScreen> {
onPressed: () => _showGifPicker(context),
tooltip: context.l10n.chat_sendGif,
),
if (settings.translationEnabled)
MessageTranslationButton(
enabled: settings.composerTranslationEnabled,
languageCode: settings.translationTargetLanguageCode,
onPressed: _showTranslationOptions,
),
Expanded(
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController,
@@ -531,6 +590,9 @@ class _ChatScreenState extends State<ChatScreen> {
const SizedBox(width: 8),
IconButton.filled(
icon: const Icon(Icons.send),
tooltip: context.l10n.chat_sendMessageTo(
_resolveContact(connector).name,
),
onPressed: () => _sendMessage(connector),
),
],
@@ -557,21 +619,78 @@ class _ChatScreenState extends State<ChatScreen> {
);
}
void _sendMessage(MeshCoreConnector connector) {
Future<void> _showTranslationOptions() async {
final settingsService = context.read<AppSettingsService>();
final settings = settingsService.settings;
await showMessageTranslationSheet(
context: context,
enabled: settings.composerTranslationEnabled,
selectedLanguageCode: settings.translationTargetLanguageCode,
onEnabledChanged: settingsService.setComposerTranslationEnabled,
onLanguageSelected: settingsService.setTranslationTargetLanguageCode,
);
}
Future<void> _sendMessage(MeshCoreConnector connector) async {
final text = _textController.text.trim();
if (text.isEmpty) return;
final now = DateTime.now();
if (_lastTextSendAt != null &&
now.difference(_lastTextSendAt!) < const Duration(seconds: 1)) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(context.l10n.chat_sendCooldown)));
return;
}
_lastTextSendAt = now;
final settings = context.read<AppSettingsService>().settings;
final translationService = context.read<TranslationService>();
var outgoingText = text;
String? originalText;
String? translatedLanguageCode;
String? translationModelId;
if (settings.translationEnabled) {
final targetLanguageCode = translationService.resolvedTargetLanguageCode(
Localizations.localeOf(context).languageCode,
);
if (translationService.shouldTranslateOutgoing(
text: text,
targetLanguageCode: targetLanguageCode,
)) {
final result = await translationService.translateOutgoingText(
text: text,
targetLanguageCode: targetLanguageCode,
);
if (!mounted) return;
if (result != null &&
result.status == MessageTranslationStatus.completed &&
result.translatedText.isNotEmpty) {
outgoingText = result.translatedText;
originalText = text;
translatedLanguageCode = result.targetLanguageCode;
translationModelId = result.modelId;
}
}
}
final maxBytes = maxContactMessageBytes();
if (utf8.encode(text).length > maxBytes) {
if (utf8.encode(outgoingText).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.chat_messageTooLong(maxBytes))),
);
return;
}
connector.sendMessage(_resolveContact(connector), text);
_textController.clear();
_textFieldFocusNode.requestFocus();
connector.sendMessage(
_resolveContact(connector),
outgoingText,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
);
}
void _showPathHistory(BuildContext context) {
@@ -950,6 +1069,7 @@ class _ChatScreenState extends State<ChatScreen> {
path: Uint8List.fromList(pathBytes),
flipPathAround: true,
targetContact: widget.contact,
pathHashByteWidth: connector.pathHashByteWidth,
),
),
),
@@ -1212,7 +1332,9 @@ class _ChatScreenState extends State<ChatScreen> {
connector.getContacts();
}
final pathForInput = currentContact.pathIdList;
final pathForInput = currentContact.pathFormattedIdList(
connector.pathHashByteWidth,
);
final currentPathLabel = _currentPathLabel(currentContact);
// Filter out the current contact from available contacts
@@ -1471,6 +1593,14 @@ class _MessageBubble extends StatelessWidget {
if (isRoomServer && !isOutgoing) {
messageText = message.text.substring(4.clamp(0, message.text.length));
}
final translatedDisplayText =
message.translatedText != null &&
message.translatedText!.trim().isNotEmpty
? message.translatedText!.trim()
: messageText;
final originalDisplayText = isOutgoing
? message.originalText
: (translatedDisplayText != messageText ? messageText : null);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
@@ -1600,16 +1730,15 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
child: LinkHandler.buildLinkifyText(
context: context,
text: messageText,
child: TranslatedMessageContent(
displayText: translatedDisplayText,
originalText: originalDisplayText,
style: TextStyle(
color: textColor,
fontSize: bodyFontSize * textScale,
),
linkStyle: TextStyle(
color: Colors.green,
decoration: TextDecoration.underline,
originalStyle: TextStyle(
color: textColor.withValues(alpha: 0.78),
fontSize: bodyFontSize * textScale,
),
),
@@ -1640,7 +1769,10 @@ class _MessageBubble extends StatelessWidget {
child: Text(
context.l10n.chat_retryCount(
message.retryCount,
4,
context
.read<AppSettingsService>()
.settings
.maxMessageRetries,
),
style: TextStyle(
fontSize: 10,
@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/models/companion_radio_stats.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:provider/provider.dart';
class CompanionRadioStatsScreen extends StatefulWidget {
const CompanionRadioStatsScreen({super.key});
@override
State<CompanionRadioStatsScreen> createState() =>
_CompanionRadioStatsScreenState();
}
class _CompanionRadioStatsScreenState extends State<CompanionRadioStatsScreen> {
final List<double> _noiseHistory = [];
static const int _maxSamples = 120;
MeshCoreConnector? _connector;
DateTime? _lastChartSampleAt;
@override
void initState() {
super.initState();
final c = context.read<MeshCoreConnector>();
_connector = c;
c.acquireRadioStatsPolling();
c.setPollingInterval(1);
c.radioStatsNotifier.addListener(_onStatsUpdate);
}
void _onStatsUpdate() {
final s = _connector?.radioStatsNotifier.value;
if (s == null || !mounted) return;
if (_lastChartSampleAt == s.receivedAt) return;
_lastChartSampleAt = s.receivedAt;
setState(() {
_noiseHistory.add(s.noiseFloorDbm.toDouble());
while (_noiseHistory.length > _maxSamples) {
_noiseHistory.removeAt(0);
}
});
}
@override
void dispose() {
_connector?.radioStatsNotifier.removeListener(_onStatsUpdate);
_connector?.releaseRadioStatsPolling();
_connector?.setPollingInterval(30);
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.radioStats_screenTitle),
centerTitle: true,
),
body: Selector<MeshCoreConnector, ({bool connected, bool supported})>(
selector: (_, c) => (
connected: c.isConnected,
supported: c.supportsCompanionRadioStats,
),
builder: (context, state, _) {
if (!state.connected) {
return Center(child: Text(l10n.radioStats_notConnected));
}
if (!state.supported) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
l10n.radioStats_firmwareTooOld,
textAlign: TextAlign.center,
),
),
);
}
final connector = context.read<MeshCoreConnector>();
final scheme = Theme.of(context).colorScheme;
final tt = Theme.of(context).textTheme;
return ValueListenableBuilder<CompanionRadioStats?>(
valueListenable: connector.radioStatsNotifier,
builder: (context, stats, _) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
if (stats != null) ...[
Text(
l10n.radioStats_noiseFloor(stats.noiseFloorDbm),
style: tt.titleMedium,
),
const SizedBox(height: 4),
Text(l10n.radioStats_lastRssi(stats.lastRssiDbm)),
Text(
l10n.radioStats_lastSnr(
stats.lastSnrDb.toStringAsFixed(1),
),
),
Text(l10n.radioStats_txAir(stats.txAirSecs)),
Text(l10n.radioStats_rxAir(stats.rxAirSecs)),
const SizedBox(height: 16),
] else
Text(l10n.radioStats_waiting),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: CustomPaint(
painter: _NoiseChartPainter(
samples: List<double>.from(_noiseHistory),
colorScheme: scheme,
textTheme: tt,
),
child: const SizedBox.expand(),
),
),
const SizedBox(height: 8),
Text(
l10n.radioStats_chartCaption,
style: tt.bodySmall?.copyWith(
color: scheme.onSurfaceVariant,
),
),
],
);
},
);
},
),
);
}
}
class _NoiseChartPainter extends CustomPainter {
final List<double> samples;
final ColorScheme colorScheme;
final TextTheme textTheme;
_NoiseChartPainter({
required this.samples,
required this.colorScheme,
required this.textTheme,
});
@override
void paint(Canvas canvas, Size size) {
final bg = Paint()..color = colorScheme.surfaceContainerHighest;
final border = Paint()
..color = colorScheme.outlineVariant
..style = PaintingStyle.stroke
..strokeWidth = 1;
final grid = Paint()
..color = colorScheme.outlineVariant.withValues(alpha: 0.5)
..strokeWidth = 1;
final line = Paint()
..color = colorScheme.primary
..strokeWidth = 2
..style = PaintingStyle.stroke;
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
bg,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(8)),
border,
);
const padL = 40.0;
const padR = 8.0;
const padT = 8.0;
const padB = 24.0;
final chart = Rect.fromLTRB(
padL,
padT,
size.width - padR,
size.height - padB,
);
for (var i = 0; i <= 4; i++) {
final y = chart.top + (chart.height * i / 4);
canvas.drawLine(Offset(chart.left, y), Offset(chart.right, y), grid);
}
if (samples.length < 2) {
final tp = TextPainter(
text: TextSpan(
text: '',
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(
canvas,
Offset(chart.left + 4, chart.top + chart.height / 2 - tp.height / 2),
);
return;
}
double minV = samples.reduce((a, b) => a < b ? a : b);
double maxV = samples.reduce((a, b) => a > b ? a : b);
if ((maxV - minV).abs() < 1) {
minV -= 2;
maxV += 2;
}
final span = maxV - minV;
for (var i = 0; i <= 2; i++) {
final v = maxV - span * i / 2;
final tp = _yAxisLabel(v);
final y = chart.top + (chart.height * i / 2) - tp.height / 2;
tp.paint(canvas, Offset(4, y));
}
final path = Path();
for (var i = 0; i < samples.length; i++) {
final x = chart.left + (chart.width * i / (samples.length - 1));
final t = (samples[i] - minV) / span;
final y = chart.bottom - t * chart.height;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, line);
}
@override
bool shouldRepaint(covariant _NoiseChartPainter oldDelegate) {
return oldDelegate.samples.length != samples.length ||
oldDelegate.colorScheme != colorScheme;
}
TextPainter _yAxisLabel(double v) {
final tp = TextPainter(
text: TextSpan(
text: v.round().toString(),
style: textTheme.labelSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
textDirection: TextDirection.ltr,
)..layout();
return tp;
}
}
+20 -13
View File
@@ -394,7 +394,7 @@ class _ContactsScreenState extends State<ContactsScreen>
children: [
const Icon(Icons.person_add_rounded),
const SizedBox(width: 8),
Text("Discovered Contacts"),
Text(context.l10n.discoveredContacts_Title),
],
),
onTap: () => Navigator.push(
@@ -1240,20 +1240,19 @@ class _ContactsScreenState extends State<ContactsScreen>
if (isRepeater) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathBytesForDisplay.isNotEmpty
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
title: Text(context.l10n.contacts_ping),
onTap: () {
final hw = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: contact.pathBytesForDisplay.isNotEmpty
? context.l10n.contacts_repeaterPathTrace
: context.l10n.contacts_repeaterPing,
path: contact.pathBytesForDisplay,
flipPathAround: true,
title: context.l10n.contacts_repeaterPing,
path: Uint8List.fromList([contact.publicKey.first]),
targetContact: contact,
pathHashByteWidth: hw,
),
),
);
@@ -1270,10 +1269,11 @@ class _ContactsScreenState extends State<ContactsScreen>
] else if (isRoom) ...[
ListTile(
leading: const Icon(Icons.radar, color: Colors.green),
title: contact.pathLength > 0
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
title: Text(context.l10n.contacts_pathTrace),
onTap: () {
final hw = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
@@ -1281,9 +1281,12 @@ class _ContactsScreenState extends State<ContactsScreen>
title: contact.pathBytesForDisplay.isNotEmpty
? context.l10n.contacts_roomPathTrace
: context.l10n.contacts_roomPing,
path: contact.pathBytesForDisplay,
path: contact.pathBytesForDisplay.isNotEmpty
? contact.pathBytesForDisplay
: Uint8List.fromList([contact.publicKey.first]),
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
targetContact: contact,
pathHashByteWidth: hw,
),
),
);
@@ -1318,6 +1321,9 @@ class _ContactsScreenState extends State<ContactsScreen>
leading: const Icon(Icons.radar, color: Colors.green),
title: Text(context.l10n.contacts_chatTraceRoute),
onTap: () {
final hw = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
@@ -1328,6 +1334,7 @@ class _ContactsScreenState extends State<ContactsScreen>
path: contact.pathBytesForDisplay,
flipPathAround: true,
targetContact: contact,
pathHashByteWidth: hw,
),
),
);
-280
View File
@@ -1,280 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../utils/dialog_utils.dart';
import '../utils/disconnect_navigation_mixin.dart';
import '../utils/route_transitions.dart';
import '../widgets/quick_switch_bar.dart';
import 'channels_screen.dart';
import 'contacts_screen.dart';
import 'map_screen.dart';
import 'settings_screen.dart';
/// Main hub screen after connecting to a MeshCore device
class DeviceScreen extends StatefulWidget {
const DeviceScreen({super.key});
@override
State<DeviceScreen> createState() => _DeviceScreenState();
}
class _DeviceScreenState extends State<DeviceScreen>
with DisconnectNavigationMixin {
bool _showBatteryVoltage = false;
int _quickIndex = 0;
@override
Widget build(BuildContext context) {
return Consumer<MeshCoreConnector>(
builder: (context, connector, child) {
// Auto-navigate back to scanner if disconnected
if (!checkConnectionAndNavigate(connector)) {
return const SizedBox.shrink();
}
final theme = Theme.of(context);
return PopScope(
canPop: false,
child: Scaffold(
appBar: AppBar(
leading: _buildBatteryIndicator(connector, context),
titleSpacing: 16,
centerTitle: false,
title: _buildAppBarTitle(connector, theme),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(Icons.bluetooth_disabled),
tooltip: context.l10n.common_disconnect,
onPressed: () => _disconnect(context, connector),
),
IconButton(
icon: const Icon(Icons.tune),
tooltip: context.l10n.common_settings,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
),
),
],
),
body: SafeArea(
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
children: [
_buildConnectionCard(connector, context),
const SizedBox(height: 16),
_buildSectionLabel(theme, context.l10n.device_quickSwitch),
const SizedBox(height: 12),
_buildQuickSwitchBar(context),
],
),
),
),
);
},
);
}
Widget _buildAppBarTitle(MeshCoreConnector connector, ThemeData theme) {
final colorScheme = theme.colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.device_meshcore,
style: theme.textTheme.labelSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.8,
color: colorScheme.onSurfaceVariant,
),
),
Text(
connector.deviceDisplayName,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
],
);
}
Widget _buildSectionLabel(ThemeData theme, String text) {
return Text(
text,
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
letterSpacing: 0.6,
color: theme.colorScheme.onSurfaceVariant,
),
);
}
Widget _buildConnectionCard(
MeshCoreConnector connector,
BuildContext context,
) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
return Card(
elevation: 0,
color: colorScheme.surfaceContainerHighest,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CircleAvatar(
radius: 24,
backgroundColor: colorScheme.primaryContainer,
child: Icon(
Icons.wifi_tethering_rounded,
color: colorScheme.onPrimaryContainer,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
connector.deviceDisplayName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
connector.deviceIdLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Chip(
avatar: Icon(
Icons.check_circle,
size: 18,
color: colorScheme.onSecondaryContainer,
),
label: Text(context.l10n.common_connected),
backgroundColor: colorScheme.secondaryContainer,
labelStyle: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
visualDensity: VisualDensity.compact,
),
_buildBatteryIndicator(connector, context),
],
),
],
),
),
);
}
Widget _buildQuickSwitchBar(BuildContext context) {
return QuickSwitchBar(
selectedIndex: _quickIndex,
onDestinationSelected: (index) {
_openQuickDestination(index, context);
},
);
}
Widget _buildBatteryIndicator(
MeshCoreConnector connector,
BuildContext context,
) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts;
final percentLabel = percent != null ? '$percent%' : '--%';
final voltageLabel = millivolts == null
? '-- V'
: '${(millivolts / 1000.0).toStringAsFixed(2)} V';
final displayLabel = _showBatteryVoltage ? voltageLabel : percentLabel;
final icon = _batteryIcon(percent);
return ActionChip(
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
label: Text(displayLabel),
labelStyle: theme.textTheme.labelMedium?.copyWith(
color: colorScheme.onSecondaryContainer,
fontWeight: FontWeight.w600,
),
backgroundColor: colorScheme.secondaryContainer,
visualDensity: VisualDensity.compact,
onPressed: () {
setState(() {
_showBatteryVoltage = !_showBatteryVoltage;
});
},
);
}
IconData _batteryIcon(int? percent) {
if (percent == null) return Icons.battery_unknown;
if (percent <= 15) return Icons.battery_alert;
return Icons.battery_full;
}
void _openQuickDestination(int index, BuildContext context) {
if (_quickIndex != index) {
setState(() {
_quickIndex = index;
});
}
switch (index) {
case 0:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
);
break;
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
}
Future<void> _disconnect(
BuildContext context,
MeshCoreConnector connector,
) async {
await showDisconnectDialog(context, connector);
}
}
+57 -5
View File
@@ -38,6 +38,13 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
super.dispose();
}
DateTime _resolveLastSeen(Contact contact) {
if (contact.type != advTypeChat) return contact.lastSeen;
return contact.lastMessageAt.isAfter(contact.lastSeen)
? contact.lastMessageAt
: contact.lastSeen;
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
@@ -108,11 +115,56 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Text(
_formatLastSeen(context, contact.lastSeen),
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
// Clamp text scaling in trailing section to prevent overflow while
// maintaining accessibility. Primary content (title/subtitle) scales normally.
trailing: MediaQuery(
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(
MediaQuery.textScalerOf(
context,
).scale(1.0).clamp(1.0, 1.3),
),
),
child: SizedBox(
width: 120,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
_formatLastSeen(
context,
_resolveLastSeen(contact),
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (contact.hasLocation)
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[400],
),
if (contact.rawPacket != null)
const SizedBox(width: 2),
if (contact.rawPacket != null)
Icon(
Icons.cell_tower,
size: 14,
color: Colors.grey[400],
),
],
),
],
),
),
),
onTap: () {
+51 -22
View File
@@ -64,6 +64,7 @@ class _MapScreenState extends State<MapScreen> {
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
final List<int> _pathTrace = [];
final List<Contact> _pathTraceContacts = [];
final List<LatLng> _points = [];
final List<Polyline> _polylines = [];
bool _legendExpanded = false;
@@ -488,7 +489,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
),
if (!_isBuildingPathTrace)
if (!settings.mapShowOverlaps)
..._buildGuessedMarker(
guessedLocations,
showLabels: _showNodeLabels,
@@ -788,17 +789,26 @@ class _MapScreenState extends State<MapScreen> {
final markers = <Marker>[];
for (final guess in guessed) {
if (guess.contact.type == advTypeChat && _isBuildingPathTrace) {
continue;
}
final color = _getNodeColor(guess.contact.type);
final marker = Marker(
point: guess.position,
width: 35,
height: 35,
child: GestureDetector(
onTap: () => _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
onLongPress: () => _isBuildingPathTrace
? _showNodeInfo(context, guess.contact)
: null,
onTap: () => _isBuildingPathTrace
? _addToPath(context, guess.contact, position: guess.position)
: _showNodeInfo(
context,
guess.contact,
guessedPosition: guess.position,
),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
@@ -870,23 +880,29 @@ class _MapScreenState extends State<MapScreen> {
addContact = true;
}
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
if (contact.type == advTypeChat && _isBuildingPathTrace) {
addContact = false;
}
if (settings.mapShowOverlaps) {
final hasOverlap = contacts
.where(
(c) =>
c.publicKeyHex != contact.publicKeyHex &&
c.publicKey.first == contact.publicKey.first &&
(c.type == advTypeRepeater || c.type == advTypeRoom) &&
(contact.type == advTypeRepeater ||
contact.type == advTypeRoom),
)
.firstOrNull;
if (hasOverlap == null &&
settings.mapShowOverlaps &&
!_isBuildingPathTrace) {
addContact = false;
}
}
if (addContact) {
filtered.add(contact);
}
@@ -2121,12 +2137,18 @@ class _MapScreenState extends State<MapScreen> {
}
}
void _addToPath(BuildContext context, Contact contact) {
void _addToPath(BuildContext context, Contact contact, {LatLng? position}) {
setState(() {
_pathTrace.add(
contact.publicKey[0],
); // Add first 16 bytes of public key to path trace
_points.add(LatLng(contact.latitude!, contact.longitude!));
_pathTraceContacts.add(
contact.copyWith(
latitude: position?.latitude ?? contact.latitude,
longitude: position?.longitude ?? contact.longitude,
),
); // Add contact to path trace contacts
_points.add(position ?? LatLng(contact.latitude!, contact.longitude!));
});
}
@@ -2134,6 +2156,7 @@ class _MapScreenState extends State<MapScreen> {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_pathTraceContacts.clear();
_points.clear();
_polylines.clear();
_points.add(position);
@@ -2142,6 +2165,7 @@ class _MapScreenState extends State<MapScreen> {
void _removePath() {
setState(() {
_pathTraceContacts.removeLast();
_pathTrace.removeLast(); // Remove last node from path trace
_points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines
@@ -2191,12 +2215,17 @@ class _MapScreenState extends State<MapScreen> {
if (_pathTrace.isNotEmpty)
IconButton(
onPressed: () {
final hashW = context
.read<MeshCoreConnector>()
.pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
pathHashByteWidth: hashW,
pathContacts: _pathTraceContacts,
),
),
);
+1 -1
View File
@@ -142,7 +142,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final contacts = connector.allContacts;
final contacts = connector.allContactsUnfiltered;
try {
final neighborCount = buffer.readUInt16LE();
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
+48 -11
View File
@@ -55,6 +55,8 @@ class PathTraceMapScreen extends StatefulWidget {
final bool flipPathAround;
final bool reversePathAround;
final Contact? targetContact;
final int pathHashByteWidth;
final List<Contact>? pathContacts;
const PathTraceMapScreen({
super.key,
@@ -64,6 +66,8 @@ class PathTraceMapScreen extends StatefulWidget {
this.flipPathAround = false,
this.reversePathAround = false,
this.targetContact,
this.pathHashByteWidth = pathHashSize,
this.pathContacts,
});
@override
@@ -72,6 +76,8 @@ class PathTraceMapScreen extends StatefulWidget {
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
static const double _labelZoomThreshold = 8.5;
//miles to meters conversion for filtering out repeaters that are too far from the last known GPS hop to be a likely match, to avoid false matches that throw off the inferred positions of other hops in the path
static const double _maxRepeaterMatchDistanceMeters = 40 * 1609.344;
StreamSubscription<Uint8List>? _frameSubscription;
Timer? _timeoutTimer;
@@ -119,8 +125,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
Uint8List traceBytes;
if (pathBytes.isEmpty) {
final pk = widget.targetContact?.publicKey;
final n = widget.pathHashByteWidth.clamp(1, pubKeySize);
if (pk != null && pk.length >= n) {
return Uint8List.fromList(pk.sublist(0, n));
}
traceBytes = Uint8List(1);
traceBytes[0] = widget.targetContact?.publicKey[0] ?? 0;
traceBytes[0] = pk?[0] ?? 0;
return traceBytes;
}
@@ -259,17 +270,43 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
.toList();
Map<int, Contact> pathContacts = {};
final contacts = connector.allContacts;
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
for (var repeaterData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([repeaterData]),
)) {
pathContacts[repeaterData] = repeater;
Contact lastContact = Contact(
path: Uint8List(0),
pathLength: 0,
publicKey: connector.selfPublicKey ?? Uint8List(0),
name: context.l10n.pathTrace_you,
type: advTypeChat,
latitude: connector.selfLatitude,
longitude: connector.selfLongitude,
lastSeen: DateTime.now(),
);
if (widget.pathContacts != null) {
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
} else {
final contacts = connector.allContactsUnfiltered;
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
if (lastContact.latitude != null &&
lastContact.longitude != null &&
repeater.hasLocation &&
lastContact.hasLocation &&
Distance().distance(
LatLng(lastContact.latitude!, lastContact.longitude!),
LatLng(repeater.latitude!, repeater.longitude!),
) >
_maxRepeaterMatchDistanceMeters) {
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
}
}
});
for (var repeaterData in pathData) {
if (listEquals(
repeater.publicKey.sublist(0, 1),
Uint8List.fromList([repeaterData]),
)) {
pathContacts[repeaterData] = repeater;
lastContact = repeater;
}
}
});
}
// For hops with no GPS contact, infer position from other contacts
// with known GPS that share the same last-hop byte.
+7 -1
View File
@@ -35,13 +35,15 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
// Common commands for quick access
late final List<Map<String, String>> _quickCommands = [
{'labelKey': 'advertise', 'command': 'advert'},
{'labelKey': 'getName', 'command': 'get name'},
{'labelKey': 'getRadio', 'command': 'get radio'},
{'labelKey': 'getTx', 'command': 'get tx'},
{'labelKey': 'discovery', 'command': 'discover.neighbors'},
{'labelKey': 'neighbors', 'command': 'neighbors'},
{'labelKey': 'version', 'command': 'ver'},
{'labelKey': 'advertise', 'command': 'advert'},
{'labelKey': 'clock', 'command': 'clock'},
{'labelKey': 'clock sync', 'command': 'clock sync'},
];
@override
@@ -407,6 +409,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
return l10n.repeater_cliQuickAdvertise;
case 'clock':
return l10n.repeater_cliQuickClock;
case 'clock sync':
return l10n.repeater_cliQuickClockSync;
case 'discovery':
return l10n.repeater_cliQuickDiscovery;
default:
return key;
}
+112 -4
View File
@@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../services/linux_ble_error_classifier.dart';
import '../utils/app_logger.dart';
import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/device_tile.dart';
@@ -288,12 +289,33 @@ class _ScannerScreenState extends State<ScannerScreen> {
MeshCoreConnector connector,
ScanResult result,
) async {
final name = result.device.platformName.isNotEmpty
? result.device.platformName
: result.advertisementData.advName;
try {
final name = result.device.platformName.isNotEmpty
? result.device.platformName
: result.advertisementData.advName;
await connector.connect(result.device, displayName: name);
await connector.connect(
result.device,
displayName: name,
linuxPairingPinProvider: PlatformInfo.isLinux
? () async {
if (!context.mounted) return null;
return _promptLinuxPairingPin(context, name);
}
: null,
);
} catch (e) {
final errorText = e.toString();
final suppressTransientLinuxConnectError =
PlatformInfo.isLinux &&
connector.isAutoReconnectScheduled &&
isLinuxBleConnectFailureText(errorText);
if (suppressTransientLinuxConnectError) {
appLogger.info(
'Suppressing transient Linux connect error while auto-reconnect is active: $e',
tag: 'ScannerScreen',
);
return;
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -305,6 +327,92 @@ class _ScannerScreenState extends State<ScannerScreen> {
}
}
Future<String?> _promptLinuxPairingPin(
BuildContext context,
String deviceName,
) async {
final l10n = context.l10n;
var pinValue = '';
var obscure = true;
appLogger.info(
'Showing Linux BLE pairing PIN prompt for $deviceName',
tag: 'ScannerScreen',
);
final pin = await showDialog<String>(
context: context,
builder: (dialogContext) {
return StatefulBuilder(
builder: (dialogContext, setDialogState) {
return AlertDialog(
title: Text(l10n.scanner_linuxPairingPinTitle),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.scanner_linuxPairingPinPrompt(deviceName)),
const SizedBox(height: 12),
TextField(
autofocus: true,
keyboardType: TextInputType.number,
textInputAction: TextInputAction.done,
obscureText: obscure,
enableSuggestions: false,
autocorrect: false,
onChanged: (value) {
pinValue = value.trim();
},
onSubmitted: (value) {
Navigator.of(dialogContext).pop(value.trim());
},
decoration: InputDecoration(
suffixIcon: IconButton(
onPressed: () {
setDialogState(() {
obscure = !obscure;
});
},
icon: Icon(
obscure ? Icons.visibility : Icons.visibility_off,
),
tooltip: obscure
? l10n.scanner_linuxPairingShowPin
: l10n.scanner_linuxPairingHidePin,
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(null),
child: Text(l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.of(dialogContext).pop(pinValue),
child: Text(l10n.common_connect),
),
],
);
},
);
},
);
if (pin == null) {
appLogger.info(
'Linux BLE pairing PIN prompt cancelled for $deviceName',
tag: 'ScannerScreen',
);
return null;
}
appLogger.info(
'Linux BLE pairing PIN prompt completed for $deviceName',
tag: 'ScannerScreen',
);
return pin;
}
Widget _bluetoothOffWarning(BuildContext context) {
final errorColor = Theme.of(context).colorScheme.error;
return Container(
+408 -21
View File
@@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meshcore_open/utils/gpx_export.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
@@ -8,10 +9,27 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/radio_settings.dart';
import '../services/app_debug_log_service.dart';
import '../widgets/app_bar.dart';
import 'app_settings_screen.dart';
import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart';
import '../widgets/radio_stats_entry.dart';
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
/// to the UI enum range (always 5-8).
int _toUiCodingRate(int deviceCr) {
return deviceCr <= 4 ? deviceCr + 4 : deviceCr;
}
/// Convert UI coding-rate value (5-8) back to firmware encoding.
/// Uses the current device CR to detect which encoding the firmware expects.
int _toDeviceCodingRate(int uiCr, int? deviceCr) {
if (deviceCr != null && deviceCr <= 4) {
return uiCr - 4;
}
return uiCr;
}
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@@ -269,6 +287,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () => _showRadioSettings(context, connector),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.sensors_outlined),
title: Text(l10n.radioStats_settingsTile),
subtitle: Text(l10n.radioStats_settingsSubtitle),
trailing: const Icon(Icons.chevron_right),
enabled:
connector.isConnected && connector.supportsCompanionRadioStats,
onTap: () => pushCompanionRadioStatsScreen(context),
),
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.location_on_outlined),
title: Text(l10n.settings_location),
@@ -1077,6 +1105,11 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
final _txPowerController = TextEditingController(text: '20');
bool _clientRepeat = false;
int? _selectedPresetIndex;
_RadioSettingsSnapshot? _lastNonRepeatSnapshot;
AppDebugLogService get _appLog =>
Provider.of<AppDebugLogService>(context, listen: false);
@override
void initState() {
@@ -1128,6 +1161,21 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
}
_clientRepeat = widget.connector.clientRepeat ?? false;
_selectedPresetIndex = _findMatchingPresetIndex();
if (_clientRepeat) {
_lastNonRepeatSnapshot =
_sessionRememberedNonRepeatSnapshot() ??
_inferNonRepeatSnapshotForRepeatEnabled();
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot!,
);
} else {
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_logRadioSettingsState('Dialog initialized');
});
}
@override
@@ -1137,14 +1185,223 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
super.dispose();
}
void _applyPreset(RadioSettings preset) {
void _applyPreset(int index) {
setState(() {
_frequencyController.text = preset.frequencyMHz.toString();
_bandwidth = preset.bandwidth;
_spreadingFactor = preset.spreadingFactor;
_codingRate = preset.codingRate;
_txPowerController.text = preset.txPowerDbm.toString();
_applyPresetState(index);
});
_logRadioSettingsState(
'Applied preset ${RadioSettings.presets[index].$1} (#$index)',
);
}
int? _findMatchingPresetIndex() {
return _findMatchingPresetIndexForSnapshot(_currentSnapshot());
}
int? _findMatchingPresetIndexForSnapshot(_RadioSettingsSnapshot snapshot) {
for (final i in _visiblePresetIndexes()) {
final preset = RadioSettings.presets[i].$2;
if (preset.frequencyHz == snapshot.frequencyHz &&
preset.bandwidth == snapshot.bandwidth &&
preset.spreadingFactor == snapshot.spreadingFactor &&
preset.codingRate == snapshot.codingRate &&
preset.txPowerDbm == snapshot.txPowerDbm) {
return i;
}
}
return null;
}
Iterable<int> _visiblePresetIndexes() sync* {
for (var i = 0; i < RadioSettings.presets.length; i++) {
if (_isOffGridPresetIndex(i)) {
continue;
}
yield i;
}
}
_RadioSettingsSnapshot _currentSnapshot() {
final frequencyMHz = double.tryParse(_frequencyController.text) ?? 915.0;
final txPowerDbm = int.tryParse(_txPowerController.text) ?? 20;
return _RadioSettingsSnapshot(
frequencyMHz: frequencyMHz,
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: txPowerDbm,
);
}
bool _isOffGridPresetIndex(int? index) {
if (index == null) return false;
return RadioSettings.presets[index].$1.startsWith('Off-Grid ');
}
double _offGridFrequencyForBaseFrequency(double baseFrequencyMHz) {
if (baseFrequencyMHz < 500) return 433.0;
if (baseFrequencyMHz < 900) return 869.0;
return 918.0;
}
double _normalFrequencyForBand(double frequencyMHz) {
if (frequencyMHz < 500) return 433.650;
if (frequencyMHz < 900) return 869.432;
return 915.8;
}
_RadioSettingsSnapshot _fallbackNonRepeatSnapshot(
double currentFrequencyMHz,
) {
return _RadioSettingsSnapshot(
frequencyMHz: _normalFrequencyForBand(currentFrequencyMHz),
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
);
}
_RadioSettingsSnapshot _nonRepeatSnapshotForCurrentSelection() {
final current = _currentSnapshot();
if (!_isOffGridPresetIndex(_selectedPresetIndex)) {
return current;
}
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
}
_RadioSettingsSnapshot? _sessionRememberedNonRepeatSnapshot() {
final snapshot = widget.connector.rememberedNonRepeatRadioState;
if (snapshot == null) return null;
return _RadioSettingsSnapshot.fromMeshCoreSnapshot(snapshot);
}
_RadioSettingsSnapshot _inferNonRepeatSnapshotForRepeatEnabled() {
final current = _currentSnapshot();
for (final i in _visiblePresetIndexes()) {
final preset = RadioSettings.presets[i].$2;
final offGridFreqHz =
(_offGridFrequencyForBaseFrequency(preset.frequencyMHz) * 1000)
.round();
if (offGridFreqHz == current.frequencyHz &&
preset.bandwidth == current.bandwidth &&
preset.spreadingFactor == current.spreadingFactor &&
preset.codingRate == current.codingRate &&
preset.txPowerDbm == current.txPowerDbm) {
return _RadioSettingsSnapshot(
frequencyMHz: preset.frequencyMHz,
bandwidth: preset.bandwidth,
spreadingFactor: preset.spreadingFactor,
codingRate: preset.codingRate,
txPowerDbm: preset.txPowerDbm,
);
}
}
return _fallbackNonRepeatSnapshot(current.frequencyMHz);
}
void _applySnapshot(_RadioSettingsSnapshot snapshot) {
_frequencyController.text = snapshot.frequencyMHz.toStringAsFixed(3);
_bandwidth = snapshot.bandwidth;
_spreadingFactor = snapshot.spreadingFactor;
_codingRate = snapshot.codingRate;
_txPowerController.text = snapshot.txPowerDbm.toString();
}
void _applyPresetState(int index) {
final preset = RadioSettings.presets[index].$2;
final baseSnapshot = _RadioSettingsSnapshot(
frequencyMHz: preset.frequencyMHz,
bandwidth: preset.bandwidth,
spreadingFactor: preset.spreadingFactor,
codingRate: preset.codingRate,
txPowerDbm: preset.txPowerDbm,
);
final frequencyMHz = _clientRepeat
? _offGridFrequencyForBaseFrequency(baseSnapshot.frequencyMHz)
: baseSnapshot.frequencyMHz;
_frequencyController.text = frequencyMHz.toString();
_bandwidth = preset.bandwidth;
_spreadingFactor = preset.spreadingFactor;
_codingRate = preset.codingRate;
_txPowerController.text = preset.txPowerDbm.toString();
_selectedPresetIndex = index;
_lastNonRepeatSnapshot = baseSnapshot;
}
void _syncPresetSelection() {
final previousPresetIndex = _selectedPresetIndex;
final previousLastNonRepeat = _lastNonRepeatSnapshot;
if (_clientRepeat) {
final baseSnapshot =
previousLastNonRepeat ?? _inferNonRepeatSnapshotForRepeatEnabled();
if (_bandwidth != baseSnapshot.bandwidth ||
_spreadingFactor != baseSnapshot.spreadingFactor ||
_codingRate != baseSnapshot.codingRate ||
(int.tryParse(_txPowerController.text) ?? 20) !=
baseSnapshot.txPowerDbm) {
_lastNonRepeatSnapshot = _RadioSettingsSnapshot(
frequencyMHz: baseSnapshot.frequencyMHz,
bandwidth: _bandwidth,
spreadingFactor: _spreadingFactor,
codingRate: _codingRate,
txPowerDbm: int.tryParse(_txPowerController.text) ?? 20,
);
}
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot ?? baseSnapshot,
);
if (previousPresetIndex != _selectedPresetIndex ||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
_logRadioSettingsState(
'Preset match updated while repeat enabled: ${_presetLabel(previousPresetIndex)} -> ${_presetLabel(_selectedPresetIndex)}',
);
}
return;
}
_lastNonRepeatSnapshot = _nonRepeatSnapshotForCurrentSelection();
_selectedPresetIndex = _findMatchingPresetIndexForSnapshot(
_lastNonRepeatSnapshot!,
);
if (previousPresetIndex != _selectedPresetIndex ||
previousLastNonRepeat != _lastNonRepeatSnapshot) {
_logRadioSettingsState(
'Preset sync updated state from ${_presetLabel(previousPresetIndex)} to ${_presetLabel(_selectedPresetIndex)}',
);
}
}
void _handleManualSettingsChanged(String source) {
_logRadioSettingsState('Manual settings edit: $source');
setState(_syncPresetSelection);
}
void _handleClientRepeatChanged(bool enabled) {
_logRadioSettingsState(
'Off-grid repeat toggle requested: $_clientRepeat -> $enabled',
);
setState(() {
final currentSnapshot = _currentSnapshot();
if (enabled) {
if (!_clientRepeat) {
_syncPresetSelection();
}
final baseSnapshot = _lastNonRepeatSnapshot ?? currentSnapshot;
_clientRepeat = true;
_frequencyController.text = _offGridFrequencyForBaseFrequency(
baseSnapshot.frequencyMHz,
).toStringAsFixed(3);
return;
}
_clientRepeat = false;
_applySnapshot(
_lastNonRepeatSnapshot ??
_fallbackNonRepeatSnapshot(currentSnapshot.frequencyMHz),
);
_syncPresetSelection();
});
_logRadioSettingsState('Off-grid repeat toggle applied');
}
Future<void> _saveSettings() async {
@@ -1192,6 +1449,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
}
try {
_logRadioSettingsState('Saving radio settings');
await widget.connector.sendFrame(
buildSetRadioParamsFrame(
freqHz,
@@ -1203,29 +1461,62 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
final rememberedSnapshot = _clientRepeat
? _lastNonRepeatSnapshot
: _currentSnapshot();
if (rememberedSnapshot != null) {
widget.connector.rememberNonRepeatRadioState(
rememberedSnapshot.toMeshCoreSnapshot(widget.connector.currentCr),
);
}
if (!mounted) return;
Navigator.pop(context);
_logRadioSettingsState('Radio settings saved successfully');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_radioSettingsUpdated)),
);
} catch (e) {
_appLog.warn('Radio settings save failed: $e', tag: 'RadioSettings');
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_error(e.toString()))),
);
}
Navigator.pop(context);
}
int _toUiCodingRate(int deviceCr) {
return deviceCr <= 4 ? deviceCr + 4 : deviceCr;
}
int _toDeviceCodingRate(int uiCr, int? deviceCr) {
if (deviceCr != null && deviceCr <= 4) {
return uiCr - 4;
String _presetLabel(int? index) {
if (index == null) {
return 'custom';
}
return uiCr;
return '${RadioSettings.presets[index].$1} (#$index)';
}
String _formatSnapshot(_RadioSettingsSnapshot? snapshot) {
if (snapshot == null) {
return 'null';
}
return '${snapshot.frequencyMHz.toStringAsFixed(3)}MHz/'
'${snapshot.bandwidth.label}/'
'${snapshot.spreadingFactor.label}/'
'${snapshot.codingRate.label}/'
'${snapshot.txPowerDbm}dBm';
}
void _logRadioSettingsState(String message) {
if (!kDebugMode) return;
_appLog.info(
'$message | '
'freq=${_frequencyController.text}MHz '
'bw=${_bandwidth.label} '
'sf=${_spreadingFactor.label} '
'cr=${_codingRate.label} '
'tx=${_txPowerController.text}dBm '
'repeat=$_clientRepeat '
'preset=${_presetLabel(_selectedPresetIndex)} '
'lastNonRepeat=${_formatSnapshot(_lastNonRepeatSnapshot)}',
tag: 'RadioSettings',
);
}
@override
@@ -1239,12 +1530,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<int>(
key: ValueKey<int?>(_selectedPresetIndex),
initialValue: _selectedPresetIndex,
decoration: InputDecoration(
labelText: l10n.settings_presets,
border: const OutlineInputBorder(),
),
items: [
for (var i = 0; i < RadioSettings.presets.length; i++)
for (final i in _visiblePresetIndexes())
DropdownMenuItem(
value: i,
child: Text(RadioSettings.presets[i].$1),
@@ -1252,13 +1545,14 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
],
onChanged: (index) {
if (index != null) {
_applyPreset(RadioSettings.presets[index].$2);
_applyPreset(index);
}
},
),
const SizedBox(height: 16),
TextField(
controller: _frequencyController,
onChanged: (_) => _handleManualSettingsChanged('frequency'),
decoration: InputDecoration(
labelText: l10n.settings_frequency,
border: const OutlineInputBorder(),
@@ -1281,7 +1575,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _bandwidth = value);
if (value != null) {
setState(() {
_bandwidth = value;
_syncPresetSelection();
});
_logRadioSettingsState('Manual settings edit: bandwidth');
}
},
),
const SizedBox(height: 16),
@@ -1297,7 +1597,15 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _spreadingFactor = value);
if (value != null) {
setState(() {
_spreadingFactor = value;
_syncPresetSelection();
});
_logRadioSettingsState(
'Manual settings edit: spreading factor',
);
}
},
),
const SizedBox(height: 16),
@@ -1313,12 +1621,19 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _codingRate = value);
if (value != null) {
setState(() {
_codingRate = value;
_syncPresetSelection();
});
_logRadioSettingsState('Manual settings edit: coding rate');
}
},
),
const SizedBox(height: 16),
TextField(
controller: _txPowerController,
onChanged: (_) => _handleManualSettingsChanged('tx power'),
decoration: InputDecoration(
labelText: l10n.settings_txPower,
border: const OutlineInputBorder(),
@@ -1334,7 +1649,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
title: Text(l10n.settings_clientRepeat),
subtitle: Text(l10n.settings_clientRepeatSubtitle),
value: _clientRepeat,
onChanged: (value) => setState(() => _clientRepeat = value),
onChanged: _handleClientRepeatChanged,
contentPadding: EdgeInsets.zero,
),
],
@@ -1351,3 +1666,75 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
);
}
}
class _RadioSettingsSnapshot {
final double frequencyMHz;
final LoRaBandwidth bandwidth;
final LoRaSpreadingFactor spreadingFactor;
final LoRaCodingRate codingRate;
final int txPowerDbm;
const _RadioSettingsSnapshot({
required this.frequencyMHz,
required this.bandwidth,
required this.spreadingFactor,
required this.codingRate,
required this.txPowerDbm,
});
/// Frequency in integer Hz avoids floating-point comparison issues.
int get frequencyHz => (frequencyMHz * 1000).round();
/// Convert from the connector's raw-int snapshot to UI-enum snapshot.
static _RadioSettingsSnapshot? fromMeshCoreSnapshot(
MeshCoreRadioStateSnapshot snapshot,
) {
final bw = LoRaBandwidth.values
.where((b) => b.hz == snapshot.bwHz)
.firstOrNull;
final sf = LoRaSpreadingFactor.values
.where((s) => s.value == snapshot.sf)
.firstOrNull;
final cr = LoRaCodingRate.values
.where((c) => c.value == _toUiCodingRate(snapshot.cr))
.firstOrNull;
if (bw == null || sf == null || cr == null) return null;
return _RadioSettingsSnapshot(
frequencyMHz: snapshot.freqHz / 1000.0,
bandwidth: bw,
spreadingFactor: sf,
codingRate: cr,
txPowerDbm: snapshot.txPowerDbm,
);
}
/// Convert back to the connector's raw-int snapshot.
MeshCoreRadioStateSnapshot toMeshCoreSnapshot(int? deviceCr) {
return MeshCoreRadioStateSnapshot(
freqHz: frequencyHz,
bwHz: bandwidth.hz,
sf: spreadingFactor.value,
cr: _toDeviceCodingRate(codingRate.value, deviceCr),
txPowerDbm: txPowerDbm,
);
}
@override
bool operator ==(Object other) {
return other is _RadioSettingsSnapshot &&
frequencyHz == other.frequencyHz &&
bandwidth == other.bandwidth &&
spreadingFactor == other.spreadingFactor &&
codingRate == other.codingRate &&
txPowerDbm == other.txPowerDbm;
}
@override
int get hashCode => Object.hash(
frequencyHz,
bandwidth,
spreadingFactor,
codingRate,
txPowerDbm,
);
}
+35
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../storage/prefs_manager.dart';
import '../utils/app_logger.dart';
@@ -218,4 +219,38 @@ class AppSettingsService extends ChangeNotifier {
Future<void> setTcpServerPort(int value) async {
await updateSettings(_settings.copyWith(tcpServerPort: value));
}
Future<void> setJumpToOldestUnread(bool value) async {
await updateSettings(_settings.copyWith(jumpToOldestUnread: value));
}
Future<void> setTranslationEnabled(bool value) async {
await updateSettings(_settings.copyWith(translationEnabled: value));
}
Future<void> setTranslationTargetLanguageCode(String? value) async {
await updateSettings(
_settings.copyWith(translationTargetLanguageCode: value),
);
}
Future<void> setComposerTranslationEnabled(bool value) async {
await updateSettings(_settings.copyWith(composerTranslationEnabled: value));
}
Future<void> setTranslationModelSourceUrl(String? value) async {
await updateSettings(_settings.copyWith(translationModelSourceUrl: value));
}
Future<void> setTranslationSelectedModelId(String? value) async {
await updateSettings(_settings.copyWith(translationSelectedModelId: value));
}
Future<void> setTranslationDownloadedModels(
List<TranslationModelRecord> value,
) async {
await updateSettings(
_settings.copyWith(translationDownloadedModels: value),
);
}
}
@@ -0,0 +1,37 @@
const String linuxConnectStageFailureMarker = 'linux connect stage failure';
bool isLinuxBleConnectFailureText(String errorText) {
final lowerErrorText = errorText.toLowerCase();
if (isLinuxBlePairingFailureText(errorText)) {
return false;
}
return lowerErrorText.contains(linuxConnectStageFailureMarker) ||
lowerErrorText.contains('| connect |') ||
lowerErrorText.contains('linux connect hard-timeout') ||
lowerErrorText.contains('org.bluez.error.failed') ||
lowerErrorText.contains('org.bluez.error.inprogress') ||
lowerErrorText.contains('le-connection-abort-by-local');
}
bool isLinuxBlePairingFailureText(String errorText) {
final lowerErrorText = errorText.toLowerCase();
final isPairingSpecificStateError =
lowerErrorText.contains('bad state: no element') &&
(lowerErrorText.contains('pair') ||
lowerErrorText.contains('bond') ||
lowerErrorText.contains('trust'));
return lowerErrorText.contains('authenticationfailed') ||
lowerErrorText.contains('authentication failed') ||
lowerErrorText.contains('notpermitted: not paired') ||
lowerErrorText.contains('pairing fallback failed') ||
lowerErrorText.contains('linux ble pairing did not complete') ||
lowerErrorText.contains('linux ble trust repair did not complete') ||
isPairingSpecificStateError ||
isLikelyLinuxBlePairingTimeoutText(errorText);
}
bool isLikelyLinuxBlePairingTimeoutText(String errorText) {
final lowerErrorText = errorText.toLowerCase();
return lowerErrorText.contains('timed out') &&
(lowerErrorText.contains('pair') || lowerErrorText.contains('bond'));
}
+423
View File
@@ -0,0 +1,423 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
typedef ProcessStartFn =
Future<Process> Function(String executable, List<String> arguments);
typedef ProcessRunFn =
Future<ProcessResult> Function(String executable, List<String> arguments);
/// Best-effort Linux BLE pairing helper using bluetoothctl.
///
/// This is used only as a fallback when BlueZ pairing via flutter_blue_plus
/// fails to surface agent prompts in-app.
class LinuxBlePairingService {
/// Maximum number of pairing attempts (initial + retries).
/// Covers one remove-and-retry plus one proactive-PIN retry.
static const int _maxAttempts = 3;
static const Duration _processExitTimeout = Duration(seconds: 6);
static const Duration _pairingCleanupTimeout = Duration(seconds: 5);
static const Duration _defaultPairingTimeout = Duration(seconds: 45);
LinuxBlePairingService({
ProcessStartFn? processStart,
ProcessRunFn? processRun,
}) : _processStart = processStart ?? Process.start,
_processRun = processRun ?? Process.run;
final ProcessStartFn _processStart;
final ProcessRunFn _processRun;
Future<bool> isBluetoothctlAvailable() async {
try {
final result = await _processRun('bluetoothctl', <String>['--version']);
return result.exitCode == 0;
} on ProcessException {
return false;
}
}
Future<void> disconnectDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {
onLog?.call('Requesting BlueZ disconnect for $remoteId');
Process process;
try {
process = await _processStart('bluetoothctl', <String>[]);
} on ProcessException catch (error) {
onLog?.call(
'bluetoothctl unavailable, skipping BlueZ disconnect: $error',
);
return;
}
process.stdin.writeln('disconnect $remoteId');
process.stdin.writeln('quit');
try {
await process.exitCode.timeout(_processExitTimeout);
} catch (_) {
process.kill();
}
onLog?.call('Issued bluetoothctl disconnect for $remoteId');
}
Future<bool> isPairedAndTrusted(String remoteId) async {
ProcessResult result;
try {
result = await _processRun('bluetoothctl', <String>['info', remoteId]);
} on ProcessException {
return false;
}
if (result.exitCode != 0) {
return false;
}
final output = (result.stdout as String).toLowerCase();
return output.contains('paired: yes') && output.contains('trusted: yes');
}
Future<bool> trustDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {
onLog?.call('Requesting BlueZ trust for $remoteId');
ProcessResult result;
try {
result = await _processRun('bluetoothctl', <String>['trust', remoteId]);
} on ProcessException catch (error) {
onLog?.call('bluetoothctl unavailable, cannot trust $remoteId: $error');
return false;
}
if (result.exitCode != 0) {
onLog?.call('bluetoothctl trust failed for $remoteId: ${result.stderr}');
return false;
}
final trusted = await isPairedAndTrusted(remoteId);
onLog?.call(
trusted
? 'Verified BlueZ trust for $remoteId'
: 'BlueZ trust verification failed for $remoteId',
);
return trusted;
}
Future<bool> pairAndTrust({
required String remoteId,
Duration timeout = _defaultPairingTimeout,
void Function(String message)? onLog,
Future<String?> Function()? onRequestPin,
}) async {
var removeRetryUsed = false;
var proactivePinRetryUsed = false;
Future<String?> Function()? currentPinProvider = onRequestPin;
for (var attempt = 0; attempt < _maxAttempts; attempt++) {
final result = await _runPairingAttempt(
remoteId: remoteId,
timeout: timeout,
onLog: onLog,
onRequestPin: currentPinProvider,
);
if (result.success) return true;
if (result.userCancelled) {
onLog?.call('Pairing cancelled by user; skipping retry/remove flow');
return false;
}
if (result.pairFailed) {
if (!removeRetryUsed) {
removeRetryUsed = true;
onLog?.call(
'Pairing failed; removing cached bond and retrying '
'(attempt ${attempt + 1}/$_maxAttempts)',
);
await _removeDevice(remoteId, onLog: onLog);
continue;
}
if (!result.pinSent &&
!proactivePinRetryUsed &&
currentPinProvider != null) {
proactivePinRetryUsed = true;
onLog?.call(
'Pairing failed before PIN challenge; requesting PIN for '
'proactive retry (attempt ${attempt + 1}/$_maxAttempts)',
);
final pin = await currentPinProvider();
if (pin == null) {
onLog?.call('PIN entry cancelled for proactive retry');
return false;
}
final capturedPin = pin.trim();
currentPinProvider = () async => capturedPin;
continue;
}
return false;
}
// Timeout path pairing neither succeeded nor failed.
onLog?.call('Pairing did not complete before timeout');
if (!result.pinSent &&
!proactivePinRetryUsed &&
currentPinProvider != null) {
proactivePinRetryUsed = true;
onLog?.call(
'No PIN challenge observed before timeout; requesting PIN for '
'proactive retry (attempt ${attempt + 1}/$_maxAttempts)',
);
final pin = await currentPinProvider();
if (pin == null) {
onLog?.call('PIN entry cancelled for proactive retry after timeout');
return false;
}
final capturedPin = pin.trim();
currentPinProvider = () async => capturedPin;
continue;
}
return false;
}
return false;
}
/// Runs a single bluetoothctl pairing attempt.
///
/// Uses a [Completer] to wake as soon as pairing succeeds or fails,
/// instead of polling.
Future<_PairingResult> _runPairingAttempt({
required String remoteId,
required Duration timeout,
void Function(String message)? onLog,
Future<String?> Function()? onRequestPin,
}) async {
onLog?.call('Starting bluetoothctl pairing flow for $remoteId');
Process process;
try {
process = await _processStart('bluetoothctl', <String>[]);
} on ProcessException catch (error) {
onLog?.call('bluetoothctl unavailable, cannot run pairing flow: $error');
return const _PairingResult();
}
final output = StringBuffer();
var pinSent = false;
var sessionClosed = false;
var userCancelledPinEntry = false;
var confirmationHandled = false;
var successHandled = false;
var failureHandled = false;
var detectorBuffer = '';
final pairingDone = Completer<void>();
var pairSucceeded = false;
var pairFailed = false;
void writeCmd(String cmd) {
if (sessionClosed) return;
try {
process.stdin.writeln(cmd);
} on StateError {
sessionClosed = true;
onLog?.call('bluetoothctl stdin already closed; ignoring "$cmd"');
}
}
unawaited(
process.exitCode.then((_) {
sessionClosed = true;
if (!pairingDone.isCompleted) pairingDone.complete();
}),
);
void handleChunk(String chunk) {
output.write(chunk);
detectorBuffer += chunk.toLowerCase();
if (detectorBuffer.length > 4096) {
detectorBuffer = detectorBuffer.substring(detectorBuffer.length - 4096);
}
final lower = detectorBuffer;
if (!pinSent &&
!sessionClosed &&
(lower.contains('enter pin code') ||
lower.contains('requestpin') ||
lower.contains('input pin code') ||
lower.contains('request passkey') ||
lower.contains('requestpasskey') ||
lower.contains('enter passkey'))) {
pinSent = true;
if (onRequestPin == null) {
onLog?.call(
'PIN/passkey requested but no onRequestPin callback; '
'sending empty line to accept default pairing',
);
writeCmd('');
} else {
onLog?.call('Pairing agent is ready for PIN/passkey input');
unawaited(
Future<void>(() async {
String? pin;
try {
pin = await onRequestPin();
} catch (e) {
onLog?.call('onRequestPin callback threw: $e');
pairFailed = true;
writeCmd('cancel');
if (!pairingDone.isCompleted) pairingDone.complete();
return;
}
if (pin == null) {
if (sessionClosed) {
onLog?.call(
'PIN prompt resolved after pairing session closed',
);
return;
}
onLog?.call('PIN entry cancelled by user; cancelling pairing');
userCancelledPinEntry = true;
pairFailed = true;
writeCmd('cancel');
if (!pairingDone.isCompleted) pairingDone.complete();
return;
}
if (sessionClosed) {
onLog?.call(
'PIN provided after pairing session closed; ignoring',
);
return;
}
if (pin.trim().isEmpty) {
onLog?.call(
'Blank PIN submitted; sending empty line to accept default pairing',
);
writeCmd('');
} else {
onLog?.call('Submitting PIN/passkey to pairing agent');
writeCmd(pin.trim());
}
}),
);
}
}
if (!confirmationHandled &&
(lower.contains('confirm passkey') ||
lower.contains('requestconfirmation') ||
lower.contains('[agent] confirm'))) {
confirmationHandled = true;
onLog?.call(
'Pairing agent requested passkey confirmation; answering yes',
);
writeCmd('yes');
}
if (!successHandled &&
(lower.contains('pairing successful') ||
lower.contains('already paired'))) {
successHandled = true;
onLog?.call('Pairing reported success');
pairSucceeded = true;
if (!pairingDone.isCompleted) pairingDone.complete();
}
if (!failureHandled &&
(lower.contains('failed to pair') ||
lower.contains('authenticationfailed') ||
lower.contains('authentication failed'))) {
failureHandled = true;
onLog?.call('Pairing reported authentication failure');
pairFailed = true;
if (!pairingDone.isCompleted) pairingDone.complete();
}
}
final stdoutSub = process.stdout
.transform(utf8.decoder)
.listen(handleChunk);
final stderrSub = process.stderr
.transform(utf8.decoder)
.listen(handleChunk);
writeCmd('power on');
writeCmd('agent KeyboardDisplay');
writeCmd('default-agent');
onLog?.call('Waiting for pairing challenge from bluetoothctl agent');
writeCmd('pair $remoteId');
// Wait for the Completer to fire (success/failure/process exit) or timeout.
await pairingDone.future.timeout(timeout, onTimeout: () {});
if (!pairFailed && pairSucceeded) {
onLog?.call('Pair succeeded; trusting and connecting device');
writeCmd('trust $remoteId');
writeCmd('connect $remoteId');
}
writeCmd('quit');
sessionClosed = true;
try {
await process.exitCode.timeout(_pairingCleanupTimeout);
} catch (_) {
process.kill();
}
await stdoutSub.cancel();
await stderrSub.cancel();
if (pairFailed) {
return _PairingResult(
pairFailed: true,
pinSent: pinSent,
userCancelled: userCancelledPinEntry,
);
}
final allOutput = output.toString().toLowerCase();
final reportedSuccess =
pairSucceeded ||
allOutput.contains('pairing successful') ||
allOutput.contains('already paired');
if (reportedSuccess) {
final trusted = await trustDevice(remoteId, onLog: onLog);
if (!trusted) {
onLog?.call('Pairing completed but BlueZ trust was not restored');
}
return _PairingResult(success: trusted, pinSent: pinSent);
}
return _PairingResult(pinSent: pinSent);
}
Future<void> _removeDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {
Process process;
try {
process = await _processStart('bluetoothctl', <String>[]);
} on ProcessException catch (error) {
onLog?.call(
'bluetoothctl unavailable, skipping remove for $remoteId: $error',
);
return;
}
process.stdin.writeln('remove $remoteId');
process.stdin.writeln('quit');
try {
await process.exitCode.timeout(_processExitTimeout);
} catch (_) {
process.kill();
}
onLog?.call('Issued bluetoothctl remove for $remoteId');
}
}
/// Outcome of a single bluetoothctl pairing attempt.
class _PairingResult {
final bool success;
final bool pairFailed;
final bool pinSent;
final bool userCancelled;
const _PairingResult({
this.success = false,
this.pairFailed = false,
this.pinSent = false,
this.userCancelled = false,
});
}
@@ -0,0 +1,28 @@
/// No-op stub for web builds where dart:io is unavailable.
///
/// The real implementation lives in linux_ble_pairing_service.dart and is
/// selected via conditional import in meshcore_connector.dart.
class LinuxBlePairingService {
LinuxBlePairingService();
Future<bool> isBluetoothctlAvailable() async => false;
Future<void> disconnectDevice(
String remoteId, {
void Function(String message)? onLog,
}) async {}
Future<bool> isPairedAndTrusted(String remoteId) async => false;
Future<bool> trustDevice(
String remoteId, {
void Function(String message)? onLog,
}) async => false;
Future<bool> pairAndTrust({
required String remoteId,
Duration timeout = const Duration(seconds: 45),
void Function(String message)? onLog,
Future<String?> Function()? onRequestPin,
}) async => false;
}
+23 -8
View File
@@ -21,11 +21,16 @@ class _AckHistoryEntry {
});
}
/// (messageId, timestamp, attemptIndex) stored per ACK hash for O(1) lookup.
/// (messageId, timestamp, attemptIndex, pathSelection) stored per ACK hash
/// for O(1) lookup. [pathSelection] snapshots the route used for this
/// specific attempt so that a late PUSH_CODE_SEND_CONFIRMED credits the
/// correct path even when the message has since been retried on a different
/// route.
typedef AckHashMapping = ({
String messageId,
DateTime timestamp,
int attemptIndex,
PathSelection? pathSelection,
});
class RetryServiceConfig {
@@ -133,6 +138,9 @@ class MessageRetryService extends ChangeNotifier {
Future<void> sendMessageWithRetry({
required Contact contact,
required String text,
String? originalText,
String? translatedLanguageCode,
String? translationModelId,
Uint8List? pathBytes,
int? pathLength,
}) async {
@@ -145,6 +153,9 @@ class MessageRetryService extends ChangeNotifier {
final message = Message(
senderKey: contact.publicKey,
text: text,
originalText: originalText,
translatedLanguageCode: translatedLanguageCode,
translationModelId: translationModelId,
timestamp: DateTime.now(),
isOutgoing: true,
status: MessageStatus.pending,
@@ -382,6 +393,7 @@ class MessageRetryService extends ChangeNotifier {
messageId: messageId,
timestamp: DateTime.now(),
attemptIndex: message.retryCount,
pathSelection: _selectionFromMessage(message),
);
// Add this ACK hash to the list of expected ACKs for this message (for history)
@@ -395,14 +407,11 @@ class MessageRetryService extends ChangeNotifier {
int actualTimeout = timeoutMs;
if (config.calculateTimeout != null) {
final calculated = config.calculateTimeout!(
actualTimeout = config.calculateTimeout!(
pathLengthValue,
message.text.length,
contactKey: contact.publicKeyHex,
);
if (timeoutMs <= 0 || calculated < timeoutMs) {
actualTimeout = calculated;
}
}
final updatedMessage = message.copyWith(
@@ -569,6 +578,7 @@ class MessageRetryService extends ChangeNotifier {
final config = _config;
String? matchedMessageId;
int? matchedAttemptIndex;
PathSelection? matchedPathSelection;
final ackHashHex = ackHash.toRadixString(16).padLeft(8, '0');
// Clean up old ACK hash mappings (older than 15 minutes)
@@ -588,6 +598,7 @@ class MessageRetryService extends ChangeNotifier {
if (mapping != null) {
matchedMessageId = mapping.messageId;
matchedAttemptIndex = mapping.attemptIndex;
matchedPathSelection = mapping.pathSelection;
} else {
config?.debugLogService?.warn(
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex not found in direct mapping, trying fallback',
@@ -618,13 +629,13 @@ class MessageRetryService extends ChangeNotifier {
}
final contact = _pendingContacts[matchedMessageId];
final ackedAttempt = matchedAttemptIndex ?? message.retryCount;
final selection = _selectionFromMessage(message);
final selection = matchedPathSelection ?? _selectionFromMessage(message);
final shortText = message.text.length > 20
? '${message.text.substring(0, 20)}...'
: message.text;
config?.debugLogService?.info(
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on retry ${ackedAttempt + 1} in ${tripTimeMs}ms',
'PUSH_CODE_SEND_CONFIRMED: ACK hash $ackHashHex ✓ "$shortText" delivered to ${contact?.name ?? "unknown"} on attempt $ackedAttempt in ${tripTimeMs}ms',
tag: 'AckHash',
);
@@ -636,6 +647,8 @@ class MessageRetryService extends ChangeNotifier {
tripTimeMs: tripTimeMs,
);
final wasAlreadyResolved = _resolvedMessages.contains(matchedMessageId);
_cleanupMessage(matchedMessageId);
config?.updateMessage(deliveredMessage);
@@ -658,7 +671,9 @@ class MessageRetryService extends ChangeNotifier {
tripTimeMs,
);
}
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
if (!wasAlreadyResolved) {
_onMessageResolved(matchedMessageId, contact.publicKeyHex);
}
}
notifyListeners();
+2
View File
@@ -0,0 +1,2 @@
export 'translation_file_store_stub.dart'
if (dart.library.io) 'translation_file_store_io.dart';
+131
View File
@@ -0,0 +1,131 @@
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../models/translation_support.dart';
class TranslationFileStore {
Future<String> modelDirectoryPath() async {
final baseDir = await getApplicationDocumentsDirectory();
final dir = Directory('${baseDir.path}/translation_models');
if (!dir.existsSync()) {
await dir.create(recursive: true);
}
return dir.path;
}
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
final dir = Directory(await modelDirectoryPath());
if (!dir.existsSync()) {
return const [];
}
final models = <TranslationModelRecord>[];
for (final entity in dir.listSync().whereType<File>()) {
final name = entity.uri.pathSegments.last;
// Skip hidden chunk files from interrupted parallel downloads.
if (name.startsWith('.')) {
await entity.delete();
continue;
}
final stat = entity.statSync();
models.add(
TranslationModelRecord(
id: name,
name: name,
sourceUrl: '',
localPath: entity.path,
downloadedAt: stat.modified,
fileSizeBytes: stat.size,
),
);
}
return models;
}
Future<void> deleteModel(TranslationModelRecord model) async {
await deleteFile(model.localPath);
}
Future<void> deleteFile(String path) async {
final file = File(path);
if (file.existsSync()) {
await file.delete();
}
}
Future<DownloadedModelFile> writeModelBytes({
required String fileName,
required Stream<List<int>> chunks,
}) async {
final directoryPath = await modelDirectoryPath();
final file = File('$directoryPath/$fileName');
final sink = file.openWrite();
var fileSizeBytes = 0;
var completed = false;
try {
await for (final chunk in chunks) {
sink.add(chunk);
fileSizeBytes += chunk.length;
}
completed = true;
} finally {
await sink.close();
if (!completed && file.existsSync()) {
await file.delete();
}
}
return DownloadedModelFile(
localPath: file.path,
fileSizeBytes: fileSizeBytes,
);
}
Future<String> chunkFilePath(String fileName, int index) async {
final dir = await modelDirectoryPath();
return '$dir/.${fileName}_chunk_$index';
}
Future<DownloadedModelFile> combineChunks({
required String fileName,
required List<String> chunkPaths,
}) async {
final dir = await modelDirectoryPath();
final finalPath = '$dir/$fileName';
final sink = File(finalPath).openWrite();
var totalSize = 0;
var completed = false;
try {
for (final chunkPath in chunkPaths) {
final chunkFile = File(chunkPath);
await sink.addStream(chunkFile.openRead());
totalSize += await chunkFile.length();
}
completed = true;
} finally {
await sink.close();
for (final chunkPath in chunkPaths) {
final file = File(chunkPath);
if (file.existsSync()) {
await file.delete();
}
}
if (!completed) {
final finalFile = File(finalPath);
if (finalFile.existsSync()) {
await finalFile.delete();
}
}
}
return DownloadedModelFile(localPath: finalPath, fileSizeBytes: totalSize);
}
}
class DownloadedModelFile {
final String localPath;
final int fileSizeBytes;
const DownloadedModelFile({
required this.localPath,
required this.fileSizeBytes,
});
}
@@ -0,0 +1,43 @@
import '../models/translation_support.dart';
class TranslationFileStore {
Future<String> modelDirectoryPath() async {
throw UnsupportedError('Local model storage is not supported on web.');
}
Future<List<TranslationModelRecord>> scanDownloadedModels() async {
return const [];
}
Future<void> deleteModel(TranslationModelRecord model) async {}
Future<void> deleteFile(String path) async {}
Future<DownloadedModelFile> writeModelBytes({
required String fileName,
required Stream<List<int>> chunks,
}) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
Future<String> chunkFilePath(String fileName, int index) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
Future<DownloadedModelFile> combineChunks({
required String fileName,
required List<String> chunkPaths,
}) async {
throw UnsupportedError('Local model downloads are not supported on web.');
}
}
class DownloadedModelFile {
final String localPath;
final int fileSizeBytes;
const DownloadedModelFile({
required this.localPath,
required this.fileSizeBytes,
});
}
+660
View File
@@ -0,0 +1,660 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:llamadart/llamadart.dart';
import '../models/app_settings.dart';
import '../models/translation_support.dart';
import '../utils/app_logger.dart';
import 'app_settings_service.dart';
import 'translation_file_store.dart';
class TranslationResult {
final String translatedText;
final String targetLanguageCode;
final String? detectedLanguageCode;
final String? modelId;
final MessageTranslationStatus status;
const TranslationResult({
required this.translatedText,
required this.targetLanguageCode,
required this.status,
this.detectedLanguageCode,
this.modelId,
});
}
class TranslationDownloadCancelled implements Exception {
const TranslationDownloadCancelled();
@override
String toString() => 'Download canceled.';
}
class TranslationService extends ChangeNotifier {
final AppSettingsService _appSettingsService;
final TranslationFileStore _fileStore;
TranslationService(
this._appSettingsService, {
TranslationFileStore? fileStore,
}) : _fileStore = fileStore ?? TranslationFileStore();
bool _isBusy = false;
bool _isDownloading = false;
bool _cancelDownloadRequested = false;
String? _lastError;
Future<void> _queue = Future<void>.value();
LlamaEngine? _engine;
String? _loadedModelPath;
String? _failedModelPath;
int _downloadedBytes = 0;
int? _downloadTotalBytes;
String? _downloadFileName;
bool get isBusy => _isBusy;
bool get isDownloading => _isDownloading;
String? get lastError => _lastError;
int get downloadedBytes => _downloadedBytes;
int? get downloadTotalBytes => _downloadTotalBytes;
String? get downloadFileName => _downloadFileName;
double? get downloadProgress {
final total = _downloadTotalBytes;
if (!_isDownloading || total == null || total <= 0) {
return null;
}
return (_downloadedBytes / total).clamp(0.0, 1.0);
}
AppSettings get _settings => _appSettingsService.settings;
String? resolvedTargetLanguageCode(String? fallbackLanguageCode) {
return _settings.translationTargetLanguageCode ??
_settings.languageOverride ??
fallbackLanguageCode;
}
String? resolvedIncomingLanguageCode(String? fallbackLanguageCode) {
return _settings.translationTargetLanguageCode ??
_settings.languageOverride ??
fallbackLanguageCode ??
'en';
}
bool shouldTranslateIncoming({
required String text,
required bool isCli,
required bool isOutgoing,
}) {
if (!_settings.translationEnabled || isCli || isOutgoing) {
return false;
}
return _isPlainTextEligible(text);
}
bool shouldTranslateOutgoing({
required String text,
required String? targetLanguageCode,
}) {
return _settings.composerTranslationEnabled &&
targetLanguageCode != null &&
targetLanguageCode.isNotEmpty &&
_isPlainTextEligible(text);
}
List<TranslationModelRecord> get availableModels =>
_settings.translationDownloadedModels;
TranslationModelRecord? get selectedModel {
final selectedId = _settings.translationSelectedModelId;
if (selectedId == null) {
return availableModels.isNotEmpty ? availableModels.first : null;
}
for (final model in availableModels) {
if (model.id == selectedId) {
return model;
}
}
return availableModels.isNotEmpty ? availableModels.first : null;
}
Future<void> refreshDownloadedModels() async {
if (_isDownloading) return;
final scanned = await _fileStore.scanDownloadedModels();
if (scanned.isEmpty) {
return;
}
final existingByPath = {
for (final model in _settings.translationDownloadedModels)
model.localPath: model,
};
final merged = scanned.map((model) {
final existing = existingByPath[model.localPath];
if (existing == null) {
return model;
}
return TranslationModelRecord(
id: existing.id,
name: existing.name,
sourceUrl: existing.sourceUrl,
localPath: existing.localPath,
downloadedAt: existing.downloadedAt,
fileSizeBytes: model.fileSizeBytes,
);
}).toList();
await _appSettingsService.setTranslationDownloadedModels(merged);
_failedModelPath = null;
if (_settings.translationSelectedModelId == null && merged.isNotEmpty) {
await _appSettingsService.setTranslationSelectedModelId(merged.first.id);
}
}
static const int _parallelChunks = 8;
static const int _parallelMinBytes = 10 * 1024 * 1024; // 10 MB
Future<TranslationModelRecord> downloadModel({
required String sourceUrl,
String? fileName,
String? id,
}) async {
final uri = Uri.tryParse(sourceUrl);
if (uri == null || !uri.hasScheme) {
throw ArgumentError('Invalid model URL.');
}
return _runExclusive(() async {
_setBusy(true);
_setDownloading(true);
_lastError = null;
try {
final resolvedFileName =
fileName ??
_sanitizeFileName(
uri.pathSegments.isNotEmpty
? uri.pathSegments.last
: 'translation-model.gguf',
);
_downloadFileName = resolvedFileName;
_downloadedBytes = 0;
_cancelDownloadRequested = false;
// HEAD request to check size and range support.
final headClient = http.Client();
int? totalSize;
bool supportsRange = false;
try {
final headResponse = await headClient.send(http.Request('HEAD', uri));
totalSize = headResponse.contentLength;
supportsRange =
headResponse.headers['accept-ranges']?.contains('bytes') == true;
await headResponse.stream.drain<void>();
} finally {
headClient.close();
}
_downloadTotalBytes = totalSize;
notifyListeners();
DownloadedModelFile downloaded;
if (supportsRange &&
totalSize != null &&
totalSize > _parallelMinBytes) {
downloaded = await _downloadParallel(
uri: uri,
fileName: resolvedFileName,
totalSize: totalSize,
);
} else {
downloaded = await _downloadSingle(
uri: uri,
fileName: resolvedFileName,
);
}
final record = TranslationModelRecord(
id: id ?? resolvedFileName,
name: resolvedFileName,
sourceUrl: sourceUrl,
localPath: downloaded.localPath,
downloadedAt: DateTime.now(),
fileSizeBytes: downloaded.fileSizeBytes,
);
final updated = [
for (final existing in _settings.translationDownloadedModels)
if (existing.id != record.id) existing,
record,
];
await _appSettingsService.setTranslationDownloadedModels(updated);
await _appSettingsService.setTranslationSelectedModelId(record.id);
await _appSettingsService.setTranslationModelSourceUrl(sourceUrl);
_failedModelPath = null;
return record;
} finally {
_setDownloading(false);
}
});
}
Future<DownloadedModelFile> _downloadSingle({
required Uri uri,
required String fileName,
}) async {
final client = http.Client();
try {
final response = await client.send(http.Request('GET', uri));
if (response.statusCode < 200 || response.statusCode >= 300) {
throw StateError('Model download failed: HTTP ${response.statusCode}');
}
_downloadTotalBytes ??= response.contentLength;
notifyListeners();
final trackedStream = _trackDownloadProgress(response.stream);
return await _fileStore.writeModelBytes(
fileName: fileName,
chunks: trackedStream,
);
} finally {
client.close();
}
}
Future<DownloadedModelFile> _downloadParallel({
required Uri uri,
required String fileName,
required int totalSize,
}) async {
final chunkSize = (totalSize / _parallelChunks).ceil();
final chunkPaths = <String>[];
final clients = <http.Client>[];
var combineReached = false;
try {
final futures = <Future<void>>[];
for (var i = 0; i < _parallelChunks; i++) {
final start = i * chunkSize;
final end = (start + chunkSize - 1).clamp(0, totalSize - 1);
if (start >= totalSize) break;
final chunkPath = await _fileStore.chunkFilePath(fileName, i);
chunkPaths.add(chunkPath);
final client = http.Client();
clients.add(client);
futures.add(
_downloadRange(
client: client,
uri: uri,
chunkPath: chunkPath,
start: start,
end: end,
),
);
}
await Future.wait(futures);
if (_cancelDownloadRequested) {
throw const TranslationDownloadCancelled();
}
_downloadFileName = 'Merging chunks...';
notifyListeners();
combineReached = true;
return await _fileStore.combineChunks(
fileName: fileName,
chunkPaths: chunkPaths,
);
} finally {
for (final client in clients) {
client.close();
}
if (!combineReached) {
for (final chunkPath in chunkPaths) {
await _fileStore.deleteFile(chunkPath);
}
}
}
}
Future<void> _downloadRange({
required http.Client client,
required Uri uri,
required String chunkPath,
required int start,
required int end,
}) async {
final request = http.Request('GET', uri);
request.headers['Range'] = 'bytes=$start-$end';
final response = await client.send(request);
if (response.statusCode != 206) {
await response.stream.drain<void>();
throw StateError(
'Range download failed: HTTP ${response.statusCode}'
'${response.statusCode == 200 ? ' (server ignored Range header)' : ''}',
);
}
final trackedStream = _trackDownloadProgress(response.stream);
await _fileStore.writeModelBytes(
fileName: chunkPath.split(RegExp(r'[/\\]')).last,
chunks: trackedStream,
);
}
void cancelDownload() {
if (!_isDownloading) {
return;
}
_cancelDownloadRequested = true;
_lastError = 'Download stopped.';
notifyListeners();
}
Future<void> removeModel(TranslationModelRecord model) async {
await _runExclusive(() async {
_setBusy(true);
_lastError = null;
await _fileStore.deleteModel(model);
final updated = _settings.translationDownloadedModels
.where((entry) => entry.id != model.id)
.toList();
await _appSettingsService.setTranslationDownloadedModels(updated);
if (_settings.translationSelectedModelId == model.id) {
await _appSettingsService.setTranslationSelectedModelId(
updated.isNotEmpty ? updated.first.id : null,
);
}
});
}
Future<TranslationResult?> translateIncomingText({
required String text,
required String? targetLanguageCode,
}) async {
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
return null;
}
final detectedLanguageCode = await detectLanguage(text);
if (detectedLanguageCode != null &&
detectedLanguageCode == targetLanguageCode) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
final translatedText = await _translateText(
text: text,
targetLanguageCode: targetLanguageCode,
sourceLanguageCode: detectedLanguageCode,
);
if (translatedText == null || translatedText.trim().isEmpty) {
return null;
}
// If translation is nearly identical, text was already in target language.
if (translatedText.trim().toLowerCase() == text.trim().toLowerCase()) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
return TranslationResult(
translatedText: translatedText.trim(),
targetLanguageCode: targetLanguageCode,
detectedLanguageCode: detectedLanguageCode,
modelId: selectedModel?.id,
status: MessageTranslationStatus.completed,
);
}
Future<TranslationResult?> translateOutgoingText({
required String text,
required String? targetLanguageCode,
}) async {
if (targetLanguageCode == null || !_isPlainTextEligible(text)) {
return null;
}
final detectedLanguageCode = await detectLanguage(text);
if (detectedLanguageCode != null &&
detectedLanguageCode == targetLanguageCode) {
return const TranslationResult(
translatedText: '',
targetLanguageCode: '',
status: MessageTranslationStatus.skipped,
);
}
final translatedText = await _translateText(
text: text,
targetLanguageCode: targetLanguageCode,
sourceLanguageCode: detectedLanguageCode,
);
if (translatedText == null || translatedText.trim().isEmpty) {
return null;
}
return TranslationResult(
translatedText: translatedText.trim(),
targetLanguageCode: targetLanguageCode,
detectedLanguageCode: detectedLanguageCode,
modelId: selectedModel?.id,
status: MessageTranslationStatus.completed,
);
}
Future<String?> detectLanguage(String text) async {
return _heuristicLanguageCode(text);
}
Future<String?> _translateText({
required String text,
required String targetLanguageCode,
String? sourceLanguageCode,
}) async {
if (!_hasUsableModel) {
return null;
}
final model = selectedModel;
if (model == null || model.localPath.isEmpty) {
return null;
}
final targetLabel = _languageLabel(targetLanguageCode);
final instruction = targetLanguageCode == 'zh'
? '将以下文本翻译为中文,注意只需要输出翻译后的结果,不要额外解释:\n\n$text'
: 'Translate the following segment into $targetLabel, without additional explanation.\n\n$text';
try {
return await _runExclusive(() async {
final engine = await _ensureContext(model.localPath);
if (engine == null) {
return null;
}
final messages = [
LlamaChatMessage.fromText(
role: LlamaChatRole.user,
text: instruction,
),
];
final output = StringBuffer();
await for (final chunk in engine.create(
messages,
params: const GenerationParams(
maxTokens: 256,
temp: 0.7,
topK: 20,
topP: 0.6,
penalty: 1.05,
reusePromptPrefix: false,
),
enableThinking: false,
sourceLangCode: sourceLanguageCode,
targetLangCode: targetLanguageCode,
)) {
final content = chunk.choices.firstOrNull?.delta.content;
if (content != null) {
output.write(content);
}
if (output.length >= text.length * 4 + 100) {
break;
}
}
return _sanitizeOutput(output.toString());
});
} catch (error) {
_lastError = error.toString();
appLogger.warn('Translation request failed: $error');
notifyListeners();
return null;
}
}
bool get _hasUsableModel {
final model = selectedModel;
return !kIsWeb && model != null && model.localPath.isNotEmpty;
}
bool _isPlainTextEligible(String text) {
final trimmed = text.trim();
if (trimmed.isEmpty) {
return false;
}
return !(trimmed.startsWith('g:') ||
trimmed.startsWith('m:') ||
trimmed.startsWith('V1|') ||
trimmed.startsWith('r:'));
}
String? _heuristicLanguageCode(String text) {
if (RegExp(r'[іїєґІЇЄҐ]').hasMatch(text)) {
return 'uk';
}
if (RegExp(r'[а-яёА-ЯЁ]').hasMatch(text)) {
return 'ru';
}
if (RegExp(r'[ぁ-んァ-ン]').hasMatch(text)) {
return 'ja';
}
if (RegExp(r'[가-힣]').hasMatch(text)) {
return 'ko';
}
if (RegExp(r'[\u4e00-\u9fff]').hasMatch(text)) {
return 'zh';
}
// Latin-script languages can't be reliably distinguished by characters
// alone return null so the translator always attempts translation.
return null;
}
String _languageLabel(String code) {
for (final option in supportedTranslationLanguages) {
if (option.code == code) {
return option.label;
}
}
return code.toUpperCase();
}
String _sanitizeOutput(String raw) {
var result = raw.trim();
result = result.replaceAll(RegExp(r'\*\*'), '');
result = result.replaceAll(RegExp(r'<[^>]+>'), '');
return result.trim();
}
String _sanitizeFileName(String fileName) {
final cleaned = fileName.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
return cleaned.isEmpty ? 'translation-model.gguf' : cleaned;
}
Future<LlamaEngine?> _ensureContext(String modelPath) async {
if (_engine != null && _loadedModelPath == modelPath) {
return _engine;
}
if (modelPath == _failedModelPath) {
return null;
}
if (_engine != null) {
await _engine!.dispose();
_engine = null;
_loadedModelPath = null;
}
final engine = LlamaEngine(LlamaBackend());
try {
await engine.loadModel(
modelPath,
modelParams: const ModelParams(
gpuLayers: 0,
preferredBackend: GpuBackend.cpu,
),
);
_engine = engine;
_loadedModelPath = modelPath;
_failedModelPath = null;
return _engine;
} catch (_) {
await engine.dispose();
_failedModelPath = modelPath;
rethrow;
}
}
Future<void> releaseModel() async {
await _runExclusive(() async {
final engine = _engine;
if (engine == null) {
_loadedModelPath = null;
return;
}
_engine = null;
_loadedModelPath = null;
await engine.dispose();
});
}
Future<T> _runExclusive<T>(Future<T> Function() action) {
final completer = Completer<T>();
_setBusy(true);
_queue = _queue.then((_) async {
try {
completer.complete(await action());
} catch (error, stackTrace) {
completer.completeError(error, stackTrace);
} finally {
_setBusy(false);
}
});
return completer.future;
}
Stream<List<int>> _trackDownloadProgress(Stream<List<int>> source) async* {
await for (final chunk in source) {
if (_cancelDownloadRequested) {
throw const TranslationDownloadCancelled();
}
_downloadedBytes += chunk.length;
notifyListeners();
yield chunk;
}
}
void _setBusy(bool value) {
if (_isBusy == value) {
return;
}
_isBusy = value;
notifyListeners();
}
void _setDownloading(bool value) {
_isDownloading = value;
if (!value) {
_cancelDownloadRequested = false;
_downloadedBytes = 0;
_downloadTotalBytes = null;
_downloadFileName = null;
}
notifyListeners();
}
@override
void dispose() {
final engine = _engine;
_engine = null;
_loadedModelPath = null;
if (engine != null) {
unawaited(engine.dispose());
}
super.dispose();
}
}
+11 -11
View File
@@ -273,7 +273,7 @@ class UsbSerialService {
throw StateError('USB serial port is not open');
}
final packet = wrapUsbSerialTxFrame(data);
_logFrameSummary('USB TX frame', data);
// _logFrameSummary('USB TX frame', data);
if (_useAndroidUsbHost) {
try {
await _androidMethodChannel.invokeMethod<void>('write', {
@@ -447,16 +447,16 @@ class UsbSerialService {
await _frameController.close();
}
void _logFrameSummary(String prefix, Uint8List bytes) {
if (bytes.isEmpty) {
_debugLogService?.info('$prefix len=0', tag: 'USB Serial');
return;
}
_debugLogService?.info(
'$prefix code=${bytes[0]} len=${bytes.length}',
tag: 'USB Serial',
);
}
// void _logFrameSummary(String prefix, Uint8List bytes) {
// if (bytes.isEmpty) {
// _debugLogService?.info('$prefix len=0', tag: 'USB Serial');
// return;
// }
// _debugLogService?.info(
// '$prefix code=${bytes[0]} len=${bytes.length}',
// tag: 'USB Serial',
// );
// }
/// Returns an ordered list of port paths to try for [portName].
///
+13
View File
@@ -3,6 +3,7 @@ import 'dart:typed_data';
import 'package:meshcore_open/utils/app_logger.dart';
import '../models/channel_message.dart';
import '../models/translation_support.dart';
import '../helpers/smaz.dart';
import 'prefs_manager.dart';
@@ -98,6 +99,11 @@ class ChannelMessageStore {
'senderKey': msg.senderKey != null ? base64Encode(msg.senderKey!) : null,
'senderName': msg.senderName,
'text': msg.text,
'originalText': msg.originalText,
'translatedText': msg.translatedText,
'translatedLanguageCode': msg.translatedLanguageCode,
'translationStatus': msg.translationStatus.value,
'translationModelId': msg.translationModelId,
'timestamp': msg.timestamp.millisecondsSinceEpoch,
'isOutgoing': msg.isOutgoing,
'status': msg.status.index,
@@ -126,6 +132,13 @@ class ChannelMessageStore {
: null,
senderName: json['senderName'] as String,
text: decodedText,
originalText: json['originalText'] as String?,
translatedText: json['translatedText'] as String?,
translatedLanguageCode: json['translatedLanguageCode'] as String?,
translationStatus: parseMessageTranslationStatus(
json['translationStatus'],
),
translationModelId: json['translationModelId'] as String?,
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp'] as int),
isOutgoing: json['isOutgoing'] as bool,
status: ChannelMessageStatus.values[json['status'] as int],
+13
View File
@@ -1,6 +1,7 @@
import 'dart:convert';
import 'dart:typed_data';
import '../models/message.dart';
import '../models/translation_support.dart';
import '../helpers/smaz.dart';
import '../utils/app_logger.dart';
import 'prefs_manager.dart';
@@ -83,6 +84,11 @@ class MessageStore {
'isCli': msg.isCli,
'status': msg.status.index,
'messageId': msg.messageId,
'originalText': msg.originalText,
'translatedText': msg.translatedText,
'translatedLanguageCode': msg.translatedLanguageCode,
'translationStatus': msg.translationStatus.value,
'translationModelId': msg.translationModelId,
'retryCount': msg.retryCount,
'estimatedTimeoutMs': msg.estimatedTimeoutMs,
'expectedAckHash': msg.expectedAckHash,
@@ -115,6 +121,13 @@ class MessageStore {
isCli: isCli,
status: MessageStatus.values[json['status'] as int],
messageId: json['messageId'] as String?,
originalText: json['originalText'] as String?,
translatedText: json['translatedText'] as String?,
translatedLanguageCode: json['translatedLanguageCode'] as String?,
translationStatus: parseMessageTranslationStatus(
json['translationStatus'],
),
translationModelId: json['translationModelId'] as String?,
retryCount: json['retryCount'] as int? ?? 0,
estimatedTimeoutMs: json['estimatedTimeoutMs'] as int?,
expectedAckHash: json['expectedAckHash'] as int? ?? 0,
+25 -9
View File
@@ -14,12 +14,13 @@ class ContactExport {
final double lon;
final String desc;
final double? ele;
final String url;
ContactExport({
required this.name,
required this.lat,
required this.lon,
required this.desc,
required this.url,
this.ele,
});
}
@@ -40,6 +41,7 @@ class GpxExport {
String name,
double lat,
double lon,
String url,
String desc, [
double? ele,
]) {
@@ -50,55 +52,66 @@ class GpxExport {
lon: lon,
desc: desc.trim(),
ele: ele,
url: url,
),
);
}
void addRepeaters() {
final contacts = _connector.contacts
.where((c) => c.type == advTypeRepeater || c.type == advTypeRoom)
.toList();
final contacts = _connector.allContacts.where(
(c) => c.type == advTypeRepeater || c.type == advTypeRoom,
);
for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) {
continue;
}
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact(
contact.name,
contact.latitude!,
contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
);
}
}
void addContacts() {
final contacts = _connector.contacts
.where((c) => c.type == advTypeChat)
.toList();
final contacts = _connector.allContacts.where((c) => c.type == advTypeChat);
for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) {
continue;
}
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact(
contact.name,
contact.latitude!,
contact.longitude!,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
);
}
}
void addAll() {
final contacts = _connector.contacts;
for (var contact in contacts.toList()) {
final contacts = _connector.allContacts;
for (var contact in contacts) {
if (contact.latitude == null || contact.longitude == null) {
continue;
}
final url = contact.rawPacket != null
? "meshcore://${pubKeyToHex(contact.rawPacket!)}"
: "";
_addContact(
contact.name,
contact.latitude ?? 0.0,
contact.longitude ?? 0.0,
"Type: ${contact.typeLabel}\nPublic Key: ${contact.publicKeyHex}",
url,
);
}
}
@@ -138,6 +151,9 @@ class GpxExport {
ele: c.ele,
name: c.name,
desc: c.desc,
extensions: {
"meshcore": {"url": c.url},
},
),
)
.toList();
+7 -2
View File
@@ -3,6 +3,7 @@ import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/widgets/battery_indicator.dart';
import 'package:provider/provider.dart';
import 'radio_stats_entry.dart';
import 'snr_indicator.dart';
class AppBarTitle extends StatelessWidget {
@@ -10,12 +11,14 @@ class AppBarTitle extends StatelessWidget {
final Widget? leading;
final Widget? trailing;
final bool indicators;
final bool showBatteryIndicator;
final bool subtitle;
const AppBarTitle(
this.title, {
this.leading,
this.trailing,
this.indicators = true,
this.showBatteryIndicator = true,
this.subtitle = true,
super.key,
});
@@ -33,7 +36,7 @@ class AppBarTitle extends StatelessWidget {
final compact = availableWidth < 170;
final showSubtitle =
!compact && connector.isConnected && selfName != null && subtitle;
final showBattery = availableWidth >= 60;
final showBattery = showBatteryIndicator && availableWidth >= 60;
final showSnr = availableWidth >= 110;
final showIndicators = (showBattery || showSnr) && indicators;
@@ -60,11 +63,13 @@ class AppBarTitle extends StatelessWidget {
if (showIndicators) const SizedBox(width: 6),
if (showIndicators)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
if (showBattery) BatteryIndicator(connector: connector),
if (showSnr) SNRIndicator(connector: connector),
if (connector.supportsCompanionRadioStats)
const RadioStatsIconButton(compact: true),
],
),
trailing ?? const SizedBox.shrink(),
+204
View File
@@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import '../l10n/l10n.dart';
import '../models/translation_support.dart';
class MessageTranslationButton extends StatelessWidget {
final bool enabled;
final String? languageCode;
final VoidCallback onPressed;
const MessageTranslationButton({
super.key,
required this.enabled,
required this.languageCode,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final label = _languageLabel(
languageCode,
context.l10n.translation_systemLanguage,
);
return IconButton(
icon: Icon(enabled ? Icons.translate : Icons.translate_outlined),
onPressed: onPressed,
tooltip: enabled
? context.l10n.translation_translateTo(label)
: context.l10n.translation_translationOptions,
);
}
}
Future<void> showMessageTranslationSheet({
required BuildContext context,
required bool enabled,
required String? selectedLanguageCode,
required ValueChanged<bool> onEnabledChanged,
required ValueChanged<String?> onLanguageSelected,
}) {
return showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (context) => _MessageTranslationSheet(
enabled: enabled,
selectedLanguageCode: selectedLanguageCode,
onEnabledChanged: onEnabledChanged,
onLanguageSelected: onLanguageSelected,
),
);
}
class _MessageTranslationSheet extends StatefulWidget {
final bool enabled;
final String? selectedLanguageCode;
final ValueChanged<bool> onEnabledChanged;
final ValueChanged<String?> onLanguageSelected;
const _MessageTranslationSheet({
required this.enabled,
required this.selectedLanguageCode,
required this.onEnabledChanged,
required this.onLanguageSelected,
});
@override
State<_MessageTranslationSheet> createState() =>
_MessageTranslationSheetState();
}
class _MessageTranslationSheetState extends State<_MessageTranslationSheet> {
late final TextEditingController _searchController;
late bool _localEnabled;
late String? _localSelectedLanguageCode;
List<TranslationLanguageOption> _filtered = supportedTranslationLanguages;
@override
void initState() {
super.initState();
_searchController = TextEditingController();
_localEnabled = widget.enabled;
_localSelectedLanguageCode = widget.selectedLanguageCode;
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _updateFilter(String query) {
final normalized = query.trim().toLowerCase();
setState(() {
_filtered = supportedTranslationLanguages.where((option) {
return option.label.toLowerCase().contains(normalized) ||
option.code.toLowerCase().contains(normalized);
}).toList();
});
}
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 16,
bottom: 16 + MediaQuery.of(context).viewInsets.bottom,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
context.l10n.translation_messageTranslation,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(context.l10n.translation_translateBeforeSending),
subtitle: Text(
_localEnabled
? context.l10n.translation_composerEnabledHint
: context.l10n.translation_composerDisabledHint,
),
value: _localEnabled,
onChanged: (value) {
setState(() => _localEnabled = value);
widget.onEnabledChanged(value);
},
),
const SizedBox(height: 8),
TextField(
controller: _searchController,
onChanged: _updateFilter,
decoration: InputDecoration(
labelText: context.l10n.translation_targetLanguage,
prefixIcon: const Icon(Icons.search),
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: _filtered.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
final selected = _localSelectedLanguageCode == null;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
selected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: Text(context.l10n.translation_useAppLanguage),
onTap: () {
setState(() => _localSelectedLanguageCode = null);
widget.onLanguageSelected(null);
Navigator.pop(context);
},
);
}
final option = _filtered[index - 1];
final selected = option.code == _localSelectedLanguageCode;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Icon(
selected
? Icons.radio_button_checked
: Icons.radio_button_unchecked,
),
title: Text(option.label),
subtitle: Text(option.code.toUpperCase()),
onTap: () {
setState(() => _localSelectedLanguageCode = option.code);
widget.onLanguageSelected(option.code);
Navigator.pop(context);
},
);
},
),
),
],
),
),
);
}
}
String _languageLabel(String? languageCode, String systemLanguageFallback) {
if (languageCode == null) {
return systemLanguageFallback;
}
for (final option in supportedTranslationLanguages) {
if (option.code == languageCode) {
return option.label;
}
}
return languageCode.toUpperCase();
}
+4 -1
View File
@@ -109,6 +109,7 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
path: Uint8List.fromList(pathBytes),
flipPathAround: true,
targetContact: widget.contact,
pathHashByteWidth: connector.pathHashByteWidth,
),
),
),
@@ -135,7 +136,9 @@ class _PathManagementDialogState extends State<_PathManagementDialog> {
connector.getContacts();
}
final pathForInput = currentContact.pathIdList;
final pathForInput = currentContact.pathFormattedIdList(
connector.pathHashByteWidth,
);
final availableContacts = connector.allContacts
.where((c) => c.publicKeyHex != currentContact.publicKeyHex)
.toList();
+147
View File
@@ -0,0 +1,147 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/models/companion_radio_stats.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:meshcore_open/screens/companion_radio_stats_screen.dart';
import 'package:provider/provider.dart';
void pushCompanionRadioStatsScreen(BuildContext context) {
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => const CompanionRadioStatsScreen(),
),
);
}
class RadioStatsIconButton extends StatefulWidget {
final bool compact;
const RadioStatsIconButton({super.key, this.compact = false});
@override
State<RadioStatsIconButton> createState() => _RadioStatsIconButtonState();
}
class _RadioStatsIconButtonState extends State<RadioStatsIconButton> {
MeshCoreConnector? _connector;
@override
void initState() {
super.initState();
final c = context.read<MeshCoreConnector>();
_connector = c;
c.acquireRadioStatsPolling();
}
@override
void dispose() {
_connector?.releaseRadioStatsPolling();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Selector<MeshCoreConnector, ({bool connected, bool supported})>(
selector: (_, c) =>
(connected: c.isConnected, supported: c.supportsCompanionRadioStats),
builder: (context, state, _) {
if (!state.connected || !state.supported) {
return const SizedBox.shrink();
}
final connector = context.read<MeshCoreConnector>();
return ValueListenableBuilder<CompanionRadioStats?>(
valueListenable: connector.radioStatsNotifier,
builder: (context, _, child) {
final dot = AirActivityDot(
active: connector.radioStatsAirActivityPulse,
);
if (widget.compact) {
return GestureDetector(
onTap: () => pushCompanionRadioStatsScreen(context),
child: Padding(
padding: const EdgeInsets.only(left: 4),
child: dot,
),
);
}
return Tooltip(
message: context.l10n.radioStats_tooltip,
child: InkWell(
customBorder: const CircleBorder(),
onTap: () => pushCompanionRadioStatsScreen(context),
child: SizedBox(
width: 48,
height: 48,
child: Center(child: dot),
),
),
);
},
);
},
);
}
}
class AirActivityDot extends StatefulWidget {
final bool active;
const AirActivityDot({super.key, required this.active});
@override
State<AirActivityDot> createState() => AirActivityDotState();
}
class AirActivityDotState extends State<AirActivityDot> {
Timer? _timer;
bool _blink = true;
@override
void initState() {
super.initState();
if (widget.active) _startTimer();
}
@override
void didUpdateWidget(covariant AirActivityDot oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.active && !oldWidget.active) {
_startTimer();
} else if (!widget.active && oldWidget.active) {
_stopTimer();
_blink = true;
}
}
void _startTimer() {
_timer ??= Timer.periodic(const Duration(milliseconds: 400), (_) {
if (!mounted) return;
setState(() => _blink = !_blink);
});
}
void _stopTimer() {
_timer?.cancel();
_timer = null;
}
@override
void dispose() {
_stopTimer();
super.dispose();
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final on = widget.active && _blink;
return Icon(
Icons.circle,
size: 12,
color: on ? scheme.primary : scheme.outline,
);
}
}
+1 -1
View File
@@ -113,7 +113,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
messageBytes: responseBytes,
);
final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs);
final timeout = Duration(milliseconds: timeoutMs + 2000);
final selectionLabel = selection.useFlood
? 'flood'
: '${selection.hopCount} hops';
+1 -1
View File
@@ -108,7 +108,7 @@ class _RoomLoginDialogState extends State<RoomLoginDialog> {
messageBytes: responseBytes,
);
final timeoutSeconds = (timeoutMs / 1000).ceil();
final timeout = Duration(milliseconds: timeoutMs);
final timeout = Duration(milliseconds: timeoutMs + 2000);
final selectionLabel = selection.useFlood
? 'flood'
: '${selection.hopCount} hops';
+84 -4
View File
@@ -1,8 +1,64 @@
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import 'signal_ui.dart';
Contact? _getRepeaterPrefixMatchNearLocation(
List<Contact> contacts,
int pubkeyFirstByte, {
LatLng? searchPoint,
bool preferFavorites = false,
}) {
final candidates = contacts
.where(
(c) =>
c.publicKey.isNotEmpty &&
c.publicKey.first == pubkeyFirstByte &&
(c.type == advTypeRepeater || c.type == advTypeRoom),
)
.toList();
if (candidates.isEmpty) return null;
candidates.sort((a, b) {
if (preferFavorites) {
final favA = a.isFavorite ? 1 : 0;
final favB = b.isFavorite ? 1 : 0;
final favCompare = favB.compareTo(favA);
if (favCompare != 0) return favCompare;
}
final seenCompare = b.lastSeen.compareTo(a.lastSeen);
if (seenCompare != 0) return seenCompare;
return a.publicKeyHex.compareTo(b.publicKeyHex);
});
if (searchPoint == null) {
return candidates.first;
}
final distance = Distance();
Contact best = candidates.first;
var bestDistance = double.infinity;
for (final c in candidates) {
if (c.hasLocation && c.latitude != null && c.longitude != null) {
final d = distance(searchPoint, LatLng(c.latitude!, c.longitude!));
if (d < bestDistance) {
bestDistance = d;
best = c;
}
}
}
return best;
}
class SNRUi {
final IconData icon;
final Color color;
@@ -64,6 +120,15 @@ class SNRIndicator extends StatefulWidget {
}
class _SNRIndicatorState extends State<SNRIndicator> {
bool _isValidSelfLocation(double lat, double lon) {
const double epsilon = 1e-6;
return (lat.abs() > epsilon || lon.abs() > epsilon) &&
lat >= -90.0 &&
lat <= 90.0 &&
lon >= -180.0 &&
lon <= 180.0;
}
@override
Widget build(BuildContext context) {
final directRepeaters = widget.connector.directRepeaters;
@@ -158,10 +223,25 @@ class _SNRIndicatorState extends State<SNRIndicator> {
widget.connector.currentSf,
);
final allContacts = widget.connector.allContacts;
final name = allContacts
.where((c) => c.publicKey.first == repeater.pubkeyFirstByte)
.map((c) => c.name)
.firstOrNull;
final selfLat = widget.connector.selfLatitude;
final selfLon = widget.connector.selfLongitude;
LatLng? selfPoint;
if (selfLat != null &&
selfLon != null &&
_isValidSelfLocation(selfLat, selfLon)) {
selfPoint = LatLng(selfLat, selfLon);
}
final contact = _getRepeaterPrefixMatchNearLocation(
allContacts,
repeater.pubkeyFirstByte,
searchPoint: selfPoint,
preferFavorites: true,
);
final name = contact?.name;
return Column(
children: [
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import '../helpers/link_handler.dart';
class TranslatedMessageContent extends StatelessWidget {
final String displayText;
final String? originalText;
final TextStyle style;
final TextStyle? originalStyle;
final bool showOriginalFirst;
const TranslatedMessageContent({
super.key,
required this.displayText,
required this.style,
this.originalText,
this.originalStyle,
this.showOriginalFirst = true,
});
@override
Widget build(BuildContext context) {
final trimmedDisplay = displayText.trim();
final trimmedOriginal = originalText?.trim();
final shouldShowOriginal =
trimmedOriginal != null &&
trimmedOriginal.isNotEmpty &&
trimmedOriginal != trimmedDisplay;
final originalWidget = shouldShowOriginal
? LinkHandler.buildLinkifyText(
context: context,
text: trimmedOriginal,
style:
originalStyle ??
style.copyWith(
fontStyle: FontStyle.italic,
fontSize: style.fontSize,
),
)
: null;
final translatedWidget = LinkHandler.buildLinkifyText(
context: context,
text: trimmedDisplay,
style: style,
);
if (!shouldShowOriginal) {
return translatedWidget;
}
final children = showOriginalFirst
? [originalWidget!, const SizedBox(height: 6), translatedWidget]
: [translatedWidget, const SizedBox(height: 6), originalWidget!];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: children,
);
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ class UnreadBadge extends StatelessWidget {
@override
Widget build(BuildContext context) {
final display = count > 99 ? '99+' : count.toString();
final display = count > 9999 ? '9999+' : count.toString();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
+1
View File
@@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)
@@ -9,22 +9,18 @@ import flutter_blue_plus_darwin
import flutter_local_notifications
import mobile_scanner
import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
import sqflite_darwin
import url_launcher_macos
import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
}
+13 -2
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 7.0.0+8
version: 7.0.0+9
environment:
sdk: ^3.9.2
@@ -55,7 +55,6 @@ dependencies:
cached_network_image: ^3.4.1
flutter_cache_manager: ^3.4.1
flutter_foreground_task: ^9.2.0
wakelock_plus: ^1.4.0
characters: ^1.4.0
package_info_plus: ^9.0.0
mobile_scanner: ^7.1.4 # QR/barcode scanning
@@ -69,8 +68,20 @@ dependencies:
material_symbols_icons: ^4.2906.0
web: ^1.1.1
flutter_svg: ^2.0.10+1
flutter_blue_plus_platform_interface: ^8.2.1
ml_algo: ^16.0.0
ml_dataframe: ^1.0.0
llamadart: '>=0.6.8 <0.7.0'
hooks:
user_defines:
llamadart:
llamadart_native_backends:
platforms:
android-arm64:
backends: [cpu]
android-x64:
backends: [cpu]
dev_dependencies:
flutter_test:
+39
View File
@@ -0,0 +1,39 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/models/companion_radio_stats.dart';
void main() {
test('CompanionRadioStats.tryParse golden 14-byte radio frame', () {
// noise -90 (0xA6FF LE), rssi -70 (0xBA), snr raw 8 -> 2.0 dB,
// tx_air 1000 LE, rx_air 2000 LE
final frame = Uint8List.fromList([
respCodeStats,
statsTypeRadio,
0xA6,
0xFF,
0xBA,
0x08,
0xE8,
0x03,
0x00,
0x00,
0xD0,
0x07,
0x00,
0x00,
]);
final s = CompanionRadioStats.tryParse(frame);
expect(s, isNotNull);
expect(s!.noiseFloorDbm, -90);
expect(s.lastRssiDbm, -70);
expect(s.lastSnrDb, 2.0);
expect(s.txAirSecs, 1000);
expect(s.rxAirSecs, 2000);
});
test('CompanionRadioStats.tryParse rejects short frame', () {
expect(CompanionRadioStats.tryParse(Uint8List(10)), isNull);
});
}
@@ -0,0 +1,150 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/services/linux_ble_error_classifier.dart';
void main() {
group('isLinuxBleConnectFailureText', () {
test('matches flutter_blue_plus connect timeout error', () {
expect(
isLinuxBleConnectFailureText(
'FlutterBluePlusException | connect | fbp-code: 1 | Timed out after 15s',
),
isTrue,
);
});
test('matches hard-timeout marker', () {
expect(
isLinuxBleConnectFailureText(
'TimeoutException: Linux connect hard-timeout after 8s',
),
isTrue,
);
});
test('matches BlueZ local abort failure', () {
expect(
isLinuxBleConnectFailureText(
'org.bluez.Error.Failed: le-connection-abort-by-local',
),
isTrue,
);
});
test('matches BlueZ in-progress failure', () {
expect(
isLinuxBleConnectFailureText(
'org.bluez.Error.InProgress: Operation already in progress',
),
isTrue,
);
});
test('matches flutter_blue_plus null-detail connect failure', () {
expect(
isLinuxBleConnectFailureText(
'FlutterBluePlusException | connect | linux-code: null | null',
),
isTrue,
);
});
test('matches tagged connect-stage failure marker', () {
expect(
isLinuxBleConnectFailureText(
'StateError: Linux connect stage failure: Bad state: No element',
),
isTrue,
);
});
test('does not match connect-shaped pairing auth failure', () {
expect(
isLinuxBleConnectFailureText(
'FlutterBluePlusException | connect | AuthenticationFailed',
),
isFalse,
);
});
test('does not match explicit pair auth failure', () {
expect(
isLinuxBleConnectFailureText(
'FlutterBluePlusException | pair | AuthenticationFailed',
),
isFalse,
);
});
});
group('isLikelyLinuxBlePairingTimeoutText', () {
test('matches pair timeout text', () {
expect(
isLikelyLinuxBlePairingTimeoutText('Timed out waiting for pair'),
isTrue,
);
});
test('matches bond timeout text', () {
expect(
isLikelyLinuxBlePairingTimeoutText('Operation timed out during bond'),
isTrue,
);
});
test('does not match generic timeout text', () {
expect(
isLikelyLinuxBlePairingTimeoutText('Timed out after 15s'),
isFalse,
);
});
});
group('isLinuxBlePairingFailureText', () {
test('matches connect-shaped authentication failure', () {
expect(
isLinuxBlePairingFailureText(
'FlutterBluePlusException | connect | AuthenticationFailed',
),
isTrue,
);
});
test('matches app pairing incomplete failure', () {
expect(
isLinuxBlePairingFailureText(
'StateError: Linux BLE pairing did not complete',
),
isTrue,
);
});
test('does not match generic bad state error', () {
expect(isLinuxBlePairingFailureText('Bad state: No element'), isFalse);
});
test('matches pair-context bad state error', () {
expect(
isLinuxBlePairingFailureText(
'Pair request failed: Bad state: No element',
),
isTrue,
);
});
test('matches app trust repair incomplete failure', () {
expect(
isLinuxBlePairingFailureText(
'StateError: Linux BLE trust repair did not complete',
),
isTrue,
);
});
test('matches pairing timeout text', () {
expect(
isLinuxBlePairingFailureText('Timed out waiting for pair'),
isTrue,
);
});
});
}
@@ -0,0 +1,418 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:meshcore_open/services/linux_ble_pairing_service.dart';
class _FakeProcess implements Process {
_FakeProcess({this.stdoutText = '', this.autoFinish = true}) {
_stdin = IOSink(_stdinController.sink);
_stdinController.stream.listen((chunk) {
_stdinBuffer.write(utf8.decode(chunk));
});
// Use Timer.run (event-loop tick) instead of microtask so that broadcast
// listeners in _runPairingAttempt are attached before the event fires.
Timer.run(() {
if (_closed) {
return;
}
if (stdoutText.isNotEmpty) {
_stdoutController.add(utf8.encode(stdoutText));
}
});
if (autoFinish) {
// Scheduled after the Timer.run above (FIFO order), so stdout is
// emitted before the process exits.
Timer(Duration.zero, () async {
await _finish(exitStatus);
});
}
}
final String stdoutText;
final bool autoFinish;
final int exitStatus = 0;
final StreamController<List<int>> _stdinController =
StreamController<List<int>>();
final StreamController<List<int>> _stdoutController =
StreamController<List<int>>.broadcast();
final StreamController<List<int>> _stderrController =
StreamController<List<int>>.broadcast();
final Completer<int> _exitCodeCompleter = Completer<int>();
final StringBuffer _stdinBuffer = StringBuffer();
late final IOSink _stdin;
bool _closed = false;
String get stdinText => _stdinBuffer.toString();
void emitStdout(String text) {
if (!_closed) {
_stdoutController.add(utf8.encode(text));
}
}
void finishProcess([int code = 0]) {
unawaited(_finish(code));
}
Future<void> _finish(int code) async {
if (_closed) {
return;
}
_closed = true;
await _stdin.close();
await _stdoutController.close();
await _stderrController.close();
if (!_exitCodeCompleter.isCompleted) {
_exitCodeCompleter.complete(code);
}
}
@override
Future<int> get exitCode => _exitCodeCompleter.future;
@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) {
unawaited(_finish(exitStatus));
return true;
}
@override
int get pid => 1;
@override
IOSink get stdin => _stdin;
@override
Stream<List<int>> get stderr => _stderrController.stream;
@override
Stream<List<int>> get stdout => _stdoutController.stream;
}
void main() {
test(
'disconnectDevice skips gracefully when bluetoothctl is unavailable',
() async {
final logs = <String>[];
final service = LinuxBlePairingService(
processStart: (executable, arguments) async {
throw const ProcessException(
'bluetoothctl',
<String>[],
'not found',
2,
);
},
);
await service.disconnectDevice('AA:BB:CC:DD:EE:FF', onLog: logs.add);
expect(
logs.any((line) => line.contains('bluetoothctl unavailable')),
isTrue,
);
},
);
test(
'isPairedAndTrusted returns false when bluetoothctl is unavailable',
() async {
final service = LinuxBlePairingService(
processRun: (executable, arguments) async {
throw const ProcessException(
'bluetoothctl',
<String>[],
'not found',
2,
);
},
);
final trusted = await service.isPairedAndTrusted('AA:BB:CC:DD:EE:FF');
expect(trusted, isFalse);
},
);
test('isBluetoothctlAvailable returns false when unavailable', () async {
final service = LinuxBlePairingService(
processRun: (executable, arguments) async {
throw const ProcessException(
'bluetoothctl',
<String>[],
'not found',
2,
);
},
);
final available = await service.isBluetoothctlAvailable();
expect(available, isFalse);
});
test(
'isBluetoothctlAvailable returns true when version command succeeds',
() async {
final service = LinuxBlePairingService(
processRun: (executable, arguments) async {
return ProcessResult(1234, 0, '5.72', '');
},
);
final available = await service.isBluetoothctlAvailable();
expect(available, isTrue);
},
);
test(
'isPairedAndTrusted returns true when paired and trusted are yes',
() async {
final service = LinuxBlePairingService(
processRun: (executable, arguments) async {
return ProcessResult(1234, 0, '''
Device AA:BB:CC:DD:EE:FF
Paired: yes
Trusted: yes
''', '');
},
);
final trusted = await service.isPairedAndTrusted('AA:BB:CC:DD:EE:FF');
expect(trusted, isTrue);
},
);
test('pairAndTrust returns false when bluetoothctl is unavailable', () async {
final service = LinuxBlePairingService(
processStart: (executable, arguments) async {
throw const ProcessException(
'bluetoothctl',
<String>[],
'not found',
2,
);
},
);
final paired = await service.pairAndTrust(remoteId: 'AA:BB:CC:DD:EE:FF');
expect(paired, isFalse);
});
test('trustDevice verifies trust after trust command succeeds', () async {
final logs = <String>[];
final service = LinuxBlePairingService(
processRun: (executable, arguments) async {
switch (arguments.first) {
case 'trust':
return ProcessResult(1234, 0, 'trust succeeded', '');
case 'info':
return ProcessResult(1234, 0, '''
Device AA:BB:CC:DD:EE:FF
Paired: yes
Trusted: yes
''', '');
}
fail('Unexpected bluetoothctl arguments: $arguments');
},
);
final trusted = await service.trustDevice(
'AA:BB:CC:DD:EE:FF',
onLog: logs.add,
);
expect(trusted, isTrue);
expect(logs.any((line) => line.contains('Verified BlueZ trust')), isTrue);
});
test(
'trustDevice returns false when trust verification stays untrusted',
() async {
final logs = <String>[];
final service = LinuxBlePairingService(
processRun: (executable, arguments) async {
switch (arguments.first) {
case 'trust':
return ProcessResult(1234, 0, 'trust succeeded', '');
case 'info':
return ProcessResult(1234, 0, '''
Device AA:BB:CC:DD:EE:FF
Paired: yes
Trusted: no
''', '');
}
fail('Unexpected bluetoothctl arguments: $arguments');
},
);
final trusted = await service.trustDevice(
'AA:BB:CC:DD:EE:FF',
onLog: logs.add,
);
expect(trusted, isFalse);
expect(
logs.any((line) => line.contains('trust verification failed')),
isTrue,
);
},
);
test(
'pairAndTrust fails when pairing reports success but trust is not restored',
() async {
final logs = <String>[];
final service = LinuxBlePairingService(
processStart: (executable, arguments) async =>
_FakeProcess(stdoutText: 'Pairing successful\n'),
processRun: (executable, arguments) async {
switch (arguments.first) {
case 'trust':
return ProcessResult(1234, 0, 'trust succeeded', '');
case 'info':
return ProcessResult(1234, 0, '''
Device AA:BB:CC:DD:EE:FF
Paired: yes
Trusted: no
''', '');
}
fail('Unexpected bluetoothctl arguments: $arguments');
},
);
final paired = await service.pairAndTrust(
remoteId: 'AA:BB:CC:DD:EE:FF',
onLog: logs.add,
);
expect(paired, isFalse);
expect(
logs.any((line) => line.contains('trust was not restored')),
isTrue,
);
},
);
test(
'pairAndTrust succeeds without requesting proactive PIN after success',
() async {
final logs = <String>[];
var pinRequests = 0;
final service = LinuxBlePairingService(
processStart: (executable, arguments) async =>
_FakeProcess(stdoutText: 'Pairing successful\n'),
processRun: (executable, arguments) async {
switch (arguments.first) {
case 'trust':
return ProcessResult(1234, 0, 'trust succeeded', '');
case 'info':
return ProcessResult(1234, 0, '''
Device AA:BB:CC:DD:EE:FF
Paired: yes
Trusted: yes
''', '');
}
fail('Unexpected bluetoothctl arguments: $arguments');
},
);
final paired = await service.pairAndTrust(
remoteId: 'AA:BB:CC:DD:EE:FF',
onLog: logs.add,
onRequestPin: () async {
pinRequests++;
return '123456';
},
);
expect(paired, isTrue);
expect(pinRequests, 0);
expect(
logs.any((line) => line.contains('did not complete before timeout')),
isFalse,
);
},
);
test(
'pairAndTrust sends empty line when blank PIN is submitted (not cancel)',
() async {
final logs = <String>[];
late final _FakeProcess fakeProc;
final service = LinuxBlePairingService(
processStart: (executable, arguments) async {
fakeProc = _FakeProcess(stdoutText: '', autoFinish: false);
// Emit PIN prompt after an event-loop tick (not microtask) so
// broadcast listeners are attached first.
Timer.run(() {
fakeProc.emitStdout('Enter PIN code:\n');
Future<void>.delayed(const Duration(milliseconds: 100), () {
fakeProc.emitStdout('Pairing successful\n');
Future<void>.delayed(const Duration(milliseconds: 50), () {
fakeProc.finishProcess();
});
});
});
return fakeProc;
},
processRun: (executable, arguments) async {
switch (arguments.first) {
case 'trust':
return ProcessResult(1234, 0, 'trust succeeded', '');
case 'info':
return ProcessResult(1234, 0, '''
Device AA:BB:CC:DD:EE:FF
Paired: yes
Trusted: yes
''', '');
}
fail('Unexpected bluetoothctl arguments: $arguments');
},
);
final paired = await service.pairAndTrust(
remoteId: 'AA:BB:CC:DD:EE:FF',
timeout: const Duration(seconds: 5),
onLog: logs.add,
onRequestPin: () async => '',
);
expect(paired, isTrue);
expect(logs.any((line) => line.contains('Blank PIN submitted')), isTrue);
expect(logs.any((line) => line.contains('cancelling pairing')), isFalse);
},
);
test('pairAndTrust cancels pairing when PIN dialog returns null', () async {
final logs = <String>[];
final service = LinuxBlePairingService(
processStart: (executable, arguments) async {
final proc = _FakeProcess(stdoutText: '', autoFinish: false);
Timer.run(() {
proc.emitStdout('Enter PIN code:\n');
// Process will be killed/quit by the pairing service after cancel
Future<void>.delayed(const Duration(milliseconds: 200), () {
proc.finishProcess();
});
});
return proc;
},
processRun: (executable, arguments) async {
return ProcessResult(1234, 0, '', '');
},
);
final paired = await service.pairAndTrust(
remoteId: 'AA:BB:CC:DD:EE:FF',
timeout: const Duration(seconds: 3),
onLog: logs.add,
onRequestPin: () async => null,
);
expect(paired, isFalse);
expect(logs.any((line) => line.contains('cancelled by user')), isTrue);
});
}
+65 -6
View File
@@ -14,6 +14,10 @@ Usage:
# Translate all locales (missing strings only):
python translate.py --in lib/l10n/app_en.arb --l10n-dir lib/l10n --missing-only
# New locales copied from app_en.arb still match English → --missing-only skips them.
# Translate every key that still equals the template (e.g. hu, ja, ko):
python translate.py --in lib/l10n/app_en.arb --l10n-dir lib/l10n --copy-of-template --only-locales hu,ja,ko
"""
import argparse
@@ -68,6 +72,7 @@ LOCALE_MAP = {
"sk": ("Slovak", "sk"),
"sl": ("Slovenian", "sl"),
"bg": ("Bulgarian", "bg"),
"hu": ("Hungarian", "hu"),
"el": ("Greek", "el"),
"he": ("Hebrew", "he"),
"th": ("Thai", "th"),
@@ -261,6 +266,25 @@ def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any])
return missing
def find_keys_still_template_copy(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]:
"""Keys whose value is still exactly the same as the template (typical after cp app_en.arb → app_xx.arb)."""
out: List[str] = []
for key in source_data:
if key == "@@locale" or key.startswith("@"):
continue
src = source_data.get(key)
if not is_translatable_entry(key, src):
continue
if not isinstance(src, str):
continue
tgt = target_data.get(key)
if not isinstance(tgt, str) or tgt.strip() == "":
out.append(key)
elif tgt == src:
out.append(key)
return out
def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]:
"""Find all locale .arb files excluding template. Returns [(locale_code, file_path)]."""
locales = []
@@ -434,6 +458,15 @@ def main() -> int:
ap.add_argument("--to-locale", help="Target locale code (es, fr, de, etc.)")
ap.add_argument("--l10n-dir", help="Directory with locale files (translates all locales)")
ap.add_argument("--missing-only", action="store_true", help="Only translate missing keys")
ap.add_argument(
"--copy-of-template",
action="store_true",
help="Only translate keys whose target text still equals app_en (use for new locales copied from English)",
)
ap.add_argument(
"--only-locales",
help="Comma-separated locale codes to process with --l10n-dir (e.g. hu,ja,ko)",
)
ap.add_argument("--model", default="translategemma:latest", help="Ollama model (translategemma:latest or specific versions)")
ap.add_argument("--fallback-model", help="Fallback model for failed translations (e.g., translategemma:27b)")
ap.add_argument("--host", default="http://localhost:11434", help="Ollama host")
@@ -458,6 +491,14 @@ def main() -> int:
print("Input JSON must be an object at top-level.", file=sys.stderr)
return 2
if args.missing_only and args.copy_of_template:
print("Use only one of --missing-only or --copy-of-template", file=sys.stderr)
return 2
only_locales: Optional[set] = None
if args.only_locales:
only_locales = {x.strip() for x in args.only_locales.split(",") if x.strip()}
# Process all locales if --l10n-dir is provided
if args.l10n_dir:
locales = get_all_locale_files(args.l10n_dir, args.in_path)
@@ -465,6 +506,12 @@ def main() -> int:
print(f"No locale files found in {args.l10n_dir}", file=sys.stderr)
return 1
if only_locales is not None:
locales = [(c, p) for c, p in locales if c in only_locales]
missing = only_locales - {c for c, _ in locales}
if missing:
print(f"Warning: no app_*.arb for locale code(s): {', '.join(sorted(missing))}", file=sys.stderr)
print(f"Found {len(locales)} locale file(s) to process")
total_translated = 0
@@ -478,7 +525,14 @@ def main() -> int:
print(f" [{locale_code}] Failed to read {locale_path}: {e}")
continue
if args.missing_only:
missing_keys: Optional[List[str]]
if args.copy_of_template:
missing_keys = find_keys_still_template_copy(source_data, target_data)
if not missing_keys:
print(f" [{locale_code}] No keys still matching template")
continue
print(f" [{locale_code}] {len(missing_keys)} key(s) still same as template")
elif args.missing_only:
missing_keys = find_missing_keys(source_data, target_data)
if not missing_keys:
print(f" [{locale_code}] No missing keys")
@@ -509,18 +563,23 @@ def main() -> int:
lang_name, lang_code = LOCALE_MAP.get(args.to_locale, (args.to_locale, args.to_locale))
# Read existing target file if --missing-only
# Read existing target file if --missing-only or --copy-of-template
target_data: Dict[str, Any] = {}
missing_keys: Optional[List[str]] = None
if args.missing_only and os.path.exists(args.out_path):
if (args.missing_only or args.copy_of_template) and os.path.exists(args.out_path):
try:
with open(args.out_path, "r", encoding="utf-8") as f:
target_data = json.load(f)
missing_keys = find_missing_keys(source_data, target_data)
if args.copy_of_template:
missing_keys = find_keys_still_template_copy(source_data, target_data)
label = "still matching template"
else:
missing_keys = find_missing_keys(source_data, target_data)
label = "missing"
if not missing_keys:
print(f"No missing keys in {args.out_path}")
print(f"No {label} keys in {args.out_path}")
return 0
print(f"Found {len(missing_keys)} missing key(s) to translate")
print(f"Found {len(missing_keys)} {label} key(s) to translate")
except Exception as e:
print(f"Failed to read target file: {e}", file=sys.stderr)
return 2
+69 -1
View File
@@ -1 +1,69 @@
{}
{
"bg": [
"chat_sendMessage"
],
"de": [
"chat_sendMessage"
],
"es": [
"chat_sendMessage"
],
"fr": [
"chat_sendMessage"
],
"hu": [
"chat_sendMessage"
],
"it": [
"chat_sendMessage"
],
"ja": [
"chat_sendMessage"
],
"ko": [
"chat_sendMessage"
],
"nl": [
"chat_sendMessage"
],
"pl": [
"chat_sendMessage"
],
"pt": [
"chat_sendMessage"
],
"ru": [
"chat_sendMessage"
],
"sk": [
"chat_sendMessage"
],
"sl": [
"chat_sendMessage"
],
"sv": [
"chat_sendMessage"
],
"uk": [
"chat_sendMessage"
],
"zh": [
"chat_sendMessage"
]
}
+1
View File
@@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
list(APPEND FLUTTER_FFI_PLUGIN_LIST
flserial
flutter_local_notifications_windows
jni
)
set(PLUGIN_BUNDLED_LIBRARIES)