diff --git a/.gitignore b/.gitignore
index a312737..779856c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,6 +58,7 @@ secrets.dart
.DS_Store
.AppleDouble
.LSOverride
+macos/Flutter/GeneratedPluginRegistrant.swift
# iOS
**/ios/Pods/
@@ -85,4 +86,4 @@ keystore.properties
.vscode/settings.json
# Cloudflare Wrangler
-.wrangler
\ No newline at end of file
+.wrangler
diff --git a/README.md b/README.md
index 3b230dd..2f87e91 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Device Management
-- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
+- **BLE, USB, TCP Connection**: Scan and connect to MeshCore devices via Bluetooth, USB or TCP
- **Device Settings**: Configure radio parameters, power settings, and network options
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
@@ -75,10 +75,16 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
### Platform Support
-- ✅ **Android**: Full support (API 21+)
-- ✅ **iOS**: Full support (iOS 12+)
-- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
-- 🚧 **Web**: Under construction (Chrome)
+| Feature | Android (API 21+) | iOS (12+) | Linux | Windows | macOS | Web |
+|--------------------|:-----------------:|:---------:|:-----:|:-------:|:-----:|:---------------------------------:|
+| BLE companion | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| USB companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ✅ |
+| TCP companion | ✅ | 🚧 | ✅ | ✅ | ✅ | ❌ (requires websocket bridge) |
+| Core Functionality | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| Mesh Network | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| Map & Location | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| Device Management | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
+| Repeater Hub | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
### Dependencies
@@ -189,6 +195,7 @@ Messages are transmitted as binary frames using a custom protocol optimized for
### App Settings
- **Theme**: System default, light, or dark mode
+- **Language**: Use one of 15 languages (English, Chinese, French, Spanish, Portuguese, German, Dutch, Polish, Swedish, Italian, Slovak, Slovene, Bulgarian, Russian, Ukrainian)
- **Notifications**: Configurable for messages, channels, and node advertisements
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
- **Message Retry**: Automatic retry with configurable path clearing
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index e0a8029..c8028e0 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -16,7 +16,7 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion
- ndkVersion = flutter.ndkVersion
+ ndkVersion = "29.0.14206865"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
diff --git a/documentation/README.md b/documentation/README.md
new file mode 100644
index 0000000..1367013
--- /dev/null
+++ b/documentation/README.md
@@ -0,0 +1,30 @@
+# MeshCore Open - Feature Documentation
+
+MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking devices. This documentation covers every user-facing feature, how to access it, and what it does.
+
+## Table of Contents
+
+1. [Scanner & Connection](scanner-and-connection.md) - BLE scanning, USB serial, and TCP connection
+2. [Navigation](navigation.md) - App flow, device screen, and quick-switch navigation
+3. [Contacts](contacts.md) - Contact management, groups, discovery, and sharing
+4. [Chat & Messaging](chat-and-messaging.md) - Direct messages, message status, reactions, and retries
+5. [Channels](channels.md) - Broadcast channels, communities, and channel chat
+6. [Map & Location](map-and-location.md) - Node map, path tracing, line-of-sight, and offline caching
+7. [Settings](settings.md) - Device settings, app settings, radio configuration, and exports
+8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
+9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
+10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
+11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
+
+## App Overview
+
+MeshCore Open connects to MeshCore LoRa mesh radios over BLE, USB, or TCP. Once connected, users can:
+
+- **Chat** with other mesh nodes via encrypted direct messages
+- **Broadcast** on shared channels (public, hashtag, private, or community-scoped)
+- **View nodes on a map** with GPS locations, predicted positions, and path traces
+- **Manage repeaters** with CLI access, telemetry, neighbor info, and settings
+- **Share contacts** via `meshcore://` URIs and QR codes
+- **Configure radio settings** including frequency, power, bandwidth, and spreading factor
+- **Cache offline maps** for use without internet connectivity
+- **Analyze line-of-sight** between nodes with terrain elevation profiles
diff --git a/documentation/additional-features.md b/documentation/additional-features.md
new file mode 100644
index 0000000..f7b8319
--- /dev/null
+++ b/documentation/additional-features.md
@@ -0,0 +1,187 @@
+# Additional Features
+
+## GIF Picker (Giphy Integration)
+
+### How to Access
+In any chat screen (direct or channel), tap the GIF button in the message input bar.
+
+### What the User Sees
+A bottom sheet with a search field and a grid of GIF thumbnails.
+
+### Key Interactions
+- On open, loads trending GIFs (G-rated, 25 results)
+- Type to search and press the keyboard submit button (search triggers on submit, not on each keystroke). Clearing the search field reloads trending GIFs
+- On network/API errors, a "Retry" button is shown in-place
+- Tap a GIF to select it — the chat input shows an inline preview with an X button to dismiss
+- Send the message to transmit the GIF reference (`g:`)
+- Recipients see the GIF rendered inline via Giphy CDN
+- "Powered by Giphy" attribution is always shown at the bottom of the picker
+- The bottom sheet occupies 70% of screen height
+
+---
+
+## Localization / Multi-Language Support
+
+### How to Access
+App Settings → Appearance → Language
+
+### Supported Languages (15)
+English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian
+
+### How It Works
+- All UI strings go through Flutter's ARB localization system
+- Language can follow the system locale or be explicitly overridden
+- Changes take effect immediately
+
+---
+
+## Discovered Contacts Screen
+
+### How to Access
+From Contacts screen → overflow menu → "Discovered Contacts"
+
+### What the User Sees
+A list of nodes heard passively over the air but not yet added as contacts. Each shows:
+- Color-coded avatar (by type)
+- Name
+- Short public key
+- Last-seen time
+
+### Key Interactions
+- Search bar with debounced filtering
+- Sort by last seen or name; filter by type
+- **Tap**: Import the contact (adds to your contact list)
+- **Long-press**: Add Contact, Copy `meshcore://` URI to clipboard, or Delete
+- Overflow menu → "Delete All" (with confirmation)
+- Already-known contacts and your own node are filtered out
+
+---
+
+## SMAZ Compression
+
+### What It Is
+An optional per-contact and per-channel text compression feature using the SMAZ algorithm (optimized for short English text).
+
+### How to Enable
+- **Per contact**: Chat screen → info button → toggle "SMAZ compression"
+- **Per channel**: Long-press channel → Edit → toggle "SMAZ compression"
+
+### How It Works
+- When enabled, compression is applied using a "compress only if smaller" strategy — the message is only transmitted compressed if the encoded result is actually shorter than the original. Otherwise, the original text is sent uncompressed
+- Compressed messages are transmitted with a `s:` prefix followed by base64-encoded data
+- Recipients using MeshCore Open will decompress automatically. **Recipients using other software** that is not SMAZ-aware will see garbled `s:...` text
+- The codec operates on ASCII. Non-ASCII / non-English text generally does not benefit from compression and may even expand. Best suited for short English messages
+- Disabled by default
+
+---
+
+## Community QR Scanner
+
+### How to Access
+From Channels screen → "+" FAB → "Scan Community QR"
+
+### What the User Sees
+A live QR scanner view with instruction text overlay.
+
+### Key Interactions
+- Scan a community QR code shared by another member
+- On valid scan: confirmation dialog showing community name and ID
+- Option to "Add public channel to device" on join
+- If already a member: shows an "Already a member" dialog
+- Invalid QR: shows an orange error snackbar
+
+---
+
+## Channel Message Path Viewing
+
+### How to Access
+In a channel chat, tap a message bubble (mobile) or use the "Path" action (desktop).
+
+### What the User Sees
+- Summary card: sender, time, repeat count, path type, observed hops
+- "Other Observed Paths" section (if multiple paths detected)
+- "Repeater Hops" section listing each hop with hex prefix, resolved name, and GPS coordinates
+
+### Actions
+- **Radar icon**: Opens path trace map for live trace
+- **Map icon**: Opens a map with hop markers and polyline
+- **Path dropdown**: Switch between observed path variants (if multiple)
+
+---
+
+## Debug Logging
+
+### BLE Debug Log
+**Access**: Settings → BLE Debug Log
+
+Two views:
+- **Frames**: Each BLE frame with direction, description, hex preview, timestamp. Long-press to copy hex.
+- **Raw Log RX**: Decoded LoRa packets with route type, payload type, path bytes, and summary.
+
+### App Debug Log
+**Access**: Settings → App Debug Log (must be enabled first in App Settings → Debug)
+
+Structured log entries with level (Info/Warning/Error), tag, message, and timestamp.
+
+Both logs support copy-all and clear operations.
+
+---
+
+## Chrome Required Screen
+
+### When It Appears
+Automatically shown on web platforms when a non-Chromium browser is detected.
+
+### What the User Sees
+A full-screen informational page explaining that Web Bluetooth requires a Chromium-based browser. No interactive elements — purely informational.
+
+---
+
+## Path History Service
+
+### What It Does (Background Service)
+Maintains an in-memory LRU cache of up to 50 contacts, each with up to 100 route history entries, tracking:
+- Hop count and trip time
+- Success/failure counts and route weights
+- Flood vs. direct discovery
+
+### Path Scoring
+Paths are scored using a weighted formula: reliability (45%), route weight (20%), latency (25%), and freshness (10%). These weights are internal and not user-configurable. Paths whose weight drops to zero or below are automatically deleted. Flood deliveries that receive an ACK give a weight boost (+0.5) to the specific return path.
+
+Used internally for:
+- **Auto route rotation**: Cycles through known paths using configurable weights on retries, with a diversity window to avoid re-using recently tried paths
+- **Path selection**: Picks the best-scored path for each retry attempt
+- **Flood statistics**: Tracks flood vs. direct discovery ratios
+
+---
+
+## Message Retry Service
+
+### What It Does (Background Service)
+Handles reliable delivery of outgoing direct messages:
+1. Assigns a UUID and sends immediately. Only one message per contact can be in-flight at a time (avoids overflowing the firmware's 8-entry ACK table); subsequent messages are queued
+2. Listens for ACK frames matched via SHA-256 hash of `[timestamp][attempt][text][sender_pubkey]`
+3. On timeout, retries with exponential backoff: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s...)
+4. Each retry may use a different path (via path history diversity window)
+5. After max retries: marks failed but keeps a **30-second grace window** during which a late ACK can still resolve the message to "delivered". Optionally clears the contact's path
+6. Reports RTT and path data for quality learning
+7. Maintains an ACK hash history (last 50 entries) to handle duplicate ACKs
+
+### Configurable Settings (App Settings → Messaging)
+- Max retries (2–10, default 5)
+- Clear path on max retry (on/off)
+- Auto route rotation with weight parameters
+
+---
+
+## Timeout Prediction (ML)
+
+### What It Does (Background Service)
+An ML-based service that predicts expected delivery timeouts:
+- Collects delivery observations (path length, message size, time since last RX, delivery time) in a sliding window of up to 100 observations (oldest evicted first)
+- Requires **10 minimum observations** before first training. After that, retrains every 5 new observations
+- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
+- Features with zero variance are automatically excluded from training
+- Blends per-contact statistics with ML predictions
+- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
+- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
diff --git a/documentation/ble-protocol.md b/documentation/ble-protocol.md
new file mode 100644
index 0000000..9f4c1d7
--- /dev/null
+++ b/documentation/ble-protocol.md
@@ -0,0 +1,249 @@
+# BLE Protocol & Data Layer
+
+This is a technical reference for the communication protocol and data architecture.
+
+## Transport Layer
+
+The app supports three transports, all sharing the same command/response protocol:
+
+| Transport | Method | Implementation |
+|---|---|---|
+| Bluetooth LE | Nordic UART Service (NUS) GATT | `flutter_blue_plus` |
+| USB Serial | Packet-framed serial | `MeshCoreUsbManager` |
+| TCP | Packet-framed socket | `MeshCoreTcpConnector` |
+
+### BLE (Nordic UART Service)
+
+- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
+- **RX Characteristic** (write to device): `6e400002-b5a3-f393-e0a9-e50e24dcca9e`
+- **TX Characteristic** (notify from device): `6e400003-b5a3-f393-e0a9-e50e24dcca9e`
+
+Raw `Uint8List` payloads are written directly to the RX characteristic. Writes use "write without response" if supported, falling back to "write with response".
+
+### USB and TCP Framing
+
+Both use a lightweight packet framing codec:
+
+```
+TX (host → device): [0x3C][len_lo][len_hi][payload...]
+RX (device → host): [0x3E][len_lo][len_hi][payload...]
+```
+
+- Frame start: `0x3C` (`<`) for outgoing, `0x3E` (`>`) for incoming
+- Length: 2-byte little-endian, payload only
+- Max payload: 172 bytes
+- TCP: `tcpNoDelay: true` (Nagle disabled), writes serialized to prevent interleaving
+- USB: 10ms post-write delay between frames
+
+## Connection State Machine
+
+```
+enum MeshCoreConnectionState {
+ disconnected,
+ scanning,
+ connecting,
+ connected,
+ disconnecting,
+}
+```
+
+## BLE Connection Lifecycle
+
+1. **Scan** with keyword filters `["MeshCore-", "Whisper-"]`
+2. **Connect** with 15-second timeout
+3. **Request MTU** 185 bytes (non-web only)
+4. **Discover services** and locate NUS
+5. **Enable TX notifications** (up to 3 attempts on native)
+6. **Subscribe** to TX characteristic for incoming frames
+7. **Initial sync**: device info query, time sync, channel sync
+
+## Auto-Reconnect (BLE Only)
+
+On unexpected disconnection, auto-reconnect with exponential backoff:
+- Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s...
+- Resets on successful connection
+- Disabled for manual disconnects
+- Not available for USB or TCP
+
+## Protocol Constants
+
+| Constant | Value | Description |
+|---|---|---|
+| Max frame size | 172 bytes | BLE/USB/TCP payload limit |
+| Public key size | 32 bytes | Ed25519 public key |
+| Max path size | 64 bytes | Maximum path data |
+| Max name size | 32 bytes | Maximum node name |
+| Max text payload | 160 bytes | Firmware `MAX_TEXT_LEN` |
+| App protocol version | 3 | Sent in device query |
+| Contact frame size | 148 bytes | Fixed-size contact record |
+
+## Command Codes (App → Device)
+
+| Code | Name | Description |
+|------|------|-------------|
+| 1 | CMD_APP_START | Announce app connection |
+| 2 | CMD_SEND_TXT_MSG | Send direct text message |
+| 3 | CMD_SEND_CHANNEL_TXT_MSG | Send channel text message |
+| 4 | CMD_GET_CONTACTS | Request contact list |
+| 5 | CMD_GET_DEVICE_TIME | Query device clock |
+| 6 | CMD_SET_DEVICE_TIME | Set device clock |
+| 7 | CMD_SEND_SELF_ADVERT | Broadcast own advertisement |
+| 8 | CMD_SET_ADVERT_NAME | Set node name |
+| 9 | CMD_ADD_UPDATE_CONTACT | Add or update a contact |
+| 10 | CMD_SYNC_NEXT_MESSAGE | Request next queued message |
+| 11 | CMD_SET_RADIO_PARAMS | Set radio parameters |
+| 12 | CMD_SET_RADIO_TX_POWER | Set TX power |
+| 13 | CMD_RESET_PATH | Reset contact path |
+| 14 | CMD_SET_ADVERT_LATLON | Set advertised location |
+| 15 | CMD_REMOVE_CONTACT | Remove a contact |
+| 16 | CMD_SHARE_CONTACT | Share contact to mesh |
+| 17 | CMD_EXPORT_CONTACT | Export contact as bytes |
+| 18 | CMD_IMPORT_CONTACT | Import contact from bytes |
+| 19 | CMD_REBOOT | Reboot device |
+| 20 | CMD_GET_BATT_AND_STORAGE | Query battery and storage |
+| 22 | CMD_DEVICE_QUERY | Query device info |
+| 26 | CMD_SEND_LOGIN | Login to repeater/room |
+| 27 | CMD_SEND_STATUS_REQ | Request repeater status |
+| 30 | CMD_GET_CONTACT_BY_KEY | Get contact by public key |
+| 31 | CMD_GET_CHANNEL | Get channel definition |
+| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
+| 36 | CMD_SEND_TRACE_PATH | Request path trace |
+| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
+| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
+| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
+| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
+| 50 | CMD_SEND_BINARY_REQ | Send binary request |
+| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
+| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
+| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
+
+## Response / Push Codes (Device → App)
+
+| Code | Name | Description |
+|------|------|-------------|
+| 0 | RESP_CODE_OK | Generic success |
+| 1 | RESP_CODE_ERR | Generic error |
+| 2 | RESP_CODE_CONTACTS_START | Contact list begins |
+| 3 | RESP_CODE_CONTACT | Single contact data |
+| 4 | RESP_CODE_END_OF_CONTACTS | Contact list complete |
+| 5 | RESP_CODE_SELF_INFO | Device self-info response |
+| 6 | RESP_CODE_SENT | Message transmitted; carries `[1]=is_flood, [2–5]=ack_hash, [6–9]=estimated_timeout_ms` |
+| 7 | RESP_CODE_CONTACT_MSG_RECV | Incoming direct message (v2) |
+| 8 | RESP_CODE_CHANNEL_MSG_RECV | Incoming channel message (v2) |
+| 10 | RESP_CODE_NO_MORE_MESSAGES | No more queued messages |
+| 11 | RESP_CODE_EXPORT_CONTACT | Exported contact data |
+| 9 | RESP_CODE_CURR_TIME | Current device time |
+| 12 | RESP_CODE_BATT_AND_STORAGE | Battery mV (uint16 LE) + storage used/total (uint32 LE each) |
+| 13 | RESP_CODE_DEVICE_INFO | Firmware info |
+| 16 | RESP_CODE_CONTACT_MSG_RECV_V3 | Incoming direct message (v3) |
+| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
+| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
+| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
+| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
+| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
+| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
+| 0x82 | PUSH_CODE_SEND_CONFIRMED | Delivery ACK from remote; carries ACK hash (4 bytes) + trip time (4 bytes) |
+| 0x83 | PUSH_CODE_MSG_WAITING | Offline messages queued |
+| 0x85 | PUSH_CODE_LOGIN_SUCCESS | Repeater/room login succeeded |
+| 0x86 | PUSH_CODE_LOGIN_FAIL | Repeater/room login failed |
+| 0x87 | PUSH_CODE_STATUS_RESPONSE | Repeater status response |
+| 0x88 | PUSH_CODE_LOG_RX_DATA | Radio RX data with SNR (int8, units 1/4 dB), RSSI, and raw radio packet |
+| 0x89 | PUSH_CODE_TRACE_DATA | Path trace result |
+| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
+| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
+| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
+
+## Data Models
+
+### Contact
+32-byte public key (primary identity), name, type (chat/repeater/room/sensor), flags, path data, GPS coordinates, last-seen timestamp. Parsed from 148-byte firmware frames with this layout:
+
+```
+[0] = resp_code
+[1–32] = public key (32 bytes)
+[33] = type (1=chat, 2=repeater, 3=room, 4=sensor)
+[34] = flags (bit 0 = favorite)
+[35] = path_length
+[36–99] = path (64 bytes)
+[100–131] = name (32 bytes, null-padded)
+[132–135] = timestamp (uint32 LE)
+[136–139] = latitude (int32 LE, × 1e-6 degrees)
+[140–143] = longitude (int32 LE, × 1e-6 degrees)
+[144–147] = last_modified (uint32 LE)
+```
+
+### Message (Direct)
+Sender key, text, timestamp, outgoing flag, status (pending/sent/delivered/failed), message ID (UUID), retry count, ACK hash, trip time, path data, reactions.
+
+### Channel Message
+Sender name, text, timestamp, status (pending/sent/failed), repeater hops, path variants, channel index, reactions, reply threading fields.
+
+### Channel
+Index (0–7), name, 16-byte PSK, unread count. PSK derivation methods for hashtag (SHA-256) and community (HMAC-SHA256) channels.
+
+### Community
+UUID, name, 32-byte secret, hashtag channel list. Shared via QR code.
+
+## Persistence
+
+All data is stored via `SharedPreferences` (JSON-serialized). No SQLite or other database.
+
+| Data | Storage Key Pattern | Scope |
+|---|---|---|
+| Contacts | `contacts` | Per device identity |
+| Messages | `messages_` | Per device + contact |
+| Channel Messages | `channel_messages_` | Per device + channel |
+| Channels | `channels` | Per device identity |
+| Channel Order | `channel_order_` | Per device identity |
+| Contact Groups | `contact_groups` | Per device identity |
+| Communities | `communities_v1` | Per device identity |
+| Unread Counts | `contact_unread_count` | Per device identity |
+| Discovered Contacts | `discovered_contacts` | Global |
+| App Settings | `app_settings` | Global |
+| Path History | `path_history_` | Per contact |
+
+## Auto-Add Configuration Bitmask
+
+Used by `CMD_SET_AUTO_ADD_CONFIG` (58) and `RESP_CODE_AUTO_ADD_CONFIG` (25):
+
+| Bit | Flag | Description |
+|-----|------|-------------|
+| 0 | 0x01 | Overwrite oldest contact when list is full |
+| 1 | 0x02 | Auto-add chat users |
+| 2 | 0x04 | Auto-add repeaters |
+| 3 | 0x08 | Auto-add room servers |
+| 4 | 0x10 | Auto-add sensors |
+
+## Radio Packet Payload Types
+
+Seen inside `PUSH_CODE_LOG_RX_DATA` raw packets:
+
+| Code | Type |
+|------|------|
+| 0x00 | REQ (request) |
+| 0x01 | RESPONSE |
+| 0x02 | TXTMSG (text message) |
+| 0x03 | ACK |
+| 0x04 | ADVERT |
+| 0x05 | GRPTXT (group/channel text) |
+| 0x06 | GRPDATA (group data) |
+| 0x07 | ANONREQ (anonymous request) |
+| 0x08 | PATH |
+| 0x09 | TRACE |
+| 0x0A | MULTIPART |
+| 0x0B | CONTROL |
+| 0x0F | RAW_CUSTOM |
+
+## State Management
+
+Uses Flutter `Provider` with `ChangeNotifier`. The central state holder is `MeshCoreConnector`, which owns all in-memory collections and fires debounced (50ms) `notifyListeners()` to update the UI. In-memory conversations are windowed to 200 messages per contact; older messages remain on disk and are loaded on demand.
+
+### Data Flow
+
+1. Raw frames arrive over BLE/USB/TCP
+2. First byte is parsed as response/push code
+3. Appropriate model factory (`fromFrame()`) parses the data
+4. In-memory collections are updated
+5. Storage stores are persisted (async)
+6. `notifyListeners()` triggers UI rebuilds
+7. Screens read current state via getters
diff --git a/documentation/channels.md b/documentation/channels.md
new file mode 100644
index 0000000..21fb52e
--- /dev/null
+++ b/documentation/channels.md
@@ -0,0 +1,164 @@
+# Channels
+
+## Overview
+
+Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
+
+Up to 8 channels (indices 0–7) can be active simultaneously on one device.
+
+## How to Access
+
+QuickSwitchBar tab 1 (middle) from any main screen.
+
+## Channel Types
+
+| Type | Icon | Color | Description |
+|---|---|---|---|
+| Public | Globe | Green | Fixed well-known PSK; any device can join |
+| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
+| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
+| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret |
+
+## Channels List Screen
+
+### What the User Sees
+
+- **Search bar** with live text filtering (300ms debounce)
+- **Sort/filter button**
+- **Scrollable list of channel cards**, each showing:
+ - Type icon with color coding (purple badge overlay for community channels)
+ - Channel name (or "Channel N" if unnamed)
+ - Subtitle: "Public channel", "Hashtag channel", "Private channel", or "Community channel - {name}"
+ - Unread badge (if messages are unread)
+ - Drag handle (when manual sort is active)
+- **"+" FAB** to add a new channel
+- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
+
+If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
+
+Pull-to-refresh (swipe down) forces a re-fetch of channels from the device firmware.
+
+### Sorting Options
+
+- **Manual** (default): Drag-and-drop reordering, persisted (drag handles are hidden when a search query is active)
+- **A–Z**: Alphabetical
+- **Latest messages**: Most recent first
+- **Unread**: Most unread first
+
+## Adding a Channel
+
+Tap the "+" FAB to open a dialog with six options:
+
+1. **Create Private Channel** — Enter a name (max 31 characters); a random PSK is generated
+2. **Join Private Channel** — Enter a name and a 32-hex PSK (non-hex characters like spaces and dashes are silently stripped, so pasted keys with formatting are accepted)
+3. **Join Public Channel** — One tap; uses the well-known public PSK (only shown if no public channel exists)
+4. **Join Hashtag Channel** — Enter a hashtag name; PSK is derived from the name. If communities exist, choose between regular hashtag (SHA-256) or community hashtag (HMAC)
+5. **Scan Community QR** — Opens QR scanner to join a community
+6. **Create Community** — Enter a name; generates a random 32-byte secret; optionally adds a community public channel; shows QR code for sharing
+
+## Channel Actions (Long-Press / Right-Click)
+
+| Action | Description |
+|---|---|
+| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) |
+| Mute / Unmute | Toggle push notification suppression for this channel |
+| Delete | Remove the channel from the device (confirmation required) |
+
+## Channel Chat
+
+Tap a channel card to open the channel chat screen.
+
+### App Bar
+
+- Type icon (public/private/hashtag)
+- Channel name
+- Subtitle: "{type} - {N} unread"
+
+### Message Display
+
+- Reverse-scrolling list (newest at bottom)
+- **Incoming messages**: Colored avatar with sender's initial (or first emoji if name starts with one; color is deterministic from sender name hash), sender name in primary color, message bubble
+- **Outgoing messages**: Primary container color bubble with a small status icon: pending (clock), sent (checkmark), or failed (red error circle)
+- Automatic older-message loading on scroll-to-top
+- Jump-to-bottom button when scrolled up
+- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset text size
+- **Message tracing mode** (when enabled in App Settings): Each bubble additionally shows path prefix bytes (`via XX,YY,...`), a timestamp, and a repeat count icon
+
+### Message Types in Chat
+
+- **Plain text** with linkified URLs
+- **GIFs** (`g:{gifId}`) rendered inline via Giphy CDN
+- **Location pins** (`m:{lat},{lon}|{label}|`) shown as tappable location cards
+- **Reactions** displayed as emoji pills below target messages
+
+### Replies (Channel Chat Only)
+
+- **Mobile**: Swipe an **incoming** message left to trigger reply (with haptic feedback). You cannot swipe your own outgoing messages. Swipe reply is not available on desktop.
+- **All platforms**: Long-press → "Reply"
+- Reply banner appears above the input bar with the quoted message (tap X to cancel)
+- Sent replies are prefixed `@[{senderName}] {text}`
+- Received replies show a bordered quote block inside the bubble; tapping scrolls to the original. Reply previews render GIF thumbnails and location pin icons, not just text.
+
+### Message Path Viewing
+
+- **Mobile**: Tap a message bubble to view its routing path
+- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
+- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
+
+### Context Actions (Long-Press / Right-Click)
+
+| Action | Availability | Description |
+|---|---|---|
+| Reply | All messages | Triggers reply mode |
+| Path | Desktop only | Opens message path view |
+| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
+| Copy | All messages | Copies text to clipboard |
+| Delete | All messages | Removes locally (not from mesh) |
+
+### Message Path Viewing
+
+Tap a message bubble to open the Channel Message Path Screen, which shows:
+- Each hop in the path as a visual chain
+- Known contacts identified by name at each hop
+- Observed vs. declared hop counts
+- Alternative path variants (if received via multiple paths)
+- Map view buttons for geographic path visualization
+
+## Communities
+
+Communities are a layer above channels that provide a private namespace.
+
+### What is a Community?
+
+A community has a name and a 32-byte random secret. Channel PSKs are derived from this secret:
+- **Public channel**: `HMAC-SHA256(secret, "channel:v1:__public__")[:16]`
+- **Hashtag channel**: `HMAC-SHA256(secret, "channel:v1:{hashtag}")[:16]`
+
+Outsiders who don't know the secret cannot discover or join community channels.
+
+### Sharing a Community
+
+Communities are shared via QR codes containing a JSON payload:
+```json
+{"v": 1, "type": "meshcore_community", "name": "...", "k": ""}
+```
+
+### Managing Communities
+
+From the channels screen overflow menu → "Manage Communities". Opens a draggable scrollable sheet (resizable 30–90% of screen height):
+
+- Each community shows its name and a short community ID (first 8 hex characters)
+- **Tap a community** to directly show its QR code for sharing
+- **Popup menu** per community:
+ - **Show QR** — displays the QR code for sharing with new members
+ - **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
+
+## How Channels Differ from Direct Messages
+
+| Aspect | Channels | Direct Messages |
+|---|---|---|
+| Addressing | Broadcast to all nodes with matching PSK | Point-to-point to a specific contact |
+| Encryption | Shared PSK (symmetric) | Contact's public key (asymmetric) |
+| Sender identity | Plain text prefix in payload | Verified via public key |
+| Replies | Supported (swipe or long-press) | Not supported |
+| Retry mechanism | No automatic retry | Exponential backoff with path rotation |
diff --git a/documentation/chat-and-messaging.md b/documentation/chat-and-messaging.md
new file mode 100644
index 0000000..22030d5
--- /dev/null
+++ b/documentation/chat-and-messaging.md
@@ -0,0 +1,120 @@
+# Chat & Messaging
+
+## Overview
+
+The app supports two chat modes:
+- **Direct messages**: Encrypted point-to-point messages to individual contacts
+- **Channel messages**: Broadcast messages to shared channels (see [Channels](channels.md))
+
+This page covers direct messaging. For channel chat, see the Channels documentation.
+
+## How to Access
+
+From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
+
+## Chat Screen Layout
+
+### App Bar
+
+- **Title**: Contact name
+- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
+- **Action buttons**:
+ - **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
+ - **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
+ - **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
+
+### Message List
+
+- Scrollable list with newest messages at the bottom
+- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
+- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
+- Bubble width capped at 65% of screen width
+- Hyperlinks rendered as tappable green underlined text
+- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset
+- **Jump to bottom**: Floating button appears when scrolled away from the bottom
+- **Lazy loading**: Scrolling to top loads older messages from storage
+
+### Input Bar
+
+- **GIF button** (left): Opens GIF picker bottom sheet
+- **Text field** (center): Auto-capitalization, enforces UTF-8 byte limit in real-time
+- **Send button** (right): Submits the message
+- On desktop: Enter/Numpad Enter also submits
+- When a GIF is selected, the text field shows an inline GIF preview with a dismiss button
+
+## Message Types
+
+| Type | Wire Format | Display |
+|---|---|---|
+| Plain text | Raw UTF-8 string | Inline text with link detection |
+| GIF | `g:` | Inline GIF image from Giphy CDN |
+| Location pin | `m:,\|\|...` | Location icon + label; tap to open map |
+| Reaction | `r::` | Applied to target message as emoji pill |
+
+## Message Status
+
+Outgoing messages display a status indicator:
+
+| Status | Icon | Meaning |
+|---|---|---|
+| Pending | Grey double-check | Queued, waiting for device to transmit (visually identical to Sent) |
+| Sent | Grey double-check | Device confirmed transmission (visually identical to Pending) |
+| Delivered | Green double-check | Remote node acknowledged receipt |
+| Failed | Red X | All retries exhausted |
+
+### Message Tracing Mode
+
+When enabled in App Settings, additional metadata appears inside each bubble:
+- Timestamp (HH:MM)
+- Retry count (e.g., "Retry 2 of 4")
+- Status icon
+- Round-trip time in seconds (if delivered)
+
+## Message Length Limits
+
+- **Direct messages**: 156 bytes (UTF-8) — enforced in real-time by the input formatter
+- **Channel messages**: 160 minus sender name length minus 2 bytes for the `": "` prefix
+- Over-length paste shows a snackbar error
+
+## Send Queue
+
+Only one message per contact can be in-flight at a time (to avoid overflowing the firmware's 8-entry ACK table). If you send multiple messages rapidly, they are queued and sent sequentially — each waits for the previous one to be delivered, fail, or exhaust retries before transmitting.
+
+## Retry Mechanism
+
+When a direct message is sent:
+
+1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
+2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
+3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise `3000 + 3000 × path_length` milliseconds (15000ms for flood)
+4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
+5. **Max retries**: Configurable (default 5, range 2–10)
+6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
+7. If **Clear Path on Max Retry** is enabled (App Settings), the contact's stored routing path is automatically cleared when max retries are exhausted
+8. **Auto route rotation**: When enabled (and no manual path override is set), the retry service uses a diversity window to avoid re-using recently tried paths, cycling through known routes on each attempt
+
+### Manual Retry
+
+Long-press a failed message → "Retry" to re-send using the current routing settings.
+
+## Reactions
+
+Add emoji reactions to incoming messages (not your own):
+
+1. Long-press (or right-click on desktop) a message
+2. Select "Add reaction" from the context menu
+3. Choose from quick emojis (thumbs up, heart, laugh, party, clap, fire) or browse the full emoji picker
+4. Reactions appear as pills below the message bubble with emoji and count
+5. Pending reactions show at 50% opacity with a spinner
+6. Failed reactions show a red retry icon (tap to retry)
+
+## Context Actions (Long-Press / Right-Click)
+
+| Action | Availability | Description |
+|---|---|---|
+| Add reaction | Incoming messages only | Opens emoji picker |
+| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
+| Copy | All messages | Copies text to clipboard |
+| Delete | All messages | Removes locally (not from mesh) |
+| Retry | Failed outgoing messages | Re-sends the message |
+| Open chat with sender | Room server chats | Opens 1:1 chat with the message sender |
diff --git a/documentation/contacts.md b/documentation/contacts.md
new file mode 100644
index 0000000..1a94ba2
--- /dev/null
+++ b/documentation/contacts.md
@@ -0,0 +1,118 @@
+# Contacts
+
+## Overview
+
+The Contacts screen is the primary hub for managing mesh nodes your radio has a relationship with. A "contact" is any node whose cryptographic advertisement has been received — it can be a chat user, repeater, room server, or sensor.
+
+## How to Access
+
+- Automatically shown after connecting to a device
+- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
+- Back navigation from Chat or Settings screens
+
+## Contact Types
+
+| Type | Avatar Color | Icon | Description |
+|---|---|---|---|
+| Chat | Blue | Chat bubble | Another user's mesh radio |
+| Repeater | Orange | Cell tower | A mesh repeater/relay node |
+| Room | Purple | Group | A room server for group chat |
+| Sensor | Green | Sensors | A sensor device |
+
+## Contact List
+
+Each contact is displayed as a list tile showing:
+
+- **Avatar**: Color-coded circle with type icon (or first emoji of the contact's name if it starts with one)
+- **Name**: Contact name (single line)
+- **Path label**: "Direct", "N hops", or "Flood" (with forced variants if a path override is active)
+- **Public key**: Shortened hex format ``
+- **Unread badge**: Red pill with count (if unread messages exist)
+- **Last seen**: Relative timestamp ("Now", "5 mins ago", "2 hours ago", "3 days ago"). For chat contacts, this shows whichever is more recent: the last advertisement time or the last message time
+- **Favorite star**: Amber star icon if favorited
+- **Location pin**: Grey pin icon if the contact has GPS coordinates
+
+Pull-to-refresh re-fetches the full contact list from the device.
+
+## Search and Filter
+
+A toolbar at the top provides:
+
+**Search**: Matches contact name (case-insensitive) or public key hex prefix. Debounced at 300ms.
+
+**Sort options**:
+- Latest Messages (by most recent message)
+- Heard Recently (by last seen / last message)
+- A–Z (alphabetical)
+
+**Filter options**:
+- All, Favorites, Users, Repeaters, Room Servers, Unread Only
+
+## Contact Groups
+
+Groups are a client-side organizational feature for grouping contacts.
+
+- **Create a group**: Tap the group dropdown → "+" icon → enter name → select members → Save
+- **Edit a group**: Group dropdown → pencil icon next to the group
+- **Delete a group**: Group dropdown → trash icon next to the group
+- **Filter by group**: Select a group from the dropdown to show only its members
+
+Groups are stored per radio identity (scoped by public key).
+
+**Validation rules**: Group names cannot be empty, cannot be "all" (reserved, case-insensitive), and must be unique (case-insensitive). The group creation dialog includes a built-in search field to filter contacts when selecting members. Creating a new group automatically selects it as the active filter.
+
+## Tap Actions
+
+| Contact Type | Action on Tap |
+|---|---|
+| Chat / Sensor | Opens ChatScreen for direct messaging |
+| Repeater | Shows password login dialog → opens RepeaterHubScreen |
+| Room | Shows password login dialog → opens ChatScreen for room chat |
+
+## Long-Press / Right-Click Menu
+
+| Action | Availability | Description |
+|---|---|---|
+| Path Trace / Ping | Repeaters, Rooms (always); Chat if `pathLength > 0` | Opens PathTraceMapScreen. Label shows "Ping" when no path bytes are known, "Path Trace" otherwise |
+| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
+| Room Login | Rooms only | Login dialog → ChatScreen |
+| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
+| Open Chat | Chat/Sensor | Same as single tap |
+| Add/Remove Favorite | All types | Toggles the favorite flag |
+| Share Contact | All types | Copies `meshcore://` URI to clipboard |
+| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
+| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
+
+## App Bar Menus
+
+The Contacts screen has **two separate popup menus** in the app bar:
+
+**Antenna icon menu** (contact sharing):
+- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
+- Flood Advert — broadcasts across the full mesh network
+- Copy Advert to Clipboard — copies your `meshcore://` URI for sharing externally
+- Add Contact from Clipboard — reads a `meshcore://` URI from clipboard and imports it
+
+**Three-dot overflow menu**:
+- Disconnect — disconnects from the device
+- Discovered Contacts — opens the DiscoveryScreen
+- Settings — opens the Settings screen
+
+## Adding Contacts
+
+### Automatic (Passive)
+When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
+
+### Import from Clipboard
+Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://` URI from clipboard and imports it to the device.
+
+### Import from Discovered Contacts
+Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
+
+## Contact Sharing Format
+
+Contacts are shared using the `meshcore://` URI scheme:
+```
+meshcore://
+```
+This contains the node's public key and metadata. Paste it into another MeshCore app to import.
diff --git a/documentation/map-and-location.md b/documentation/map-and-location.md
new file mode 100644
index 0000000..f293abe
--- /dev/null
+++ b/documentation/map-and-location.md
@@ -0,0 +1,186 @@
+# Map & Location
+
+## Overview
+
+The Map feature is a full-featured node-location visualization and radio-planning tool built on OpenStreetMap tiles. It is one of the three primary views accessible from the QuickSwitchBar.
+
+## How to Access
+
+- **QuickSwitchBar tab 2** (rightmost) from Contacts or Channels
+- **Deep-link from a chat message**: Tapping a shared location pin in a chat opens the map centered on that pin
+- **Settings → Offline Map Cache**: Opens the tile cache management screen
+
+## What the Map Displays
+
+### Self Location (Teal Circle)
+Your own node's position, obtained from the device firmware. Displayed as a teal `person_pin_circle` icon. Only appears if the device has GPS data or a manually-set location.
+
+### Contact / Node Markers (Color-Coded)
+All contacts with known GPS coordinates are plotted:
+
+| Type | Color | Icon |
+|---|---|---|
+| Chat user | Blue | Person |
+| Repeater | Green | Router |
+| Room | Purple | Meeting room |
+| Sensor | Orange | Sensors |
+
+Node name labels appear automatically at zoom level 12 and above.
+
+### Shared Map Pins (Flag Icons)
+Location pins shared in chat messages are displayed as flags:
+- **Blue flag**: From a direct message
+- **Purple flag**: From a private channel
+- **Orange flag**: From a public channel
+
+Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
+
+### Predicted / Guessed Locations (Semi-Transparent)
+
+Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
+
+#### Why guessed locations exist
+
+In a mesh network, every message hops through one or more repeaters on its way to the destination. Each repeater in the path is identified by the first byte of its public key. If any of those repeaters have a known GPS location (because they advertise it), then a contact that routes through those repeaters must be somewhere within radio range of them. By combining the positions of multiple repeaters a contact is known to use, the app can triangulate a rough area where the contact is likely located.
+
+#### How the algorithm works
+
+1. **Build a repeater index**: The app collects all known contacts of type Repeater that have a valid GPS position and indexes them by the first byte of their public key.
+
+2. **Collect anchor points**: For each contact that lacks GPS, the app looks at the **last-hop byte** of the contact's current path and also searches the `PathHistoryService` for recent paths. Each last-hop byte that matches a located repeater becomes an "anchor point" — a GPS coordinate the contact is likely near.
+
+3. **Resolve ambiguity**: If multiple repeaters share the same first public-key byte (a hash collision), that byte is discarded as ambiguous. Only unambiguous one-to-one matches are kept.
+
+4. **Filter geometric inconsistencies**: Two anchor points separated by more than `2 × maxRangeKm` (the estimated LoRa radio range, computed from the current frequency, bandwidth, spreading factor, and TX power using a free-space path loss model) cannot both be in range of the same node. Outlier anchors are removed to keep only a geometrically consistent set.
+
+5. **Compute the estimated position**:
+ - **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
+ - **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80–120m) applied for visual separation.
+
+6. **Assign confidence level**:
+ - **High confidence** (2+ anchors): Displayed at 55% opacity.
+ - **Low confidence** (1 anchor): Displayed at 30% opacity.
+
+7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
+
+#### How to read guessed locations on the map
+
+- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
+- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
+- **More transparent** (30%): Lower confidence — based on a single repeater anchor only.
+- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
+- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
+
+## Map Interactions
+
+### Zoom and Pan
+Standard pinch-to-zoom (range 2–18). Initial camera position is calculated from the statistical spread of all plotted points.
+
+### Tap on a Node Marker
+Opens a dialog showing: type, path (hop chain), coordinates, last-seen time, and public key. Action buttons vary by type:
+- **Chat nodes**: "Open Chat"
+- **Repeaters**: "Manage Repeater"
+- **Rooms**: "Join Room"
+
+### Long-Press on Empty Map Area
+Shows a bottom sheet with:
+- **Share marker here**: Prompts for a label, then pick a DM contact or channel to send the location to. Wire format: `m:,||poi`
+- **Set as my location**: Updates your device's advertised location
+
+### Filter Dialog (FAB)
+Toggle visibility of: chat nodes, repeaters, other nodes, guessed locations, discovery contacts.
+Additional filters:
+- **Key prefix filter**: Show only contacts whose public key starts with a given prefix
+- **Last-seen time slider**: From 1 hour to "all time"
+
+### Legend Card (Top-Right)
+Shows node count and pin count. Tappable to expand a legend of all marker types.
+
+---
+
+## Path Trace Map
+
+### How to Access
+- From the main map's radar icon
+- From a contact's long-press menu → "Path Trace / Ping"
+- From a message's path view → radar icon
+
+### What the User Sees
+A map with a polyline showing the route from your node through repeater hops to the target:
+- **Green circles**: Hops with known GPS coordinates
+- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
+- **Red endpoint**: Target contact with known GPS
+- **Purple semi-transparent endpoint**: Target with guessed position
+
+A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
+
+### How It Works
+Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
+
+---
+
+## Line-of-Sight (LOS) Analysis
+
+### How to Access
+From the main map, tap the terrain/antenna icon.
+
+### What the User Sees
+A full-screen map with a collapsible control panel containing:
+- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
+- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
+- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
+
+### Key Interactions
+- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable)
+- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
+- **Antenna heights** are adjustable for both endpoints
+- **Map line** between endpoints is colored green (clear) or red (blocked)
+- Terrain elevation is fetched from the Open-Meteo API (21–81 sample points, cached 24 hours)
+- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
+
+---
+
+## Offline Map Cache
+
+### How to Access
+Settings → App Settings → Map Display → Offline Map Cache
+
+### What the User Sees
+- Map with a blue polygon overlay showing previously selected cache bounds
+- Bounding box coordinates card
+- **Cache Area** controls: "Use Current View" and Clear buttons
+- **Zoom Range** slider (3–18) with estimated tile count
+- **Download progress** bar (when downloading)
+- **Download Tiles** and **Clear Cache** buttons
+
+### Key Interactions
+1. Pan/zoom the map to the desired area
+2. Tap "Use Current View" to capture the viewport as cache bounds
+3. Adjust the zoom range slider
+4. Tap "Download Tiles" (confirmation dialog shows estimated count)
+5. Tiles are downloaded with up to 8 concurrent connections
+6. Once cached, tiles are served from disk without internet (365-day stale period)
+
+---
+
+## GPX Export
+
+### How to Access
+Settings → Export section
+
+### What It Does
+Exports contacts with GPS coordinates to a `.gpx` file via the OS share sheet. Three export options:
+- **Export Repeaters**: Repeater and Room contacts with locations
+- **Export Contacts**: Chat contacts with locations
+- **Export All**: All contacts with locations
+
+Each waypoint includes: name, lat/lon, type label, and public key hex.
+
+---
+
+## Location Data Sources
+
+The phone's own GPS is **never used**. All location data comes from the mesh:
+
+1. **Device self-location**: Read from firmware device-info response. Set manually in Settings → Location, or updated automatically if the device has a GPS module.
+2. **Remote node locations**: Extracted from advertisement packets received over the mesh. Encoded as integer lat/lon × 1,000,000.
diff --git a/documentation/navigation.md b/documentation/navigation.md
new file mode 100644
index 0000000..9003122
--- /dev/null
+++ b/documentation/navigation.md
@@ -0,0 +1,87 @@
+# Navigation
+
+## App Flow
+
+The app follows this general flow:
+
+```
+Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen
+```
+
+After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
+
+## Quick Switch Bar
+
+The QuickSwitchBar is a Material 3 `NavigationBar` with a frosted-glass visual treatment (blur backdrop, transparent theme, rounded corners). It appears at the bottom of all three main screens.
+
+| Index | Icon | Label | Screen |
+|---|---|---|---|
+| 0 | People | Contacts | ContactsScreen |
+| 1 | Tag | Channels | ChannelsScreen |
+| 2 | Map | Map | MapScreen |
+
+Tapping a tab replaces the current screen with a subtle fade + slight horizontal nudge transition (220ms forward, 200ms reverse). The back button is suppressed on all three main screens — navigation between them is flat, not stacked. All icons use outline variants (`people_outline`, `tag`, `map_outlined`) following Material 3 conventions.
+
+## Device Screen
+
+The Device Screen is a transitional hub that shows after connection. In practice, the app navigates directly to Contacts after connecting, but the Device Screen is reachable via the QuickSwitchBar.
+
+### What the User Sees
+
+**App Bar**:
+- Left: Battery indicator chip (tappable — toggles between percentage and voltage display). Icon changes based on level: `battery_unknown` when data unavailable, `battery_alert` (orange) at 15% or below, `battery_full` otherwise
+- Left-aligned title (`centerTitle: false`): Two-line layout — small grey "MeshCore" label above the device name in bold
+- Right: Disconnect button (`bluetooth_disabled` crossed-out icon) and Settings button (tune icon)
+
+**Body**:
+- **Connection Card**: Device avatar, device name, device ID, "Connected" chip, and battery chip
+- **Quick Switch** section: The QuickSwitchBar widget for navigating to Contacts/Channels/Map
+
+### Disconnection
+
+- The disconnect button shows a confirmation dialog before disconnecting
+- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
+- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
+
+## Theme and Locale
+
+- **Theme mode** is user-configurable in App Settings (System / Light / Dark) — not locked to system
+- **Language** can be overridden to one of 15 supported languages, or follow the system locale
+- On web, if a non-Chromium browser is detected, the app shows a `ChromeRequiredScreen` instead of the Scanner (Web Bluetooth requires Chromium)
+
+## Full Navigation Graph
+
+```
+ScannerScreen (root, always on stack)
+ ├─ [BLE connect] → push → ContactsScreen
+ ├─ [TCP FAB] → push → TcpScreen
+ │ └─ [TCP connected] → pushReplacement → ContactsScreen
+ └─ [USB FAB] → push → UsbScreen
+ └─ [USB connected] → pushReplacement → ContactsScreen
+
+ContactsScreen (selected=0)
+ ├─ [quick-switch 1] → pushReplacement → ChannelsScreen
+ ├─ [quick-switch 2] → pushReplacement → MapScreen
+ ├─ [tap contact] → push → ChatScreen
+ ├─ [overflow > Settings] → push → SettingsScreen
+ └─ [overflow > Discovered] → push → DiscoveryScreen
+
+ChannelsScreen (selected=1)
+ ├─ [quick-switch 0] → pushReplacement → ContactsScreen
+ ├─ [quick-switch 2] → pushReplacement → MapScreen
+ ├─ [tap channel] → push → ChannelChatScreen
+ └─ [overflow > Settings] → push → SettingsScreen
+
+MapScreen (selected=2)
+ ├─ [quick-switch 0] → pushReplacement → ContactsScreen
+ ├─ [quick-switch 1] → pushReplacement → ChannelsScreen
+ ├─ [radar button] → push → PathTraceMapScreen
+ ├─ [terrain button] → push → LineOfSightMapScreen
+ └─ [long-press] → share marker / set location
+
+Settings (push from any main screen)
+ └─ [App Settings] → push → AppSettingsScreen
+ └─ [Offline Map Cache] → push → MapCacheScreen
+```
+
+Any disconnection from any screen triggers `popUntil(route.isFirst)`, returning to the Scanner.
diff --git a/documentation/notifications.md b/documentation/notifications.md
new file mode 100644
index 0000000..0eb2574
--- /dev/null
+++ b/documentation/notifications.md
@@ -0,0 +1,92 @@
+# Notifications
+
+## Overview
+
+MeshCore Open provides both **system notifications** (push-style OS alerts) and **in-app unread badges** to inform users of new activity.
+
+## Notification Types
+
+### 1. Direct Message Notifications
+- **Triggered when**: A new incoming message arrives from a Chat or Room contact
+- **Title**: Contact's name
+- **Body**: Message text (reactions show "Reacted [emoji]", GIFs show "Sent a GIF")
+- **Priority**: High
+- **Android channel**: `messages`
+
+### 2. Channel Message Notifications
+- **Triggered when**: A new message arrives on a non-muted channel
+- **Title**: Channel name (or "Channel N" if unnamed)
+- **Body**: `": "`
+- **Priority**: High
+- **Android channel**: `channel_messages`
+
+### 3. Advertisement Notifications
+- **Triggered when**: A new node is discovered on the mesh for the first time
+- **Title**: "New [type] discovered" (e.g., "New chat node discovered")
+- **Body**: Contact's name
+- **Priority**: Default
+- **Android channel**: `adverts`
+
+### 4. Background Service Notification (Android Only)
+- A persistent low-priority notification: "MeshCore running — Keeping BLE connected"
+- Required by Android for foreground services to keep BLE alive in the background
+- Tap to re-launch the app
+- **Does not auto-start on reboot** — the user must re-open the app manually after a phone restart
+
+### Notification Tap Behavior
+
+Tapping a notification currently re-launches the app at the root route. It does **not** navigate directly to the relevant chat or channel.
+
+## In-App Unread Badges
+
+Red numeric badges appear throughout the UI:
+- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
+- **Channels list**: Each channel row shows an unread badge
+- **Chat screen subtitle**: Shows unread count inline
+- Badges cap at "99+" for display
+
+### How Unread Counts Work
+
+- Stored per contact (by public key) and per channel, **scoped to the connected device's identity** (first 10 hex characters of its public key). Switching between different radios gives each its own independent unread state
+- **Suppressed when viewing**: Opening a chat resets the count to 0 and cancels the OS notification
+- **Ignored for**: Outgoing messages, CLI messages, and repeater contacts
+- Debounced writes (500ms) to avoid excessive storage I/O during message bursts
+
+## Notification Settings
+
+Access via **App Settings → Notifications**:
+
+| Setting | Default | Description |
+|---|---|---|
+| Enable Notifications | On | Master toggle; requests OS permission when turned on |
+| Message Notifications | On | DM alerts (greyed out if master is off) |
+| Channel Message Notifications | On | Channel alerts (greyed out if master is off) |
+| Advertisement Notifications | On | New node alerts (greyed out if master is off) |
+
+### Per-Channel Muting
+
+Long-press a channel in the channels list → "Mute channel" / "Unmute channel". Muted channels do not generate OS notifications.
+
+There is no per-contact muting.
+
+## Rate Limiting
+
+The notification system prevents notification storms:
+- **Minimum interval**: 3 seconds between individual notifications
+- **Batch window**: If multiple notifications arrive within 5 seconds, they are combined into a single summary notification on a fourth Android channel (`batch_summary`): "MeshCore Activity — 2 messages, 1 channel message, 3 new nodes". Note: batch summaries are Android-only; on Apple platforms individual notifications are shown
+
+## Notification Clearing
+
+- **Opening a contact chat**: Cancels the OS notification and resets unread count
+- **Opening a channel**: Cancels the channel notification and resets unread count
+- **Opening Contacts screen**: Cancels all advertisement notifications
+
+## Platform Support
+
+| Platform | Message Notifs | Badge | Background Service |
+|---|---|---|---|
+| Android | Yes | Via notification number | Yes (foreground service) |
+| iOS | Yes | Yes (app badge) | No |
+| macOS | Yes | Yes | No |
+| Windows | Yes | No | No |
+| Linux | Yes (if D-Bus available) | No | No |
diff --git a/documentation/repeater-management.md b/documentation/repeater-management.md
new file mode 100644
index 0000000..be5015f
--- /dev/null
+++ b/documentation/repeater-management.md
@@ -0,0 +1,186 @@
+# Repeater Management
+
+## Overview
+
+Repeater Management provides tools for administering MeshCore repeater and room server nodes. It includes device status monitoring, CLI access, telemetry reading, neighbor discovery, and remote configuration.
+
+## How to Access
+
+From the Contacts screen:
+1. Long-press a **Repeater** or **Room** contact
+2. Select "Manage Repeater" or "Room Management"
+3. Enter the admin password in the login dialog
+4. Navigate to the Repeater Hub Screen
+
+### Login Dialog
+
+- Password field with show/hide toggle
+- "Save password" checkbox (persists for future logins). If a saved password exists, it is pre-filled and the checkbox is pre-checked, making login one-tap
+- Routing mode selector and "Manage Paths" link are available directly in the dialog (configure routing before login)
+- Auto-retries up to 5 times on timeout, showing progress ("Attempt 2 of 5"). A wrong password stops immediately after the first attempt — only timeouts trigger retries
+- After 5 failed attempts, further login attempts are blocked
+
+---
+
+## Repeater Hub Screen
+
+The central management screen showing:
+
+- **Header card**: Repeater name, short public key, path label, GPS coordinates (if known)
+- **Battery chemistry selector**: NMC / LiFePO4 / LiPo (saved per repeater)
+- **Management tool cards** (full-width cards with chevron arrows, not a grid). Title dynamically shows "Repeater Management" or "Room Management" based on contact type:
+
+| Card | Destination |
+|---|---|
+| Status | Repeater Status Screen |
+| Telemetry | Telemetry Screen |
+| CLI | Repeater CLI Screen |
+| Neighbors | Neighbors Screen |
+| Settings | Repeater Settings Screen |
+
+---
+
+## Repeater Status
+
+### What the User Sees
+
+Three information cards:
+
+**System Information**:
+- Battery percentage
+- Uptime
+- Queue length
+- Error flags
+- Clock at login time
+
+**Radio Statistics**:
+- Last RSSI and SNR
+- Noise floor
+- TX and RX airtime
+
+**Packet Statistics**:
+- Packets sent, received, and duplicates
+- Broken down by flood vs. direct
+
+### Key Interactions
+- Auto-queries the repeater on open; shows a loading spinner until data arrives
+- On timeout: red snackbar error. On success: data appears with a green snackbar confirmation
+- Pull-to-refresh or refresh button to re-query
+- Routing mode popup and path management dialog in app bar (these controls appear on **all** management sub-screens, not just Status)
+
+---
+
+## Repeater CLI
+
+A terminal-style interface for sending commands directly to the repeater.
+
+### What the User Sees
+
+- **Quick-command bar** (horizontal scroll): Shortcut buttons for common commands (get name, get radio, get tx, neighbors, ver, advert, clock)
+- **Command history list**: Sent commands in primary color, responses in secondary color
+- **Input bar**: Up/down history arrows, monospace text field with `> ` prefix, send button
+
+### Key Interactions
+
+- Type a command and press send (or Enter on desktop)
+- Up/down arrows navigate through command history
+- Quick-command buttons populate and send common commands
+- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
+- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
+- Clear icon: Wipes the command/response history
+- Failed/timed-out commands are automatically retried once
+
+### Available CLI Commands
+
+**General**: `advert`, `reboot`, `clock`, `password`, `ver`, `clear stats`
+
+**Settings**: `set name`, `set af`, `set tx`, `set repeat`, `set allow.read.only`, `set flood.max`, `set int.thresh`, `set agc.reset.interval`, `set multi.acks`, `set advert.interval`, `set flood.advert.interval`, `set guest.password`, `set lat`, `set lon`, `set radio`, `set rxdelay`, `set txdelay`, `set direct.txdelay`, `set bridge.*`, `set adc.multiplier`, `tempradio`, `setperm`
+
+**Bridge**: `get bridge.type`
+
+**Logging**: `log start`, `log stop`, `log erase`
+
+**Neighbors**: `neighbors`, `neighbor.remove`
+
+**Region Management**: `region`, `region load/get/put/remove/allowf/denyf/home/save`
+
+**GPS**: `gps`, `gps on/off/sync/setloc/advert`
+
+---
+
+## Telemetry
+
+### What the User Sees
+
+A list of Cayenne LPP sensor channel cards:
+
+- **Channel 1** (special): Battery voltage (shown as percentage or raw mV) and MCU temperature
+- **Other channels**: Raw sensor values with appropriate labels
+
+Shows "No data" until a response arrives from the repeater.
+
+### Key Interactions
+- Auto-queries on open
+- Pull-to-refresh
+- Temperature respects metric/imperial setting
+- Battery readings are stored for the repeater's battery snapshot
+
+---
+
+## Neighbors
+
+### What the User Sees
+
+A card titled "Repeater's Neighbors - N" listing each neighbor as:
+- Repeater name (or hex key prefix if unknown)
+- Time since last heard
+- SNR quality icon with color coding and label
+
+### Key Interactions
+- Auto-queries up to 15 neighbors on open
+- Matches public key prefixes against known contacts to show names
+- Pull-to-refresh
+
+---
+
+## Repeater Settings
+
+### What the User Sees
+
+Five configuration cards:
+
+**1. Basic Settings**
+- Name field
+- Admin password field
+- Guest password field
+
+**2. Radio Settings**
+- Frequency (MHz)
+- TX Power (dBm)
+- Bandwidth dropdown (kHz)
+- Spreading Factor (SF5–SF12)
+- Coding Rate (4/5–4/8)
+
+**3. Location Settings**
+- Latitude and longitude fields
+
+**4. Features**
+- Packet forwarding toggle
+- Guest access toggle
+
+**5. Advertisement Settings**
+- Local advert interval slider (60–240 minutes) with enable/disable toggle
+- Flood advert interval slider (3–168 hours) with enable/disable toggle
+
+**6. Danger Zone** (red-styled card)
+- Reboot repeater
+- Erase filesystem (serial-only warning)
+
+### Key Interactions
+- **Settings are NOT auto-fetched on open**. Only name and location are pre-filled from locally cached contact data. You must tap each section's refresh button to fetch live values from the repeater
+- TX Power has its own separate refresh button, independent from the main Radio Settings refresh
+- Save button appears when changes are detected
+- Settings are sent sequentially with 200ms delays between commands (fire-and-forget, no per-command acknowledgment wait)
+- Validation prevents invalid values (e.g., frequency range, LoRa parameter compatibility)
+- Advertisement interval sliders reset to defaults when re-enabled (local: 60 min, flood: 3 hours)
+- **Erase Filesystem** does NOT send any command over the air — tapping it only shows a snackbar explaining the operation requires physical serial access. It is effectively non-functional when connected wirelessly
diff --git a/documentation/scanner-and-connection.md b/documentation/scanner-and-connection.md
new file mode 100644
index 0000000..2c5dbae
--- /dev/null
+++ b/documentation/scanner-and-connection.md
@@ -0,0 +1,124 @@
+# Scanner & Connection
+
+## BLE Scanner (Home Screen)
+
+The BLE Scanner is the app's home screen, displayed immediately on launch.
+
+### How to Access
+
+- Opens automatically when the app starts
+- Returns here when disconnecting from any device
+- Accessible by navigating back from a connected session
+
+### What the User Sees
+
+**App Bar**: Centered title "Scanner".
+
+**Bluetooth-Off Warning Banner** (conditional): Appears when the Bluetooth adapter is off, showing a `bluetooth_disabled` icon, a warning message, and on Android, an "Enable Bluetooth" button.
+
+**Status Bar**: A full-width colored strip reflecting the current connection state:
+
+| State | Text | Color |
+|---|---|---|
+| Disconnected | "Not connected" | Grey |
+| Scanning | "Scanning..." | Blue |
+| Connecting | "Connecting..." | Orange |
+| Connected | "Connected to \" | Green |
+| Disconnecting | "Disconnecting..." | Orange |
+
+**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
+
+**Bottom FAB Row**: Up to three floating action buttons:
+- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
+- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
+- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
+
+### Device Tile
+
+Each discovered device is displayed as a list tile showing:
+- **Signal strength icon** (color-coded by RSSI):
+ - Green: >= -60 dBm (excellent)
+ - Light green: -60 to -70 dBm (good)
+ - Amber: -70 to -80 dBm (fair)
+ - Orange: -80 to -90 dBm (weak)
+ - Red: < -90 dBm (poor)
+- **RSSI value** in dBm (e.g., "-72 dBm")
+- **Device name** (falls back to "Unknown Device")
+- **Device ID** (BLE MAC address on Android; a system-assigned UUID on iOS/macOS)
+- **Connect button** (the entire tile row is also tappable — both trigger connection)
+
+Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon shape and are only differentiated by color (orange vs. red).
+
+### How Scanning Works
+
+- Filters for devices with names starting with `MeshCore-` or `Whisper-`
+- Uses low-latency scan mode on Android
+- Scans for 10 seconds then auto-stops
+- On iOS/macOS, waits for BLE adapter initialization before starting
+- If Bluetooth is turned off during a scan, scanning stops immediately
+
+### Connecting to a Device
+
+Tap a device tile or its Connect button:
+1. The connector stops scanning and transitions to "connecting"
+2. Connects to the device with a 15-second timeout
+3. Requests MTU 185 bytes for optimal throughput
+4. Discovers BLE services and locates the Nordic UART Service
+5. Subscribes to TX notifications for receiving data
+6. On success, automatically navigates to the Contacts screen
+7. On failure, shows a red error snackbar
+
+---
+
+## USB Connection
+
+### How to Access
+
+From the Scanner screen, tap the **USB** FAB button.
+
+### What the User Sees
+
+- A colored status bar at the top (same color scheme as BLE scanner)
+- A list of detected USB serial ports, each showing:
+ - Friendly display name
+ - Raw port name (subtitle, only shown when it differs from the display name)
+ - "Connect" button
+- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
+
+### Key Interactions
+
+- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
+- On mobile: tap the "Scan" FAB to manually refresh
+- Tap a port or its Connect button to connect
+- On successful connection, navigates to Contacts screen
+- On connection failure, the port list automatically refreshes
+- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
+
+---
+
+## TCP Connection
+
+### How to Access
+
+From the Scanner screen, tap the **TCP/IP** FAB button.
+
+### What the User Sees
+
+- A colored status bar at the top
+- **Host address** text field
+- **Port number** text field
+- **Connect** button
+- FABs at the bottom to switch to USB or BLE
+
+### Key Interactions
+
+- Last-used host and port are pre-populated from saved settings
+- Tap Connect to validate inputs and connect
+ - Host must not be empty
+ - Port must be a number between 1 and 65535
+ - Validation errors are shown as red snackbars
+- The Connect button shows a spinner and "Connecting..." label while in progress
+- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
+- On success, navigates to Contacts screen and saves the host/port to settings
+- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
+- Error messages for timeout, unsupported platform, and connection failures
diff --git a/documentation/settings.md b/documentation/settings.md
new file mode 100644
index 0000000..70e39e2
--- /dev/null
+++ b/documentation/settings.md
@@ -0,0 +1,169 @@
+# Settings
+
+## How to Access
+
+- From the Device Screen: tap the tune/sliders icon in the app bar
+- From Contacts or Channels: overflow menu (three-dot) → Settings
+
+Settings are only accessible while a device is connected.
+
+## Settings Screen Layout
+
+The settings screen is a scrollable list of cards:
+
+1. [Device Info](#device-info)
+2. [App Settings](#app-settings) (link to sub-screen)
+3. [Node Settings](#node-settings)
+4. [Actions](#actions)
+5. [Debug](#debug)
+6. [Export](#export)
+7. [About](#about)
+
+---
+
+## Device Info
+
+A collapsible card showing read-only device information. **Collapsed by default** — tap the header to expand with an animated chevron indicator:
+
+| Field | Description |
+|---|---|
+| Name | Connected device's display name |
+| ID | Device identifier |
+| Status | Connected / Disconnected |
+| Battery | Percentage or voltage (tap to toggle) |
+| Node Name | The node's mesh identity name |
+| Public Key | First 16 hex characters + "..." |
+| Contacts Count | Number of known contacts |
+| Channel Count | Number of configured channels |
+
+Battery shows an alert icon and orange text when at 15% or below. The toggle only works when millivolt data is available from the firmware.
+
+---
+
+## App Settings
+
+A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
+
+### Appearance
+- **Theme**: System / Light / Dark
+- **Language**: System default or one of 15 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian)
+- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
+
+### Notifications
+- **Master enable/disable**: Requests OS permission when enabling
+- **Message notifications**: New direct message alerts
+- **Channel message notifications**: New channel message alerts
+- **Advertisement notifications**: New node discovery alerts
+
+### Messaging
+- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
+- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
+ - Max Route Weight (1–10, default 5, integer steps)
+ - Initial Route Weight (0.5–5.0, default 3.0)
+ - Success Increment (0.1–2.0, default 0.5, 0.1 steps)
+ - Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
+ - Max Message Retries (2–10, default 5)
+
+### Battery
+- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
+
+### Map Display
+- **Show Repeaters**: Toggle repeater markers on map
+- **Show Chat Nodes**: Toggle chat node markers
+- **Show Other Nodes**: Toggle room/sensor markers
+- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
+- **Units**: Metric / Imperial
+- **Offline Map Cache**: Navigate to tile download screen
+
+### Debug
+- **App Debug Logging**: Enable the in-app debug log
+
+---
+
+## Node Settings
+
+These settings are sent directly to the connected device firmware.
+
+### Node Name
+- Opens a dialog with a text field (max 31 characters)
+- Sends the new name to the device
+- Confirmed via snackbar
+
+### Radio Settings
+Opens a dialog pre-populated with the device's current radio settings. Contains:
+- **Preset dropdown**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA/WA/QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
+- **Frequency** (MHz): Free text, validated 300–2500 MHz
+- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
+- **Spreading Factor**: SF5–SF12
+- **Coding Rate**: 4/5, 4/6, 4/7, 4/8
+- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
+- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
+
+### Location
+Opens a dialog pre-populated with the device's current coordinates (if known):
+- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
+- If GPS-capable hardware (detected via `gps` custom variable):
+ - GPS Update Interval (seconds, 60–86399, default 900 = 15 minutes). Validated and sent separately before lat/lon
+ - Enable GPS toggle (takes effect immediately, not deferred to Save)
+- Validation: lat ±90, lon ±180
+
+### Contact Settings
+Five toggles controlling which node types are auto-added when heard:
+- Auto-add Chat Users
+- Auto-add Repeaters
+- Auto-add Room Servers
+- Auto-add Sensors
+- Overwrite Oldest (when contact list is full)
+
+### Privacy Mode
+Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
+
+---
+
+## Actions
+
+One-tap device operations:
+
+| Action | Description |
+|---|---|
+| Send Advertisement | Floods the mesh with your node's advertisement |
+| Sync Time | Sends current Unix timestamp to the device |
+| Refresh Contacts | Re-requests the full contact list |
+| Reboot Device | Confirmation dialog → reboots the device (shown in orange) |
+
+---
+
+## Debug
+
+Two log viewers accessible via list tiles:
+
+### BLE Debug Log
+Two views (togglable via segmented button):
+- **Frames view**: Direction icon, description, hex preview, timestamp per frame. Long-press to copy hex.
+- **Raw Log RX view**: Decoded LoRa packets with route type, payload type, path, and summary.
+- Copy-all and Clear buttons in the app bar.
+
+### App Debug Log
+Structured log entries (Info / Warning / Error), with tag, message, and timestamp.
+- Must be enabled first in App Settings → Debug
+- Copy-all and Clear buttons
+
+---
+
+## Export
+
+Three GPX export options (not available on web):
+
+| Option | Exports |
+|---|---|
+| Export Repeaters | Repeaters and Rooms with GPS coordinates |
+| Export Contacts | Chat contacts with GPS coordinates |
+| Export All | All contacts with GPS coordinates |
+
+Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
+
+---
+
+## About
+
+Shows the standard Flutter about dialog with app name, version, and legal notice.
diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart
index 7211992..b99ecf7 100644
--- a/lib/connector/meshcore_connector.dart
+++ b/lib/connector/meshcore_connector.dart
@@ -1,13 +1,16 @@
import 'dart:async';
import 'dart:convert';
+import 'dart:math' as math;
import 'package:crypto/crypto.dart' as crypto;
import 'package:pointycastle/export.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+import 'package:flutter_blue_plus_platform_interface/flutter_blue_plus_platform_interface.dart';
import '../models/channel.dart';
import '../models/channel_message.dart';
+import '../models/companion_radio_stats.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_selection.dart';
@@ -15,6 +18,9 @@ import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart';
import '../services/app_debug_log_service.dart';
import '../services/ble_debug_log_service.dart';
+import '../services/linux_ble_error_classifier.dart';
+import '../services/linux_ble_pairing_service_stub.dart'
+ if (dart.library.io) '../services/linux_ble_pairing_service.dart';
import '../services/message_retry_service.dart';
import '../services/path_history_service.dart';
import '../services/app_settings_service.dart';
@@ -35,14 +41,9 @@ import '../storage/unread_store.dart';
import '../utils/app_logger.dart';
import '../utils/battery_utils.dart';
import '../utils/platform_info.dart';
+import 'meshcore_uuids.dart';
import 'meshcore_protocol.dart';
-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";
-}
-
class DirectRepeater {
static const int maxAgeMinutes = 30; // Max age for direct repeater info
final int pubkeyFirstByte;
@@ -116,11 +117,14 @@ class MeshCoreConnector extends ChangeNotifier {
String? _lastDeviceDisplayName;
bool _manualDisconnect = false;
final MeshCoreUsbManager _usbManager = MeshCoreUsbManager();
+ final LinuxBlePairingService _linuxBlePairingService =
+ LinuxBlePairingService();
StreamSubscription? _usbFrameSubscription;
final MeshCoreTcpConnector _tcpConnector = MeshCoreTcpConnector();
MeshCoreTransportType _activeTransport = MeshCoreTransportType.bluetooth;
final List _scanResults = [];
+ final List _linuxSystemScanResults = [];
final List _contacts = [];
final List _discoveredContacts = [];
final List _channels = [];
@@ -143,6 +147,10 @@ class MeshCoreConnector extends ChangeNotifier {
Timer? _selfInfoRetryTimer;
Timer? _reconnectTimer;
Timer? _batteryPollTimer;
+ Timer? _radioStatsPollTimer;
+ int _radioStatsPollRefCount = 0;
+ final ValueNotifier radioStatsNotifier =
+ ValueNotifier(null);
int _reconnectAttempts = 0;
bool _notifyListenersDirty = false;
static const Duration _notifyListenersDebounce = Duration(milliseconds: 50);
@@ -160,6 +168,10 @@ class MeshCoreConnector extends ChangeNotifier {
int? _currentCr;
bool? _clientRepeat;
int? _firmwareVerCode;
+ int _pathHashByteWidth = 1;
+ CompanionRadioStats? _latestRadioStats;
+ Stopwatch? _airtimeBumpStopwatch;
+ int _prevTotalAirSecs = 0;
int? _batteryMillivolts;
double? _selfLatitude;
double? _selfLongitude;
@@ -171,6 +183,16 @@ class MeshCoreConnector extends ChangeNotifier {
// Intentionally global (not per-contact): tracks overall network activity.
// Frequent RX from any source indicates a busy network with more collisions.
DateTime _lastRxTime = DateTime.now();
+ DateTime _lastRadioRxTime = DateTime.fromMillisecondsSinceEpoch(0);
+ DateTime _lastContactMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
+ DateTime _lastChannelMsgRxTime = DateTime.fromMillisecondsSinceEpoch(0);
+ static const int _radioQuietMs = 3000;
+ static const int _radioQuietMaxWaitMs = 3000;
+
+ /// When companion radio stats are unavailable, keep the legacy fixed backoff.
+ static const int _contactMsgBackoffFallbackMs = 5000;
+ static const int _contactMsgBackoffMinMs = 500;
+ static const int _contactMsgBackoffMaxMs = 15000;
bool _batteryRequested = false;
bool _awaitingSelfInfo = false;
bool _hasReceivedDeviceInfo = false;
@@ -252,6 +274,9 @@ class MeshCoreConnector extends ChangeNotifier {
int? _activeChannelIndex;
List _channelOrder = [];
+ int _storageUsedKb = -1;
+ int _storageTotalKb = -1;
+
// Getters
MeshCoreConnectionState get state => _state;
BluetoothDevice? get device => _device;
@@ -264,6 +289,8 @@ class MeshCoreConnector extends ChangeNotifier {
bool get isUsbTransportConnected =>
_state == MeshCoreConnectionState.connected &&
_activeTransport == MeshCoreTransportType.usb;
+ bool get isAutoReconnectScheduled =>
+ _shouldAutoReconnect && (_reconnectTimer?.isActive ?? false);
String? get activeTcpEndpoint => _tcpConnector.activeEndpoint;
bool get isTcpTransportConnected =>
_state == MeshCoreConnectionState.connected &&
@@ -315,6 +342,19 @@ class MeshCoreConnector extends ChangeNotifier {
List get directRepeaters => _directRepeaters;
int? get currentTxPower => _currentTxPower;
int? get maxTxPower => _maxTxPower;
+
+ int get pathHashByteWidth => _pathHashByteWidth;
+
+ CompanionRadioStats? get latestRadioStats => _latestRadioStats;
+
+ bool get supportsCompanionRadioStats => (_firmwareVerCode ?? 0) >= 8;
+
+ bool get radioStatsAirActivityPulse {
+ final sw = _airtimeBumpStopwatch;
+ if (sw == null || !sw.isRunning) return false;
+ return sw.elapsed < const Duration(seconds: 2);
+ }
+
int? get currentFreqHz => _currentFreqHz;
int? get currentBwHz => _currentBwHz;
int? get currentSf => _currentSf;
@@ -324,10 +364,17 @@ class MeshCoreConnector extends ChangeNotifier {
bool? get autoAddRoomServers => _autoAddRoomServers;
bool? get autoAddSensors => _autoAddSensors;
bool? get autoAddOverwriteOldest => _overwriteOldest;
+ int get telemetryModeBase => _telemetryModeBase;
+ int get telemetryModeLoc => _telemetryModeLoc;
+ int get telemetryModeEnv => _telemetryModeEnv;
+ int get advertLocationPolicy => _advertLocPolicy;
+ int get multiAcks => _multiAcks;
bool? get clientRepeat => _clientRepeat;
int? get firmwareVerCode => _firmwareVerCode;
Map? get currentCustomVars => _currentCustomVars;
int? get batteryMillivolts => _batteryMillivolts;
+ int? get storageUsedKb => _storageUsedKb;
+ int? get storageTotalKb => _storageTotalKb;
int get maxContacts => _maxContacts;
int get maxChannels => _maxChannels;
Set get knownContactKeys => Set.unmodifiable(_knownContactKeys);
@@ -695,34 +742,45 @@ class MeshCoreConnector extends ChangeNotifier {
// Initialize retry service callbacks
_retryService?.initialize(
- sendMessageCallback: _sendMessageDirect,
- addMessageCallback: _addMessage,
- updateMessageCallback: _updateMessage,
- clearContactPathCallback: clearContactPath,
- setContactPathCallback: setContactPath,
- calculateTimeoutCallback:
- (pathLength, messageBytes, {String? contactKey}) => calculateTimeout(
- pathLength: pathLength,
- messageBytes: messageBytes,
- contactKey: contactKey,
- ),
- getSelfPublicKeyCallback: () => _selfPublicKey,
- prepareContactOutboundTextCallback: prepareContactOutboundText,
- appSettingsService: appSettingsService,
- debugLogService: _appDebugLogService,
- recordPathResultCallback: _recordPathResult,
- onDeliveryObservedCallback:
- (contactKey, pathLength, messageBytes, tripTimeMs) {
- final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds;
- _timeoutPredictionService?.recordObservation(
- contactKey: contactKey,
+ RetryServiceConfig(
+ sendMessage: _sendMessageDirect,
+ addMessage: _addMessage,
+ updateMessage: _updateMessage,
+ clearContactPath: clearContactPath,
+ setContactPath: setContactPath,
+ calculateTimeout: (pathLength, messageBytes, {String? contactKey}) =>
+ calculateTimeout(
pathLength: pathLength,
messageBytes: messageBytes,
- tripTimeMs: tripTimeMs,
- secondsSinceLastRx: secSinceRx,
- );
- },
+ contactKey: contactKey,
+ ),
+ getSelfPublicKey: () => _selfPublicKey,
+ prepareContactOutboundText: prepareContactOutboundText,
+ appSettingsService: appSettingsService,
+ debugLogService: _appDebugLogService,
+ recordPathResult: _recordPathResult,
+ selectRetryPath:
+ (contactKey, attemptIndex, maxRetries, recentSelections) =>
+ _selectAutoPathForAttempt(
+ contactKey,
+ attemptIndex: attemptIndex,
+ maxRetries: maxRetries,
+ recentSelections: recentSelections,
+ ),
+ onDeliveryObserved: (contactKey, pathLength, messageBytes, tripTimeMs) {
+ final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds;
+ _timeoutPredictionService?.recordObservation(
+ contactKey: contactKey,
+ pathLength: pathLength,
+ messageBytes: messageBytes,
+ tripTimeMs: tripTimeMs,
+ secondsSinceLastRx: secSinceRx,
+ );
+ },
+ ),
);
+ final maxRetries = _appSettingsService?.settings.maxMessageRetries ?? 5;
+ _retryService?.setMaxRetries(maxRetries);
}
Future loadContactCache() async {
@@ -753,22 +811,116 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
- void _sendMessageDirect(
+ /// After an incoming DM or channel message, wait before TX so we do not
+ /// collide with mesh propagation. With companion stats, scale wait by RF
+ /// conditions (up to [_contactMsgBackoffMaxMs]); otherwise use
+ /// [_contactMsgBackoffFallbackMs].
+ int _contactMessageBackoffTargetMs() {
+ if (!supportsCompanionRadioStats || _latestRadioStats == null) {
+ return _contactMsgBackoffFallbackMs;
+ }
+ final stats = _latestRadioStats!;
+ final nf = stats.noiseFloorDbm.toDouble();
+ // Quieter (more negative) → lower score; noisier → higher.
+ const noiseQuietDbm = -118.0;
+ const noiseNoisyDbm = -88.0;
+ final noiseT = ((nf - noiseQuietDbm) / (noiseNoisyDbm - noiseQuietDbm))
+ .clamp(0.0, 1.0);
+
+ final snr = stats.lastSnrDb;
+ const snrGood = 12.0;
+ const snrBad = -2.0;
+ final snrT = (1.0 - ((snr - snrBad) / (snrGood - snrBad))).clamp(0.0, 1.0);
+
+ final airBusy = _recentAirtimeBusyFraction();
+ final severity = (math.max(noiseT, snrT) * 0.82 + airBusy * 0.18).clamp(
+ 0.0,
+ 1.0,
+ );
+
+ return (_contactMsgBackoffMinMs +
+ severity * (_contactMsgBackoffMaxMs - _contactMsgBackoffMinMs))
+ .round();
+ }
+
+ /// 1.0 shortly after TX/RX airtime counters increase, decaying to 0 over ~8s.
+ double _recentAirtimeBusyFraction() {
+ final sw = _airtimeBumpStopwatch;
+ if (sw == null || !sw.isRunning) return 0;
+ final ms = sw.elapsedMilliseconds;
+ const windowMs = 8000;
+ if (ms >= windowMs) return 0;
+ return 1.0 - (ms / windowMs);
+ }
+
+ /// Start of the post-inbound cool-down: the later of BLE message RX time and
+ /// companion airtime bump ([_airtimeBumpStopwatch], same as the activity dot).
+ DateTime _postTxBackoffAnchor(DateTime lastInboundRxTime) {
+ if (!supportsCompanionRadioStats) return lastInboundRxTime;
+ final sw = _airtimeBumpStopwatch;
+ if (sw == null || !sw.isRunning) return lastInboundRxTime;
+ final bumpAt = DateTime.now().subtract(sw.elapsed);
+ return bumpAt.isAfter(lastInboundRxTime) ? bumpAt : lastInboundRxTime;
+ }
+
+ Future _waitForRadioQuiet({required DateTime lastInboundRxTime}) async {
+ // Wait for backoff after inbound traffic / RF airtime (avoid collision with
+ // mesh propagation). Elapsed time uses the dot's airtime bump when newer.
+ final backoffTargetMs = _contactMessageBackoffTargetMs();
+ final anchor = _postTxBackoffAnchor(lastInboundRxTime);
+ final msSinceAnchor = DateTime.now().difference(anchor).inMilliseconds;
+ if (msSinceAnchor < backoffTargetMs) {
+ final waitMs = backoffTargetMs - msSinceAnchor;
+ debugPrint(
+ 'Post-inbound backoff: waiting ${waitMs}ms '
+ '(target=${backoffTargetMs}ms, anchorAge=${msSinceAnchor}ms)',
+ );
+ await Future.delayed(Duration(milliseconds: waitMs));
+ }
+
+ // Then wait for radio silence (no RF activity for 3s)
+ final msSinceRx = DateTime.now()
+ .difference(_lastRadioRxTime)
+ .inMilliseconds;
+ if (msSinceRx >= _radioQuietMs) return;
+
+ final deadline = DateTime.now().add(
+ const Duration(milliseconds: _radioQuietMaxWaitMs),
+ );
+ while (DateTime.now().isBefore(deadline)) {
+ final quiet = DateTime.now().difference(_lastRadioRxTime).inMilliseconds;
+ if (quiet >= _radioQuietMs) {
+ debugPrint('Radio quiet for ${quiet}ms, proceeding with send');
+ return;
+ }
+ await Future.delayed(const Duration(milliseconds: 200));
+ }
+ debugPrint(
+ 'Radio quiet wait exceeded ${_radioQuietMaxWaitMs}ms, sending anyway',
+ );
+ }
+
+ Future _sendMessageDirect(
Contact contact,
String text,
int attempt,
int timestampSeconds,
) async {
if (!isConnected || text.isEmpty) return;
- final outboundText = prepareContactOutboundText(contact, text);
- await sendFrame(
- buildSendTextMsgFrame(
- contact.publicKey,
- outboundText,
- attempt: attempt,
- timestampSeconds: timestampSeconds,
- ),
- );
+ try {
+ await _waitForRadioQuiet(lastInboundRxTime: _lastContactMsgRxTime);
+ final outboundText = prepareContactOutboundText(contact, text);
+ await sendFrame(
+ buildSendTextMsgFrame(
+ contact.publicKey,
+ outboundText,
+ attempt: attempt,
+ timestampSeconds: timestampSeconds,
+ ),
+ );
+ } catch (e) {
+ appLogger.error('Failed to send message: $e', tag: 'Connector');
+ }
}
void _updateMessage(Message message) {
@@ -784,6 +936,20 @@ class MeshCoreConnector extends ChangeNotifier {
notifyListeners();
}
}
+
+ // If this is a reaction message, update the target message's reaction status
+ final reactionInfo = ReactionHelper.parseReaction(message.text);
+ if (reactionInfo != null &&
+ (message.status == MessageStatus.delivered ||
+ message.status == MessageStatus.failed)) {
+ final contactKey2 = pubKeyToHex(message.senderKey);
+ _setReactionStatus(contactKey2, reactionInfo, message.status);
+ _messageStore.saveMessages(
+ contactKey2,
+ _conversations[contactKey2] ?? [],
+ );
+ notifyListeners();
+ }
}
void _recordPathResult(
@@ -793,35 +959,69 @@ class MeshCoreConnector extends ChangeNotifier {
int? tripTimeMs,
) {
if (_pathHistoryService == null) return;
+ final settings = _appSettingsService?.settings;
_pathHistoryService!.recordPathResult(
contactPubKeyHex,
selection,
success: success,
tripTimeMs: tripTimeMs,
+ successIncrement: settings?.routeWeightSuccessIncrement ?? 0.2,
+ failureDecrement: settings?.routeWeightFailureDecrement ?? 0.2,
+ maxWeight: settings?.maxRouteWeight ?? 5.0,
);
+
+ // Flood path attribution: when a flood delivery succeeds, credit the
+ // contact's current device path so the route the ACK traveled back
+ // through gets a weight boost in the path history.
+ if (selection.useFlood && success) {
+ final contact = _contacts.cast().firstWhere(
+ (c) => c?.publicKeyHex == contactPubKeyHex,
+ orElse: () => null,
+ );
+ if (contact != null &&
+ contact.pathLength >= 0 &&
+ contact.path.isNotEmpty) {
+ _pathHistoryService!.recordFloodPathAttribution(
+ contactPubKeyHex: contactPubKeyHex,
+ pathBytes: contact.path,
+ hopCount: contact.pathLength,
+ tripTimeMs: tripTimeMs,
+ successIncrement: settings?.routeWeightSuccessIncrement ?? 0.2,
+ maxWeight: settings?.maxRouteWeight ?? 5.0,
+ );
+ }
+
+ // Request a fresh contact from the device so the next flood
+ // attribution uses the most up-to-date path.
+ if (contact != null) {
+ unawaited(getContactByKey(contact.publicKey));
+ }
+ }
}
- Contact _applyAutoSelection(Contact contact, PathSelection? selection) {
- if (selection == null ||
- selection.useFlood ||
- selection.pathBytes.isEmpty) {
- return contact;
+ PathSelection? _selectAutoPathForAttempt(
+ String contactPubKeyHex, {
+ required int attemptIndex,
+ required int maxRetries,
+ List recentSelections = const [],
+ }) {
+ final hasKnownPaths =
+ _pathHistoryService?.getRecentPaths(contactPubKeyHex).isNotEmpty ??
+ false;
+ if (!hasKnownPaths) {
+ return null;
}
- return Contact(
- publicKey: contact.publicKey,
- name: contact.name,
- type: contact.type,
- flags: contact.flags,
- pathLength: selection.hopCount >= 0
- ? selection.hopCount
- : contact.pathLength,
- path: Uint8List.fromList(selection.pathBytes),
- latitude: contact.latitude,
- longitude: contact.longitude,
- lastSeen: contact.lastSeen,
- lastMessageAt: contact.lastMessageAt,
+ final selection = _pathHistoryService?.selectPathForAttempt(
+ contactPubKeyHex,
+ attemptIndex: attemptIndex,
+ maxRetries: maxRetries,
+ recentSelections: recentSelections,
);
+ if (selection != null) {
+ _pathHistoryService?.recordPathAttempt(contactPubKeyHex, selection);
+ }
+ return selection;
}
Future startScan({
@@ -830,6 +1030,7 @@ class MeshCoreConnector extends ChangeNotifier {
if (_state == MeshCoreConnectionState.scanning) return;
_scanResults.clear();
+ _linuxSystemScanResults.clear();
_setState(MeshCoreConnectionState.scanning);
// Ensure any previous scan is fully stopped. Guard with isScanningNow to
@@ -868,15 +1069,21 @@ class MeshCoreConnector extends ChangeNotifier {
await Future.delayed(const Duration(milliseconds: 300));
}
+ if (PlatformInfo.isLinux) {
+ await _loadLinuxSystemDevicesForScan();
+ }
+
_scanSubscription = FlutterBluePlus.scanResults.listen((results) {
- _scanResults.clear();
- _scanResults.addAll(results);
+ _scanResults
+ ..clear()
+ ..addAll(results);
+ _mergeLinuxSystemScanResults();
notifyListeners();
});
try {
await FlutterBluePlus.startScan(
- withKeywords: ["MeshCore-", "Whisper-"],
+ withKeywords: MeshCoreUuids.deviceNamePrefixes,
webOptionalServices: [Guid(MeshCoreUuids.service)],
timeout: timeout,
androidScanMode: AndroidScanMode.lowLatency,
@@ -891,6 +1098,62 @@ class MeshCoreConnector extends ChangeNotifier {
await stopScan();
}
+ Future _loadLinuxSystemDevicesForScan() async {
+ try {
+ final systemDevices = await FlutterBluePlus.systemDevices([
+ Guid(MeshCoreUuids.service),
+ ]);
+ _linuxSystemScanResults
+ ..clear()
+ ..addAll(
+ systemDevices
+ .where(
+ (device) => MeshCoreUuids.deviceNamePrefixes.any(
+ device.platformName.startsWith,
+ ),
+ )
+ .map(
+ (device) => ScanResult(
+ device: device,
+ advertisementData: AdvertisementData(
+ advName: device.platformName,
+ txPowerLevel: null,
+ appearance: null,
+ connectable: true,
+ manufacturerData: const >{},
+ serviceData: const >{},
+ serviceUuids: [Guid(MeshCoreUuids.service)],
+ ),
+ rssi: 0,
+ timeStamp: DateTime.now(),
+ ),
+ ),
+ );
+ _mergeLinuxSystemScanResults();
+ notifyListeners();
+ } catch (error) {
+ _appDebugLogService?.warn(
+ 'Failed loading Linux paired/system BLE devices: $error',
+ tag: 'BLE Scan',
+ );
+ }
+ }
+
+ void _mergeLinuxSystemScanResults() {
+ if (!PlatformInfo.isLinux || _linuxSystemScanResults.isEmpty) {
+ return;
+ }
+ final existingIds = _scanResults
+ .map((result) => result.device.remoteId.str)
+ .toSet();
+ for (final result in _linuxSystemScanResults) {
+ if (existingIds.contains(result.device.remoteId.str)) {
+ continue;
+ }
+ _scanResults.add(result);
+ }
+ }
+
Future stopScan() async {
// Only call FlutterBluePlus.stopScan() when a scan is actually running.
// Calling it when idle triggers a native BLE completion callback even
@@ -984,6 +1247,7 @@ class MeshCoreConnector extends ChangeNotifier {
);
await _requestDeviceInfo();
_startBatteryPolling();
+ if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
var gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
);
@@ -1089,6 +1353,7 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingInitialChannelSync = true;
await _requestDeviceInfo();
_startBatteryPolling();
+ if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
var gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
@@ -1147,7 +1412,11 @@ class MeshCoreConnector extends ChangeNotifier {
activeTransport == MeshCoreTransportType.tcp;
}
- Future connect(BluetoothDevice device, {String? displayName}) async {
+ Future connect(
+ BluetoothDevice device, {
+ String? displayName,
+ Future Function()? linuxPairingPinProvider,
+ }) async {
if (_state == MeshCoreConnectionState.connecting ||
_state == MeshCoreConnectionState.connected) {
return;
@@ -1192,22 +1461,149 @@ class MeshCoreConnector extends ChangeNotifier {
}
});
- try {
- await device.connect(
- timeout: const Duration(seconds: 15),
- mtu: null,
- license: License.free,
- );
- } catch (error) {
- _appDebugLogService?.error(
- 'device.connect() failure: $error',
+ if (PlatformInfo.isLinux) {
+ final remoteId = device.remoteId.str;
+ _appDebugLogService?.info(
+ 'Linux pre-connect BlueZ disconnect for $remoteId',
tag: 'BLE Connect',
);
- rethrow;
+ await _linuxBlePairingService.disconnectDevice(
+ remoteId,
+ onLog: (message) {
+ _appDebugLogService?.info(message, tag: 'BLE Pair');
+ },
+ );
}
- // Request larger MTU only on native platforms; web does not support it.
- if (!PlatformInfo.isWeb) {
+ final connectTimeout = PlatformInfo.isLinux
+ ? const Duration(seconds: 6)
+ : const Duration(seconds: 15);
+ _appDebugLogService?.info(
+ 'device.connect timeout set to ${connectTimeout.inSeconds}s',
+ tag: 'BLE Connect',
+ );
+ if (PlatformInfo.isLinux) {
+ Future attemptConnect() {
+ return device
+ .connect(
+ timeout: connectTimeout,
+ mtu: null,
+ license: License.free,
+ )
+ .timeout(
+ connectTimeout + const Duration(seconds: 2),
+ onTimeout: () {
+ throw TimeoutException(
+ 'Linux connect hard-timeout after ${connectTimeout.inSeconds + 2}s',
+ );
+ },
+ );
+ }
+
+ try {
+ await attemptConnect();
+ } catch (error) {
+ _appDebugLogService?.error(
+ 'device.connect() failure: $error',
+ tag: 'BLE Connect',
+ );
+ final remoteId = device.remoteId.str;
+ _appDebugLogService?.warn(
+ 'Linux immediate retry: forcing BlueZ disconnect before second connect attempt',
+ tag: 'BLE Connect',
+ );
+ await _linuxBlePairingService.disconnectDevice(
+ remoteId,
+ onLog: (message) {
+ _appDebugLogService?.info(message, tag: 'BLE Pair');
+ },
+ );
+ await Future.delayed(const Duration(milliseconds: 700));
+ try {
+ await attemptConnect();
+ _appDebugLogService?.info(
+ 'Linux immediate retry connect succeeded',
+ tag: 'BLE Connect',
+ );
+ } catch (retryError, retryStackTrace) {
+ Object finalConnectError = retryError;
+ StackTrace finalConnectStackTrace = retryStackTrace;
+ final retryErrorText = retryError.toString().toLowerCase();
+ final isAbortByLocal = retryErrorText.contains(
+ 'le-connection-abort-by-local',
+ );
+ var recoveredOnThirdAttempt = false;
+ if (isAbortByLocal) {
+ _appDebugLogService?.warn(
+ 'Linux immediate retry aborted by local stack; waiting and retrying once more',
+ tag: 'BLE Connect',
+ );
+ await Future.delayed(const Duration(milliseconds: 1200));
+ try {
+ await attemptConnect();
+ _appDebugLogService?.info(
+ 'Linux third-attempt connect succeeded after local abort',
+ tag: 'BLE Connect',
+ );
+ recoveredOnThirdAttempt = true;
+ } catch (thirdError, thirdStackTrace) {
+ finalConnectError = thirdError;
+ finalConnectStackTrace = thirdStackTrace;
+ _appDebugLogService?.error(
+ 'device.connect() third-attempt failure: $thirdError',
+ tag: 'BLE Connect',
+ );
+ }
+ }
+ if (!recoveredOnThirdAttempt) {
+ final recoveredByPairing = await _recoverLinuxConnectFailure(
+ device,
+ attemptConnect: attemptConnect,
+ onRequestPin: linuxPairingPinProvider,
+ );
+ if (recoveredByPairing) {
+ _appDebugLogService?.info(
+ 'Linux connect succeeded after pairing/trust recovery',
+ tag: 'BLE Connect',
+ );
+ } else {
+ _appDebugLogService?.error(
+ 'device.connect() retry failure: $finalConnectError',
+ tag: 'BLE Connect',
+ );
+ Error.throwWithStackTrace(
+ _wrapLinuxConnectStageError(finalConnectError),
+ finalConnectStackTrace,
+ );
+ }
+ }
+ }
+ }
+ } else {
+ try {
+ await device.connect(
+ timeout: connectTimeout,
+ mtu: null,
+ license: License.free,
+ );
+ } catch (error) {
+ _appDebugLogService?.error(
+ 'device.connect() failure: $error',
+ tag: 'BLE Connect',
+ );
+ rethrow;
+ }
+ }
+
+ if (PlatformInfo.isLinux) {
+ await _ensureLinuxBleBond(
+ device,
+ onRequestPin: linuxPairingPinProvider,
+ );
+ }
+
+ // Request larger MTU only where the platform path supports it.
+ if (!PlatformInfo.isWeb && !PlatformInfo.isLinux) {
try {
final mtu = await device.requestMtu(185);
_appDebugLogService?.info('MTU set to: $mtu', tag: 'BLE Connect');
@@ -1217,6 +1613,11 @@ class MeshCoreConnector extends ChangeNotifier {
tag: 'BLE Connect',
);
}
+ } else if (PlatformInfo.isLinux) {
+ _appDebugLogService?.info(
+ 'Skipping MTU request on Linux; flutter_blue_plus only supports requestMtu on Android',
+ tag: 'BLE Connect',
+ );
}
late final List services;
@@ -1330,11 +1731,222 @@ class MeshCoreConnector extends ChangeNotifier {
await _startBleInitialSync();
} catch (e) {
_appDebugLogService?.error('Connection error: $e', tag: 'BLE Connect');
- await disconnect(manual: false);
+ final errorText = e.toString();
+ final lowerErrorText = errorText.toLowerCase();
+ final isLinuxPairingFailure =
+ PlatformInfo.isLinux && isLinuxBlePairingFailureText(errorText);
+ final isLikelyPairingTimeout = isLikelyLinuxBlePairingTimeoutText(
+ errorText,
+ );
+ final isConnectFailure = isLinuxBleConnectFailureText(errorText);
+ final isConnectTimeoutFailure =
+ isConnectFailure && lowerErrorText.contains('timed out');
+ final isLinuxConnectFailure = PlatformInfo.isLinux && isConnectFailure;
+ // Linux pairing failures should not enter auto-reconnect loops; user
+ // needs to retry manually so they can re-enter PIN / resolve pairing.
+ if (isLinuxPairingFailure) {
+ _appDebugLogService?.warn(
+ isLikelyPairingTimeout
+ ? 'Linux pairing timed out: stopping reconnect until user retries manually'
+ : 'Linux pairing failure: stopping reconnect until user retries manually',
+ tag: 'BLE Connect',
+ );
+ await disconnect(manual: true);
+ } else if (isLinuxConnectFailure) {
+ _appDebugLogService?.warn(
+ isConnectTimeoutFailure
+ ? 'Linux connect timeout: issuing BlueZ disconnect before reconnect'
+ : 'Linux connect failure: issuing BlueZ disconnect before reconnect',
+ tag: 'BLE Connect',
+ );
+ final remoteId = _device?.remoteId.str;
+ if (remoteId != null) {
+ await _linuxBlePairingService.disconnectDevice(
+ remoteId,
+ onLog: (message) {
+ _appDebugLogService?.info(message, tag: 'BLE Pair');
+ },
+ );
+ }
+ await disconnect(manual: false, skipBleDeviceDisconnect: true);
+ } else {
+ await disconnect(manual: false);
+ }
rethrow;
}
}
+ Future _recoverLinuxConnectFailure(
+ BluetoothDevice device, {
+ required Future Function() attemptConnect,
+ Future Function()? onRequestPin,
+ }) async {
+ if (!PlatformInfo.isLinux ||
+ !await _linuxBlePairingService.isBluetoothctlAvailable()) {
+ return false;
+ }
+ final remoteId = device.remoteId.str;
+ final pluginBondState = await _getLinuxPluginBondState(device);
+ final trustedByBluez = await _linuxBlePairingService.isPairedAndTrusted(
+ remoteId,
+ );
+ final needsBondRecovery =
+ (pluginBondState != null &&
+ pluginBondState != BmBondStateEnum.bonded) ||
+ !trustedByBluez;
+ if (!needsBondRecovery) {
+ return false;
+ }
+ _appDebugLogService?.warn(
+ pluginBondState == BmBondStateEnum.bonded
+ ? 'Linux connect failed with an untrusted bond; attempting trust/pair recovery'
+ : 'Linux connect failed before bond completed; attempting pairing fallback',
+ tag: 'BLE Connect',
+ );
+ await _ensureLinuxBleBond(device, onRequestPin: onRequestPin);
+ _appDebugLogService?.info(
+ 'Resetting BlueZ connection after Linux pairing/trust recovery',
+ tag: 'BLE Connect',
+ );
+ await _linuxBlePairingService.disconnectDevice(
+ remoteId,
+ onLog: (message) {
+ _appDebugLogService?.info(message, tag: 'BLE Pair');
+ },
+ );
+ await Future.delayed(const Duration(milliseconds: 700));
+ try {
+ await attemptConnect();
+ } catch (error, stackTrace) {
+ Error.throwWithStackTrace(_wrapLinuxConnectStageError(error), stackTrace);
+ }
+ return true;
+ }
+
+ Object _wrapLinuxConnectStageError(Object error) {
+ final errorText = error.toString();
+ if (errorText.toLowerCase().contains(linuxConnectStageFailureMarker)) {
+ return error;
+ }
+ return StateError('Linux connect stage failure: $error');
+ }
+
+ Future _getLinuxPluginBondState(
+ BluetoothDevice device,
+ ) async {
+ try {
+ final response = await FlutterBluePlusPlatform.instance.getBondState(
+ BmBondStateRequest(remoteId: device.remoteId),
+ );
+ return response.bondState;
+ } catch (error) {
+ _appDebugLogService?.warn(
+ 'Linux getBondState unavailable for ${device.remoteId.str}: $error',
+ tag: 'BLE Connect',
+ );
+ return null;
+ }
+ }
+
+ Future _ensureLinuxBleBond(
+ BluetoothDevice device, {
+ Future Function()? onRequestPin,
+ }) async {
+ final remoteId = device.remoteId.str;
+ final bluetoothctlAvailable = await _linuxBlePairingService
+ .isBluetoothctlAvailable();
+ final beforeBondState = await _getLinuxPluginBondState(device);
+ if (!bluetoothctlAvailable) {
+ if (beforeBondState == BmBondStateEnum.bonded) {
+ _appDebugLogService?.warn(
+ 'bluetoothctl unavailable; continuing with plugin bonded state',
+ tag: 'BLE Connect',
+ );
+ } else if (beforeBondState == null) {
+ _appDebugLogService?.warn(
+ 'bluetoothctl unavailable and plugin bond state is unknown; skipping Linux pairing fallback',
+ tag: 'BLE Connect',
+ );
+ } else {
+ _appDebugLogService?.warn(
+ 'bluetoothctl unavailable and device is not bonded; skipping Linux pairing fallback',
+ tag: 'BLE Connect',
+ );
+ }
+ return;
+ }
+
+ final trustedByBluez = await _linuxBlePairingService.isPairedAndTrusted(
+ remoteId,
+ );
+ if (trustedByBluez) {
+ _appDebugLogService?.info(
+ 'Linux BLE device already paired/trusted, skipping pairing flow',
+ tag: 'BLE Connect',
+ );
+ return;
+ }
+
+ if (beforeBondState == BmBondStateEnum.bonded && !trustedByBluez) {
+ _appDebugLogService?.warn(
+ 'Linux BLE device is bonded but not trusted in BlueZ; repairing trust',
+ tag: 'BLE Connect',
+ );
+ final trustRepaired = await _linuxBlePairingService.trustDevice(
+ remoteId,
+ onLog: (message) {
+ _appDebugLogService?.info(message, tag: 'BLE Pair');
+ },
+ );
+ if (trustRepaired) {
+ _appDebugLogService?.info(
+ 'Linux BLE trust repair succeeded without re-pairing',
+ tag: 'BLE Connect',
+ );
+ return;
+ }
+ _appDebugLogService?.warn(
+ 'Linux BLE trust repair did not stick; retrying pairing flow',
+ tag: 'BLE Connect',
+ );
+ }
+
+ _appDebugLogService?.info(
+ beforeBondState == BmBondStateEnum.bonded
+ ? 'Linux BLE device still untrusted after repair; requesting pair'
+ : beforeBondState == null
+ ? 'Linux BLE device bond state unknown; requesting pair'
+ : 'Linux BLE device not bonded, requesting pair',
+ tag: 'BLE Connect',
+ );
+ final paired = await _linuxBlePairingService.pairAndTrust(
+ remoteId: remoteId,
+ onLog: (message) {
+ _appDebugLogService?.info(message, tag: 'BLE Pair');
+ },
+ onRequestPin: onRequestPin,
+ );
+ if (!paired) {
+ throw StateError('Linux pairing fallback failed');
+ }
+
+ final afterBondState = await _getLinuxPluginBondState(device);
+ if (afterBondState != null && afterBondState != BmBondStateEnum.bonded) {
+ throw StateError('Linux BLE pairing did not complete');
+ } else if (afterBondState == null) {
+ _appDebugLogService?.warn(
+ 'Linux plugin bond state unavailable after pairing; relying on BlueZ trust verification',
+ tag: 'BLE Connect',
+ );
+ }
+ final trustedAfter = await _linuxBlePairingService.isPairedAndTrusted(
+ remoteId,
+ );
+ if (!trustedAfter) {
+ throw StateError('Linux BLE trust repair did not complete');
+ }
+ }
+
Future _waitForSelfInfo({required Duration timeout}) async {
if (_selfPublicKey != null) return true;
if (!isConnected) return false;
@@ -1376,6 +1988,7 @@ class MeshCoreConnector extends ChangeNotifier {
await _requestDeviceInfo();
_startBatteryPolling();
+ if (_radioStatsPollRefCount > 0) _startRadioStatsPolling();
final gotSelfInfo = await _waitForSelfInfo(
timeout: const Duration(seconds: 3),
@@ -1403,6 +2016,7 @@ class MeshCoreConnector extends ChangeNotifier {
_pendingInitialContactsSync = false;
_bleInitialSyncStarted = false;
_pendingDeferredChannelSyncAfterContacts = false;
+ _pathHashByteWidth = 1;
}
bool get _shouldAutoReconnect =>
@@ -1456,7 +2070,10 @@ class MeshCoreConnector extends ChangeNotifier {
});
}
- Future disconnect({bool manual = true}) async {
+ Future disconnect({
+ bool manual = true,
+ bool skipBleDeviceDisconnect = false,
+ }) async {
if (_state == MeshCoreConnectionState.disconnecting) return;
final transportAtDisconnect = _activeTransport;
final transportLabel = switch (transportAtDisconnect) {
@@ -1479,6 +2096,7 @@ class MeshCoreConnector extends ChangeNotifier {
}
_setState(MeshCoreConnectionState.disconnecting);
_stopBatteryPolling();
+ _stopRadioStatsPolling();
await _usbFrameSubscription?.cancel();
_usbFrameSubscription = null;
@@ -1499,11 +2117,18 @@ class MeshCoreConnector extends ChangeNotifier {
_channelSyncTimeout = null;
_channelSyncRetries = 0;
- try {
- // Skip queued BLE operations so disconnect doesn't get stuck behind them.
- await _device?.disconnect(queue: false);
- } catch (e) {
- _appDebugLogService?.warn('Disconnect error: $e', tag: 'BLE Connect');
+ if (!skipBleDeviceDisconnect) {
+ try {
+ // Skip queued BLE operations so disconnect doesn't get stuck behind them.
+ await _device?.disconnect(queue: false);
+ } catch (e) {
+ _appDebugLogService?.warn('Disconnect error: $e', tag: 'BLE Connect');
+ }
+ } else {
+ _appDebugLogService?.info(
+ 'Skipping plugin BLE disconnect and continuing cleanup',
+ tag: 'BLE Connect',
+ );
}
_device = null;
@@ -1617,6 +2242,49 @@ class MeshCoreConnector extends ChangeNotifier {
_batteryPollTimer = null;
}
+ void _startRadioStatsPolling() {
+ _radioStatsPollTimer?.cancel();
+ _radioStatsPollTimer = Timer.periodic(const Duration(seconds: 1), (_) {
+ if (!isConnected) {
+ _stopRadioStatsPolling();
+ return;
+ }
+ unawaited(requestRadioStats());
+ });
+ }
+
+ void _stopRadioStatsPolling() {
+ _radioStatsPollTimer?.cancel();
+ _radioStatsPollTimer = null;
+ }
+
+ void acquireRadioStatsPolling() {
+ _radioStatsPollRefCount++;
+ if (_radioStatsPollRefCount == 1 && isConnected) {
+ _startRadioStatsPolling();
+ }
+ }
+
+ void releaseRadioStatsPolling() {
+ _radioStatsPollRefCount = (_radioStatsPollRefCount - 1).clamp(0, 999);
+ if (_radioStatsPollRefCount == 0) {
+ _stopRadioStatsPolling();
+ }
+ }
+
+ Future requestRadioStats() async {
+ if (!isConnected) return;
+ if (!supportsCompanionRadioStats) return;
+ try {
+ await sendFrame(buildGetStatsFrame(statsTypeRadio));
+ } catch (_) {}
+ }
+
+ Future setPathHashMode(int mode) async {
+ if (!isConnected) return;
+ await sendFrame(buildSetPathHashModeFrame(mode.clamp(0, 2)));
+ }
+
Future refreshDeviceInfo() async {
if (!isConnected) return;
if (PlatformInfo.isWeb &&
@@ -1730,47 +2398,43 @@ class MeshCoreConnector extends ChangeNotifier {
Future sendMessage(Contact contact, String text) async {
if (!isConnected || text.isEmpty) return;
- // Handle auto-rotation if enabled
- PathSelection? autoSelection;
- if (_appSettingsService?.settings.autoRouteRotationEnabled == true) {
- autoSelection = _pathHistoryService?.getNextAutoPathSelection(
+ // Check if this is a reaction - apply locally with pending status and route through retry service
+ final reactionInfo = ReactionHelper.parseReaction(text);
+ if (reactionInfo != null) {
+ _conversations.putIfAbsent(contact.publicKeyHex, () => []);
+ final messages = _conversations[contact.publicKeyHex]!;
+
+ // Apply reaction locally with pending status
+ _processOutgoingContactReaction(messages, reactionInfo, contact);
+ _setReactionStatus(
contact.publicKeyHex,
+ reactionInfo,
+ MessageStatus.pending,
);
- if (autoSelection != null) {
- _pathHistoryService?.recordPathAttempt(
- contact.publicKeyHex,
- autoSelection,
- );
- if (!autoSelection.useFlood && autoSelection.pathBytes.isNotEmpty) {
- await setContactPath(
- contact,
- Uint8List.fromList(autoSelection.pathBytes),
- autoSelection.pathBytes.length,
- );
- }
+ _messageStore.saveMessages(contact.publicKeyHex, messages);
+ notifyListeners();
+
+ // Route through retry service (same as normal messages)
+ // Don't use auto-rotation for reactions — just send directly
+ if (_retryService != null) {
+ _retryService!.sendMessageWithRetry(contact: contact, text: text);
+ } else {
+ final outboundText = prepareContactOutboundText(contact, text);
+ await sendFrame(buildSendTextMsgFrame(contact.publicKey, outboundText));
}
+ return;
}
if (_retryService != null) {
- final pathBytes = _resolveOutgoingPathBytes(contact, autoSelection);
- final pathLength = _resolveOutgoingPathLength(contact, autoSelection);
- final selectedContact = _applyAutoSelection(contact, autoSelection);
- await _retryService!.sendMessageWithRetry(
- contact: selectedContact,
- text: text,
- pathSelection: autoSelection,
- pathBytes: pathBytes,
- pathLength: pathLength,
- );
+ await _retryService!.sendMessageWithRetry(contact: contact, text: text);
} else {
// Fallback to old behavior if retry service not initialized
- final pathBytes = _resolveOutgoingPathBytes(contact, autoSelection);
- final pathLength = _resolveOutgoingPathLength(contact, autoSelection);
+ final resolved = resolvePathSelection(contact);
final message = Message.outgoing(
contact.publicKey,
text,
- pathLength: pathLength,
- pathBytes: pathBytes,
+ pathLength: resolved.useFlood ? -1 : resolved.hopCount,
+ pathBytes: Uint8List.fromList(resolved.pathBytes),
);
_addMessage(contact.publicKeyHex, message);
notifyListeners();
@@ -1808,18 +2472,51 @@ class MeshCoreConnector extends ChangeNotifier {
if (_activeTransport == MeshCoreTransportType.usb) {
await Future.delayed(const Duration(milliseconds: 100));
}
+ final idx = _contacts.indexWhere(
+ (c) => c.publicKeyHex == contact.publicKeyHex,
+ );
+ if (idx != -1) {
+ _contacts[idx] = _contacts[idx].copyWith(
+ pathLength: customPath.length,
+ path: customPath,
+ );
+ notifyListeners();
+ }
} finally {
completer.complete();
}
}
- Future setContactFavorite(Contact contact, bool isFavorite) async {
+ Future setContactFlags(
+ Contact contact, {
+ bool? isFavorite,
+ bool? teleBase,
+ bool? teleLoc,
+ bool? teleEnv,
+ }) async {
if (!isConnected) return;
final latestContact =
await _fetchContactSnapshotFromDevice(contact.publicKey) ?? contact;
- final updatedFlags = isFavorite
- ? (latestContact.flags | contactFlagFavorite)
- : (latestContact.flags & ~contactFlagFavorite);
+ int updatedFlags = isFavorite != null
+ ? (isFavorite
+ ? (latestContact.flags | contactFlagFavorite)
+ : (latestContact.flags & ~contactFlagFavorite))
+ : latestContact.flags;
+ updatedFlags = teleBase != null
+ ? (teleBase
+ ? (updatedFlags | contactFlagTeleBase)
+ : (updatedFlags & ~contactFlagTeleBase))
+ : updatedFlags;
+ updatedFlags = teleLoc != null
+ ? (teleLoc
+ ? (updatedFlags | contactFlagTeleLoc)
+ : (updatedFlags & ~contactFlagTeleLoc))
+ : updatedFlags;
+ updatedFlags = teleEnv != null
+ ? (teleEnv
+ ? (updatedFlags | contactFlagTeleEnv)
+ : (updatedFlags & ~contactFlagTeleEnv))
+ : updatedFlags;
await sendFrame(
buildUpdateContactPathFrame(
@@ -1924,6 +2621,9 @@ class MeshCoreConnector extends ChangeNotifier {
await _contactStore.saveContacts(_contacts);
appLogger.info('Saved contacts to storage', tag: 'Connector');
+ // Update any in-flight retries so they use the new path override
+ _retryService?.updatePendingContact(_contacts[index]);
+
// If setting a specific path (not flood, not auto), also sync with device
if (pathLen != null && pathLen >= 0 && pathBytes != null) {
appLogger.info('Sending path to device...', tag: 'Connector');
@@ -1942,27 +2642,27 @@ class MeshCoreConnector extends ChangeNotifier {
final autoRotationEnabled =
_appSettingsService?.settings.autoRouteRotationEnabled == true;
if (autoRotationEnabled && contact.pathOverride == null) {
- autoSelection = _pathHistoryService?.getNextAutoPathSelection(
+ final maxRetries = _appSettingsService?.settings.maxMessageRetries ?? 5;
+ autoSelection = _selectAutoPathForAttempt(
contact.publicKeyHex,
+ attemptIndex: 0,
+ maxRetries: maxRetries,
);
- if (autoSelection != null) {
- _pathHistoryService?.recordPathAttempt(
- contact.publicKeyHex,
- autoSelection,
- );
- }
}
- final pathBytes = _resolveOutgoingPathBytes(contact, autoSelection);
- final pathLength = _resolveOutgoingPathLength(contact, autoSelection) ?? -1;
+ final resolved = resolvePathSelection(contact, selection: autoSelection);
- if (pathLength < 0) {
+ if (resolved.useFlood) {
await clearContactPath(contact);
} else {
- await setContactPath(contact, pathBytes, pathLength);
+ await setContactPath(
+ contact,
+ Uint8List.fromList(resolved.pathBytes),
+ resolved.hopCount,
+ );
}
- return _selectionFromPath(pathLength, pathBytes);
+ return resolved;
}
void trackRepeaterAck({
@@ -1982,9 +2682,7 @@ class MeshCoreConnector extends ChangeNotifier {
outboundText,
selfKey,
);
- final ackHashHex = ackHash
- .map((b) => b.toRadixString(16).padLeft(2, '0'))
- .join();
+ final ackHashHex = ackHashToHex(ackHash);
final messageBytes = utf8.encode(outboundText).length;
_pendingRepeaterAcks[ackHashHex]?.timeout?.cancel();
_pendingRepeaterAcks[ackHashHex] = _RepeaterAckContext(
@@ -2076,6 +2774,7 @@ class MeshCoreConnector extends ChangeNotifier {
// Send the reaction to the device (don't add as a visible message)
final reactionQueueId = _nextReactionSendQueueId();
_pendingChannelSentQueue.add(reactionQueueId);
+ await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, text),
channelSendQueueId: reactionQueueId,
@@ -2100,6 +2799,7 @@ class MeshCoreConnector extends ChangeNotifier {
(isChannelSmazEnabled(channel.index) && !isStructuredPayload)
? Smaz.encodeIfSmaller(text)
: text;
+ await _waitForRadioQuiet(lastInboundRxTime: _lastChannelMsgRxTime);
await sendFrame(
buildSendChannelTextMsgFrame(channel.index, outboundText),
channelSendQueueId: message.messageId,
@@ -2356,6 +3056,31 @@ class MeshCoreConnector extends ChangeNotifier {
await sendCliCommand('set privacy ${enabled ? 'on' : 'off'}');
}
+ Future setTelemetryModeBase(
+ int base,
+ int location,
+ int env,
+ int advert,
+ int multiAcks,
+ ) async {
+ if (!isConnected) return;
+ _telemetryModeBase = base.clamp(teleModeDeny, teleModeAllowAll).toInt();
+ _telemetryModeLoc = location.clamp(teleModeDeny, teleModeAllowAll).toInt();
+ _telemetryModeEnv = env.clamp(teleModeDeny, teleModeAllowAll).toInt();
+ _advertLocPolicy = advert.clamp(0, 1).toInt();
+ _multiAcks = multiAcks.clamp(0, 2).toInt();
+ await sendFrame(
+ buildSetOtherParamsFrame(
+ (_telemetryModeEnv << 4) |
+ (_telemetryModeLoc << 2) |
+ _telemetryModeBase,
+ _advertLocPolicy,
+ _multiAcks,
+ ),
+ );
+ notifyListeners();
+ }
+
Future getChannels({int? maxChannels, bool force = false}) async {
if (!isConnected) return;
if (_isSyncingChannels) {
@@ -2538,7 +3263,7 @@ class MeshCoreConnector extends ChangeNotifier {
_bleDebugLogService?.logFrame(frame, outgoing: false);
final code = frame[0];
- debugPrint('RX frame: code=$code len=${frame.length}');
+ // debugPrint('RX frame: code=$code len=${frame.length}');
switch (code) {
case respCodeOk:
@@ -2626,6 +3351,7 @@ class MeshCoreConnector extends ChangeNotifier {
case pushCodeStatusResponse:
break;
case pushCodeLogRxData:
+ _lastRadioRxTime = DateTime.now();
_handleRxData(frame);
_handleLogRxData(frame);
break;
@@ -2639,6 +3365,9 @@ class MeshCoreConnector extends ChangeNotifier {
case respCodeBattAndStorage:
_handleBatteryAndStorage(frame);
break;
+ case respCodeStats:
+ _handleStatsFrame(frame);
+ break;
case respCodeCustomVars:
_handleCustomVars(frame);
break;
@@ -2711,8 +3440,8 @@ class MeshCoreConnector extends ChangeNotifier {
final reader = BufferReader(frame);
try {
reader.skipBytes(2);
- _currentTxPower = reader.readByte();
- _maxTxPower = reader.readByte();
+ _currentTxPower = reader.readInt8();
+ _maxTxPower = reader.readInt8();
_selfPublicKey = reader.readBytes(pubKeySize);
_selfLatitude = reader.readInt32LE() / 1000000.0;
_selfLongitude = reader.readInt32LE() / 1000000.0;
@@ -2730,7 +3459,7 @@ class MeshCoreConnector extends ChangeNotifier {
_currentSf = reader.readByte();
_currentCr = reader.readByte();
- _selfName = reader.readString();
+ _selfName = reader.readCString();
} catch (e) {
_appDebugLogService?.error(
'Error parsing SELF_INFO frame: $e',
@@ -2806,6 +3535,13 @@ class MeshCoreConnector extends ChangeNotifier {
if (frame.length >= 81) {
_clientRepeat = frame[80] != 0;
}
+ // Path hash mode v10+ (byte 81): width = mode + 1 byte(s) per hop
+ if (frame.length >= 82) {
+ final mode = (frame[81] & 0xFF).clamp(0, 2);
+ _pathHashByteWidth = mode + 1;
+ } else {
+ _pathHashByteWidth = 1;
+ }
// Firmware reports MAX_CONTACTS / 2 for v3+ device info.
final reportedContacts = frame[2];
@@ -2865,20 +3601,42 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_requestNextQueuedMessage());
}
+ void _handleStatsFrame(Uint8List frame) {
+ final stats = CompanionRadioStats.tryParse(frame);
+ if (stats == null) return;
+ final total = stats.txAirSecs + stats.rxAirSecs;
+ if (total > _prevTotalAirSecs) {
+ (_airtimeBumpStopwatch ??= Stopwatch()).reset();
+ _airtimeBumpStopwatch!.start();
+ }
+ _prevTotalAirSecs = total;
+ _latestRadioStats = stats;
+ radioStatsNotifier.value = stats;
+ }
+
void _handleBatteryAndStorage(Uint8List frame) {
// Frame format from C++:
// [0] = RESP_CODE_BATT_AND_STORAGE
// [1-2] = battery_mv (uint16 LE)
// [3-6] = storage_used_kb (uint32 LE)
// [7-10] = storage_total_kb (uint32 LE)
- if (frame.length >= 3) {
- _batteryMillivolts = readUint16LE(frame, 1);
+ try {
+ final reader = BufferReader(frame);
+ reader.skipBytes(1);
+ _batteryMillivolts = reader.readUInt16LE();
+ _storageUsedKb = reader.readUInt32LE();
+ _storageTotalKb = reader.readUInt32LE();
final volts = (_batteryMillivolts! / 1000.0).toStringAsFixed(2);
_appDebugLogService?.info(
'Pulled battery: $volts V ($_batteryMillivolts mV)',
tag: 'Battery',
);
notifyListeners();
+ } catch (e) {
+ _appDebugLogService?.error(
+ 'Error parsing battery and storage frame: $e',
+ tag: 'Connector',
+ );
}
}
@@ -2929,16 +3687,17 @@ class MeshCoreConnector extends ChangeNotifier {
/// Physics-based worst-case timeout (ceiling).
int _physicsMaxTimeout(int pathLength, int airtime) {
if (pathLength < 0) {
+ // Match firmware: SEND_TIMEOUT_BASE_MILLIS + (FLOOD_SEND_TIMEOUT_FACTOR * airtime)
return 500 + (16 * airtime);
} else {
return 500 + ((airtime * 6 + 250) * (pathLength + 1));
}
}
- /// Physics-based minimum timeout (floor): raw traversal time.
int _physicsMinTimeout(int pathLength, int airtime) {
if (pathLength < 0) {
- return airtime;
+ // Same as max for flood — firmware uses a single formula
+ return 500 + (16 * airtime);
} else {
return airtime * (pathLength + 1);
}
@@ -2955,7 +3714,7 @@ class MeshCoreConnector extends ChangeNotifier {
final physicsMin = _physicsMinTimeout(pathLength, airtime);
final physicsMax = _physicsMaxTimeout(pathLength, airtime);
- // Try ML-based prediction, clamped between physics bounds
+ // Try ML-based prediction
final secSinceRx = DateTime.now().difference(_lastRxTime).inSeconds;
final mlTimeout = _timeoutPredictionService?.predictTimeout(
contactKey: contactKey,
@@ -2964,9 +3723,14 @@ class MeshCoreConnector extends ChangeNotifier {
secondsSinceLastRx: secSinceRx,
);
if (mlTimeout != null) {
+ if (pathLength < 0) {
+ // Flood: trust ML, only enforce firmware formula as floor
+ return mlTimeout.clamp(physicsMin, mlTimeout);
+ }
return mlTimeout.clamp(physicsMin, physicsMax);
}
+ // No ML data — use firmware formula
return physicsMax;
}
@@ -3218,9 +3982,10 @@ class MeshCoreConnector extends ChangeNotifier {
}
bool _pathMatchesContact(Uint8List pathBytes, Uint8List publicKey) {
- if (pathBytes.isEmpty || publicKey.length < pathHashSize) return false;
- for (int i = 0; i + pathHashSize <= pathBytes.length; i += pathHashSize) {
- final prefix = pathBytes.sublist(i, i + pathHashSize);
+ final w = _pathHashByteWidth;
+ if (pathBytes.isEmpty || publicKey.length < w) return false;
+ for (int i = 0; i + w <= pathBytes.length; i += w) {
+ final prefix = pathBytes.sublist(i, i + w);
if (_matchesPrefix(publicKey, prefix)) {
return true;
}
@@ -3255,6 +4020,9 @@ class MeshCoreConnector extends ChangeNotifier {
}
if (message != null) {
+ if (!message.isOutgoing) {
+ _lastContactMsgRxTime = DateTime.now();
+ }
// Ignore messages from self (device hearing its own broadcast)
// BUT allow repeated messages (pathLength indicates it went through repeater)
if (_selfPublicKey != null &&
@@ -3302,7 +4070,6 @@ class MeshCoreConnector extends ChangeNotifier {
_appSettingsService != null) {
final settings = _appSettingsService!.settings;
if (settings.notificationsEnabled && settings.notifyOnNewMessage) {
- // Find the contact name
if (contact?.type == advTypeChat) {
_notificationService.showMessageNotification(
contactName: contact?.name ?? 'Unknown',
@@ -3313,7 +4080,9 @@ class MeshCoreConnector extends ChangeNotifier {
} else if (contact?.type == advTypeRoom) {
_notificationService.showMessageNotification(
contactName: contact?.name ?? 'Unknown Room',
- message: message.text.substring(4),
+ message: message.text.length > 4
+ ? message.text.substring(4)
+ : message.text,
contactId: message.senderKeyHex,
badgeCount: getTotalUnreadCount(),
);
@@ -3364,7 +4133,7 @@ class MeshCoreConnector extends ChangeNotifier {
reader.skipBytes(4); // Skip extra 4 bytes for signed/plain variants
}
- final msgText = reader.readString();
+ final msgText = reader.readCString();
final flags = txtType;
final shiftedType = flags >> 2;
@@ -3488,6 +4257,7 @@ class MeshCoreConnector extends ChangeNotifier {
_notificationService.showChannelMessageNotification(
channelName: label,
+ senderName: message.senderName,
message: message.text,
channelIndex: channelIndex,
badgeCount: getTotalUnreadCount(),
@@ -3495,14 +4265,18 @@ class MeshCoreConnector extends ChangeNotifier {
}
void _handleIncomingChannelMessage(Uint8List frame) {
- final message = ChannelMessage.fromFrame(frame);
- if (message != null && message.channelIndex != null) {
- if (_shouldDropSelfChannelMessage(
- message.senderName,
- message.pathBytes,
- )) {
+ final parsed = ChannelMessage.fromFrame(frame);
+ if (parsed != null && parsed.channelIndex != null) {
+ if (_shouldDropSelfChannelMessage(parsed.senderName, parsed.pathBytes)) {
return;
}
+ _lastChannelMsgRxTime = DateTime.now();
+ final contentHash = _computeContentHash(
+ parsed.channelIndex!,
+ parsed.timestamp.millisecondsSinceEpoch ~/ 1000,
+ '${parsed.senderName}: ${parsed.text}',
+ );
+ final message = parsed.copyWith(packetHash: contentHash);
_updateContactLastMessageAtByName(
message.senderName,
message.timestamp,
@@ -3522,65 +4296,87 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleLogRxData(Uint8List frame) {
if (frame.length < 4) return;
- final raw = Uint8List.fromList(frame.sublist(3));
- final packet = _parseRawPacket(raw);
- if (packet == null || packet.payloadType != _payloadTypeGroupText) return;
+ try {
+ final reader = BufferReader(frame);
+ reader.skipBytes(3); // Skip header
- final payload = packet.payload;
- if (payload.length <= _cipherMacSize) return;
- final channelHash = payload[0];
- final encrypted = Uint8List.fromList(payload.sublist(1));
+ final raw = reader.readRemainingBytes();
+ final packet = _parseRawPacket(raw);
+ if (packet == null || packet.payloadType != _payloadTypeGroupText) return;
- // Use cached channels as fallback if live channels not yet loaded
- final channelsToSearch = _channels.isNotEmpty ? _channels : _cachedChannels;
- for (final channel in channelsToSearch) {
- if (channel.isEmpty) continue;
- final hash = _computeChannelHash(channel.psk);
- if (hash != channelHash) continue;
+ final payload = BufferReader(packet.payload);
+ final channelHash = payload.readByte();
+ final encrypted = Uint8List.fromList(payload.readRemainingBytes());
- final decrypted = _decryptPayload(channel.psk, encrypted);
- if (decrypted == null || decrypted.length < 6) return;
+ // Use cached channels as fallback if live channels not yet loaded
+ final channelsToSearch = _channels.isNotEmpty
+ ? _channels
+ : _cachedChannels;
+ for (final channel in channelsToSearch) {
+ if (channel.isEmpty) continue;
+ final hash = _computeChannelHash(channel.psk);
+ if (hash != channelHash) continue;
+ try {
+ final decryptedBytes = _decryptPayload(channel.psk, encrypted);
+ if (decryptedBytes == null || decryptedBytes.length < 6) return;
+ final decrypted = BufferReader(decryptedBytes);
- final txtType = decrypted[4];
- if ((txtType >> 2) != 0) {
- return;
+ final timestampRaw = decrypted.readUInt32LE();
+ final txtType = decrypted.readByte();
+ if ((txtType >> 2) != 0) {
+ return;
+ }
+
+ final text = decrypted.readCString();
+ final parsed = _splitSenderText(text);
+ final decodedText =
+ Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text;
+ if (_shouldDropSelfChannelMessage(
+ parsed.senderName,
+ packet.pathBytes,
+ )) {
+ return;
+ }
+
+ final pktHash = _computePacketHash(
+ packet.payloadType,
+ packet.payload,
+ );
+
+ final message = ChannelMessage(
+ senderKey: null,
+ senderName: parsed.senderName,
+ text: decodedText,
+ timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
+ isOutgoing: false,
+ status: ChannelMessageStatus.sent,
+ pathLength: packet.isFlood ? packet.hopCount : 0,
+ pathBytes: packet.pathBytes,
+ channelIndex: channel.index,
+ packetHash: pktHash,
+ );
+
+ _updateContactLastMessageAtByName(
+ parsed.senderName,
+ message.timestamp,
+ pathBytes: message.pathBytes,
+ );
+ final isNew = _addChannelMessage(channel.index, message);
+ _maybeIncrementChannelUnread(message, isNew: isNew);
+ notifyListeners();
+ if (isNew) {
+ final label = channel.name.isEmpty
+ ? 'Channel ${channel.index}'
+ : channel.name;
+ _maybeNotifyChannelMessage(message, channelName: label);
+ }
+ return;
+ } catch (e) {
+ appLogger.warn('Decryption failed for channel ${channel.index}: $e');
+ }
}
-
- final timestampRaw = readUint32LE(decrypted, 0);
- final text = readCString(decrypted, 5, decrypted.length - 5);
- final parsed = _splitSenderText(text);
- final decodedText = Smaz.tryDecodePrefixed(parsed.text) ?? parsed.text;
- if (_shouldDropSelfChannelMessage(parsed.senderName, packet.pathBytes)) {
- return;
- }
-
- final message = ChannelMessage(
- senderKey: null,
- senderName: parsed.senderName,
- text: decodedText,
- timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
- isOutgoing: false,
- status: ChannelMessageStatus.sent,
- pathLength: packet.isFlood ? packet.pathBytes.length : 0,
- pathBytes: packet.pathBytes,
- channelIndex: channel.index,
- );
-
- _updateContactLastMessageAtByName(
- parsed.senderName,
- message.timestamp,
- pathBytes: message.pathBytes,
- );
- final isNew = _addChannelMessage(channel.index, message);
- _maybeIncrementChannelUnread(message, isNew: isNew);
- notifyListeners();
- if (isNew) {
- final label = channel.name.isEmpty
- ? 'Channel ${channel.index}'
- : channel.name;
- _maybeNotifyChannelMessage(message, channelName: label);
- }
- return;
+ } catch (e) {
+ appLogger.warn('Error handling log RX data frame: $e');
}
}
@@ -3591,15 +4387,15 @@ class MeshCoreConnector extends ChangeNotifier {
// [2-5] = expected_ack_hash (uint32)
// [6-9] = estimated_timeout_ms (uint32)
- if (frame.length >= 10) {
- final ackHash = Uint8List.fromList(frame.sublist(2, 6));
- final timeoutMs = readUint32LE(frame, 6);
+ try {
+ final reader = BufferReader(frame);
+ reader.skipBytes(2); //Skip code and is_flood
+ final ackHash = reader.readUInt32LE();
+ final timeoutMs = reader.readUInt32LE();
// Check if this is a CLI command ACK - if so, ignore it
if (_lastSentWasCliCommand) {
- final ackHashHex = ackHash
- .map((b) => b.toRadixString(16).padLeft(2, '0'))
- .join();
+ final ackHashHex = ackHashToHex(ackHash);
debugPrint('Ignoring CLI command ACK (sent): $ackHashHex');
_lastSentWasCliCommand = false;
return;
@@ -3611,22 +4407,15 @@ class MeshCoreConnector extends ChangeNotifier {
final retryService = _retryService;
if (retryService != null &&
- retryService.updateMessageFromSent(
- ackHash,
- timeoutMs,
- allowQueueFallback: false,
- )) {
+ retryService.updateMessageFromSent(ackHash, timeoutMs)) {
return;
}
if (_markNextPendingChannelMessageSent()) {
return;
}
-
- if (retryService != null) {
- retryService.updateMessageFromSent(ackHash, timeoutMs);
- }
- } else {
+ } catch (e) {
+ appLogger.warn('Error handling message sent frame: $e');
// Fallback to old behavior
for (var messages in _conversations.values) {
for (int i = messages.length - 1; i >= 0; i--) {
@@ -3705,9 +4494,11 @@ class MeshCoreConnector extends ChangeNotifier {
// [1-4] = ack_hash (uint32)
// [5-8] = trip_time_ms (uint32)
- if (frame.length >= 9) {
- final ackHash = Uint8List.fromList(frame.sublist(1, 5));
- final tripTimeMs = readUint32LE(frame, 5);
+ try {
+ final reader = BufferReader(frame);
+ reader.skipBytes(1); // Skip code
+ final ackHash = reader.readUInt32LE();
+ final tripTimeMs = reader.readUInt32LE();
// CLI command ACKs are already filtered in _handleMessageSent, so this should only see real messages
@@ -3719,7 +4510,8 @@ class MeshCoreConnector extends ChangeNotifier {
if (_retryService != null) {
_retryService!.handleAckReceived(ackHash, tripTimeMs);
}
- } else {
+ } catch (e) {
+ appLogger.warn('Error handling send confirmed frame: $e');
// Fallback to old behavior
for (var messages in _conversations.values) {
for (int i = messages.length - 1; i >= 0; i--) {
@@ -3734,10 +4526,8 @@ class MeshCoreConnector extends ChangeNotifier {
}
}
- bool _handleRepeaterCommandSent(Uint8List ackHash, int timeoutMs) {
- final ackHashHex = ackHash
- .map((b) => b.toRadixString(16).padLeft(2, '0'))
- .join();
+ bool _handleRepeaterCommandSent(int ackHash, int timeoutMs) {
+ final ackHashHex = ackHashToHex(ackHash);
final entry = _pendingRepeaterAcks[ackHashHex];
if (entry == null) return false;
@@ -3755,10 +4545,8 @@ class MeshCoreConnector extends ChangeNotifier {
return true;
}
- bool _handleRepeaterCommandAck(Uint8List ackHash, int tripTimeMs) {
- final ackHashHex = ackHash
- .map((b) => b.toRadixString(16).padLeft(2, '0'))
- .join();
+ bool _handleRepeaterCommandAck(int ackHash, int tripTimeMs) {
+ final ackHashHex = ackHashToHex(ackHash);
final entry = _pendingRepeaterAcks.remove(ackHashHex);
if (entry == null) return false;
entry.timeout?.cancel();
@@ -4016,82 +4804,128 @@ class MeshCoreConnector extends ChangeNotifier {
ReactionInfo reactionInfo,
String contactPubKeyHex,
) {
- // Find target message by computing hash and comparing
- final targetHash = reactionInfo.targetHash;
final contact = _contacts.cast().firstWhere(
(c) => c?.publicKeyHex == contactPubKeyHex,
orElse: () => null,
);
final isRoomServer = contact?.type == advTypeRoom;
+ ReactionHelper.applyReaction(
+ messages: messages,
+ reactionInfo: reactionInfo,
+ // Incoming reactions in 1:1: match against outgoing messages only
+ shouldSkip: (msg) => isRoomServer != true && !msg.isOutgoing,
+ getTimestampSecs: (msg) => msg.timestamp.millisecondsSinceEpoch ~/ 1000,
+ getSenderName: (msg) =>
+ _resolveContactSenderName(msg, contact, isRoomServer == true),
+ getMessageText: (msg) => msg.text,
+ getReactions: (msg) => msg.reactions,
+ updateMessage: (i, reactions) {
+ messages[i] = messages[i].copyWith(reactions: reactions);
+ },
+ );
+ }
+
+ void _processOutgoingContactReaction(
+ List messages,
+ ReactionInfo reactionInfo,
+ Contact contact,
+ ) {
+ final isRoomServer = contact.type == advTypeRoom;
+
+ ReactionHelper.applyReaction(
+ messages: messages,
+ reactionInfo: reactionInfo,
+ // Outgoing reactions in 1:1: match against incoming messages
+ shouldSkip: (msg) => !isRoomServer && msg.isOutgoing,
+ getTimestampSecs: (msg) => msg.timestamp.millisecondsSinceEpoch ~/ 1000,
+ getSenderName: (msg) =>
+ _resolveContactSenderName(msg, contact, isRoomServer),
+ getMessageText: (msg) => msg.text,
+ getReactions: (msg) => msg.reactions,
+ updateMessage: (i, reactions) {
+ messages[i] = messages[i].copyWith(reactions: reactions);
+ },
+ );
+ }
+
+ void _setReactionStatus(
+ String pubKeyHex,
+ ReactionInfo reactionInfo,
+ MessageStatus status,
+ ) {
+ final messages = _conversations[pubKeyHex];
+ if (messages == null) return;
+ final contact = _contacts.cast().firstWhere(
+ (c) => c?.publicKeyHex == pubKeyHex,
+ orElse: () => null,
+ );
+ final isRoomServer = contact?.type == advTypeRoom;
for (int i = messages.length - 1; i >= 0; i--) {
final msg = messages[i];
-
- // For 1:1 chats: contact reacts to my outgoing messages only
- // For room servers: any message can be reacted to (multi-user)
- if (!isRoomServer && !msg.isOutgoing) continue;
-
final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
-
- // For room servers, include sender name (resolve from fourByteRoomContactKey)
- // For 1:1 chats, sender is implicit (null)
- String? senderName;
- if (isRoomServer && !msg.isOutgoing) {
- final senderContact = _contacts.cast().firstWhere(
- (c) =>
- c != null &&
- _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
- orElse: () => null,
- );
- senderName = senderContact?.name;
- } else if (isRoomServer && msg.isOutgoing) {
- senderName = selfName;
- }
- // For 1:1, senderName stays null
-
final msgHash = ReactionHelper.computeReactionHash(
timestampSecs,
- senderName,
+ _resolveContactSenderName(msg, contact, isRoomServer == true),
msg.text,
);
- if (msgHash == targetHash) {
- final currentReactions = Map.from(msg.reactions);
- currentReactions[reactionInfo.emoji] =
- (currentReactions[reactionInfo.emoji] ?? 0) + 1;
-
- messages[i] = msg.copyWith(reactions: currentReactions);
+ if (msgHash == reactionInfo.targetHash) {
+ final statuses = Map.from(msg.reactionStatuses);
+ statuses[reactionInfo.emoji] = status;
+ messages[i] = msg.copyWith(reactionStatuses: statuses);
break;
}
}
}
- _RawPacket? _parseRawPacket(Uint8List raw) {
- if (raw.length < 3) return null;
- var index = 0;
- final header = raw[index++];
- final routeType = header & _phRouteMask;
- final hasTransport =
- routeType == _routeTransportFlood || routeType == _routeTransportDirect;
- if (hasTransport) {
- if (raw.length < index + 4) return null;
- index += 4;
+ String? _resolveContactSenderName(
+ Message msg,
+ Contact? contact,
+ bool isRoomServer,
+ ) {
+ if (!isRoomServer) return null;
+ if (!msg.isOutgoing) {
+ final senderContact = _contacts.cast().firstWhere(
+ (c) =>
+ c != null &&
+ _matchesPrefix(c.publicKey, msg.fourByteRoomContactKey),
+ orElse: () => null,
+ );
+ return senderContact?.name;
}
- if (raw.length <= index) return null;
- final pathLen = raw[index++];
- if (raw.length < index + pathLen) return null;
- final pathBytes = Uint8List.fromList(raw.sublist(index, index + pathLen));
- index += pathLen;
- if (raw.length <= index) return null;
- final payload = Uint8List.fromList(raw.sublist(index));
+ return selfName;
+ }
- return _RawPacket(
- header: header,
- routeType: routeType,
- payloadType: (header >> _phTypeShift) & _phTypeMask,
- payloadVer: (header >> _phVerShift) & _phVerMask,
- pathBytes: pathBytes,
- payload: payload,
- );
+ _RawPacket? _parseRawPacket(Uint8List raw) {
+ try {
+ final reader = BufferReader(raw);
+ final header = reader.readByte();
+ final routeType = header & _phRouteMask;
+ final hasTransport =
+ routeType == _routeTransportFlood ||
+ routeType == _routeTransportDirect;
+ if (hasTransport) {
+ // Skip reserved bytes in transport header made up of two u16 fields
+ reader.skipBytes(4);
+ }
+ final pathLenRaw = reader.readByte();
+ final pathByteLen = _decodePathByteLen(pathLenRaw);
+ final pathBytes = reader.readBytes(pathByteLen);
+ final payload = reader.readBytes(reader.remaining);
+
+ return _RawPacket(
+ header: header,
+ routeType: routeType,
+ payloadType: (header >> _phTypeShift) & _phTypeMask,
+ payloadVer: (header >> _phVerShift) & _phVerMask,
+ pathLenRaw: pathLenRaw,
+ pathBytes: pathBytes,
+ payload: payload,
+ );
+ } catch (e) {
+ appLogger.warn('Error parsing raw packet: $e');
+ return null;
+ }
}
int _computeChannelHash(Uint8List psk) {
@@ -4099,6 +4933,37 @@ class MeshCoreConnector extends ChangeNotifier {
return digest[0];
}
+ /// Firmware-compatible packet hash: SHA256(payloadType + payload) -> first 8 bytes as hex.
+ String _computePacketHash(int payloadType, Uint8List payload) {
+ final input = Uint8List(1 + payload.length);
+ input[0] = payloadType;
+ input.setRange(1, input.length, payload);
+ final digest = crypto.sha256.convert(input).bytes;
+ return digest
+ .sublist(0, 8)
+ .map((b) => b.toRadixString(16).padLeft(2, '0'))
+ .join();
+ }
+
+ /// Content-based dedup hash for sync queue messages (no raw payload available).
+ /// Prefixed with 'c:' to avoid collisions with packet hashes.
+ String _computeContentHash(
+ int channelIdx,
+ int timestampSecs,
+ String fullText,
+ ) {
+ final textBytes = utf8.encode(fullText);
+ final input = Uint8List(5 + textBytes.length);
+ input[0] = channelIdx;
+ input[1] = timestampSecs & 0xFF;
+ input[2] = (timestampSecs >> 8) & 0xFF;
+ input[3] = (timestampSecs >> 16) & 0xFF;
+ input[4] = (timestampSecs >> 24) & 0xFF;
+ input.setRange(5, 5 + textBytes.length, textBytes);
+ final digest = crypto.sha256.convert(input).bytes;
+ return 'c:${digest.sublist(0, 8).map((b) => b.toRadixString(16).padLeft(2, '0')).join()}';
+ }
+
Uint8List? _decryptPayload(Uint8List psk, Uint8List encrypted) {
if (encrypted.length <= _cipherMacSize) return null;
final mac = encrypted.sublist(0, _cipherMacSize);
@@ -4146,63 +5011,6 @@ class MeshCoreConnector extends ChangeNotifier {
return _ParsedText(senderName: 'Unknown', text: text);
}
- Uint8List _resolveOutgoingPathBytes(
- Contact contact,
- PathSelection? selection,
- ) {
- // Priority 1: Check user's path override
- if (contact.pathOverride != null) {
- if (contact.pathOverride! < 0) {
- return Uint8List(0); // Force flood
- }
- return contact.pathOverrideBytes ?? Uint8List(0);
- }
-
- // Priority 2: Check device flood mode or PathSelection flood
- if (contact.pathLength < 0 || selection?.useFlood == true) {
- return Uint8List(0);
- }
-
- // Priority 3: Check PathSelection (auto-rotation)
- if (selection != null && selection.pathBytes.isNotEmpty) {
- return Uint8List.fromList(selection.pathBytes);
- }
-
- // Priority 4: Use device's discovered path
- return contact.path;
- }
-
- int? _resolveOutgoingPathLength(Contact contact, PathSelection? selection) {
- // Priority 1: Check user's path override
- if (contact.pathOverride != null) {
- return contact.pathOverride;
- }
-
- // Priority 2: Check device flood mode or PathSelection flood
- if (contact.pathLength < 0 || selection?.useFlood == true) {
- return -1;
- }
-
- // Priority 3: Check PathSelection (auto-rotation)
- if (selection != null && selection.pathBytes.isNotEmpty) {
- return selection.hopCount;
- }
-
- // Priority 4: Use device's discovered path
- return contact.pathLength;
- }
-
- PathSelection _selectionFromPath(int pathLength, Uint8List pathBytes) {
- if (pathLength < 0) {
- return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
- }
- return PathSelection(
- pathBytes: pathBytes,
- hopCount: pathLength,
- useFlood: false,
- );
- }
-
bool _addChannelMessage(int channelIndex, ChannelMessage message) {
_channelMessages.putIfAbsent(channelIndex, () => []);
final messages = _channelMessages[channelIndex]!;
@@ -4292,6 +5100,7 @@ class MeshCoreConnector extends ChangeNotifier {
pathLength: mergedPathLength,
pathBytes: mergedPathBytes,
pathVariants: mergedPathVariants,
+ packetHash: existing.packetHash ?? processedMessage.packetHash,
// Mark as sent when first repeat is heard
status: promotedFromPending
? ChannelMessageStatus.sent
@@ -4326,35 +5135,38 @@ class MeshCoreConnector extends ChangeNotifier {
List messages,
ReactionInfo reactionInfo,
) {
- // Find target message by computing hash and comparing
- final targetHash = reactionInfo.targetHash;
- for (int i = messages.length - 1; i >= 0; i--) {
- final msg = messages[i];
- final timestampSecs = msg.timestamp.millisecondsSinceEpoch ~/ 1000;
- final msgHash = ReactionHelper.computeReactionHash(
- timestampSecs,
- msg.senderName,
- msg.text,
- );
- if (msgHash == targetHash) {
- final currentReactions = Map.from(msg.reactions);
- currentReactions[reactionInfo.emoji] =
- (currentReactions[reactionInfo.emoji] ?? 0) + 1;
-
- messages[i] = msg.copyWith(reactions: currentReactions);
+ ReactionHelper.applyReaction(
+ messages: messages,
+ reactionInfo: reactionInfo,
+ shouldSkip: (_) => false,
+ getTimestampSecs: (msg) => msg.timestamp.millisecondsSinceEpoch ~/ 1000,
+ getSenderName: (msg) => msg.senderName,
+ getMessageText: (msg) => msg.text,
+ getReactions: (msg) => msg.reactions,
+ updateMessage: (i, reactions) {
+ messages[i] = messages[i].copyWith(reactions: reactions);
notifyListeners();
- break;
- }
- }
+ },
+ );
}
int _findChannelRepeatIndex(
List messages,
ChannelMessage incoming,
) {
+ // First pass: match by packet hash (exact dedup)
+ final incomingHash = incoming.packetHash;
+ if (incomingHash != null) {
+ for (int i = messages.length - 1; i >= 0; i--) {
+ final existingHash = messages[i].packetHash;
+ if (existingHash != null && existingHash == incomingHash) {
+ return i;
+ }
+ }
+ }
+ // Second pass: heuristic fallback (outgoing echo, old messages without hash)
for (int i = messages.length - 1; i >= 0; i--) {
- final existing = messages[i];
- if (_isChannelRepeat(existing, incoming)) {
+ if (_isChannelRepeat(messages[i], incoming)) {
return i;
}
}
@@ -4368,7 +5180,7 @@ class MeshCoreConnector extends ChangeNotifier {
(existing.timestamp.millisecondsSinceEpoch -
incoming.timestamp.millisecondsSinceEpoch)
.abs();
- if (diffMs > 5000) return false;
+ if (diffMs > 30000) return false;
if (existing.senderName == incoming.senderName) return true;
@@ -4450,6 +5262,12 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleDisconnection() {
_stopBatteryPolling();
+ _stopRadioStatsPolling();
+ _latestRadioStats = null;
+ radioStatsNotifier.value = null;
+ _prevTotalAirSecs = 0;
+ _airtimeBumpStopwatch?.stop();
+ _airtimeBumpStopwatch = null;
for (final entry in _pendingRepeaterAcks.values) {
entry.timeout?.cancel();
@@ -4532,7 +5350,7 @@ class MeshCoreConnector extends ChangeNotifier {
void _handleCustomVars(Uint8List frame) {
final buf = BufferReader(frame.sublist(1));
try {
- _currentCustomVars = _parseKeyValueString(buf.readString());
+ _currentCustomVars = _parseKeyValueString(buf.readCString());
} catch (e) {
appLogger.warn('Malformed custom vars frame: $e', tag: 'Connector');
}
@@ -4588,6 +5406,8 @@ class MeshCoreConnector extends ChangeNotifier {
_notifyListenersTimer?.cancel();
_reconnectTimer?.cancel();
_batteryPollTimer?.cancel();
+ _radioStatsPollTimer?.cancel();
+ radioStatsNotifier.dispose();
_receivedFramesController.close();
_usbManager.dispose();
_tcpConnector.dispose();
@@ -4613,8 +5433,9 @@ class MeshCoreConnector extends ChangeNotifier {
packet.skipBytes(4); // Skip transport-specific bytes
}
//final payloadVer = (header >> 6) & 0x03;
- final pathLen = packet.readByte();
- final pathBytes = packet.readBytes(pathLen);
+ final pathLenRaw = packet.readByte();
+ final pathByteLen = _decodePathByteLen(pathLenRaw);
+ final pathBytes = packet.readBytes(pathByteLen);
final payload = packet.readBytes(packet.remaining);
final rawPacket = frame.sublist(3);
@@ -4652,8 +5473,9 @@ class MeshCoreConnector extends ChangeNotifier {
packet.skipBytes(4); // Skip transport-specific bytes
}
//final payloadVer = (header >> 6) & 0x03;
- final pathLen = packet.readByte();
- pathBytes = packet.readBytes(pathLen);
+ final pathLenRaw = packet.readByte();
+ final pathByteLen = _decodePathByteLen(pathLenRaw);
+ pathBytes = packet.readBytes(pathByteLen);
} catch (e) {
appLogger.warn('Malformed RX frame: $e', tag: 'Connector');
return;
@@ -4687,7 +5509,7 @@ class MeshCoreConnector extends ChangeNotifier {
longitude = packet.readInt32LE() / 1e6;
}
if (hasName && packet.remaining > 0) {
- name = packet.readString();
+ name = packet.readCString();
}
} catch (e) {
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
@@ -4711,6 +5533,17 @@ class MeshCoreConnector extends ChangeNotifier {
);
}
+ bool hasValidLocation(double? latitude, double? longitude) {
+ const double epsilon = 1e-6;
+ final lat = latitude ?? 0.0;
+ final lon = longitude ?? 0.0;
+ return (lat.abs() > epsilon || lon.abs() > epsilon) &&
+ lat >= -90.0 &&
+ lat <= 90.0 &&
+ lon >= -180.0 &&
+ lon <= 180.0;
+ }
+
void _handlePayloadAdvertReceived(
Uint8List rawPacket,
Uint8List payload,
@@ -4748,8 +5581,11 @@ class MeshCoreConnector extends ChangeNotifier {
latitude = advert.readInt32LE() / 1e6;
longitude = advert.readInt32LE() / 1e6;
}
+ // Validate location values if present
+ hasLocation = hasValidLocation(latitude, longitude);
+
if (hasName && advert.remaining > 0) {
- name = advert.readString();
+ name = advert.readCString();
}
} catch (e) {
appLogger.warn('Malformed advert frame: $e', tag: 'Connector');
@@ -4813,20 +5649,8 @@ class MeshCoreConnector extends ChangeNotifier {
// CRITICAL: Preserve user's path override when contact is refreshed from device
_contacts[existingIndex] = existing.copyWith(
- latitude:
- hasLocation &&
- latitude != null &&
- latitude.abs() <= 90 &&
- (latitude != 0 || longitude != 0)
- ? latitude
- : existing.latitude,
- longitude:
- hasLocation &&
- longitude != null &&
- longitude.abs() <= 180 &&
- (latitude != 0 || longitude != 0)
- ? longitude
- : existing.longitude,
+ latitude: hasLocation ? latitude : existing.latitude,
+ longitude: hasLocation ? longitude : existing.longitude,
name: hasName ? name : existing.name,
path: Uint8List.fromList(path.reversed.toList()),
pathLength: path.length,
@@ -4975,6 +5799,29 @@ class MeshCoreConnector extends ChangeNotifier {
unawaited(_persistDiscoveredContacts());
notifyListeners();
}
+
+ void clearMessagesForContact(Contact contact) {
+ final contactKeyHex = contact.publicKeyHex;
+ final messages = _conversations[contactKeyHex];
+ if (messages == null) return;
+ messages.clear();
+ unawaited(_messageStore.saveMessages(contactKeyHex, messages));
+ markContactRead(contactKeyHex);
+ notifyListeners();
+ }
+
+ void clearMessagesForChannel(int channelIndex) {
+ final messages = _channelMessages[channelIndex];
+ if (messages == null) return;
+ messages.clear();
+ unawaited(_channelMessageStore.saveChannelMessages(channelIndex, messages));
+ markChannelRead(channelIndex);
+ notifyListeners();
+ }
+
+ void deleteAllPaths() {
+ _pathHistoryService?.clearAllHistories();
+ }
}
const int _phRouteMask = 0x03;
@@ -4990,11 +5837,20 @@ const int _routeTransportDirect = 0x03;
const int _payloadTypeGroupText = 0x05;
const int _cipherMacSize = 2;
+/// Decodes the firmware's encoded path_len byte into actual byte length.
+/// Bits 0-5: hash count (0-63), Bits 6-7: hash size code (0=1byte, 1=2bytes, 2=3bytes).
+int _decodePathByteLen(int pathLenRaw) {
+ final hashCount = pathLenRaw & 63;
+ final hashSize = ((pathLenRaw >> 6) & 0x03) + 1;
+ return hashCount * hashSize;
+}
+
class _RawPacket {
final int header;
final int routeType;
final int payloadType;
final int payloadVer;
+ final int pathLenRaw;
final Uint8List pathBytes;
final Uint8List payload;
@@ -5003,12 +5859,15 @@ class _RawPacket {
required this.routeType,
required this.payloadType,
required this.payloadVer,
+ required this.pathLenRaw,
required this.pathBytes,
required this.payload,
});
bool get isFlood =>
routeType == _routeFlood || routeType == _routeTransportFlood;
+
+ int get hopCount => pathLenRaw & 63;
}
class _ParsedText {
diff --git a/lib/connector/meshcore_protocol.dart b/lib/connector/meshcore_protocol.dart
index dc9a9f5..b42e3e5 100644
--- a/lib/connector/meshcore_protocol.dart
+++ b/lib/connector/meshcore_protocol.dart
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
+import 'package:flutter/widgets.dart';
+
// Buffer Reader - sequential binary data reader with pointer tracking
class BufferReader {
int _pointer = 0;
@@ -37,16 +39,6 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
- String readString() {
- _lastPointer = _pointer;
- final value = readRemainingBytes();
- try {
- return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
- } catch (e) {
- return String.fromCharCodes(value); // Latin-1 fallback
- }
- }
-
String readCStringGreedy(int maxLength) {
_lastPointer = _pointer;
final value = [];
@@ -62,11 +54,12 @@ class BufferReader {
}
}
- String readCString(int maxLength) {
+ String readCString({int maxLength = -1}) {
final backupPointer = _pointer;
final value = [];
int counter = 0;
- while (counter < maxLength) {
+ final maxLen = maxLength >= 0 ? maxLength : remaining;
+ while (counter < maxLen) {
final byte = readByte();
if (byte == 0) break;
value.add(byte);
@@ -210,16 +203,19 @@ const int cmdSetChannel = 32;
const int cmdSendTracePath = 36;
const int cmdSetOtherParams = 38;
const int cmdSendAnonReq = 57;
-const int cmdGetTelemetryReq = 39;
+const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
const int cmdSetAutoAddConfig = 58;
const int cmdGetAutoAddConfig = 59;
+const int cmdSetPathHashMode = 61;
+const int cmdGetStats = 56;
// Text message types
const int txtTypePlain = 0;
const int txtTypeCliData = 1;
+const int txtTypeSigned = 2;
// Repeater request types (for server requests)
const int reqTypeGetStatus = 0x01;
@@ -251,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;
@@ -272,6 +273,10 @@ const int advTypeRepeater = 2;
const int advTypeRoom = 3;
const int advTypeSensor = 4;
+const int teleModeDeny = 0;
+const int teleModeAllowFlags = 1; // use contact.flags
+const int teleModeAllowAll = 2;
+
// Payload Types
const int payloadTypeREQ =
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
@@ -310,6 +315,7 @@ const int autoAddSensorFlag =
// Sizes
const int pubKeySize = 32;
+const int signatureSize = 64;
const int maxPathSize = 64;
const int pathHashSize = 1;
const int maxNameSize = 32;
@@ -352,6 +358,9 @@ const int contactPubKeyOffset = 1;
const int contactTypeOffset = 33;
const int contactFlagsOffset = 34;
const int contactFlagFavorite = 0x01;
+const int contactFlagTeleBase = 0x02; // 'base' permission includes battery
+const int contactFlagTeleLoc = 0x04;
+const int contactFlagTeleEnv = 0x08; //access environment sensors
const int contactPathLenOffset = 35;
const int contactPathOffset = 36;
const int contactNameOffset = 100;
@@ -370,52 +379,44 @@ const int msgTextOffset = 38;
class ParsedContactText {
final Uint8List senderPrefix;
final String text;
-
const ParsedContactText({required this.senderPrefix, required this.text});
}
ParsedContactText? parseContactMessageText(Uint8List frame) {
if (frame.isEmpty) return null;
- final code = frame[0];
- if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
+
+ final message = BufferReader(frame);
+ try {
+ final code = message.readByte();
+ if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
+ return null;
+ }
+
+ // Companion radio layout:
+ // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
+ if (code == respCodeContactMsgRecvV3) {
+ // Skip SNR and reserved bytes in v3 layout
+ message.skipBytes(3);
+ }
+ final senderPrefix = message.readBytes(6); // public key
+ message.skipBytes(1); // path length
+ final textType = message.readByte();
+ message.skipBytes(4); // timestamp (4 bytes)
+
+ final shiftedType = textType >> 2;
+ final isSigned = shiftedType == txtTypeSigned || textType == txtTypeSigned;
+ if (isSigned) {
+ // Signed messages have a 4-byte signature after the timestamp, before the text
+ message.skipBytes(4);
+ }
+ final text = message.readCString();
+ if (text.isEmpty) return null;
+
+ return ParsedContactText(senderPrefix: senderPrefix, text: text);
+ } catch (e) {
+ debugPrint('Error parsing contact message text: $e');
return null;
}
-
- // Companion radio layout:
- // [code][snr?][res?][res?][prefix x6][path_len][txt_type][timestamp x4][extra?][text...]
- final isV3 = code == respCodeContactMsgRecvV3;
- final prefixOffset = isV3 ? 4 : 1;
- const prefixLen = 6;
- final txtTypeOffset = prefixOffset + prefixLen + 1;
- final timestampOffset = txtTypeOffset + 1;
- final baseTextOffset = timestampOffset + 4;
- if (frame.length <= baseTextOffset) return null;
-
- final flags = frame[txtTypeOffset];
- final shiftedType = flags >> 2;
- final rawType = flags;
- final isPlain = shiftedType == txtTypePlain || rawType == txtTypePlain;
- final isCli = shiftedType == txtTypeCliData || rawType == txtTypeCliData;
- if (!isPlain && !isCli) {
- return null;
- }
-
- var text = readCString(
- frame,
- baseTextOffset,
- frame.length - baseTextOffset,
- ).trim();
- if (text.isEmpty && frame.length > baseTextOffset + 4) {
- text = readCString(
- frame,
- baseTextOffset + 4,
- frame.length - (baseTextOffset + 4),
- ).trim();
- }
- if (text.isEmpty) return null;
-
- final senderPrefix = frame.sublist(prefixOffset, prefixOffset + prefixLen);
- return ParsedContactText(senderPrefix: senderPrefix, text: text);
}
// Helper to read uint32 little-endian
@@ -438,18 +439,9 @@ int readInt32LE(Uint8List data, int offset) {
return val;
}
-// Helper to read null-terminated UTF-8 string
-String readCString(Uint8List data, int offset, int maxLen) {
- int end = offset;
- while (end < offset + maxLen && end < data.length && data[end] != 0) {
- end++;
- }
- try {
- return utf8.decode(data.sublist(offset, end), allowMalformed: true);
- } catch (e) {
- // Fallback to Latin-1 if UTF-8 decoding fails
- return String.fromCharCodes(data.sublist(offset, end));
- }
+// Helper to convert uint32 to hex string
+String ackHashToHex(int ackHash) {
+ return ackHash.toRadixString(16).padLeft(8, '0');
}
// Helper to convert public key to hex string
@@ -509,7 +501,7 @@ Uint8List buildSendTextMsgFrame(
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain);
- writer.writeByte(attempt.clamp(0, 3));
+ writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
writer.writeBytes(recipientPubKey.sublist(0, 6));
writer.writeString(text);
@@ -569,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();
@@ -838,7 +841,7 @@ Uint8List buildSendCliCommandFrame(
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData);
- writer.writeByte(attempt.clamp(0, 3));
+ writer.writeByte(attempt.clamp(0, 255));
writer.writeUInt32LE(timestamp);
writer.writeBytes(repeaterPubKey.sublist(0, 6));
writer.writeString(command);
@@ -937,3 +940,18 @@ Uint8List buildSetAutoAddConfigFrame({
writer.writeByte(flags);
return writer.toBytes();
}
+
+//Build CMD_SEND_TELEMETRY_REQ
+// Format: [cmd][reserved x3][pub_key? x32]
+Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
+ final writer = BufferWriter();
+ writer.writeByte(cmdSendTelemetryReq);
+
+ if (pubKey != null && pubKey.length == pubKeySize) {
+ writer.writeBytes(Uint8List(3)); // reserved bytes
+ writer.writeBytes(pubKey);
+ } else {
+ writer.writeBytes(Uint8List(4)); // reserved bytes
+ }
+ return writer.toBytes();
+}
diff --git a/lib/connector/meshcore_uuids.dart b/lib/connector/meshcore_uuids.dart
new file mode 100644
index 0000000..da7f6b5
--- /dev/null
+++ b/lib/connector/meshcore_uuids.dart
@@ -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 deviceNamePrefixes = [
+ "MeshCore-",
+ "Whisper-",
+ "WisCore-",
+ "HT-",
+ ];
+}
diff --git a/lib/helpers/link_handler.dart b/lib/helpers/link_handler.dart
index 7a032ef..b931ca1 100644
--- a/lib/helpers/link_handler.dart
+++ b/lib/helpers/link_handler.dart
@@ -1,8 +1,50 @@
import 'package:flutter/material.dart';
+import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
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,
+ required String text,
+ required TextStyle style,
+ TextStyle? linkStyle,
+ }) {
+ final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
+ const options = LinkifyOptions(humanize: false, defaultToHttps: false);
+ const linkifiers = [UrlLinkifier(), EmailLinkifier()];
+ void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
+
+ if (PlatformInfo.isDesktop) {
+ return SelectableLinkify(
+ text: text,
+ style: style,
+ linkStyle: effectiveLinkStyle,
+ options: options,
+ linkifiers: linkifiers,
+ onOpen: onOpen,
+ );
+ }
+ return Linkify(
+ text: text,
+ style: style,
+ linkStyle: effectiveLinkStyle,
+ options: options,
+ linkifiers: linkifiers,
+ onOpen: onOpen,
+ );
+ }
+
static Future handleLinkTap(BuildContext context, String url) async {
// Show confirmation dialog
final shouldOpen = await showDialog(
diff --git a/lib/helpers/path_helper.dart b/lib/helpers/path_helper.dart
new file mode 100644
index 0000000..fe51d63
--- /dev/null
+++ b/lib/helpers/path_helper.dart
@@ -0,0 +1,31 @@
+import '../models/contact.dart';
+import '../connector/meshcore_protocol.dart';
+
+class PathHelper {
+ static String formatPathHex(List pathBytes) {
+ return pathBytes
+ .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
+ .join(',');
+ }
+
+ static String resolvePathNames(
+ List pathBytes,
+ List allContacts,
+ ) {
+ return pathBytes
+ .map((b) {
+ final hex = b.toRadixString(16).padLeft(2, '0').toUpperCase();
+ final matches = allContacts
+ .where(
+ (c) =>
+ c.publicKey.first == b &&
+ (c.type == advTypeRepeater || c.type == advTypeRoom),
+ )
+ .toList();
+ if (matches.isEmpty) return hex;
+ if (matches.length == 1) return matches.first.name;
+ return matches.map((c) => c.name).join(' | ');
+ })
+ .join(' \u2192 ');
+ }
+}
diff --git a/lib/helpers/reaction_helper.dart b/lib/helpers/reaction_helper.dart
index 88138d6..90733c3 100644
--- a/lib/helpers/reaction_helper.dart
+++ b/lib/helpers/reaction_helper.dart
@@ -8,6 +8,50 @@ class ReactionInfo {
}
class ReactionHelper {
+ /// Apply a reaction to a list of messages by matching the reaction hash.
+ ///
+ /// [messages] - the message list to search
+ /// [reactionInfo] - the parsed reaction
+ /// [getTimestampSecs] - extract timestamp seconds from a message
+ /// [getSenderName] - extract sender name for hash (null for 1:1 implicit)
+ /// [getMessageText] - extract message text
+ /// [getReactions] - extract current reactions map
+ /// [shouldSkip] - filter function to skip messages (e.g., skip outgoing for incoming reactions)
+ /// [updateMessage] - callback to update the message at index with new reactions
+ ///
+ /// Returns whether a match was found.
+ static bool applyReaction({
+ required List messages,
+ required ReactionInfo reactionInfo,
+ required int Function(T) getTimestampSecs,
+ required String? Function(T) getSenderName,
+ required String Function(T) getMessageText,
+ required Map Function(T) getReactions,
+ required bool Function(T) shouldSkip,
+ required void Function(int index, Map newReactions)
+ updateMessage,
+ }) {
+ final targetHash = reactionInfo.targetHash;
+ for (int i = messages.length - 1; i >= 0; i--) {
+ final msg = messages[i];
+ if (shouldSkip(msg)) continue;
+
+ final msgHash = computeReactionHash(
+ getTimestampSecs(msg),
+ getSenderName(msg),
+ getMessageText(msg),
+ );
+ if (msgHash == targetHash) {
+ final currentReactions = Map.from(getReactions(msg));
+ currentReactions[reactionInfo.emoji] =
+ (currentReactions[reactionInfo.emoji] ?? 0) + 1;
+ updateMessage(i, currentReactions);
+ return true;
+ }
+ }
+ return false;
+ }
+
static List? _cachedEmojis;
/// Combined list of all reaction emojis in fixed order.
diff --git a/lib/l10n/app_bg.arb b/lib/l10n/app_bg.arb
index ca27f8c..0f5145d 100644
--- a/lib/l10n/app_bg.arb
+++ b/lib/l10n/app_bg.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "Връзката TCP изтекла.",
"tcpConnectionFailed": "Неуспешно е установено TCP връзката: {error}",
"map_showDiscoveryContacts": "Покажи контакти за откриване",
- "map_setAsMyLocation": "Задайте като моя местоположение"
+ "map_setAsMyLocation": "Задайте като моя местоположение",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_denyAll": "Откажи всичко",
+ "settings_allowAll": "Позволи всичко",
+ "settings_allowByContact": "Позволи по флагове за контакт",
+ "settings_privacy": "Настройки на поверителността",
+ "settings_privacySettingsDescription": "Изберете каква информация устройството ви споделя с другите.",
+ "settings_privacySubtitle": "Контролирайте каква информация се споделя.",
+ "settings_telemetryBaseMode": "Базов режим на телеметрия",
+ "settings_telemetryLocationMode": "Режим на местоположение на телеметрията",
+ "settings_advertLocation": "Място на обявата",
+ "settings_advertLocationSubtitle": "Включи местоположение в обявата",
+ "contact_info": "Контактна информация",
+ "settings_telemetryEnvironmentMode": "Режим на средата на телеметрията",
+ "contact_telemetry": "Телеметрия",
+ "contact_lastSeen": "Последно видян",
+ "contact_clearChat": "Изчисти чата",
+ "contact_teleBase": "Базата данни за телеметрия",
+ "contact_settings": "Настройки за контакти",
+ "contact_teleBaseSubtitle": "Позволи споделяне на ниво на батерията и основна телеметрия",
+ "contact_teleEnv": "Среда на телеметрия",
+ "contact_teleLocSubtitle": "Позволи споделяне на данни за местоположение",
+ "contact_teleLoc": "Местоположение на телеметрията",
+ "contact_teleEnvSubtitle": "Позволи споделяне на данни от средносферните датчици",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeight": "Първоначална тежест на маршрута",
+ "appSettings_maxRouteWeight": "Максимално допустимо тегло на маршрута",
+ "appSettings_initialRouteWeightSubtitle": "Начално тегло за новооткрити маршрути",
+ "appSettings_maxRouteWeightSubtitle": "Максималното тегло, което един маршрут може да събере от успешни доставки.",
+ "appSettings_routeWeightSuccessIncrement": "Увеличение на теглото за успех",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Тегло, добавено към път след успешно доставяне.",
+ "appSettings_routeWeightFailureDecrement": "Намаляване на теглото, свързано с неуспех",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Тегло, което е било премахнато от пътя след неуспешен опит за доставка.",
+ "appSettings_maxMessageRetries": "Максимален брой опити за изпращане на съобщение",
+ "appSettings_maxMessageRetriesSubtitle": "Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_multiAck": "Мулти-потвърди: {value}",
+ "settings_telemetryModeUpdated": "Режим на телеметрията е обновен",
+ "map_showOverlaps": "Покриване на ключа на повтаряча",
+ "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 и време на пренос",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingHidePin": "Скрий ПИН",
+ "scanner_linuxPairingShowPin": "Покажи PIN",
+ "scanner_linuxPairingPinTitle": "PIN код за сдвояване на Bluetooth",
+ "scanner_linuxPairingPinPrompt": "Въведете ПИН за {deviceName} (оставете празно, ако няма)."
}
diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb
index bd4aed5..c156a44 100644
--- a/lib/l10n/app_de.arb
+++ b/lib/l10n/app_de.arb
@@ -1917,5 +1917,133 @@
"tcpErrorTimedOut": "Die TCP-Verbindung ist abgelaufen.",
"tcpConnectionFailed": "Fehler beim TCP-Verbindungsaufbau: {error}",
"map_showDiscoveryContacts": "Entdeckungs-Kontakte anzeigen",
- "map_setAsMyLocation": "Als meine aktuelle Position festlegen"
+ "map_setAsMyLocation": "Als meine aktuelle Position festlegen",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_allowByContact": "Zulassen durch Kontaktflaggen",
+ "settings_privacy": "Datenschutzeinstellungen",
+ "settings_allowAll": "Alles zulassen",
+ "settings_privacySettingsDescription": "Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.",
+ "settings_denyAll": "Alle ablehnen",
+ "settings_privacySubtitle": "Steuern Sie die Informationen, die freigegeben werden.",
+ "settings_telemetryLocationMode": "Telemetrie-Ortsmodus",
+ "settings_telemetryEnvironmentMode": "Telemetrie-Umgebungsmodus",
+ "settings_advertLocation": "Anzeigenort",
+ "settings_advertLocationSubtitle": "Ort in der Anzeige einbeziehen",
+ "settings_telemetryBaseMode": "Telemetrie-Basismodus",
+ "contact_teleBase": "Telemetriebasis",
+ "contact_teleBaseSubtitle": "Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie",
+ "contact_teleLoc": "Telemetrieort",
+ "contact_teleLocSubtitle": "Teilen von Standortdaten zulassen",
+ "contact_info": "Kontaktinformationen",
+ "contact_settings": "Kontakteinstellungen",
+ "contact_telemetry": "Telemetrie",
+ "contact_teleEnv": "Telemetrieumgebung",
+ "contact_lastSeen": "Zuletzt gesehen",
+ "contact_clearChat": "Chat löschen",
+ "contact_teleEnvSubtitle": "Teilen von Umgebungsensordaten zulassen",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeightSubtitle": "Ausgangsgewicht für neu entdeckte Pfade",
+ "appSettings_maxRouteWeightSubtitle": "Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.",
+ "appSettings_maxRouteWeight": "Maximale Gesamtstreckenlänge",
+ "appSettings_initialRouteWeight": "Anfangs-Streckengewicht",
+ "appSettings_routeWeightSuccessIncrement": "Erhöhung des Erfolgsgewichts",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.",
+ "appSettings_routeWeightFailureDecrement": "Reduzierung des Gewichts bei Fehlern",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde",
+ "appSettings_maxMessageRetries": "Maximale Anzahl an Wiederholungsversuchen",
+ "appSettings_maxMessageRetriesSubtitle": "Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_telemetryModeUpdated": "Telemetriemodus aktualisiert",
+ "settings_multiAck": "Mehrfach-Bestätigungen: {value}",
+ "map_showOverlaps": "Überlappungen der Repeater-Taste",
+ "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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "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)."
}
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 5c95e60..d8d73ab 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -127,6 +127,7 @@
}
}
},
+
"scanner_stop": "Stop",
"scanner_scan": "Scan",
"scanner_bluetoothOff": "Bluetooth is off",
@@ -166,6 +167,26 @@
"settings_privacyModeToggle": "Toggle privacy mode to hide your name and location in advertisements.",
"settings_privacyModeEnabled": "Privacy mode enabled",
"settings_privacyModeDisabled": "Privacy mode disabled",
+ "settings_privacy": "Privacy Settings",
+ "settings_privacySubtitle": "Control what information is shared.",
+ "settings_privacySettingsDescription": "Choose what information your device shares with others.",
+ "settings_denyAll": "Deny all",
+ "settings_allowByContact": "Allow by contact flags",
+ "settings_allowAll": "Allow all",
+ "settings_telemetryBaseMode": "Telemetry Base Mode",
+ "settings_telemetryLocationMode": "Telemetry Location Mode",
+ "settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
+ "settings_advertLocation": "Advert Location",
+ "settings_advertLocationSubtitle": "Include location in advert.",
+ "settings_multiAck": "Multi-ACKs: {value}",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions",
"settings_sendAdvertisement": "Send Advertisement",
"settings_sendAdvertisementSubtitle": "Broadcast presence now",
@@ -269,6 +290,23 @@
"appSettings_autoRouteRotationSubtitle": "Cycle between best paths and flood mode",
"appSettings_autoRouteRotationEnabled": "Auto route rotation enabled",
"appSettings_autoRouteRotationDisabled": "Auto route rotation disabled",
+ "appSettings_maxRouteWeight": "Max Route Weight",
+ "appSettings_maxRouteWeightSubtitle": "Maximum weight a path can accumulate from successful deliveries",
+ "appSettings_initialRouteWeight": "Initial Route Weight",
+ "appSettings_initialRouteWeightSubtitle": "Starting weight for newly discovered paths",
+ "appSettings_routeWeightSuccessIncrement": "Success Weight Increment",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Weight added to a path after successful delivery",
+ "appSettings_routeWeightFailureDecrement": "Failure Weight Decrement",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Weight removed from a path after failed delivery",
+ "appSettings_maxMessageRetries": "Max Message Retries",
+ "appSettings_maxMessageRetriesSubtitle": "Number of retry attempts before marking a message as failed",
+ "path_routeWeight": "{weight}/{max}",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": { "type": "String" },
+ "max": { "type": "String" }
+ }
+ },
"appSettings_battery": "Battery",
"appSettings_batteryChemistry": "Battery Chemistry",
"appSettings_batteryChemistryPerDevice": "Set per device ({deviceName})",
@@ -455,6 +493,17 @@
}
}
},
+ "contact_info": "Contact Info",
+ "contact_settings": "Contact Settings",
+ "contact_telemetry": "Telemetry",
+ "contact_lastSeen": "Last seen",
+ "contact_clearChat": "Clear Chat",
+ "contact_teleBase": "Telemetry Base",
+ "contact_teleBaseSubtitle": "Allow sharing battery level and basic telemetry",
+ "contact_teleLoc": "Telemetry Location",
+ "contact_teleLocSubtitle": "Allow sharing location data",
+ "contact_teleEnv": "Telemetry Environment",
+ "contact_teleEnvSubtitle": "Allow sharing environment sensor data",
"channels_title": "Channels",
"channels_noChannelsConfigured": "No channels configured",
"channels_addPublicChannel": "Add Public Channel",
@@ -830,6 +879,7 @@
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Other Nodes",
+ "map_showOverlaps": "Repeater Key Overlaps",
"map_keyPrefix": "Key Prefix",
"map_filterByKeyPrefix": "Filter by key prefix",
"map_publicKeyPrefix": "Public key prefix",
@@ -843,7 +893,8 @@
"map_joinRoom": "Join Room",
"map_manageRepeater": "Manage Repeater",
"map_tapToAdd": "Tap on nodes to add them to the path.",
- "map_runTrace": "Run Path Trace",
+ "map_runTrace": "Run path trace",
+ "map_runTraceWithReturnPath": "Return back on the same path.",
"map_removeLast": "Remove Last",
"map_pathTraceCancelled": "Path trace cancelled.",
"mapCache_title": "Offline Map Cache",
@@ -1927,5 +1978,79 @@
"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",
+ "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"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb
index 085b0c8..245f732 100644
--- a/lib/l10n/app_es.arb
+++ b/lib/l10n/app_es.arb
@@ -1917,5 +1917,133 @@
"tcpErrorTimedOut": "La conexión TCP ha caducado.",
"tcpConnectionFailed": "Error en la conexión TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contactos de Descubrimiento",
- "map_setAsMyLocation": "Establecer mi ubicación"
+ "map_setAsMyLocation": "Establecer mi ubicación",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacySubtitle": "Controlar qué información se comparte.",
+ "settings_allowByContact": "Permitir por banderas de contacto",
+ "settings_denyAll": "Denegar todo",
+ "settings_telemetryBaseMode": "Modo base de telemetría",
+ "settings_telemetryEnvironmentMode": "Modo de entorno de telemetría",
+ "settings_advertLocationSubtitle": "Incluir ubicación en anuncio",
+ "contact_info": "Información de contacto",
+ "settings_privacySettingsDescription": "Elige qué información comparte tu dispositivo con otros.",
+ "settings_allowAll": "Permitir todo",
+ "settings_privacy": "Configuración de privacidad",
+ "contact_settings": "Configuración de contacto",
+ "settings_telemetryLocationMode": "Modo de ubicación de telemetría",
+ "contact_teleBase": "Base de Telemetría",
+ "contact_teleLoc": "Ubicación de telemetría",
+ "settings_advertLocation": "Ubicación de anuncio",
+ "contact_teleLocSubtitle": "Permitir el intercambio de datos de ubicación",
+ "contact_clearChat": "Borrar chat",
+ "contact_telemetry": "Telemetría",
+ "contact_lastSeen": "Visto por última vez",
+ "contact_teleBaseSubtitle": "Permitir el intercambio de nivel de batería y telemetría básica",
+ "contact_teleEnv": "Entorno de Telemetría",
+ "contact_teleEnvSubtitle": "Permitir el intercambio de datos de sensores de entorno",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeight": "Peso inicial de la ruta",
+ "appSettings_maxRouteWeight": "Peso máximo permitido para la ruta",
+ "appSettings_initialRouteWeightSubtitle": "Peso inicial para rutas recién descubiertas",
+ "appSettings_maxRouteWeightSubtitle": "Peso máximo que una ruta puede acumular gracias a entregas exitosas.",
+ "appSettings_routeWeightSuccessIncrement": "Incremento de peso para el éxito",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Peso añadido a una ruta después de una entrega exitosa.",
+ "appSettings_routeWeightFailureDecrement": "Reducción del peso asociado al fallo",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Peso retirado de un camino después de un intento de entrega fallido.",
+ "appSettings_maxMessageRetries": "Número máximo de reintentos de envío de mensajes",
+ "appSettings_maxMessageRetriesSubtitle": "Número de intentos de reintento antes de marcar un mensaje como fallido.",
+ "path_routeWeight": "{weight}/{max}",
+ "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.",
+ "@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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "Mostrar PIN",
+ "scanner_linuxPairingPinTitle": "PIN de emparejamiento Bluetooth",
+ "scanner_linuxPairingHidePin": "Ocultar PIN",
+ "scanner_linuxPairingPinPrompt": "Introduzca el PIN para {deviceName} (déjelo en blanco si no hay ninguno)."
}
diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb
index b7617bb..21b231a 100644
--- a/lib/l10n/app_fr.arb
+++ b/lib/l10n/app_fr.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "La connexion TCP a expiré.",
"tcpConnectionFailed": "Échec de la connexion TCP : {error}",
"map_showDiscoveryContacts": "Afficher les contacts de découverte",
- "map_setAsMyLocation": "Définir comme ma localisation"
+ "map_setAsMyLocation": "Définir comme ma localisation",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "Paramètres de confidentialité",
+ "settings_privacySubtitle": "Contrôlez les informations partagées",
+ "settings_telemetryLocationMode": "Mode d'emplacement de télémétrie",
+ "settings_telemetryEnvironmentMode": "Mode d'environnement de télémétrie",
+ "settings_advertLocation": "Emplacement de l'annonce",
+ "settings_advertLocationSubtitle": "Inclure l'emplacement dans l'annonce",
+ "settings_denyAll": "Refuser tout",
+ "settings_allowByContact": "Autoriser par drapeaux de contact",
+ "settings_privacySettingsDescription": "Choisissez les informations que votre appareil partage avec les autres.",
+ "settings_allowAll": "Autoriser tout",
+ "contact_info": "Informations de contact",
+ "settings_telemetryBaseMode": "Mode de base Télémétrie",
+ "contact_teleBase": "Base de télémétrie",
+ "contact_teleLoc": "Emplacement de télémétrie",
+ "contact_teleLocSubtitle": "Autoriser le partage des données de localisation",
+ "contact_teleEnv": "Environnement Télémétrie",
+ "contact_teleEnvSubtitle": "Autoriser le partage des données des capteurs d'environnement",
+ "contact_telemetry": "Télémétrie",
+ "contact_settings": "Paramètres de contact",
+ "contact_lastSeen": "Dernière fois vu",
+ "contact_clearChat": "Effacer la conversation",
+ "contact_teleBaseSubtitle": "Autoriser le partage du niveau de batterie et de la télémétrie de base",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_maxRouteWeightSubtitle": "Poids maximal qu'un itinéraire peut accumuler grâce à des livraisons réussies.",
+ "appSettings_initialRouteWeight": "Poids initial de l'itinéraire",
+ "appSettings_maxRouteWeight": "Poids maximal autorisé pour le trajet",
+ "appSettings_initialRouteWeightSubtitle": "Poids de départ pour les nouveaux chemins découverts",
+ "appSettings_routeWeightSuccessIncrement": "Augmentation du poids de réussite",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Poids ajouté à un itinéraire après une livraison réussie.",
+ "appSettings_routeWeightFailureDecrement": "Réduction du poids de pénalité",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Poids retiré d'un itinéraire après une tentative de livraison infructueuse.",
+ "appSettings_maxMessageRetries": "Nombre maximal de tentatives de récupération de messages",
+ "appSettings_maxMessageRetriesSubtitle": "Nombre de tentatives de relance avant de marquer un message comme ayant échoué.",
+ "path_routeWeight": "{weight}/{max}",
+ "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.",
+ "@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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "Afficher le code PIN",
+ "scanner_linuxPairingHidePin": "Masquer le code PIN",
+ "scanner_linuxPairingPinTitle": "Code PIN d’appairage Bluetooth",
+ "scanner_linuxPairingPinPrompt": "Entrez le code PIN pour {deviceName} (laissez vide si aucun)."
}
diff --git a/lib/l10n/app_hu.arb b/lib/l10n/app_hu.arb
new file mode 100644
index 0000000..dc96020
--- /dev/null
+++ b/lib/l10n/app_hu.arb
@@ -0,0 +1,2059 @@
+{
+ "@@locale": "hu",
+ "appTitle": "MeshCore Open",
+ "nav_contacts": "Kapcsolatok",
+ "nav_channels": "Csatornák",
+ "nav_map": "Térkép",
+ "common_cancel": "Át kell venni",
+ "common_ok": "Rendben",
+ "common_connect": "Kapcsolódj",
+ "common_unknownDevice": "Tudatlan eszköz",
+ "common_save": "Mentés",
+ "common_delete": "Töröl",
+ "common_deleteAll": "Minden törlés",
+ "common_close": "Bezárás",
+ "common_edit": "Szerkesztés",
+ "common_add": "Hozzáad",
+ "common_settings": "Beállítások",
+ "common_disconnect": "Csatlakozást megszakasztani",
+ "common_connected": "Kapcsolódó",
+ "common_disconnected": "Elválasztva",
+ "common_create": "Készítsd",
+ "common_continue": "Folytatás",
+ "common_share": "Ossza meg",
+ "common_copy": "Másolat",
+ "common_retry": "Újrapróbálja",
+ "common_hide": "Elrejt",
+ "common_remove": "Eltávolít",
+ "common_enable": "Engedélyezés",
+ "common_disable": "Leteteszt",
+ "common_reboot": "Újraindítás",
+ "common_loading": "Betöltés...",
+ "common_notAvailable": "—",
+ "common_voltageValue": "{volts} V",
+ "@common_voltageValue": {
+ "placeholders": {
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "common_percentValue": "{percent}%",
+ "@common_percentValue": {
+ "placeholders": {
+ "percent": {
+ "type": "int"
+ }
+ }
+ },
+ "scanner_title": "MeshCore nyitott",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "Bluetooth",
+ "connectionChoiceTcpLabel": "TCP",
+ "tcpScreenTitle": "TCP-n keresztül kapcsolódjon",
+ "tcpHostLabel": "IP-cím",
+ "tcpHostHint": "192.168.40.10",
+ "tcpPortLabel": "Múzeum",
+ "tcpPortHint": "5000",
+ "tcpStatus_notConnected": "Adja meg a célpontot, majd kapcsolja össze.",
+ "tcpStatus_connectingTo": "Kapcsolat a {endpoint}-hez...",
+ "@tcpStatus_connectingTo": {
+ "placeholders": {
+ "endpoint": {
+ "type": "String"
+ }
+ }
+ },
+ "tcpErrorHostRequired": "Az IP-címet meg kell adni.",
+ "tcpErrorPortInvalid": "Az érték 1 és 65535 között kell lennie.",
+ "tcpErrorUnsupported": "A TCP-protokoll nem támogatott ez a platformon.",
+ "tcpErrorTimedOut": "A TCP-kapcsolat időtúllépett.",
+ "tcpConnectionFailed": "A TCP-kapcsolat sikertelen: {error}",
+ "@tcpConnectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "usbScreenTitle": "USB-en keresztül csatlakoztassuk",
+ "usbScreenSubtitle": "Válasszon egy azonosított soros eszközt, és közvetlenül csatlakoztassa a MeshCore-hoz.",
+ "usbScreenStatus": "Válasszon egy USB-es eszközt",
+ "usbScreenNote": "Az USB-es soros kommunikáció a támogatott Android eszközökön és asztali rendszereken is elérhető.",
+ "usbScreenEmptyState": "Nincs USB eszköz megtalálva. Csatlakoztasson egyet, majd frissítse a rendszert.",
+ "usbErrorPermissionDenied": "A USB-es hozzáférés megtagadva.",
+ "usbErrorDeviceMissing": "Az kiválasztott USB eszköz már nem elérhető.",
+ "usbErrorInvalidPort": "Válasszon egy érvényes USB-eszközt.",
+ "usbErrorBusy": "Egy másik USB-csatlakozás kérése már folyamatban van.",
+ "usbErrorNotConnected": "Nincs csatlakoztatott USB eszköz.",
+ "usbErrorOpenFailed": "Nem sikerült megnyitni a kiválasztott USB-eszközöt.",
+ "usbErrorConnectFailed": "Nem sikerült kapcsolatot létesíteni a kiválasztott USB-eszközhöz.",
+ "usbErrorUnsupported": "Ez a platform nem támogat USB-es soros kommunikációt.",
+ "usbErrorAlreadyActive": "Az USB-kapcsolat már be van állítva.",
+ "usbErrorNoDeviceSelected": "Nincs kiválasztva USB eszköz.",
+ "usbErrorPortClosed": "Az USB-kapcsolat nem aktív.",
+ "usbErrorConnectTimedOut": "Kapcsolódás sikertelen. Ellenőrizze, hogy a eszköz rendelkezik-e USB-hez tartozó firmware-rel.",
+ "usbFallbackDeviceName": "Web-szériás eszköz",
+ "usbStatus_notConnected": "Válasszon egy USB-es eszközt",
+ "usbStatus_connecting": "USB eszközhez való csatlakozás...",
+ "usbStatus_searching": "USB eszközök keresése...",
+ "usbConnectionFailed": "USB-kapcsolat sikertelen: {error}",
+ "@usbConnectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_scanning": "Készülékek keresése...",
+ "scanner_connecting": "Kapcsolódás...",
+ "scanner_disconnecting": "Kapcsolat megszakad...",
+ "scanner_notConnected": "Nem csatlakozva",
+ "scanner_connectedTo": "Kapcsolódik a {deviceName}-hez",
+ "@scanner_connectedTo": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_searchingDevices": "MeshCore eszközök keresése...",
+ "scanner_tapToScan": "A Tap Scan funkció segítségével kereshet MeshCore eszközöket.",
+ "scanner_connectionFailed": "Kapcsolódás sikertelen: {error}",
+ "@scanner_connectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_stop": "Megállj",
+ "scanner_scan": "Szkenálás",
+ "scanner_bluetoothOff": "A Bluetooth kikapcsolva",
+ "scanner_bluetoothOffMessage": "Kérjük, kapcsolja be a Bluetooth-ot, hogy eszközök keresése lehessen.",
+ "scanner_chromeRequired": "Chrome böngésző szükséges",
+ "scanner_chromeRequiredMessage": "Ez az alkalmazás a Bluetooth funkcióhoz Google Chrome-ot vagy Chromium alapú böngészőt igényel.",
+ "scanner_enableBluetooth": "Engedje be a Bluetooth funkciót",
+ "device_quickSwitch": "Gyors váltás",
+ "device_meshcore": "MeshCore",
+ "settings_title": "Beállítások",
+ "settings_deviceInfo": "A készülék információi",
+ "settings_appSettings": "Alkalmazási beállítások",
+ "settings_appSettingsSubtitle": "Értesítések, üzenetküldés és térképi beállítások",
+ "settings_nodeSettings": "Műközép beállítások",
+ "settings_nodeName": "Vonal neve",
+ "settings_nodeNameNotSet": "Nem megállapított",
+ "settings_nodeNameHint": "Adja meg a csomópont nevét",
+ "settings_nodeNameUpdated": "Neve frissítve",
+ "settings_radioSettings": "Rádióbeállítások",
+ "settings_radioSettingsSubtitle": "Frekvencia, teljesítmény, szélesítési tényező",
+ "settings_radioSettingsUpdated": "A rádió beállítások frissítve",
+ "settings_location": "Helyszín",
+ "settings_locationSubtitle": "GPS koordináták",
+ "settings_locationUpdated": "A helyzet és a GPS beállítások frissítve",
+ "settings_locationBothRequired": "Kérjük, adja meg a földrajzi szélességet és hosszúságot.",
+ "settings_locationInvalid": "Érvénytelen szélesszög vagy hosszszög.",
+ "settings_locationGPSEnable": "GPS engedélyezve",
+ "settings_locationGPSEnableSubtitle": "Lehetővé teszi, hogy a GPS automatikusan frissítse a helyzetet.",
+ "settings_locationIntervalSec": "GPS-számolási intervallum (másodpercek)",
+ "settings_locationIntervalInvalid": "Az intervallum legalább 60 másodpercnek, de legfeljebb 86400 másodpercnak kell lennie.",
+ "settings_latitude": "Nyugat-–––––––––––––––––––––––––––––––",
+ "settings_longitude": "hosszúság",
+ "settings_contactSettings": "Kapcsolat beállítások",
+ "settings_contactSettingsSubtitle": "Beállítások, amelyek meghatározzák, hogyan lehet új kapcsolatokat hozzáadni.",
+ "settings_privacyMode": "Adatvédelem mód",
+ "settings_privacyModeSubtitle": "Elrejtsük a nevét/a helyszínt az űrianyagokban",
+ "settings_privacyModeToggle": "Engedje be a privát üzemmódot, hogy elrejtse a nevét és a helyét az online hirdetésekben.",
+ "settings_privacyModeEnabled": "Adatvédelem mód beállítva",
+ "settings_privacyModeDisabled": "Adatvédelem mód kikapcsolva",
+ "settings_actions": "Tevékenységek",
+ "settings_sendAdvertisement": "Hirdetés küldése",
+ "settings_sendAdvertisementSubtitle": "A nyilvános megjelenés",
+ "settings_advertisementSent": "Hirdetés elküldve",
+ "settings_syncTime": "Szinkronizációs idő",
+ "settings_syncTimeSubtitle": "Állítsa a készülék időzítését a telefon időjére",
+ "settings_timeSynchronized": "Időben szinkronizált",
+ "settings_refreshContacts": "Újraindítsd a kapcsolatok listát",
+ "settings_refreshContactsSubtitle": "Újra töltse a kontaktlista-adatokat a készülékről",
+ "settings_rebootDevice": "Újraindítás",
+ "settings_rebootDeviceSubtitle": "Indítsa újra a MeshCore eszközt.",
+ "settings_rebootDeviceConfirm": "Biztosan szeretné újraindítani a készüléket? Ebben az esetben a kapcsolat megszűnik.",
+ "settings_debug": "Hibakeresés",
+ "settings_bleDebugLog": "BLE hibaelhárítási napló",
+ "settings_bleDebugLogSubtitle": "BLE parancsok, válaszok és alapvető adatok",
+ "settings_appDebugLog": "App-debug log",
+ "settings_appDebugLogSubtitle": "Programozási hibajelzések",
+ "settings_about": "Ról",
+ "settings_aboutVersion": "MeshCore Open {version} verzió",
+ "@settings_aboutVersion": {
+ "placeholders": {
+ "version": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_aboutLegalese": "2026-os MeshCore nyílt forráskódú projekt",
+ "settings_aboutDescription": "Egy nyílt forráskódú Flutter kliens a MeshCore LoRa hálózati eszközök számára.",
+ "settings_aboutOpenMeteoAttribution": "LOS magassági adatok: Open-Meteo (CC BY 4.0)",
+ "settings_infoName": "Név",
+ "settings_infoId": "Az azonosító",
+ "settings_infoStatus": "Állapot",
+ "settings_infoBattery": "Akku",
+ "settings_infoPublicKey": "Nyelvkönyv",
+ "settings_infoContactsCount": "Kapcsolatok száma",
+ "settings_infoChannelCount": "Csatorna száma",
+ "settings_presets": "Előre beállított beállítások",
+ "settings_frequency": "Frekvencia (MHz)",
+ "settings_frequencyHelper": "300,0 – 2500,0",
+ "settings_frequencyInvalid": "Érvénytelen frekvencia (300-2500 MHz)",
+ "settings_bandwidth": "Kapacitás",
+ "settings_spreadingFactor": "Terjesztési tényező",
+ "settings_codingRate": "Kódolási sebesség",
+ "settings_txPower": "TX teljesítmény (dBm)",
+ "settings_txPowerHelper": "0 – 22",
+ "settings_txPowerInvalid": "Érvénytelen TX teljesítmény (0-22 dBm)",
+ "settings_clientRepeat": "Autonóm rendszer újra",
+ "settings_clientRepeatSubtitle": "Engedje, hogy ez a eszköz mások számára is ismételje a hálózati csomagokat.",
+ "settings_clientRepeatFreqWarning": "A hálózat nélküli kommunikációhoz 433, 869 vagy 918 MHz frekvenciát igényel.",
+ "settings_error": "Hiba: {message}",
+ "@settings_error": {
+ "placeholders": {
+ "message": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_title": "Alkalmazási beállítások",
+ "appSettings_appearance": "Megjelenés",
+ "appSettings_theme": "Téma",
+ "appSettings_themeSystem": "Alapértékek",
+ "appSettings_themeLight": "Világítás",
+ "appSettings_themeDark": "Sötét",
+ "appSettings_language": "Nyelv",
+ "appSettings_languageSystem": "Alapértékek",
+ "appSettings_languageEn": "Angol",
+ "appSettings_languageFr": "Francia",
+ "appSettings_languageEs": "Spanyol",
+ "appSettings_languageDe": "Német",
+ "appSettings_languagePl": "Lengyel",
+ "appSettings_languageSl": "szlovén nyelv",
+ "appSettings_languagePt": "Portugál",
+ "appSettings_languageIt": "Olasz",
+ "appSettings_languageZh": "Kínai",
+ "appSettings_languageSv": "Svéd",
+ "appSettings_languageNl": "Hollandi",
+ "appSettings_languageSk": "Szlovén nyelvre fordítás",
+ "appSettings_languageBg": "Bulgár",
+ "appSettings_languageRu": "Orosz",
+ "appSettings_languageUk": "Украинский",
+ "appSettings_enableMessageTracing": "Engedje meg a üzenetek nyomon követését",
+ "appSettings_enableMessageTracingSubtitle": "Adja meg a üzenetek részletes útvonal- és időzítési adatokat.",
+ "appSettings_notifications": "Értesítések",
+ "appSettings_enableNotifications": "Engedélyezze az értesítéseket",
+ "appSettings_enableNotificationsSubtitle": "Kapjon értesítéseket üzenetekről és hirdetésekről.",
+ "appSettings_notificationPermissionDenied": "A értesítési engedély megtagadva",
+ "appSettings_notificationsEnabled": "A figyelmeztetések engedélyezve",
+ "appSettings_notificationsDisabled": "A figyelmeztetések kikapcsolva",
+ "appSettings_messageNotifications": "Üzenet értesítések",
+ "appSettings_messageNotificationsSubtitle": "A figyelmeztetést megjelenítve, amikor új üzenet érkezik",
+ "appSettings_channelMessageNotifications": "Csatorna-üzenetek értesítése",
+ "appSettings_channelMessageNotificationsSubtitle": "A figyelmeztetést megjelenítve, amikor új üzenet érkezik a csatornáról",
+ "appSettings_advertisementNotifications": "Reklám értesítések",
+ "appSettings_advertisementNotificationsSubtitle": "A figyelmeztetést megjelenítve, amikor új csomópontok kerülnek felfedezésre.",
+ "appSettings_messaging": "Üzenetek küldése",
+ "appSettings_clearPathOnMaxRetry": "Egyértelmű út a Max Retry funkció használatával",
+ "appSettings_clearPathOnMaxRetrySubtitle": "A kapcsolat visszaállítás 5 sikertelen továbbítás után",
+ "appSettings_pathsWillBeCleared": "Ha 5-szer sikertelenül próbálunk, a útvonalat automatikusan tisztítjuk.",
+ "appSettings_pathsWillNotBeCleared": "A utak automatikusan nem tisztítódnak.",
+ "appSettings_autoRouteRotation": "Autóútok forgása",
+ "appSettings_autoRouteRotationSubtitle": "Válasszon a legjobb útvonalak között, vagy válassza a vízözön-módot.",
+ "appSettings_autoRouteRotationEnabled": "Az automatikus útvonalváltás engedélyezve",
+ "appSettings_autoRouteRotationDisabled": "Az automatikus útvonal-választás funkció kikapcsolva.",
+ "appSettings_maxRouteWeight": "Maximális útvonal súly",
+ "appSettings_maxRouteWeightSubtitle": "A lehető legnagyobb súly, amit egy útvonal sikeres szállítmányok során összegyűjthet.",
+ "appSettings_initialRouteWeight": "A kezdeti útvonal súlya",
+ "appSettings_initialRouteWeightSubtitle": "Az új, felfedezett útvonalakhoz tartozó kezdeti súly",
+ "appSettings_routeWeightSuccessIncrement": "Sikerhez vezető növelés",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "A sikeresen teljesített útvonalhoz hozzáadott súly.",
+ "appSettings_routeWeightFailureDecrement": "Hibás súly csökkenése",
+ "appSettings_routeWeightFailureDecrementSubtitle": "A jártatásból eltávolított súly, ami a sikertelen szállítás következménye.",
+ "appSettings_maxMessageRetries": "Maximális üzenetek újraküldési próbálkozások",
+ "appSettings_maxMessageRetriesSubtitle": "A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.",
+ "path_routeWeight": "{weight}/{max}",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_battery": "Akku",
+ "appSettings_batteryChemistry": "Aakkum töltés kémia",
+ "appSettings_batteryChemistryPerDevice": "Beállítások {deviceName}-hez",
+ "@appSettings_batteryChemistryPerDevice": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_batteryChemistryConnectFirst": "Csatlakozzon egy eszközhez, hogy kiválassza",
+ "appSettings_batteryNmc": "18650 NMC (3,0-4,2 V)",
+ "appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)",
+ "appSettings_batteryLipo": "LiPo (3,0-4,2 V)",
+ "appSettings_mapDisplay": "Térkép megjelenítése",
+ "appSettings_showRepeaters": "Megismétlés",
+ "appSettings_showRepeatersSubtitle": "A térképen megjelenítsük a repeater-eket.",
+ "appSettings_showChatNodes": "Megjeleníts kommunikációs pontokat",
+ "appSettings_showChatNodesSubtitle": "A chat-szobákat megjelenítsük a térképen",
+ "appSettings_showOtherNodes": "Mutasson további csomópontokat",
+ "appSettings_showOtherNodesSubtitle": "Mutassa meg a többi hálózati elemet a térképen",
+ "appSettings_timeFilter": "Időbeli szűrés",
+ "appSettings_timeFilterShowAll": "Mutassa meg az összes csomópontot",
+ "appSettings_timeFilterShowLast": "Mutasson az utolsó {hours} órából származó adatokat.",
+ "@appSettings_timeFilterShowLast": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "appSettings_mapTimeFilter": "Térkép időszűrő",
+ "appSettings_showNodesDiscoveredWithin": "Megjeleníts olyan węzveket, amelyek a következő területen lettek felfedezve:",
+ "appSettings_allTime": "Minden időpont",
+ "appSettings_lastHour": "Az utolsó óra",
+ "appSettings_last6Hours": "Az utóban 6 óra",
+ "appSettings_last24Hours": "Az utóbbi 24 óra",
+ "appSettings_lastWeek": "A múlt héten",
+ "appSettings_offlineMapCache": "Offline térkép tárolás",
+ "appSettings_unitsTitle": "Egységek",
+ "appSettings_unitsMetric": "Méter (m / kilométer)",
+ "appSettings_unitsImperial": "Királyi (láb / mérföld)",
+ "appSettings_noAreaSelected": "Nincs kiválasztott terület.",
+ "appSettings_areaSelectedZoom": "Kiválasztott terület (zoom: {minZoom}-{maxZoom})",
+ "@appSettings_areaSelectedZoom": {
+ "placeholders": {
+ "minZoom": {
+ "type": "int"
+ },
+ "maxZoom": {
+ "type": "int"
+ }
+ }
+ },
+ "appSettings_debugCard": "Hibakeresés",
+ "appSettings_appDebugLogging": "App-ban történő hibakereséshez használt naplózás",
+ "appSettings_appDebugLoggingSubtitle": "Log alkalmazás hibaelhárítási üzenetek",
+ "appSettings_appDebugLoggingEnabled": "Az alkalmazás hibaelhárítási naplózás engedélyezve",
+ "appSettings_appDebugLoggingDisabled": "Az alkalmazás hibaelhárítási naplózatának bekapcsolása kiküszöbölve",
+ "contacts_title": "Kapcsolatok",
+ "contacts_noContacts": "Jelenleg még nincs kapcsolat.",
+ "contacts_contactsWillAppear": "A kapcsolatok megjelennek, amikor a eszközök hirdetnek.",
+ "contacts_unread": "Olvasatlan",
+ "contacts_searchContactsNoNumber": "Kapcsolatok keresése...",
+ "contacts_searchContacts": "Keresés {number}-ban {str}…",
+ "@contacts_searchContacts": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchFavorites": "Keresés {number}{str}... Kedvencek",
+ "@contacts_searchFavorites": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchUsers": "Search {number}{str} Users...",
+ "@contacts_searchUsers": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchRepeaters": "Keresés {number}-on, {str} típusú adóállomások között...",
+ "@contacts_searchRepeaters": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchRoomServers": "Keresés {number}-ban {str}...",
+ "@contacts_searchRoomServers": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_noUnreadContacts": "Nincs olvasatlan üzenetek",
+ "contacts_noContactsFound": "Nincs megtalálva semmilyen kapcsolat vagy csoport.",
+ "contacts_deleteContact": "Kapcsolattól töröl",
+ "contacts_removeConfirm": "Hogy töröljem a {contactName} nevű személyt a kontaktlistából?",
+ "@contacts_removeConfirm": {
+ "placeholders": {
+ "contactName": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_manageRepeater": "Ellenőriző eszköz kezelése",
+ "contacts_manageRoom": "A szobai szerver kezelése",
+ "contacts_roomLogin": "Szoba szerverbe való bejelentkezés",
+ "contacts_openChat": "Nyitott beszélgetés",
+ "contacts_editGroup": "Edit csoport",
+ "contacts_deleteGroup": "Csoport törlése",
+ "contacts_deleteGroupConfirm": "Hogy töröljem a \"{groupName}\"-t?",
+ "@contacts_deleteGroupConfirm": {
+ "placeholders": {
+ "groupName": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_newGroup": "Új csoport",
+ "contacts_groupName": "Csoport neve",
+ "contacts_groupNameRequired": "A csoportnak meg kell adni a nevét.",
+ "contacts_groupNameReserved": "Ez a csoportnév foglalt",
+ "contacts_groupAlreadyExists": "A \"{name}\" nevű csoport már létezik.",
+ "@contacts_groupAlreadyExists": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_filterContacts": "Szűrj kontaktokat...",
+ "contacts_noContactsMatchFilter": "Nincs találat a megadott szűrés alapján.",
+ "contacts_noMembers": "Nincsenek tagok",
+ "contacts_lastSeenNow": "utóbbi időben",
+ "contacts_lastSeenMinsAgo": "~ {minutes} perc",
+ "@contacts_lastSeenMinsAgo": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "contacts_lastSeenHourAgo": "Kb. 1 óra",
+ "contacts_lastSeenHoursAgo": "~ {hours} óra",
+ "@contacts_lastSeenHoursAgo": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "contacts_lastSeenDayAgo": "Kb. 1 nap",
+ "contacts_lastSeenDaysAgo": "~ {days} days",
+ "@contacts_lastSeenDaysAgo": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_title": "Csatornák",
+ "channels_noChannelsConfigured": "Nincs konfigurált csatorna.",
+ "channels_addPublicChannel": "Hozzon létre nyilvános csatornát",
+ "channels_searchChannels": "Keresési opciók...",
+ "channels_noChannelsFound": "Nincs megtalálható csatorna",
+ "channels_channelIndex": "{index}-os csatorna",
+ "@channels_channelIndex": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_hashtagChannel": "Hashtag-ok közössége",
+ "channels_public": "A nyilvánosság számára",
+ "channels_private": "Személyes",
+ "channels_publicChannel": "Össztávos csatorna",
+ "channels_privateChannel": "Személyes csatorna",
+ "channels_editChannel": "Csatorna szerkesztése",
+ "channels_muteChannel": "Csendes csatorna",
+ "channels_unmuteChannel": "Engedje be a hangot",
+ "channels_deleteChannel": "Mozdony törlése",
+ "channels_deleteChannelConfirm": "Törlés {name}? Ez nem visszafordítható.",
+ "@channels_deleteChannelConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_channelDeleteFailed": "Nem sikerült törölni a \"{name}\" nevű csatornát.",
+ "@channels_channelDeleteFailed": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_channelDeleted": "A \"{name}\" nevű csatorna törölve",
+ "@channels_channelDeleted": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_addChannel": "Csatorna hozzáadása",
+ "channels_channelIndexLabel": "Csatorna index",
+ "channels_channelName": "Csatorna neve",
+ "channels_usePublicChannel": "Használja a nyilvános csatornát",
+ "channels_standardPublicPsk": "Általános, állami által finanszírozott PSK",
+ "channels_pskHex": "PSK (Hexadecimális kód)",
+ "channels_generateRandomPsk": "Véletlenszerűen generáljon PSK-t",
+ "channels_enterChannelName": "Kérjük, adja meg egy csatorna nevét",
+ "channels_pskMustBe32Hex": "A PSK 32-bázisú hexadecimális karakterből áll.",
+ "channels_channelAdded": "A \"{name}\" csatorna hozzáadva",
+ "@channels_channelAdded": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_editChannelTitle": "Módosítsd a csatornát {index}",
+ "@channels_editChannelTitle": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_smazCompression": "SMAZ kompresszió",
+ "channels_channelUpdated": "A {name} csatorna frissítve",
+ "@channels_channelUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_publicChannelAdded": "A nyilvános csatorna hozzáadva",
+ "channels_sortBy": "Szűrés",
+ "channels_sortManual": "Használati útmutató",
+ "channels_sortAZ": "A-Z",
+ "channels_sortLatestMessages": "Legfrissebb üzenetek",
+ "channels_sortUnread": "Olvasatlan",
+ "channels_createPrivateChannel": "Létrehoz egy privát csatornát",
+ "channels_createPrivateChannelDesc": "Titkos kulcs segítségével védelem.",
+ "channels_joinPrivateChannel": "Csatlakozzon egy privát csatornához",
+ "channels_joinPrivateChannelDesc": "Kézzel adja meg a titkos kulcsot.",
+ "channels_joinPublicChannel": "Csatlakozzon a nyilvános csatornához",
+ "channels_joinPublicChannelDesc": "Bárki csatlakozhat ehhez a csatornához.",
+ "channels_joinHashtagChannel": "Csatlakozzon egy hashtage-os csatornához",
+ "channels_joinHashtagChannelDesc": "Bárkinek lehet csatlakoznia a hashtagekhez tartozó csatornához.",
+ "channels_scanQrCode": "Scanned egy QR-kódot",
+ "channels_scanQrCodeComingSoon": "Hamarosan",
+ "channels_enterHashtag": "Írja be a hashtaget",
+ "channels_hashtagHint": "pl. #csapat",
+ "chat_noMessages": "Még nincs üzenet.",
+ "chat_sendMessageToStart": "Küldj egy üzenetet, hogy elindulj!",
+ "chat_originalMessageNotFound": "A eredeti üzenet nem található.",
+ "chat_replyingTo": "Replying to {name}",
+ "@chat_replyingTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_replyTo": "Reply to {name}",
+ "@chat_replyTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_location": "Helyszín",
+ "chat_sendMessageTo": "Küldj üzenetet {contactName}-nek",
+ "@chat_sendMessageTo": {
+ "placeholders": {
+ "contactName": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_typeMessage": "Írjon üzenetet...",
+ "chat_messageTooLong": "A üzenet túl hosszú (a maximális {maxBytes} bájt).",
+ "@chat_messageTooLong": {
+ "placeholders": {
+ "maxBytes": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_messageCopied": "Üzenet másolva",
+ "chat_messageDeleted": "Üzenet törölve",
+ "chat_retryingMessage": "Újrapróbálási üzenet",
+ "chat_retryCount": "Újrapróbál {current}/{max}",
+ "@chat_retryCount": {
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "max": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_sendGif": "Küldj GIF-ot",
+ "chat_reply": "Válasz",
+ "chat_addReaction": "Hozzon létre reakciót",
+ "chat_me": "Én",
+ "emojiCategorySmileys": "Emoji",
+ "emojiCategoryGestures": "Testmozgások",
+ "emojiCategoryHearts": "Szívak",
+ "emojiCategoryObjects": "Tárgyak",
+ "gifPicker_title": "Válasszon egy GIF-et",
+ "gifPicker_searchHint": "GIF-ek keresése...",
+ "gifPicker_poweredBy": "Forrás: GIPHY",
+ "gifPicker_noGifsFound": "Nincsenek GIF-ek megtalálva.",
+ "gifPicker_failedLoad": "Nem sikerült betölteni a GIF-fájlokat.",
+ "gifPicker_failedSearch": "Nem sikerült a GIF-eket megtalálni.",
+ "gifPicker_noInternet": "Nincs internetkapcsolat.",
+ "debugLog_appTitle": "App-debug log",
+ "debugLog_bleTitle": "BLE hibajelentő napló",
+ "debugLog_copyLog": "Másolat napló",
+ "debugLog_clearLog": "Jelzett napló",
+ "debugLog_copied": "Hibajelentő napló másolva",
+ "debugLog_bleCopied": "BLE-log másolva",
+ "debugLog_noEntries": "Jelenleg még nem léteznek hibaelhárítási naplókat.",
+ "debugLog_enableInSettings": "Engedje be az alkalmazás hibaelhárítási naplózását a beállítások menüben.",
+ "debugLog_frames": "Keretek",
+ "debugLog_rawLogRx": "Az eredeti Log-RX",
+ "debugLog_noBleActivity": "Jelenleg nincs BLE-hez kapcsolódó tevékenység.",
+ "debugFrame_length": "Keret hossza: {count} bájt",
+ "@debugFrame_length": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "debugFrame_command": "Parancs: 0x{value}",
+ "@debugFrame_command": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textMessageHeader": "Címzett:",
+ "debugFrame_destinationPubKey": "- Célhely: {pubKey}",
+ "@debugFrame_destinationPubKey": {
+ "placeholders": {
+ "pubKey": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_timestamp": "- Időbélyeg: {timestamp}",
+ "@debugFrame_timestamp": {
+ "placeholders": {
+ "timestamp": {
+ "type": "int"
+ }
+ }
+ },
+ "debugFrame_flags": "- Jelvények: 0x{value}",
+ "@debugFrame_flags": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textType": "- Tartalom típusa: {type} ({label})",
+ "@debugFrame_textType": {
+ "placeholders": {
+ "type": {
+ "type": "int"
+ },
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textTypeCli": "Parancssori felület (CLI)",
+ "debugFrame_textTypePlain": "Egyszerű, alap, hagyományos",
+ "debugFrame_text": "- Tartalom: \"{text}\"",
+ "@debugFrame_text": {
+ "placeholders": {
+ "text": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_hexDump": "Hex-dump:",
+ "chat_pathManagement": "Útvonal-kezelés",
+ "chat_ShowAllPaths": "Mutasson meg minden útvonalat",
+ "chat_routingMode": "Útvonal-kezelési mód",
+ "chat_autoUseSavedPath": "Automatikus (az eddigi útvonal használata)",
+ "chat_forceFloodMode": "Erőforrás-alapú áramlás mód",
+ "chat_recentAckPaths": "Legutóbbi használt útvonalak (gombra kattintva):",
+ "chat_pathHistoryFull": "Az előző lépések listája teljes. Törölj ki a bejegyzéseket, hogy újokat hozzáadhatsd.",
+ "chat_hopSingular": "ugor",
+ "chat_hopPlural": "babér",
+ "chat_hopsCount": "{count} {count, plural, =1{ugrás} other{ugrások}}",
+ "@chat_hopsCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_successes": "sikerek",
+ "chat_removePath": "Törölje a elérési útvonalat",
+ "chat_noPathHistoryYet": "Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.",
+ "chat_pathActions": "Céltúrások:",
+ "chat_setCustomPath": "Beállítsd a saját útvonalat",
+ "chat_setCustomPathSubtitle": "Kézzel megadott útvonal",
+ "chat_clearPath": "Egyértelmű út",
+ "chat_clearPathSubtitle": "A parancs új küldéskor újra kell aktivizálnia.",
+ "chat_pathCleared": "Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.",
+ "chat_floodModeSubtitle": "Használja a \"útvonal\" kapcsolót az alkalmazás sávjában.",
+ "chat_floodModeEnabled": "Árvízvédelmi mód bekapcsolva. A visszaállítás a alkalmazásban található útvonal ikon segítségével.",
+ "chat_fullPath": "Teljes elérési út",
+ "chat_pathDetailsNotAvailable": "Az útvonal részletei még nem elérhetők. Próbálja meg küldeni egy üzenetet, hogy frissítse az információkat.",
+ "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
+ "@chat_pathSetHops": {
+ "placeholders": {
+ "hopCount": {
+ "type": "int"
+ },
+ "status": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_pathSavedLocally": "Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.",
+ "chat_pathDeviceConfirmed": "A készülék megvan.",
+ "chat_pathDeviceNotConfirmed": "A készülék még nem bizonyított.",
+ "chat_type": "Típus",
+ "chat_path": "Út",
+ "chat_publicKey": "Nyelvkönyv",
+ "chat_compressOutgoingMessages": "A küldött üzenetek tömörítése",
+ "chat_floodForced": "Áradás (kényszerített)",
+ "chat_directForced": "Közvetlen (erélyes)",
+ "chat_hopsForced": "{count} ánusz (erővel)",
+ "@chat_hopsForced": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_floodAuto": "Vízosztás (autó)",
+ "chat_direct": "Közvetlen",
+ "chat_poiShared": "Közös erőforrás",
+ "chat_unread": "Olvasatlan: {count}",
+ "@chat_unread": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_openLink": "Nyisd meg a linket?",
+ "chat_openLinkConfirmation": "Szeretnéd megnyitni ezt a linket a böngésződben?",
+ "chat_open": "Nyitott",
+ "chat_couldNotOpenLink": "Nem sikerült megnyitni a hivat: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "Érvénytelen hivatkozás formátum",
+ "map_title": "Grafikus ábrázás",
+ "map_lineOfSight": "Látási vonal",
+ "map_losScreenTitle": "Látási vonal",
+ "map_noNodesWithLocation": "Nincs olyan adatpont, amelyhez helyszín-információk tartoznak.",
+ "map_nodesNeedGps": "A pontoknak meg kell osztaniuk GPS koordinátáikat, hogy megjelenjenek a térképen.",
+ "map_nodesCount": "Csúcsok: {count}",
+ "@map_nodesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "map_pinsCount": "Csapok: {count}",
+ "@map_pinsCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "map_chat": "Csevegés",
+ "map_repeater": "Ismétlő",
+ "map_room": "szoba",
+ "map_sensor": "Érzékelő",
+ "map_pinDm": "Jel (DM)",
+ "map_pinPrivate": "Titkos (privát)",
+ "map_pinPublic": "Jelmez (nyilvános)",
+ "map_lastSeen": "Utoljára látva",
+ "map_disconnectConfirm": "Biztosan szeretné kiírni ezt a készüléket?",
+ "map_from": "Attól",
+ "map_source": "Forrás",
+ "map_flags": "Zászló",
+ "map_shareMarkerHere": "Osztja ezt a tartalmat itt",
+ "map_setAsMyLocation": "Állítsa be a jelenlegi helyzetemként",
+ "map_pinLabel": "Címkét ragasztani",
+ "map_label": "Címke",
+ "map_pointOfInterest": "Érdekes hely",
+ "map_sendToContact": "Kapcsolatfelvételi űrlap",
+ "map_sendToChannel": "Küldés a csatornán",
+ "map_noChannelsAvailable": "Nincs elérhető csatorna.",
+ "map_publicLocationShare": "Térköz, nyilvános hely",
+ "map_publicLocationShareConfirm": "Most egy helyszínt megosztasz a {channelLabel} csatornán. Ez a csatorna nyilvános, és bárki, aki rendelkezik a PSK-val, megtekintheti.",
+ "@map_publicLocationShareConfirm": {
+ "placeholders": {
+ "channelLabel": {
+ "type": "String"
+ }
+ }
+ },
+ "map_connectToShareMarkers": "Kapcsolódjon egy eszközhöz, hogy megoszthassa a vonalzókat.",
+ "map_filterNodes": "Szűrési pontok",
+ "map_nodeTypes": "Vonalak típusai",
+ "map_chatNodes": "Csevegési pontok",
+ "map_repeaters": "Újraküldők",
+ "map_otherNodes": "Egyéb csomópontok",
+ "map_keyPrefix": "Kulcsfontosságú előtag",
+ "map_filterByKeyPrefix": "Szűrj a kulcsos előtér szerint",
+ "map_publicKeyPrefix": "Névfelhasználó kulc-prefix",
+ "map_markers": "Jelölők",
+ "map_showSharedMarkers": "Mutassa meg a közös jeleket",
+ "map_showGuessedLocations": "Megjelenítsa a megjósolt csomópontok helyét",
+ "map_showDiscoveryContacts": "Megjelenítse a Discovery-nál elérhet kontaktokat",
+ "map_guessedLocation": "Tippolt hely",
+ "map_lastSeenTime": "Utoljára megjelent idő",
+ "map_sharedPin": "Gemeinsames PIN-kód",
+ "map_joinRoom": "Csatlakozás a szobához",
+ "map_manageRepeater": "Ellenőriző eszköz kezelése",
+ "map_tapToAdd": "Nyomj meg a csomópontokhoz, hogy hozzáadd őket az útvonalhoz.",
+ "map_runTrace": "Útvonal követés",
+ "map_removeLast": "Törölj utolsó",
+ "map_pathTraceCancelled": "Az útvonal követés megszakadt.",
+ "mapCache_title": "Offline térkép tárolás",
+ "mapCache_selectAreaFirst": "Válasszon egy területet, amelyet először cache-oljon.",
+ "mapCache_noTilesToDownload": "Nincsenek letölthető tile-ok ebben a területben.",
+ "mapCache_downloadTilesTitle": "Letöltsd a tile-okat",
+ "mapCache_downloadTilesPrompt": "Töltse le {count} darab tile-t offline használatra?",
+ "@mapCache_downloadTilesPrompt": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadAction": "Letöltés",
+ "mapCache_cachedTiles": "Tárolt {count} darab",
+ "@mapCache_cachedTiles": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)",
+ "@mapCache_cachedTilesWithFailed": {
+ "placeholders": {
+ "downloaded": {
+ "type": "int"
+ },
+ "failed": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_clearOfflineCacheTitle": "Tiszta offline tárhely",
+ "mapCache_clearOfflineCachePrompt": "Távolítsa el az összes tárolt térképmegjelenítőt?",
+ "mapCache_offlineCacheCleared": "A helyi memóriát töröltük.",
+ "mapCache_noAreaSelected": "Nincs kiválasztott terület.",
+ "mapCache_cacheArea": "Tároló terület",
+ "mapCache_useCurrentView": "Használja a jelenlegi nézetet",
+ "mapCache_zoomRange": "Zoom tartomány",
+ "mapCache_estimatedTiles": "Becsült kerámiák: {count}",
+ "@mapCache_estimatedTiles": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadedTiles": "Letöltve {completed} / {total}",
+ "@mapCache_downloadedTiles": {
+ "placeholders": {
+ "completed": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadTilesButton": "Letöltsd a tile-okat",
+ "mapCache_clearCacheButton": "Ósztótt adatokat",
+ "mapCache_failedDownloads": "Sikertelen letöltések: {count}",
+ "@mapCache_failedDownloads": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
+ "@mapCache_boundsLabel": {
+ "placeholders": {
+ "north": {
+ "type": "String"
+ },
+ "south": {
+ "type": "String"
+ },
+ "east": {
+ "type": "String"
+ },
+ "west": {
+ "type": "String"
+ }
+ }
+ },
+ "time_justNow": "Most",
+ "time_minutesAgo": "{minutes} perckel ezelőtt",
+ "@time_minutesAgo": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "time_hoursAgo": "{hours} óva",
+ "@time_hoursAgo": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "time_daysAgo": "{days}d ago",
+ "@time_daysAgo": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
+ "time_hour": "óra",
+ "time_hours": "órák",
+ "time_day": "nap",
+ "time_days": "napok",
+ "time_week": "het",
+ "time_weeks": "het, hetek",
+ "time_month": "hónap",
+ "time_months": "hónapok",
+ "time_minutes": "percek",
+ "time_allTime": "Bármely időpont",
+ "dialog_disconnect": "Csatlakozást megszakasztani",
+ "dialog_disconnectConfirm": "Biztosan szeretné kiírni ezt a készüléket?",
+ "login_repeaterLogin": "Ismételt bejelentkezés",
+ "login_roomLogin": "Szoba szerverbe való bejelentkezés",
+ "login_password": "Jelszó",
+ "login_enterPassword": "Adja meg a jelszót",
+ "login_savePassword": "Mentse el a jelszót",
+ "login_savePasswordSubtitle": "A jelszó biztonságosan tárolódik ezen a készüléken.",
+ "login_repeaterDescription": "Adja meg a repeater (ismétítő) jelszót, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.",
+ "login_roomDescription": "Adja meg a belépési kódot, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.",
+ "login_routing": "Útvonal meghatározás",
+ "login_routingMode": "Útvonal-kezelési mód",
+ "login_autoUseSavedPath": "Automatikus (az eddigi útvonal használata)",
+ "login_forceFloodMode": "Erőforrás-alapú áramlás mód",
+ "login_managePaths": "Útvonalak kezelése",
+ "login_login": "Bejelentkezés",
+ "login_attempt": "Megpróbálás {current}/{max}-adik",
+ "@login_attempt": {
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "max": {
+ "type": "int"
+ }
+ }
+ },
+ "login_failed": "Belépés sikertelen: {error}",
+ "@login_failed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "login_failedMessage": "Belépés sikertelen. Vagy a jelszó helytelen, vagy a hálózati kapcsolat nem létesül.",
+ "common_reload": "Újra töltés",
+ "common_clear": "Egyértelmű",
+ "path_currentPath": "Jelenlegi útvonal: {path}",
+ "@path_currentPath": {
+ "placeholders": {
+ "path": {
+ "type": "String"
+ }
+ }
+ },
+ "path_usingHopsPath": "{count} {count, plural, =1{ugrás} other{ugrások}} útvonal használata",
+ "@path_usingHopsPath": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "path_enterCustomPath": "Adja meg a saját elérési útvonalat",
+ "path_currentPathLabel": "Jelenlegi útvonal",
+ "path_hexPrefixInstructions": "Adja meg a 2 karakteres hexadecimális előtagokat minden lépéshez, tagolva kommával.",
+ "path_hexPrefixExample": "Példa: A1, F2, 3C (minden csomó az első részét használja a nyilvános kulcsából)",
+ "path_labelHexPrefixes": "Út (hex-prefixek)",
+ "path_helperMaxHops": "A maximális hossz 64 karakter. Minden előző rész 2 hatos számjegyből áll (1 bájt).",
+ "path_selectFromContacts": "Válasszon a kontaktlista elembek közül:",
+ "path_noRepeatersFound": "Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.",
+ "path_customPathsRequire": "Az egyedi útvonalaknak szükségük van átjáró pontokra, amelyek képesek üzeneteket továbbítani.",
+ "path_invalidHexPrefixes": "Érvénytelen hexadecimális előtagok: {prefixes}",
+ "@path_invalidHexPrefixes": {
+ "placeholders": {
+ "prefixes": {
+ "type": "String"
+ }
+ }
+ },
+ "path_tooLong": "Az út túl hosszú. A maximális engedélyezett lépések száma 64.",
+ "path_setPath": "Útvonal meghatározása",
+ "repeater_management": "Adatkapcsolás kezelése",
+ "room_management": "Szoba-szerver kezelés",
+ "repeater_managementTools": "Menedzsmentes eszközök",
+ "repeater_status": "Állapot",
+ "repeater_statusSubtitle": "Megtekintheted a repeater állapotát, statisztikáit és a környező eszközök adatait.",
+ "repeater_telemetry": "Adatvisszaadás",
+ "repeater_telemetrySubtitle": "Tekintsük a szenzorok és a rendszer állapotának adatát",
+ "repeater_cli": "Parancssori felület (CLI)",
+ "repeater_cliSubtitle": "Küldj parancsokat a repeaternek.",
+ "repeater_neighbors": "Szomszédok",
+ "repeater_neighborsSubtitle": "Tekintsük a nullás lépésű szomszédokat.",
+ "repeater_settings": "Beállítások",
+ "repeater_settingsSubtitle": "Állítsa be a repeater paramétereket",
+ "repeater_statusTitle": "Adatkapcsolódás állapot",
+ "repeater_routingMode": "Útvonal-kezelési mód",
+ "repeater_autoUseSavedPath": "Automatikus (az eddigi útvonal használata)",
+ "repeater_forceFloodMode": "Erőforrás-alapú áramlás mód",
+ "repeater_pathManagement": "Útvonal-kezelés",
+ "repeater_refresh": "Újrafriszol",
+ "repeater_statusRequestTimeout": "Az állapotkérés időtúlt.",
+ "repeater_errorLoadingStatus": "Hiba a státusz betöltés közben: {error}",
+ "@repeater_errorLoadingStatus": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_systemInformation": "Rendszerinformációk",
+ "repeater_battery": "Akku",
+ "repeater_clockAtLogin": "Óra (bejelentkezéskor)",
+ "repeater_uptime": "A rendszer elérhetősége",
+ "repeater_queueLength": "Várakozási sor hossza",
+ "repeater_debugFlags": "Hibakeresési beállítások",
+ "repeater_radioStatistics": "Rádió statisztika",
+ "repeater_lastRssi": "Utolsó RSSI érték",
+ "repeater_lastSnr": "Utolsó SNR",
+ "repeater_noiseFloor": "Háttérzaj szint",
+ "repeater_txAirtime": "TX Airtime",
+ "repeater_rxAirtime": "RX Airtime",
+ "repeater_packetStatistics": "Csomagok statisztikája",
+ "repeater_sent": "Elküldve",
+ "repeater_received": "Megérkezett",
+ "repeater_duplicates": "Duplák",
+ "repeater_daysHoursMinsSecs": "{days} days {hours}h {minutes}m {seconds}s",
+ "@repeater_daysHoursMinsSecs": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ },
+ "hours": {
+ "type": "int"
+ },
+ "minutes": {
+ "type": "int"
+ },
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_packetTxTotal": "Összesen: {total}, Árvíz: {flood}, Közvetlen: {direct}",
+ "@repeater_packetTxTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ },
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_packetRxTotal": "Összesen: {total}, Árvíz: {flood}, Közvetlen: {direct}",
+ "@repeater_packetRxTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ },
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_duplicatesFloodDirect": "Áradás: {flood}, Közvetlen: {direct}",
+ "@repeater_duplicatesFloodDirect": {
+ "placeholders": {
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_duplicatesTotal": "Összesen: {total}",
+ "@repeater_duplicatesTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_settingsTitle": "Adatátvisszaadási beállítások",
+ "repeater_basicSettings": "Alapbeállítások",
+ "repeater_repeaterName": "Adóállomás neve",
+ "repeater_repeaterNameHelper": "Ez a repeater neve",
+ "repeater_adminPassword": "Adminisztrátori jelszó",
+ "repeater_adminPasswordHelper": "Teljes jogosultságú jelszó",
+ "repeater_guestPassword": "Vendég felhasználói név/jelszó",
+ "repeater_guestPasswordHelper": "Csak olvasási jogosítást biztosító jelszó",
+ "repeater_radioSettings": "Rádióbeállítások",
+ "repeater_frequencyMhz": "Frekvencia (MHz)",
+ "repeater_frequencyHelper": "300–2500 MHz",
+ "repeater_txPower": "TX Power",
+ "repeater_txPowerHelper": "1-30 dBm",
+ "repeater_bandwidth": "Adatkapacitás",
+ "repeater_spreadingFactor": "Terjesztési tényező",
+ "repeater_codingRate": "Kódolási sebesség",
+ "repeater_locationSettings": "Helyszínbeállítások",
+ "repeater_latitude": "Nyugat–keleti szélesség",
+ "repeater_latitudeHelper": "Desztes fokok (pl. 37,7749)",
+ "repeater_longitude": "hosszúság",
+ "repeater_longitudeHelper": "Desztes fokok (pl. -122.4194)",
+ "repeater_features": "Jellemzők",
+ "repeater_packetForwarding": "Csomagok továbbítás",
+ "repeater_packetForwardingSubtitle": "Engedje, hogy a repeater továbbítsa a csomagokat.",
+ "repeater_guestAccess": "Vendégek számára elérhető",
+ "repeater_guestAccessSubtitle": "Engedje meg a vendégek számára, hogy csak olvassák a tartalmat",
+ "repeater_privacyMode": "Adatvédelem mód",
+ "repeater_privacyModeSubtitle": "Elrejtse a nevét/a helyszínt az űrlapon",
+ "repeater_advertisementSettings": "Reklámbeállítások",
+ "repeater_localAdvertInterval": "Helyi hirdetés időtartama",
+ "repeater_localAdvertIntervalMinutes": "{minutes} perc",
+ "@repeater_localAdvertIntervalMinutes": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_floodAdvertInterval": "Vízosztály-hirdetés időtartama",
+ "repeater_floodAdvertIntervalHours": "{hours} óra",
+ "@repeater_floodAdvertIntervalHours": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_encryptedAdvertInterval": "Kódolt hirdetés-szünet",
+ "repeater_dangerZone": "Veszélyzóna",
+ "repeater_rebootRepeater": "Újraindítás",
+ "repeater_rebootRepeaterSubtitle": "Indítsa újra a repeater-t.",
+ "repeater_rebootRepeaterConfirm": "Biztosan szeretné újraindítani ezt a repeatert?",
+ "repeater_regenerateIdentityKey": "Újra generálja az azonosító kulcsot",
+ "repeater_regenerateIdentityKeySubtitle": "Új nyilvános/személyes kulcs-párt generáljon",
+ "repeater_regenerateIdentityKeyConfirm": "Ez új azonosítást fog létrehozni a repeater számára. Folytatni?",
+ "repeater_eraseFileSystem": "Törölje a fájlrendszert",
+ "repeater_eraseFileSystemSubtitle": "Formázza a duplázó fájlrendszert.",
+ "repeater_eraseFileSystemConfirm": "FIGYELEM: Ez törli az összes adatot a repeater-en. Ez nem visszafordítható!",
+ "repeater_eraseSerialOnly": "Az Erase funkció csak a soros konzolon érhető el.",
+ "repeater_commandSent": "Parancs elküldve: {command}",
+ "@repeater_commandSent": {
+ "placeholders": {
+ "command": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_errorSendingCommand": "Hibás parancs küldés: {error}",
+ "@repeater_errorSendingCommand": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_confirm": "Beküldve",
+ "repeater_settingsSaved": "Beállítások sikeresen mentve",
+ "repeater_errorSavingSettings": "Hibás beállítások mentése: {error}",
+ "@repeater_errorSavingSettings": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_refreshBasicSettings": "Visszaállítás az alapértékekre",
+ "repeater_refreshRadioSettings": "Frissítse a rádió beállításait",
+ "repeater_refreshTxPower": "Újraindítás TX-támogatással",
+ "repeater_refreshLocationSettings": "Újraindítás helyszín beállításokkal",
+ "repeater_refreshPacketForwarding": "Csomagok továbbításának frissítése",
+ "repeater_refreshGuestAccess": "Újraindítás vendégHozzáférés",
+ "repeater_refreshPrivacyMode": "Visszaállítás a magánéletvédő módra",
+ "repeater_refreshAdvertisementSettings": "Újraindítás hirdetés beállítások",
+ "repeater_refreshed": "{label} frissítve",
+ "@repeater_refreshed": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_errorRefreshing": "Hiba a {label} frissítés közben",
+ "@repeater_errorRefreshing": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_cliTitle": "CLI (parancssori felület)",
+ "repeater_debugNextCommand": "Hibakeresés, következő parancs",
+ "repeater_commandHelp": "Segítség",
+ "repeater_clearHistory": "Egyértelmű történet",
+ "repeater_noCommandsSent": "Még egyik parancsot sem küldtünk.",
+ "repeater_typeCommandOrUseQuick": "Írja be a parancsot alább, vagy használja a gyors parancsokat.",
+ "repeater_enterCommandHint": "Írja be a parancsot...",
+ "repeater_previousCommand": "Előző parancs",
+ "repeater_nextCommand": "Következő parancs",
+ "repeater_enterCommandFirst": "Add meg először egy parancsot",
+ "repeater_cliCommandFrameTitle": "CLI parancssor felépítése",
+ "repeater_cliCommandError": "Hiba: {error}",
+ "@repeater_cliCommandError": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_cliQuickGetName": "Kapcsold össze a nevet",
+ "repeater_cliQuickGetRadio": "Szerezd a rádiót",
+ "repeater_cliQuickGetTx": "Szerezd a TX-t",
+ "repeater_cliQuickNeighbors": "Szomszédok",
+ "repeater_cliQuickVersion": "Verzió",
+ "repeater_cliQuickAdvertise": "Hirdetés",
+ "repeater_cliQuickClock": "óra",
+ "repeater_cliHelpAdvert": "Elküldi egy hirdetési csomagot",
+ "repeater_cliHelpReboot": "Újraindítja a készüléket. (Kérjük, vegye figyelembe, hogy valószínűleg \"Időhiba\" üzenetet fog kapni, ami normális)",
+ "repeater_cliHelpClock": "A jelenlegi időt mutatja az egyes eszközök karórája alapján.",
+ "repeater_cliHelpPassword": "Új adminisztrációs jelszót állít be a eszköz számára.",
+ "repeater_cliHelpVersion": "Megjeleníti a készülék verzióját és a szoftver verziószámát.",
+ "repeater_cliHelpClearStats": "Visszaállítja a különböző statisztikai mérőszámokat a nullára.",
+ "repeater_cliHelpSetAf": "Beállítja az idő-szabályozási tényezőt.",
+ "repeater_cliHelpSetTx": "Beállítja a LoRa átviteli teljesítményt dBm-ben (a rendszer újraindításával alkalmazható).",
+ "repeater_cliHelpSetRepeat": "Engedélyezi vagy tiltja meg a repeater szerepet ezen a csomón.",
+ "repeater_cliHelpSetAllowReadOnly": "(Szoba szerver) Ha \"igen\", akkor üres jelszóval történő bejelentkezés engedélyezett lesz, de nem lehet üzeneteket küldeni a szobában. (Csak olvasási funkció)",
+ "repeater_cliHelpSetFloodMax": "Beállítja a bejövő adatcsomagok maximális számát (ha ez a érték nagyobb vagy egyenlő a maximális értékkel, a csomag nem továbbítódik).",
+ "repeater_cliHelpSetIntThresh": "Beállítja az interferencia határértéket (dB-ben). Az alapérték 14. Ha 0-ra állítja, kiküntheti a csatornák közötti interferencia detektálást.",
+ "repeater_cliHelpSetAgcResetInterval": "Beállítja az intervallumot, amely a \"Automatikus gain\" szabályozó újraindításához szükséges. Beállítás értéke 0, ha a funkciót le kell tiltani.",
+ "repeater_cliHelpSetMultiAcks": "Engedélyezi vagy kikapcsolja a „dupla visszaigazolás” funkciót.",
+ "repeater_cliHelpSetAdvertInterval": "Beállítja az időzítő intervallumot percenként, hogy egy helyi (nincs átjáró) hirdetési csomagot küldjen. Beállítás értéke 0, ha a funkciót le szeretné tiltani.",
+ "repeater_cliHelpSetFloodAdvertInterval": "Beállítja az időzítő intervallumot órában, hogy egy \"áramló\" hirdetési üzenetet küldjön. Beállítás értéke 0, ha a funkciót kikapcsolni kell.",
+ "repeater_cliHelpSetGuestPassword": "Beállítja/frissíti a vendég felhasználói fiókot. (Ez lehetővé teszi a visszatérő felhasználók számára, hogy a \"Statistika lekérdezése\" kérést elküldjék)",
+ "repeater_cliHelpSetName": "Megadja az űrlap neve.",
+ "repeater_cliHelpSetLat": "Beállítja az hirdetés térképen megjelenő pont koordinátájának (tizedes fokokban) a latitude-ját.",
+ "repeater_cliHelpSetLon": "Beállítja az hirdetés térképen megjelenő hosszúság koordinátát (tizedes fokokban).",
+ "repeater_cliHelpSetRadio": "Teljesen új rádióparamétereket állít be, és azokat a beállításokba menti. Az alkalmazásához \"újraindítás\" parancs szükséges.",
+ "repeater_cliHelpSetRxDelay": "Beállítások (kísérleti): Alapérték (legalább 1 értékre kell állítani, hogy hatás legyen), amely alapján a fogadott csomagokhoz enyhe késést alkalmazunk, a jelet ereje/pontszám alapján. 0-ra állítva a funkciót lekapcsoljuk.",
+ "repeater_cliHelpSetTxDelay": "Beállítja egy tényezőt, amely a légköri idővel szorozva, egy áramlás-üzem módú csomaghoz, valamint egy véletlenszerű slot-rendszerhez, hogy késleltesse a továbbítását. (az ütközések valószínűségének csökkentése érdekében)",
+ "repeater_cliHelpSetDirectTxDelay": "Hasonló a txdelay-hez, de ebben az esetben egy véletlenszerű késést alkalmazunk a közvetlen módú csomagok továbbításakor.",
+ "repeater_cliHelpSetBridgeEnabled": "Engedélyez/Tiltás a híd funkciójának.",
+ "repeater_cliHelpSetBridgeDelay": "Állíts be egy késleztatást a csomagok újbóli továbbításakor.",
+ "repeater_cliHelpSetBridgeSource": "Döntse el, hogy a híd fogadott vagy elküldött csomagokat fogja-e továbbítani.",
+ "repeater_cliHelpSetBridgeBaud": "Állítsa be a soros kommunikáció sebességét az RS232 hídok számára.",
+ "repeater_cliHelpSetBridgeSecret": "Állítsa be a titkos kapcsolatot az ESPNOW hídokhoz.",
+ "repeater_cliHelpSetAdcMultiplier": "Lehetővé teszi a felhasználónak, hogy egyedi tényezőt állíts be a riportolt akkumulátor feszültségének módosításához (ez csak bizonyos alkatrészeken támogatott).",
+ "repeater_cliHelpTempRadio": "Időjárás szerinti rádióparamétereket állít be a megadott időtartamra, majd visszaállítja az eredeti beállításokat. (Nem menti a beállításokat a beállítások részben).",
+ "repeater_cliHelpSetPerm": "A ACL-t módosítja. Ha a \"permissions\" érték 0, akkor eltávolítja a megfelelő bejegyzést (a pubkey előtag alapján). Új bejegyzést hoz létre, ha a pubkey-hex teljes hossza, és jelenleg nem szerepel az ACL-ben. A bejegyzést frissíti a megfelelő pubkey előtag alapján. A engedélyek különbözőek a különböző firmware szerepek között, de az alsó 2 bit a következő értékeket képviseli: 0 (Vendég), 1 (Csak olvasás), 2 (Olvasás és írás), 3 (Adminisztrátor)",
+ "repeater_cliHelpGetBridgeType": "Kapcsolatok: hid típusú, RS232, ESPNOW",
+ "repeater_cliHelpLogStart": "Elindítja a csomagok naplózását a fájlrendszerbe.",
+ "repeater_cliHelpLogStop": "Megállítja a csomagok naplózását a fájlrendszerbe.",
+ "repeater_cliHelpLogErase": "Törli a fájlrendszerből a csomagok log-fájljait.",
+ "repeater_cliHelpNeighbors": "Mutat egy listát, amely tartalmazza a más repeater-ek által hallott adatok listáját, amelyek 0-hop hirdetések révén érhetők el. Minden sor az alábbi formát követi: id-prefix-hex:timestamp:snr-times-4",
+ "repeater_cliHelpNeighborRemove": "Törli az első, a megadott kulcs-prefix (hexadecimális formában) alapján megegyező bejegyzést a szomszédok listájából.",
+ "repeater_cliHelpRegion": "(sorozat) Lista az összes meghatározott területet és a jelenlegi árvízvédelmi engedélyeket.",
+ "repeater_cliHelpRegionLoad": "FIGYELEM: ez egy speciális, több parancsot tartalmazó futtatás. Minden következő parancs egy területtel kapcsolatos, amely egyenletes szóközökkel (a szülő-gyermek kapcsolatot jelző) megkülönböztethető. A parancs végrehajtása egy üres sor/parancs küldésével történik.",
+ "repeater_cliHelpRegionGet": "Keresések egy adott név előtérrel (vagy \"*\" globális hatókörre). Válasz: \"-> region-név (szülő-név) 'F'\"",
+ "repeater_cliHelpRegionPut": "Hozzáad vagy frissíti egy régió definíciót megadott néven.",
+ "repeater_cliHelpRegionRemove": "Eltávolítja a megadott nevet használó régió-definíciót. (pontosan meg kell egyeznie, és nem lehet gyermekrégiója)",
+ "repeater_cliHelpRegionAllowf": "Beállítja a megadott területre vonatkozó \"víz\" jogosultságot. (A globális/régi beállítások esetén a \"*\" jelölő)",
+ "repeater_cliHelpRegionDenyf": "Eltávolítja a megadott területre vonatkozó \"F\"lood (víz) engedélyt. (FIGYELEM: jelenleg nem javasolt ezt a globális/régi verzióban használni!!)",
+ "repeater_cliHelpRegionHome": "Visszaállítja a jelenlegi „otthoni” régiót. (Ez a beállítás még nem került alkalmazásra, csak jövőbeli használatra fenyelve)",
+ "repeater_cliHelpRegionHomeSet": "Beállítja a \"házi\" régiót.",
+ "repeater_cliHelpRegionSave": "Megőrzi a régió listát/térképet a tárolóban.",
+ "repeater_cliHelpGps": "Megadja a GPS állapotát. Ha a GPS kikapcsolva van, akkor csak \"ki\" választot ad, ha be van, akkor \"be\", \"állapot\", \"pozíció\", \"satellitok száma\" értékeket ad.",
+ "repeater_cliHelpGpsOnOff": "Engedi a GPS működés állapotát.",
+ "repeater_cliHelpGpsSync": "A hálózati időt az GPS óra időjével szinkronizálja.",
+ "repeater_cliHelpGpsSetLoc": "Beállítja a węsz pozícióját GPS koordináták alapján, és menti a beállításokat.",
+ "repeater_cliHelpGpsAdvert": "Adja meg a hirdetés konfigurációjának helyszín-információját:\n- none: ne tartalmazza a helyszínt a hirdetésekben\n- share: megosztja a GPS-helyszínt (SensorManager-ből)\n- prefs: hirdeti a beállításokban tárolt helyszínt",
+ "repeater_cliHelpGpsAdvertSet": "Beállítja a hirdetés helyszín-specifikus beállításait.",
+ "repeater_commandsListTitle": "Parancsok listája",
+ "repeater_commandsListNote": "FIGYELEM: a különböző \"set ...\" parancsok mellett létezik egy \"get ...\" parancs is.",
+ "repeater_general": "Általános",
+ "repeater_settingsCategory": "Beállítások",
+ "repeater_bridge": "Híd",
+ "repeater_logging": "Naplózás",
+ "repeater_neighborsRepeaterOnly": "Szomszédok (Csak ismétlő funkció)",
+ "repeater_regionManagementRepeaterOnly": "Regionális menedzsment (Csak egyirányú kommunikáció)",
+ "repeater_regionNote": "Region-specifikus parancsokat vezettek be a régiók definiálására és a hozzájuk tartozó engedélyek kezelésére.",
+ "repeater_gpsManagement": "GPS-vezérlés",
+ "repeater_gpsNote": "Az GPS-al kapcsolatos funkciók lehetővé teszik a helyszín-személyesítéssel kapcsolatos feladatok kezelését.",
+ "telemetry_receivedData": "Kapott adatokat a szenzorokról",
+ "telemetry_requestTimeout": "Az adatkapcsolati kérés sikertelen.",
+ "telemetry_errorLoading": "Hiba az adatok begyűjtésében: {error}",
+ "@telemetry_errorLoading": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_noData": "Nincsenek elérhető telemetriadatok.",
+ "telemetry_channelTitle": "{channel} csatorna",
+ "@telemetry_channelTitle": {
+ "placeholders": {
+ "channel": {
+ "type": "int"
+ }
+ }
+ },
+ "telemetry_batteryLabel": "Akku",
+ "telemetry_voltageLabel": "Feszültség",
+ "telemetry_mcuTemperatureLabel": "MCU hőmérséklet",
+ "telemetry_temperatureLabel": "Hőmérséklet",
+ "telemetry_currentLabel": "Jelenlegi",
+ "telemetry_batteryValue": "{percent}% / {volts}V",
+ "@telemetry_batteryValue": {
+ "placeholders": {
+ "percent": {
+ "type": "int"
+ },
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_voltageValue": "{volts}V",
+ "@telemetry_voltageValue": {
+ "placeholders": {
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_currentValue": "{amps}A",
+ "@telemetry_currentValue": {
+ "placeholders": {
+ "amps": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_temperatureValue": "{celsius} °C / {fahrenheit} °F",
+ "@telemetry_temperatureValue": {
+ "placeholders": {
+ "celsius": {
+ "type": "String"
+ },
+ "fahrenheit": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_receivedData": "Kapott szomszédok adatait",
+ "neighbors_requestTimedOut": "A szomszédok kérik, hogy tiltsák le a kamerát.",
+ "neighbors_errorLoading": "Hiba a szomszédok betöltésében: {error}",
+ "@neighbors_errorLoading": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_repeatersNeighbors": "Ismétlő eszközök, szomszédok",
+ "neighbors_noData": "Nincsenek elérhető szomszédokról adatok.",
+ "neighbors_unknownContact": "Tudatlan {pubkey}",
+ "@neighbors_unknownContact": {
+ "placeholders": {
+ "pubkey": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_heardAgo": "Értsd: {time} sitten",
+ "@neighbors_heardAgo": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_title": "Csomagok útvonala",
+ "channelPath_viewMap": "Megtekinthető térkép",
+ "channelPath_otherObservedPaths": "Egyéb megfigyelt utak",
+ "channelPath_repeaterHops": "Adat továbbító lépések",
+ "channelPath_noHopDetails": "Ez a csomag nem tartalmaz részletes információkat a \"hop\" (vagy más hasonló) szót használó kifejezésekről.",
+ "channelPath_messageDetails": "Üzenet részletei",
+ "channelPath_senderLabel": "Megküldő",
+ "channelPath_timeLabel": "Idő",
+ "channelPath_repeatsLabel": "Ismétli",
+ "channelPath_pathLabel": "Útvonal {index}",
+ "channelPath_observedLabel": "Megfigyelt",
+ "channelPath_observedPathTitle": "Megfigyelt útvonal: {index} • {hops}",
+ "@channelPath_observedPathTitle": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ },
+ "hops": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_noLocationData": "Nincs helyszínadat.",
+ "channelPath_timeWithDate": "{day}/{month} {time}",
+ "@channelPath_timeWithDate": {
+ "placeholders": {
+ "day": {
+ "type": "int"
+ },
+ "month": {
+ "type": "int"
+ },
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_timeOnly": "{time}",
+ "@channelPath_timeOnly": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_unknownPath": "Megfejt",
+ "channelPath_floodPath": "Árvíz",
+ "channelPath_directPath": "Közvetlen",
+ "channelPath_observedZeroOf": "0-ból {total}",
+ "@channelPath_observedZeroOf": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_observedSomeOf": "{observed} of {total} hops",
+ "@channelPath_observedSomeOf": {
+ "placeholders": {
+ "observed": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_mapTitle": "Útvonal térkép",
+ "channelPath_noRepeaterLocations": "Ez a útvonal nem támogat repeater-t.",
+ "channelPath_primaryPath": "Útvonal {index} (Elsődleges)",
+ "@channelPath_primaryPath": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "@channelPath_pathLabel": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_pathLabelTitle": "Út",
+ "channelPath_observedPathHeader": "Megfigyelt útvonal",
+ "channelPath_selectedPathLabel": "{label} • {prefixes}",
+ "@channelPath_selectedPathLabel": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ },
+ "prefixes": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_noHopDetailsAvailable": "Ez a csomag nem tartalmaz részletes információkat a szállításhoz.",
+ "channelPath_unknownRepeater": "Tudatlan erősítő",
+ "community_title": "Helyi közösség",
+ "community_create": "Teremtsd meg a közösséget",
+ "community_createDesc": "Légyon létre egy új közösséget, és osszák meg QR-kód segítségével.",
+ "community_join": "Csatlakozjon",
+ "community_joinTitle": "Csatlakozzon a közösséghez",
+ "community_joinConfirmation": "Szeretne csatlakozni a közösséghez, {name}?",
+ "@community_joinConfirmation": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_scanQr": "QR-kód olvasó a közösség számára",
+ "community_scanInstructions": "Fordítsa a kamerát egy közösségi QR-kód irányába.",
+ "community_showQr": "Megjelenítse a QR-kódot",
+ "community_publicChannel": "Összetartó, közösségi",
+ "community_hashtagChannel": "Helyi hashtaget",
+ "community_name": "Helyi közösség neve",
+ "community_enterName": "Kérjük, a közösség nevét írja be.",
+ "community_created": "A \"{name}\" nevű közösség létrehozva",
+ "@community_created": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_joined": "Csatlakozott a {name} közösséghez",
+ "@community_joined": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_qrTitle": "Osszpontosítás a közösségben",
+ "community_qrInstructions": "Scanned this QR-kódot, hogy csatlakozhat a {name} csoporthoz.",
+ "@community_qrInstructions": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_hashtagPrivacyHint": "A közösségi hashtagekhez tartozó csatornák csak a közösség tagjai számára érhetők el.",
+ "community_invalidQrCode": "Érvénytelen közösségi QR-kód",
+ "community_alreadyMember": "Már tag vagy",
+ "community_alreadyMemberMessage": "Már tagja {name}-nek.",
+ "@community_alreadyMemberMessage": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_addPublicChannel": "Hozzon létre egy közösségi nyilvános csatornát",
+ "community_addPublicChannelHint": "Automatikusan hozzon létre ezt a csatornát a közösség számára.",
+ "community_noCommunities": "Még egyik közösség sem csatlakozott.",
+ "community_scanOrCreate": "Scelle egy QR-kódot, vagy hozzon létre egy közösséget, hogy elinduljon.",
+ "community_manageCommunities": "Közösségek kezelése",
+ "community_delete": "Hagyományos közösségi élet",
+ "community_deleteConfirm": "Hagyom {name}-et?",
+ "@community_deleteConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_deleteChannelsWarning": "Ezem törli is {count} csatornát és a hozzá tartozó üzeneteket.",
+ "@community_deleteChannelsWarning": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "community_deleted": "A közösség, amely {name}",
+ "@community_deleted": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecret": "Titkos visszaállítás",
+ "community_regenerateSecretConfirm": "Újra kell generálni a titkos kulcsot {name} számára? Minden tagnak be kell szkennelnie az új QR-kódot, hogy továbbra is kommunikálhasson.",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerate": "Újraalakítás",
+ "community_secretRegenerated": "Titkos kulcs megújult {name} számára.",
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_updateSecret": "Frissítési titok",
+ "community_secretUpdated": "Titkos információ frissítve {name} számára",
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_scanToUpdateSecret": "Scanned a új QR-kódot, hogy frissítsük a {name} számára megőrzött titkos információt.",
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_addHashtagChannel": "Adjon egy közösségi hashtaget",
+ "community_addHashtagChannelDesc": "Hozz létre egy hashtage-os csatornát ennek a közösségnek",
+ "community_selectCommunity": "Válasszon közösséget",
+ "community_regularHashtag": "Rendszeres hashtag",
+ "community_regularHashtagDesc": "Önmagas szintű hashtaget (bárki csatlakozhat)",
+ "community_communityHashtag": "Helyi hashtaget",
+ "community_communityHashtagDesc": "Csak a közösség tagjai számára",
+ "community_forCommunity": "{name} számára",
+ "@community_forCommunity": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "listFilter_tooltip": "Szűrés és rendezés",
+ "listFilter_sortBy": "Szűrés",
+ "listFilter_latestMessages": "Legfrissebb üzenetek",
+ "listFilter_heardRecently": "Úgy hallottam, hogy...",
+ "listFilter_az": "A-Z",
+ "listFilter_filters": "Szűrők",
+ "listFilter_all": "Mind",
+ "listFilter_favorites": "Kedvencek",
+ "listFilter_addToFavorites": "Megerősítés kívánságlistára",
+ "listFilter_removeFromFavorites": "Törölj a kedvencekből",
+ "listFilter_users": "Felhasználók",
+ "listFilter_repeaters": "Újraküldők",
+ "listFilter_roomServers": "Szoba-szolgálatok",
+ "listFilter_unreadOnly": "Csak olvasatlan",
+ "listFilter_newGroup": "Új csoport",
+ "pathTrace_you": "Te",
+ "pathTrace_failed": "A útvonal követése sikertelen.",
+ "pathTrace_notAvailable": "Az útvonal követési funkció nem elérhető.",
+ "pathTrace_refreshTooltip": "Út mentesség frissítése.",
+ "pathTrace_someHopsNoLocation": "Egy vagy több búzavirág hiányozik a helyszínéről!",
+ "pathTrace_clearTooltip": "Egyértelmű út.",
+ "losSelectStartEnd": "Válassza ki a kezdő és a végpontokat a LOS-hoz.",
+ "losRunFailed": "A látószög ellenőrzése sikertelen: {error}",
+ "@losRunFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "losClearAllPoints": "Teljesen tisztázzuk az összes pontot",
+ "losRunToViewElevationProfile": "Használja a LOS-t, hogy megtekinthesse a magasságkülönbségek diagramját.",
+ "losMenuTitle": "LOS menü",
+ "losMenuSubtitle": "A térképen található pontok kiválasztására vagy a térképen hosszúra nyomva, hogy egyedi pontokat definiálhassunk.",
+ "losShowDisplayNodes": "Megjelenítsen a megjelenítési egységeket",
+ "losCustomPoints": "Egyedi pontok",
+ "losCustomPointLabel": "Egyedi {index}",
+ "@losCustomPointLabel": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "losPointA": "A pont A",
+ "losPointB": "Pont B",
+ "losAntennaA": "Antenna A: {value} {unit}",
+ "@losAntennaA": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "unit": {
+ "type": "String"
+ }
+ }
+ },
+ "losAntennaB": "Antenna B: {value} {unit}",
+ "@losAntennaB": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "unit": {
+ "type": "String"
+ }
+ }
+ },
+ "losRun": "Futtass a LOS-on",
+ "losNoElevationData": "Nincsenek emelkedési adatok.",
+ "losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}",
+ "@losProfileClear": {
+ "placeholders": {
+ "distance": {
+ "type": "String"
+ },
+ "distanceUnit": {
+ "type": "String"
+ },
+ "clearance": {
+ "type": "String"
+ },
+ "heightUnit": {
+ "type": "String"
+ }
+ }
+ },
+ "losProfileBlocked": "{distance} {distanceUnit}, amelyet {obstruction} akadályoz meg {heightUnit}-ban",
+ "@losProfileBlocked": {
+ "placeholders": {
+ "distance": {
+ "type": "String"
+ },
+ "distanceUnit": {
+ "type": "String"
+ },
+ "obstruction": {
+ "type": "String"
+ },
+ "heightUnit": {
+ "type": "String"
+ }
+ }
+ },
+ "losStatusChecking": "LOS: ellenőrzés...",
+ "losStatusNoData": "LOS: nincs adat",
+ "losStatusSummary": "LOS: {clear}/{total} tisztított, {blocked} blokkolt, {unknown} ismeretlen",
+ "@losStatusSummary": {
+ "placeholders": {
+ "clear": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "blocked": {
+ "type": "int"
+ },
+ "unknown": {
+ "type": "int"
+ }
+ }
+ },
+ "losErrorElevationUnavailable": "Az alábbi minták esetében nem áll rendelkezésre magasságadat.",
+ "losErrorInvalidInput": "Hibás vagy hiányos táblázatok a LOS (Loss of Signal) számításához.",
+ "losRenameCustomPoint": "Állítsa meg a saját pont nevét",
+ "losPointName": "Pont neve",
+ "losShowPanelTooltip": "Megjelenítse a LOS paneelt",
+ "losHidePanelTooltip": "Rejtse el a LOS paneelt",
+ "losElevationAttribution": "Magasságadatok: Open-Meteo (CC BY 4.0)",
+ "losLegendRadioHorizon": "Radio Horizont",
+ "losLegendLosBeam": "LOS jelzés",
+ "losLegendTerrain": "Terület",
+ "losFrequencyLabel": "Hatósság",
+ "losFrequencyInfoTooltip": "Lásd a számítás részleteit",
+ "losFrequencyDialogTitle": "A rádióhullámok hatótávolságának kiszámítása",
+ "losFrequencyDialogDescription": "A {baselineK} értékből kezdve, {baselineFreq} MHz-os frekvencián, a számítás az aktuális {frequencyMHz} MHz-os sávhoz igazítja a k-tényezőt, amely meghatározza a görbös rádióhatótávolság határát.",
+ "@losFrequencyDialogDescription": {
+ "description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
+ "placeholders": {
+ "baselineK": {
+ "type": "double"
+ },
+ "baselineFreq": {
+ "type": "double"
+ },
+ "frequencyMHz": {
+ "type": "double"
+ },
+ "kFactor": {
+ "type": "double"
+ }
+ }
+ },
+ "contacts_pathTrace": "Útvonal követése",
+ "contacts_ping": "Ping",
+ "contacts_repeaterPathTrace": "Az útvonal követése a repeaterig",
+ "contacts_repeaterPing": "Ping-szinkronizáló",
+ "contacts_roomPathTrace": "Kapcsolat a szobai szerverrel",
+ "contacts_roomPing": "Ping-szolgáló szerver",
+ "contacts_chatTraceRoute": "Útvonal meghatározása",
+ "contacts_pathTraceTo": "Keresse meg a {name} címét.",
+ "@contacts_pathTraceTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_clipboardEmpty": "A kiválasztott szöveg üres.",
+ "contacts_invalidAdvertFormat": "Érvénytelen kontaktinformáció",
+ "contacts_contactImported": "Kapcsolat létrejött.",
+ "contacts_contactImportFailed": "Nem sikerült a kapcsolatot importálni.",
+ "contacts_zeroHopAdvert": "Zero Hop reklám",
+ "contacts_floodAdvert": "Árvízre vonatkozó hirdetés",
+ "contacts_copyAdvertToClipboard": "Másolja a hirdetést a kiválasztási ablakba",
+ "contacts_addContactFromClipboard": "Adjon hozzá egy kapcsolatot a kiválasztott listából",
+ "contacts_ShareContact": "Másolja a kapcsolatot a kiválasztóba",
+ "contacts_ShareContactZeroHop": "Ossza meg a kapcsolatot hirdetés segítségével",
+ "contacts_zeroHopContactAdvertSent": "Kapcsolatot a hirdetésen keresztül.",
+ "contacts_zeroHopContactAdvertFailed": "Nem sikerült a kapcsolatot elküldeni.",
+ "contacts_contactAdvertCopied": "A hirdetés másolva a vágólapra.",
+ "contacts_contactAdvertCopyFailed": "Az hirdetés másolása a vágólapra sikertelen.",
+ "notification_activityTitle": "MeshCore tevékenységek",
+ "notification_messagesCount": "{count} {count, plural, =1{üzenet} other{üzenetek}}",
+ "@notification_messagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_channelMessagesCount": "{count} {count, plural, =1{csatornaüzenet} other{csatornaüzenetek}}",
+ "@notification_channelMessagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_newNodesCount": "{count} {count, plural, =1{új csomópont} other{új csomópontok}}",
+ "@notification_newNodesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_newTypeDiscovered": "Új {contactType} megtalálva",
+ "@notification_newTypeDiscovered": {
+ "placeholders": {
+ "contactType": {
+ "type": "String"
+ }
+ }
+ },
+ "notification_receivedNewMessage": "Új üzenetet kaptam",
+ "settings_gpxExportRepeaters": "Külső eszközök / helyi szerver a GPX formátumba",
+ "settings_gpxExportRepeatersSubtitle": "Exportálható repeater/szobaterm-szerver, amely egy GPX fájlban tárolja a helyzetet.",
+ "settings_gpxExportContacts": "GPX export funkciók",
+ "settings_gpxExportContactsSubtitle": "Az export funkció lehetővé teszi, hogy a GPS fájlban megadott helyszínen is megőrizzük az útvonalat.",
+ "settings_gpxExportAll": "Exportálja az összes kapcsolatot GPX formátumban.",
+ "settings_gpxExportAllSubtitle": "Az összes elérhetőséget, amelyekhez egy helyszín tartozik, egy GPX fájlba exportálja.",
+ "settings_gpxExportSuccess": "A GPX fájl sikeresen exportálva lett.",
+ "settings_gpxExportNoContacts": "Nincs exportálható kapcsolatok.",
+ "settings_gpxExportNotAvailable": "Nem támogatott a jelenlegi eszközön/rendszeren.",
+ "settings_gpxExportError": "Hiba történt az export során.",
+ "settings_gpxExportRepeatersRoom": "Adatátvisszaadó eszközök és helyiségi szerverek helyei",
+ "settings_gpxExportChat": "Kapcsolódó helyszínek",
+ "settings_gpxExportAllContacts": "Az összes kapcsolat helyszíne",
+ "settings_gpxExportShareText": "A meshcore-open-ból exportált térkéadatumok",
+ "settings_gpxExportShareSubject": "meshcore-open GPX formátumú térképi adatok export",
+ "snrIndicator_nearByRepeaters": "Helyszíni erősítők",
+ "snrIndicator_lastSeen": "Utoljára, amikor látták",
+ "contactsSettings_title": "Kapcsolatok beállításai",
+ "contactsSettings_autoAddTitle": "Automatikus felfedezés",
+ "contactsSettings_otherTitle": "Egyéb kapcsolattal kapcsolatos beállítások",
+ "contactsSettings_autoAddUsersTitle": "Automatikus felhasználói hozzáadás",
+ "contactsSettings_autoAddUsersSubtitle": "Engedje, hogy a segítő automatikusan hozzáadja az új felhasználókat.",
+ "contactsSettings_autoAddRepeatersTitle": "Automatikus visszatöltés",
+ "contactsSettings_autoAddRepeatersSubtitle": "Engedje, hogy a segítő eszköz automatikusan hozzáadja az új, megtalált jelzőállomásokat.",
+ "contactsSettings_autoAddRoomServersTitle": "Automatikus szobák szerverek hozzáadása",
+ "contactsSettings_autoAddRoomServersSubtitle": "Engedje, hogy a segítő automatikusan hozzáadja az új, megtalált hálózati szervereket.",
+ "contactsSettings_autoAddSensorsTitle": "Automatikus érzékelők hozzáadása",
+ "contactsSettings_autoAddSensorsSubtitle": "Engedje, hogy a kísérő automatikusan hozzáadja az új, megtalált szenzorokat.",
+ "contactsSettings_overwriteOldestTitle": "Felülírja a legrégebbet",
+ "contactsSettings_overwriteOldestSubtitle": "Amikor a névsor telítődik, a legidősebb, de még nem kedvencként jelölt személyt helyettesíti egy újabb.",
+ "discoveredContacts_Title": "Megtalált kapcsolatok",
+ "discoveredContacts_noMatching": "Nincs megegyező kapcsolat.",
+ "discoveredContacts_searchHint": "Keress új kapcsolatokat",
+ "discoveredContacts_contactAdded": "Kapcsolat hozzáadva",
+ "discoveredContacts_addContact": "Adjon személyhez",
+ "discoveredContacts_copyContact": "Másolja a kapcsolatot a vágólapra",
+ "discoveredContacts_deleteContact": "Törölj a feltalált kapcsolatot",
+ "discoveredContacts_deleteContactAll": "Törölj minden megtalált kapcsolatot",
+ "discoveredContacts_deleteContactAllContent": "Biztos, hogy szeretné törölni az összes eddig megtalált kapcsolatot?",
+ "chat_sendCooldown": "Kérjük, várjon egy pillanatot, mielőtt újra elküldené.",
+ "appSettings_jumpToOldestUnread": "Jelentkezzen az legörebb, olvasatlan üzenetre",
+ "appSettings_jumpToOldestUnreadSubtitle": "Amikor egy új csevet indítunk, amelyben vannak olvashatatlan üzenetek, görgessük a listát, hogy a legelső, olvashatatlan üzenet megjelenjen, nem pedig az utolsó.",
+ "appSettings_languageHu": "Magyar",
+ "appSettings_languageJa": "Japán",
+ "appSettings_languageKo": "Koreai",
+ "radioStats_tooltip": "Rádió és hálózati statisztikák",
+ "radioStats_screenTitle": "Rádió statisztikák",
+ "radioStats_notConnected": "Csatlakozzon egy eszközhöz, hogy megtekinthesse a rádió adatok statisztikáit.",
+ "radioStats_firmwareTooOld": "A rádió statisztikákhoz v8 vagy újabb verziójú szoftver szükséges.",
+ "radioStats_waiting": "Adatokra vár…",
+ "radioStats_noiseFloor": "Háttérzaj szint: {noiseDbm} dBm",
+ "@radioStats_noiseFloor": {
+ "placeholders": {
+ "noiseDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_lastRssi": "Utolsó RSSI érték: {rssiDbm} dBm",
+ "@radioStats_lastRssi": {
+ "placeholders": {
+ "rssiDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_lastSnr": "Utolsó SNR: {snr} dB",
+ "@radioStats_lastSnr": {
+ "placeholders": {
+ "snr": {
+ "type": "String"
+ }
+ }
+ },
+ "radioStats_txAir": "TX-es idő (összesen): {seconds} másodperc",
+ "@radioStats_txAir": {
+ "placeholders": {
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_rxAir": "RX használat időtartama (összesen): {seconds} s",
+ "@radioStats_rxAir": {
+ "placeholders": {
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_chartCaption": "Háttérzaj szint (dBm) a legutóbbi minták alapján.",
+ "radioStats_stripNoise": "Háttérzaj szint: {noiseDbm} dBm",
+ "@radioStats_stripNoise": {
+ "placeholders": {
+ "noiseDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_stripWaiting": "Rádió adatok begyűjtése…",
+ "radioStats_settingsTile": "Rádió statisztikák",
+ "radioStats_settingsSubtitle": "Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_denyAll": "Elutasítom",
+ "settings_privacySettingsDescription": "Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.",
+ "settings_privacySubtitle": "Ellenőrizd, hogy milyen információkat osztanak meg.",
+ "settings_privacy": "Adatvédelem beállítások",
+ "settings_allowByContact": "Lehetővé teszi a kapcsolatok kezelését",
+ "settings_allowAll": "Engedje meg mindent",
+ "settings_telemetryBaseMode": "Adatkapcsolati alapállapot",
+ "settings_telemetryLocationMode": "Adatkapcsolási helyszín mód",
+ "settings_telemetryEnvironmentMode": "Adatkapcsolati környezeti mód",
+ "settings_advertLocation": "Reklám megjelenési hely",
+ "settings_advertLocationSubtitle": "A hirdetés tartalmazza a helyszínt.",
+ "settings_multiAck": "Többszöri visszaigazolások: {value}",
+ "settings_telemetryModeUpdated": "A telemetriamód frissítve",
+ "contact_info": "Kapcsolattartási információk",
+ "contact_settings": "Kapcsolat beállítások",
+ "contact_telemetry": "Adatvisszaadás",
+ "contact_lastSeen": "Utoljára, amikor látták",
+ "contact_clearChat": "Tiszta beszélgetés",
+ "contact_teleBase": "Adatgyűjtő központ",
+ "contact_teleBaseSubtitle": "Engedje meg a akkumulátor töltöttségi szintjének és alapvető adatoknak megosztását.",
+ "contact_teleLoc": "Adatkapcsolati helyszín",
+ "contact_teleLocSubtitle": "Engedje meg a helyadatok megosztását",
+ "contact_teleEnv": "Adatkapcsolati környezet",
+ "contact_teleEnvSubtitle": "Engedje meg az érzékelő adatok megosztását",
+ "map_showOverlaps": "Az ismétlő kulcsok ütköznek",
+ "map_runTraceWithReturnPath": "Visszaforduljon az eredeti úton.",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingHidePin": "PIN elrejtése",
+ "scanner_linuxPairingShowPin": "PIN megjelenítése",
+ "scanner_linuxPairingPinTitle": "Bluetooth párosítási PIN",
+ "scanner_linuxPairingPinPrompt": "Adja meg a(z) {deviceName} PIN-kódját (hagyja üresen, ha nincs)."
+}
diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb
index 728eaac..13a9602 100644
--- a/lib/l10n/app_it.arb
+++ b/lib/l10n/app_it.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "La connessione TCP è scaduta.",
"tcpConnectionFailed": "Impossibile stabilire la connessione TCP: {error}",
"map_showDiscoveryContacts": "Mostra Contatti di Discovery",
- "map_setAsMyLocation": "Imposta come la mia posizione"
+ "map_setAsMyLocation": "Imposta come la mia posizione",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacySettingsDescription": "Scegli le informazioni che il tuo dispositivo condivide con gli altri.",
+ "settings_allowByContact": "Consenti in base ai flag di contatto",
+ "settings_telemetryLocationMode": "Modalità di posizionamento telemetrico",
+ "settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
+ "settings_advertLocation": "Posizione dell'annuncio",
+ "settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
+ "settings_privacy": "Impostazioni sulla privacy",
+ "settings_denyAll": "Negare tutto",
+ "settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
+ "settings_allowAll": "Consenti tutto",
+ "contact_info": "Informazioni di Contatto",
+ "settings_telemetryBaseMode": "Modalità di base di telemetria",
+ "contact_teleBase": "Base di telemetria",
+ "contact_teleLoc": "Posizione telemetria",
+ "contact_teleLocSubtitle": "Consenti la condivisione dei dati di posizione",
+ "contact_clearChat": "Cancella chat",
+ "contact_telemetry": "Telemetria",
+ "contact_settings": "Impostazioni di contatto",
+ "contact_lastSeen": "Ultimo accesso",
+ "contact_teleBaseSubtitle": "Consenti la condivisione del livello della batteria e della telemetria di base",
+ "contact_teleEnvSubtitle": "Consenti la condivisione dei dati del sensore ambientale",
+ "contact_teleEnv": "Ambiente di telemetria",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeight": "Peso iniziale del percorso",
+ "appSettings_initialRouteWeightSubtitle": "Peso di partenza per nuovi percorsi",
+ "appSettings_maxRouteWeightSubtitle": "Il peso massimo che un percorso può accumulare grazie a consegne di successo.",
+ "appSettings_maxRouteWeight": "Massimo peso consentito per il percorso",
+ "appSettings_routeWeightSuccessIncrement": "Aumento del peso del successo",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Peso aggiunto a un percorso dopo una consegna riuscita.",
+ "appSettings_routeWeightFailureDecrement": "Riduzione del peso associato al fallimento",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Peso rimosso da un percorso dopo un tentativo di consegna fallito.",
+ "appSettings_maxMessageRetries": "Numero massimo di tentativi di invio del messaggio",
+ "appSettings_maxMessageRetriesSubtitle": "Numero di tentativi di riprova prima di considerare un messaggio come fallito.",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_telemetryModeUpdated": "Modalità telemetria aggiornata",
+ "settings_multiAck": "Multi-ACKs: {value}",
+ "map_showOverlaps": "Sovrapposizioni della chiave ripetitore",
+ "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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "Mostra PIN",
+ "scanner_linuxPairingHidePin": "Nascondi PIN",
+ "scanner_linuxPairingPinTitle": "PIN di associazione Bluetooth",
+ "scanner_linuxPairingPinPrompt": "Inserisci il PIN per {deviceName} (lascia vuoto se non ce n'è)."
}
diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb
new file mode 100644
index 0000000..adb4eea
--- /dev/null
+++ b/lib/l10n/app_ja.arb
@@ -0,0 +1,2059 @@
+{
+ "@@locale": "ja",
+ "appTitle": "MeshCore Open",
+ "nav_contacts": "連絡先",
+ "nav_channels": "チャンネル",
+ "nav_map": "地図",
+ "common_cancel": "キャンセル",
+ "common_ok": "了解",
+ "common_connect": "接続する",
+ "common_unknownDevice": "不明なデバイス",
+ "common_save": "保存",
+ "common_delete": "削除",
+ "common_deleteAll": "すべて削除",
+ "common_close": "閉じる",
+ "common_edit": "編集",
+ "common_add": "追加",
+ "common_settings": "設定",
+ "common_disconnect": "切断する",
+ "common_connected": "接続されている",
+ "common_disconnected": "切断",
+ "common_create": "作成する",
+ "common_continue": "続き",
+ "common_share": "共有する",
+ "common_copy": "コピー",
+ "common_retry": "再試",
+ "common_hide": "隠す",
+ "common_remove": "削除",
+ "common_enable": "有効化する",
+ "common_disable": "無効化する",
+ "common_reboot": "再起動",
+ "common_loading": "読み込み中...",
+ "common_notAvailable": "—",
+ "common_voltageValue": "{volts} V",
+ "@common_voltageValue": {
+ "placeholders": {
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "common_percentValue": "{percent}%",
+ "@common_percentValue": {
+ "placeholders": {
+ "percent": {
+ "type": "int"
+ }
+ }
+ },
+ "scanner_title": "MeshCore オープン",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "ブルートゥース",
+ "connectionChoiceTcpLabel": "TCP",
+ "tcpScreenTitle": "TCP を使用して接続",
+ "tcpHostLabel": "IPアドレス",
+ "tcpHostHint": "192.168.40.10",
+ "tcpPortLabel": "港",
+ "tcpPortHint": "5000",
+ "tcpStatus_notConnected": "エンドポイントを入力し、接続する",
+ "tcpStatus_connectingTo": "{endpoint} への接続中...",
+ "@tcpStatus_connectingTo": {
+ "placeholders": {
+ "endpoint": {
+ "type": "String"
+ }
+ }
+ },
+ "tcpErrorHostRequired": "IPアドレスが必要です。",
+ "tcpErrorPortInvalid": "ポート番号は1から65535の範囲で指定してください。",
+ "tcpErrorUnsupported": "このプラットフォームでは、TCP 転送はサポートされていません。",
+ "tcpErrorTimedOut": "TCP 接続がタイムアウトしました。",
+ "tcpConnectionFailed": "TCP接続に失敗しました:{error}",
+ "@tcpConnectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "usbScreenTitle": "USB経由で接続",
+ "usbScreenSubtitle": "検出されたシリアルデバイスを選択し、MeshCoreノードに直接接続してください。",
+ "usbScreenStatus": "USBデバイスを選択する",
+ "usbScreenNote": "USBシリアルポートは、サポートされているAndroidデバイスおよびデスクトッププラットフォームで利用可能です。",
+ "usbScreenEmptyState": "USBデバイスが見つかりませんでした。「別のUSBデバイスを接続して、再度確認してください。」",
+ "usbErrorPermissionDenied": "USBへのアクセス許可が拒否されました。",
+ "usbErrorDeviceMissing": "選択されたUSBデバイスは、もう利用できません。",
+ "usbErrorInvalidPort": "有効なUSBデバイスを選択してください。",
+ "usbErrorBusy": "別のUSB接続の要求がすでに処理中です。",
+ "usbErrorNotConnected": "USBデバイスは接続されていません。",
+ "usbErrorOpenFailed": "選択したUSBデバイスを開くことができません。",
+ "usbErrorConnectFailed": "選択したUSBデバイスへの接続に失敗しました。",
+ "usbErrorUnsupported": "このプラットフォームでは、USBシリアル通信はサポートされていません。",
+ "usbErrorAlreadyActive": "USB接続はすでに確立されています。",
+ "usbErrorNoDeviceSelected": "USBデバイスは選択されていません。",
+ "usbErrorPortClosed": "USB接続は確立されていません。",
+ "usbErrorConnectTimedOut": "接続がタイムアウトしました。デバイスにUSBコンパニオンファームウェアがインストールされていることを確認してください。",
+ "usbFallbackDeviceName": "ウェブシリアルデバイス",
+ "usbStatus_notConnected": "USBデバイスを選択する",
+ "usbStatus_connecting": "USBデバイスへの接続中...",
+ "usbStatus_searching": "USBデバイスを検索中...",
+ "usbConnectionFailed": "USB接続に失敗しました:{error}",
+ "@usbConnectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_scanning": "デバイスをスキャン中...",
+ "scanner_connecting": "接続中...",
+ "scanner_disconnecting": "切断...",
+ "scanner_notConnected": "接続されていない",
+ "scanner_connectedTo": "{deviceName} に接続",
+ "@scanner_connectedTo": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_searchingDevices": "MeshCoreデバイスの検索",
+ "scanner_tapToScan": "MeshCore デバイスを検索するには、「スキャン」ボタンをタップしてください。",
+ "scanner_connectionFailed": "接続に失敗しました:{error}",
+ "@scanner_connectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_stop": "停止",
+ "scanner_scan": "スキャン",
+ "scanner_bluetoothOff": "Bluetooth はオフになっています",
+ "scanner_bluetoothOffMessage": "Bluetoothを有効にして、デバイスを検索してください。",
+ "scanner_chromeRequired": "Chrome ブラウザが必須です",
+ "scanner_chromeRequiredMessage": "このWebアプリケーションは、Bluetooth機能を利用するために、Google ChromeまたはChromiumベースのブラウザが必要です。",
+ "scanner_enableBluetooth": "Bluetoothを有効にする",
+ "device_quickSwitch": "素早い切り替え",
+ "device_meshcore": "メッシュコア",
+ "settings_title": "設定",
+ "settings_deviceInfo": "デバイス情報",
+ "settings_appSettings": "アプリ設定",
+ "settings_appSettingsSubtitle": "通知、メッセージング、および地図の表示設定",
+ "settings_nodeSettings": "ノード設定",
+ "settings_nodeName": "ノード名",
+ "settings_nodeNameNotSet": "設定されていない",
+ "settings_nodeNameHint": "ノード名を入力してください",
+ "settings_nodeNameUpdated": "氏名変更",
+ "settings_radioSettings": "ラジオ設定",
+ "settings_radioSettingsSubtitle": "周波数、電力、スプレッドファクター",
+ "settings_radioSettingsUpdated": "ラジオの設定が更新されました",
+ "settings_location": "場所",
+ "settings_locationSubtitle": "GPS 座標",
+ "settings_locationUpdated": "場所とGPS設定が更新されました",
+ "settings_locationBothRequired": "緯度と経度をそれぞれ入力してください。",
+ "settings_locationInvalid": "無効な緯度または経度。",
+ "settings_locationGPSEnable": "GPS機能有効",
+ "settings_locationGPSEnableSubtitle": "GPSが自動的に位置情報を更新できるようにする。",
+ "settings_locationIntervalSec": "GPS データの取得間隔(秒)",
+ "settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。",
+ "settings_latitude": "緯度",
+ "settings_longitude": "経度",
+ "settings_contactSettings": "連絡設定",
+ "settings_contactSettingsSubtitle": "連絡先を追加する設定",
+ "settings_privacyMode": "プライバシーモード",
+ "settings_privacyModeSubtitle": "広告に名前/場所を記載しない",
+ "settings_privacyModeToggle": "プライバシーモードをオンにして、広告に表示される名前や場所を非表示にします。",
+ "settings_privacyModeEnabled": "プライバシーモードが有効になっています",
+ "settings_privacyModeDisabled": "プライバシーモードは無効化されています",
+ "settings_actions": "行動",
+ "settings_sendAdvertisement": "広告を送信する",
+ "settings_sendAdvertisementSubtitle": "現在、放送での活動",
+ "settings_advertisementSent": "広告が送信されました",
+ "settings_syncTime": "同期時間",
+ "settings_syncTimeSubtitle": "デバイスの時刻を、携帯電話の時刻に合わせる",
+ "settings_timeSynchronized": "時間同期",
+ "settings_refreshContacts": "連絡先を更新する",
+ "settings_refreshContactsSubtitle": "デバイスから連絡先リストを再読み込みする",
+ "settings_rebootDevice": "デバイスを再起動する",
+ "settings_rebootDeviceSubtitle": "MeshCore デバイスを再起動する",
+ "settings_rebootDeviceConfirm": "本当にデバイスを再起動したいですか? その場合、接続が切断されます。",
+ "settings_debug": "デバッグ",
+ "settings_bleDebugLog": "BLE デバッグログ",
+ "settings_bleDebugLogSubtitle": "BLEコマンド、応答、および生のデータ",
+ "settings_appDebugLog": "アプリケーションのデバッグログ",
+ "settings_appDebugLogSubtitle": "アプリケーションのデバッグメッセージ",
+ "settings_about": "概要",
+ "settings_aboutVersion": "MeshCore Open {version}版",
+ "@settings_aboutVersion": {
+ "placeholders": {
+ "version": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_aboutLegalese": "2026年のMeshCoreオープンソースプロジェクト",
+ "settings_aboutDescription": "MeshCore LoRaメッシュネットワークデバイス用の、オープンソースのFlutterクライアント。",
+ "settings_aboutOpenMeteoAttribution": "LOS 標高データ:Open-Meteo (CC BY 4.0)",
+ "settings_infoName": "名前",
+ "settings_infoId": "ID",
+ "settings_infoStatus": "ステータス",
+ "settings_infoBattery": "バッテリー",
+ "settings_infoPublicKey": "公開鍵",
+ "settings_infoContactsCount": "連絡先数",
+ "settings_infoChannelCount": "チャンネル数",
+ "settings_presets": "プリセット",
+ "settings_frequency": "周波数 (MHz)",
+ "settings_frequencyHelper": "300.0 - 2500.0",
+ "settings_frequencyInvalid": "無効な周波数 (300-2500 MHz)",
+ "settings_bandwidth": "帯域幅",
+ "settings_spreadingFactor": "伝播係数",
+ "settings_codingRate": "コーディング速度",
+ "settings_txPower": "TX 信号電力 (dBm)",
+ "settings_txPowerHelper": "0 - 22",
+ "settings_txPowerInvalid": "無効な送信電力 (0-22 dBm)",
+ "settings_clientRepeat": "オフグリッド(電力網から孤立した状態)の繰り返し",
+ "settings_clientRepeatSubtitle": "このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。",
+ "settings_clientRepeatFreqWarning": "オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。",
+ "settings_error": "エラー:{message}",
+ "@settings_error": {
+ "placeholders": {
+ "message": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_title": "アプリ設定",
+ "appSettings_appearance": "外観",
+ "appSettings_theme": "テーマ",
+ "appSettings_themeSystem": "システムデフォルト",
+ "appSettings_themeLight": "光",
+ "appSettings_themeDark": "暗い",
+ "appSettings_language": "言語",
+ "appSettings_languageSystem": "システムデフォルト",
+ "appSettings_languageEn": "英語",
+ "appSettings_languageFr": "フランス語",
+ "appSettings_languageEs": "スペイン語",
+ "appSettings_languageDe": "ドイツ語",
+ "appSettings_languagePl": "ポーランド語",
+ "appSettings_languageSl": "スロベニア語",
+ "appSettings_languagePt": "ポルトガル語",
+ "appSettings_languageIt": "イタリア語",
+ "appSettings_languageZh": "中国語",
+ "appSettings_languageSv": "スウェーデン語",
+ "appSettings_languageNl": "オランダ語",
+ "appSettings_languageSk": "スロベニア語",
+ "appSettings_languageBg": "ブルガリア語",
+ "appSettings_languageRu": "ロシア語",
+ "appSettings_languageUk": "ウクライナ語",
+ "appSettings_enableMessageTracing": "メッセージ追跡機能を有効にする",
+ "appSettings_enableMessageTracingSubtitle": "メッセージに関する詳細な経路およびタイミングに関するメタデータを表示する",
+ "appSettings_notifications": "通知",
+ "appSettings_enableNotifications": "通知を有効にする",
+ "appSettings_enableNotificationsSubtitle": "メッセージや広告に関する通知を受け取る",
+ "appSettings_notificationPermissionDenied": "通知の許可が拒否されました",
+ "appSettings_notificationsEnabled": "通知機能が有効になっています",
+ "appSettings_notificationsDisabled": "通知が無効化されています",
+ "appSettings_messageNotifications": "メッセージ通知",
+ "appSettings_messageNotificationsSubtitle": "新しいメッセージを受信した際に、通知を表示する",
+ "appSettings_channelMessageNotifications": "チャネルメッセージの通知",
+ "appSettings_channelMessageNotificationsSubtitle": "チャンネルからのメッセージを受信した際に、通知を表示する",
+ "appSettings_advertisementNotifications": "広告通知",
+ "appSettings_advertisementNotificationsSubtitle": "新しいノードが発見された場合に通知を表示する",
+ "appSettings_messaging": "メッセージング",
+ "appSettings_clearPathOnMaxRetry": "マックスリトライでの明確な手順",
+ "appSettings_clearPathOnMaxRetrySubtitle": "5回送信に失敗した場合、連絡経路をリセットする",
+ "appSettings_pathsWillBeCleared": "5回失敗した後、経路が再開されます。",
+ "appSettings_pathsWillNotBeCleared": "パスは自動で削除されません。",
+ "appSettings_autoRouteRotation": "自動ルートの切り替え",
+ "appSettings_autoRouteRotationSubtitle": "最適なルートと、洪水モードを切り替える",
+ "appSettings_autoRouteRotationEnabled": "自動ルートの切り替え機能が有効になっています",
+ "appSettings_autoRouteRotationDisabled": "自動ルートの変更機能が無効になっています。",
+ "appSettings_maxRouteWeight": "最大ルート重量",
+ "appSettings_maxRouteWeightSubtitle": "ある経路が、成功裏に配送された場合に、積み上げられる最大重量",
+ "appSettings_initialRouteWeight": "初期ルートの重み",
+ "appSettings_initialRouteWeightSubtitle": "新たに発見された経路の初期重量",
+ "appSettings_routeWeightSuccessIncrement": "成功時の重み増加",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "配送が成功した場合に、経路に追加される重量",
+ "appSettings_routeWeightFailureDecrement": "失敗時の重み減少",
+ "appSettings_routeWeightFailureDecrementSubtitle": "配送に失敗した際に、経路から取り除かれた重量",
+ "appSettings_maxMessageRetries": "最大メッセージ再試行回数",
+ "appSettings_maxMessageRetriesSubtitle": "メッセージを「失敗」とマークするまでの、再試行回数",
+ "path_routeWeight": "{weight}/{max}",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_battery": "バッテリー",
+ "appSettings_batteryChemistry": "電池の化学",
+ "appSettings_batteryChemistryPerDevice": "{deviceName} 単位",
+ "@appSettings_batteryChemistryPerDevice": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_batteryChemistryConnectFirst": "デバイスを選択するために接続する",
+ "appSettings_batteryNmc": "18650型 NMC (3.0-4.2V)",
+ "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)",
+ "appSettings_batteryLipo": "LiPo (3.0-4.2V)",
+ "appSettings_mapDisplay": "地図の表示",
+ "appSettings_showRepeaters": "繰り返し再生機能",
+ "appSettings_showRepeatersSubtitle": "地図上にリピーターノードを表示する",
+ "appSettings_showChatNodes": "チャットノードの表示",
+ "appSettings_showChatNodesSubtitle": "地図上にチャットノードを表示する",
+ "appSettings_showOtherNodes": "他のノードを表示する",
+ "appSettings_showOtherNodesSubtitle": "地図上に、他のノードの種類を表示する",
+ "appSettings_timeFilter": "時間フィルター",
+ "appSettings_timeFilterShowAll": "すべてのノードを表示する",
+ "appSettings_timeFilterShowLast": "過去 {hours} 時間のノードを表示する",
+ "@appSettings_timeFilterShowLast": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "appSettings_mapTimeFilter": "地図の表示期間を絞り込む",
+ "appSettings_showNodesDiscoveredWithin": "以下の範囲内で発見されたノードを表示する:",
+ "appSettings_allTime": "すべての期間",
+ "appSettings_lastHour": "直前の",
+ "appSettings_last6Hours": "過去6時間",
+ "appSettings_last24Hours": "過去24時間",
+ "appSettings_lastWeek": "先週",
+ "appSettings_offlineMapCache": "オフライン用地図キャッシュ",
+ "appSettings_unitsTitle": "単位",
+ "appSettings_unitsMetric": "メートル (m) / キロメートル (km)",
+ "appSettings_unitsImperial": "帝国 (フィート / マイル)",
+ "appSettings_noAreaSelected": "選択されたエリアはありません",
+ "appSettings_areaSelectedZoom": "選択された範囲(ズームレベル:{minZoom}~{maxZoom})",
+ "@appSettings_areaSelectedZoom": {
+ "placeholders": {
+ "minZoom": {
+ "type": "int"
+ },
+ "maxZoom": {
+ "type": "int"
+ }
+ }
+ },
+ "appSettings_debugCard": "デバッグ",
+ "appSettings_appDebugLogging": "アプリケーションのデバッグ用ログ",
+ "appSettings_appDebugLoggingSubtitle": "ログアプリのデバッグメッセージ(トラブルシューティング用)",
+ "appSettings_appDebugLoggingEnabled": "アプリケーションのデバッグ用ログ機能が有効になっています。",
+ "appSettings_appDebugLoggingDisabled": "アプリケーションのデバッグログが無効化されています。",
+ "contacts_title": "連絡先",
+ "contacts_noContacts": "現時点では、連絡先はまだありません。",
+ "contacts_contactsWillAppear": "デバイスが広告を行う際に、連絡先が表示されます。",
+ "contacts_unread": "未読",
+ "contacts_searchContactsNoNumber": "連絡先を検索...",
+ "contacts_searchContacts": "{number}件の{str}に関する連絡先を検索...",
+ "@contacts_searchContacts": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchFavorites": "{number}件の{str}を検索...",
+ "@contacts_searchFavorites": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchUsers": "{number}件の{str}に関するユーザーを検索する...",
+ "@contacts_searchUsers": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchRepeaters": "{number} {str} までの検索...",
+ "@contacts_searchRepeaters": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchRoomServers": "{number} {str} 部屋のサーバーを検索する...",
+ "@contacts_searchRoomServers": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_noUnreadContacts": "未読の連絡先はありません",
+ "contacts_noContactsFound": "連絡先またはグループは見つかりませんでした。",
+ "contacts_deleteContact": "連絡先を削除",
+ "contacts_removeConfirm": "{contactName} を連絡先から削除しますか?",
+ "@contacts_removeConfirm": {
+ "placeholders": {
+ "contactName": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_manageRepeater": "リピーターの管理",
+ "contacts_manageRoom": "ルームサーバーの管理",
+ "contacts_roomLogin": "ルームサーバーへのログイン",
+ "contacts_openChat": "自由な会話",
+ "contacts_editGroup": "編集グループ",
+ "contacts_deleteGroup": "グループを削除",
+ "contacts_deleteGroupConfirm": "{groupName} を削除しますか?",
+ "@contacts_deleteGroupConfirm": {
+ "placeholders": {
+ "groupName": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_newGroup": "新しいグループ",
+ "contacts_groupName": "グループ名",
+ "contacts_groupNameRequired": "グループ名が必須です",
+ "contacts_groupNameReserved": "このグループ名はすでに使用されています。",
+ "contacts_groupAlreadyExists": "グループ「{name}」はすでに存在しています",
+ "@contacts_groupAlreadyExists": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_filterContacts": "連絡先をフィルタリングする…",
+ "contacts_noContactsMatchFilter": "指定された条件に合致する連絡先は見つかりませんでした。",
+ "contacts_noMembers": "メンバーはいない",
+ "contacts_lastSeenNow": "最近",
+ "contacts_lastSeenMinsAgo": "~{minutes} 分",
+ "@contacts_lastSeenMinsAgo": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "contacts_lastSeenHourAgo": "約1時間",
+ "contacts_lastSeenHoursAgo": "~ {hours} 時間",
+ "@contacts_lastSeenHoursAgo": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "contacts_lastSeenDayAgo": "~1日",
+ "contacts_lastSeenDaysAgo": "~{days}日間",
+ "@contacts_lastSeenDaysAgo": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_title": "チャンネル",
+ "channels_noChannelsConfigured": "設定されたチャンネルがありません",
+ "channels_addPublicChannel": "パブリックチャンネルを追加する",
+ "channels_searchChannels": "検索オプション...",
+ "channels_noChannelsFound": "チャンネルが見つかりませんでした",
+ "channels_channelIndex": "チャンネル {index}",
+ "@channels_channelIndex": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_hashtagChannel": "ハッシュタグチャンネル",
+ "channels_public": "一般の人々",
+ "channels_private": "個人の",
+ "channels_publicChannel": "一般チャンネル",
+ "channels_privateChannel": "プライベートチャンネル",
+ "channels_editChannel": "チャンネルを編集する",
+ "channels_muteChannel": "ミュート機能",
+ "channels_unmuteChannel": "ミュートを解除する",
+ "channels_deleteChannel": "チャンネルを削除する",
+ "channels_deleteChannelConfirm": "{name} を削除しますか? これは取り消すことができません。",
+ "@channels_deleteChannelConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_channelDeleteFailed": "チャンネル「{name}」の削除に失敗しました。",
+ "@channels_channelDeleteFailed": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_channelDeleted": "チャンネル「{name}」が削除されました",
+ "@channels_channelDeleted": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_addChannel": "チャンネルを追加",
+ "channels_channelIndexLabel": "チャンネルインデックス",
+ "channels_channelName": "チャンネル名",
+ "channels_usePublicChannel": "パブリックチャンネルを使用する",
+ "channels_standardPublicPsk": "標準的な公用 PSK",
+ "channels_pskHex": "PSK (ヘックス)",
+ "channels_generateRandomPsk": "ランダムなPSK(正交符号分割変調)を生成する",
+ "channels_enterChannelName": "チャンネル名を入力してください",
+ "channels_pskMustBe32Hex": "PSKは32桁の16進数で構成されている必要があります。",
+ "channels_channelAdded": "チャンネル「{name}」を追加",
+ "@channels_channelAdded": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_editChannelTitle": "チャンネル {index} の編集",
+ "@channels_editChannelTitle": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_smazCompression": "SMAZ 圧縮",
+ "channels_channelUpdated": "チャンネル「{name}」が更新されました",
+ "@channels_channelUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_publicChannelAdded": "パブリックチャンネルが追加されました",
+ "channels_sortBy": "並び替え",
+ "channels_sortManual": "マニュアル",
+ "channels_sortAZ": "AからZ",
+ "channels_sortLatestMessages": "最新のメッセージ",
+ "channels_sortUnread": "未読",
+ "channels_createPrivateChannel": "プライベートチャンネルを作成する",
+ "channels_createPrivateChannelDesc": "秘密鍵を使用して保護されています。",
+ "channels_joinPrivateChannel": "プライベートチャンネルに参加する",
+ "channels_joinPrivateChannelDesc": "手動で秘密のキーを入力する。",
+ "channels_joinPublicChannel": "公開チャンネルに参加する",
+ "channels_joinPublicChannelDesc": "このチャンネルには、誰でも参加できます。",
+ "channels_joinHashtagChannel": "ハッシュタグチャンネルに参加する",
+ "channels_joinHashtagChannelDesc": "誰でもハッシュタグチャンネルに参加できます。",
+ "channels_scanQrCode": "QRコードをスキャンする",
+ "channels_scanQrCodeComingSoon": "近日公開",
+ "channels_enterHashtag": "ハッシュタグを入力してください",
+ "channels_hashtagHint": "例:#チーム",
+ "chat_noMessages": "まだメッセージは届いていません",
+ "chat_sendMessageToStart": "開始するためにメッセージを送信してください",
+ "chat_originalMessageNotFound": "元のメッセージが見つかりませんでした",
+ "chat_replyingTo": "{name} への返信",
+ "@chat_replyingTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_replyTo": "{name}への返信",
+ "@chat_replyTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_location": "場所",
+ "chat_sendMessageTo": "{contactName} へのメッセージを送信する",
+ "@chat_sendMessageTo": {
+ "placeholders": {
+ "contactName": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_typeMessage": "メッセージを入力してください…",
+ "chat_messageTooLong": "メッセージが長すぎる({maxBytes} バイトを超える)。",
+ "@chat_messageTooLong": {
+ "placeholders": {
+ "maxBytes": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_messageCopied": "メッセージがコピーされました",
+ "chat_messageDeleted": "メッセージは削除されました",
+ "chat_retryingMessage": "再試行メッセージ",
+ "chat_retryCount": "{current} / {max} 回目",
+ "@chat_retryCount": {
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "max": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_sendGif": "GIFを送信する",
+ "chat_reply": "返信",
+ "chat_addReaction": "反応を追加",
+ "chat_me": "私",
+ "emojiCategorySmileys": "笑顔の絵文字",
+ "emojiCategoryGestures": "身振り、動作",
+ "emojiCategoryHearts": "心",
+ "emojiCategoryObjects": "対象物",
+ "gifPicker_title": "GIF を選択してください",
+ "gifPicker_searchHint": "GIFの検索...",
+ "gifPicker_poweredBy": "GIPHYによる提供",
+ "gifPicker_noGifsFound": "GIF形式のファイルは見つかりませんでした",
+ "gifPicker_failedLoad": "GIFファイルの読み込みに失敗しました",
+ "gifPicker_failedSearch": "GIFファイルの検索に失敗しました",
+ "gifPicker_noInternet": "インターネット接続なし",
+ "debugLog_appTitle": "アプリケーションのデバッグログ",
+ "debugLog_bleTitle": "BLE デバッグログ",
+ "debugLog_copyLog": "記録",
+ "debugLog_clearLog": "詳細なログ",
+ "debugLog_copied": "デバッグログをコピー",
+ "debugLog_bleCopied": "BLEログのコピー",
+ "debugLog_noEntries": "デバッグログはまだ生成されていません",
+ "debugLog_enableInSettings": "アプリのデバッグログを有効にするには、設定から操作してください。",
+ "debugLog_frames": "フレーム",
+ "debugLog_rawLogRx": "生のログ-RX",
+ "debugLog_noBleActivity": "現時点では、BLE関連の活動は行われていません。",
+ "debugFrame_length": "フレーム長: {count} バイト",
+ "@debugFrame_length": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "debugFrame_command": "コマンド: 0x{value}",
+ "@debugFrame_command": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textMessageHeader": "テキストメッセージ用フレーム:",
+ "debugFrame_destinationPubKey": "- 宛先公開鍵: {pubKey}",
+ "@debugFrame_destinationPubKey": {
+ "placeholders": {
+ "pubKey": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_timestamp": "- タイムスタンプ: {timestamp}",
+ "@debugFrame_timestamp": {
+ "placeholders": {
+ "timestamp": {
+ "type": "int"
+ }
+ }
+ },
+ "debugFrame_flags": "- フラグ: 0x{value}",
+ "@debugFrame_flags": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textType": "- テキストの種類: {type} ({label})",
+ "@debugFrame_textType": {
+ "placeholders": {
+ "type": {
+ "type": "int"
+ },
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textTypeCli": "CLI(コマンドラインインターフェース)",
+ "debugFrame_textTypePlain": "シンプルな",
+ "debugFrame_text": "- テキスト:「{text}」",
+ "@debugFrame_text": {
+ "placeholders": {
+ "text": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_hexDump": "ヘックスダンプ:",
+ "chat_pathManagement": "経路管理",
+ "chat_ShowAllPaths": "すべての経路を表示",
+ "chat_routingMode": "ルーティングモード",
+ "chat_autoUseSavedPath": "自動 (保存されたパスを使用)",
+ "chat_forceFloodMode": "強制的に洪水モードを起動",
+ "chat_recentAckPaths": "最近使用したACKパス(タップして使用):",
+ "chat_pathHistoryFull": "パスの履歴は完全です。エントリを削除して、新しいものを追加できます。",
+ "chat_hopSingular": "ジャンプ",
+ "chat_hopPlural": "ホップ",
+ "chat_hopsCount": "{count} {count, plural, =1{ホップ} other{ホップ}}",
+ "@chat_hopsCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_successes": "成功事例",
+ "chat_removePath": "パスを削除する",
+ "chat_noPathHistoryYet": "まだ履歴はありません。\nパスを特定するためにメッセージを送信してください。",
+ "chat_pathActions": "パスの操作:",
+ "chat_setCustomPath": "カスタムパスを設定",
+ "chat_setCustomPathSubtitle": "手動で経路を指定する",
+ "chat_clearPath": "明確な道",
+ "chat_clearPathSubtitle": "次回送信時に、以前の情報を再取得する",
+ "chat_pathCleared": "経路が確保されました。次のメッセージでルートを再確認します。",
+ "chat_floodModeSubtitle": "アプリのバーにあるルーティング切り替え機能を使用する",
+ "chat_floodModeEnabled": "洪水モードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。",
+ "chat_fullPath": "フルパス",
+ "chat_pathDetailsNotAvailable": "経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。",
+ "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
+ "@chat_pathSetHops": {
+ "placeholders": {
+ "hopCount": {
+ "type": "int"
+ },
+ "status": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_pathSavedLocally": "ローカルで保存。同期のために接続する。",
+ "chat_pathDeviceConfirmed": "デバイスの確認済み。",
+ "chat_pathDeviceNotConfirmed": "デバイスの確認はまだできていません。",
+ "chat_type": "種類",
+ "chat_path": "道",
+ "chat_publicKey": "公開鍵",
+ "chat_compressOutgoingMessages": "送信されるメッセージを圧縮する",
+ "chat_floodForced": "洪水(強制的な)",
+ "chat_directForced": "直接的な(強制的な)",
+ "chat_hopsForced": "{count} 本のホップ(強制的に採取)",
+ "@chat_hopsForced": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_floodAuto": "洪水 (自動)",
+ "chat_direct": "直接",
+ "chat_poiShared": "共有されたPOI",
+ "chat_unread": "未読: {count}",
+ "@chat_unread": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_openLink": "リンクを開く?",
+ "chat_openLinkConfirmation": "このリンクをブラウザで開くことはご希望ですか?",
+ "chat_open": "開く",
+ "chat_couldNotOpenLink": "リンクを開けられませんでした: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "無効なリンク形式",
+ "map_title": "ノードマップ",
+ "map_lineOfSight": "視界",
+ "map_losScreenTitle": "視界",
+ "map_noNodesWithLocation": "位置情報データを持つノードは存在しません",
+ "map_nodesNeedGps": "ノードは、地図上に表示されるために、GPS座標を共有する必要があります。",
+ "map_nodesCount": "ノード:{count}",
+ "@map_nodesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "map_pinsCount": "ピン:{count}個",
+ "@map_pinsCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "map_chat": "チャット",
+ "map_repeater": "繰り返し送信装置",
+ "map_room": "部屋",
+ "map_sensor": "センサー",
+ "map_pinDm": "ピン(DM)",
+ "map_pinPrivate": "プライベート(非公開)",
+ "map_pinPublic": "公開 (一般公開)",
+ "map_lastSeen": "最後に確認された場所",
+ "map_disconnectConfirm": "本当にこのデバイスとの接続を解除したいですか?",
+ "map_from": "~から",
+ "map_source": "出典",
+ "map_flags": "旗",
+ "map_shareMarkerHere": "この場所でシェア",
+ "map_setAsMyLocation": "現在地として設定",
+ "map_pinLabel": "ピンラベル",
+ "map_label": "ラベル",
+ "map_pointOfInterest": "注目すべき点",
+ "map_sendToContact": "連絡先へ送信",
+ "map_sendToChannel": "特定のチャンネルに送信する",
+ "map_noChannelsAvailable": "利用可能なチャンネルはありません",
+ "map_publicLocationShare": "公共スペースの共有",
+ "map_publicLocationShareConfirm": "現在、{channelLabel} で位置情報を共有する準備をしています。このチャンネルは公開されており、PSK を持つ誰でも閲覧できます。",
+ "@map_publicLocationShareConfirm": {
+ "placeholders": {
+ "channelLabel": {
+ "type": "String"
+ }
+ }
+ },
+ "map_connectToShareMarkers": "他のデバイスと接続して、マーカーを共有する",
+ "map_filterNodes": "フィルタノード",
+ "map_nodeTypes": "ノードの種類",
+ "map_chatNodes": "チャットノード",
+ "map_repeaters": "繰り返し送信装置",
+ "map_otherNodes": "その他のノード",
+ "map_keyPrefix": "主要なプレフィックス",
+ "map_filterByKeyPrefix": "主要なプレフィックスでフィルタリングする",
+ "map_publicKeyPrefix": "公開鍵のプレフィックス",
+ "map_markers": "マーカー",
+ "map_showSharedMarkers": "共有のマーカーを表示する",
+ "map_showGuessedLocations": "推測されたノードの位置を表示する",
+ "map_showDiscoveryContacts": "Discovery社の連絡先を表示する",
+ "map_guessedLocation": "推測された場所",
+ "map_lastSeenTime": "最後に確認された時間",
+ "map_sharedPin": "共有パスワード",
+ "map_joinRoom": "部屋に参加する",
+ "map_manageRepeater": "リピーターの管理",
+ "map_tapToAdd": "ノードをクリックして、パスに追加します。",
+ "map_runTrace": "パスの追跡を実行",
+ "map_removeLast": "最後のものを削除",
+ "map_pathTraceCancelled": "パスの追跡は中止。",
+ "mapCache_title": "オフライン用地図キャッシュ",
+ "mapCache_selectAreaFirst": "最初にキャッシュする領域を選択してください",
+ "mapCache_noTilesToDownload": "この地域にはダウンロードできるタイルは存在しません。",
+ "mapCache_downloadTilesTitle": "タイルをダウンロードする",
+ "mapCache_downloadTilesPrompt": "オフラインでの使用のために、{count}個のタイルをダウンロードしますか?",
+ "@mapCache_downloadTilesPrompt": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadAction": "ダウンロード",
+ "mapCache_cachedTiles": "{count} 個のタイルをキャッシュ",
+ "@mapCache_cachedTiles": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)",
+ "@mapCache_cachedTilesWithFailed": {
+ "placeholders": {
+ "downloaded": {
+ "type": "int"
+ },
+ "failed": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_clearOfflineCacheTitle": "オフラインキャッシュをクリアする",
+ "mapCache_clearOfflineCachePrompt": "キャッシュされた地図のタイルをすべて削除しますか?",
+ "mapCache_offlineCacheCleared": "オフラインキャッシュをクリア",
+ "mapCache_noAreaSelected": "選択されたエリアはありません",
+ "mapCache_cacheArea": "キャッシュエリア",
+ "mapCache_useCurrentView": "現在表示されている内容を保持する",
+ "mapCache_zoomRange": "ズーム範囲",
+ "mapCache_estimatedTiles": "推定されるタイル数: {count}",
+ "@mapCache_estimatedTiles": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadedTiles": "Downloaded {completed} / {total}",
+ "@mapCache_downloadedTiles": {
+ "placeholders": {
+ "completed": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadTilesButton": "タイルをダウンロードする",
+ "mapCache_clearCacheButton": "キャッシュをクリアする",
+ "mapCache_failedDownloads": "失敗したダウンロード: {count}",
+ "@mapCache_failedDownloads": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
+ "@mapCache_boundsLabel": {
+ "placeholders": {
+ "north": {
+ "type": "String"
+ },
+ "south": {
+ "type": "String"
+ },
+ "east": {
+ "type": "String"
+ },
+ "west": {
+ "type": "String"
+ }
+ }
+ },
+ "time_justNow": "まさに今",
+ "time_minutesAgo": "{minutes}分前",
+ "@time_minutesAgo": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "time_hoursAgo": "{hours}時間前",
+ "@time_hoursAgo": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "time_daysAgo": "{days}日前",
+ "@time_daysAgo": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
+ "time_hour": "1時間",
+ "time_hours": "時間",
+ "time_day": "一日",
+ "time_days": "日",
+ "time_week": "1週間",
+ "time_weeks": "週",
+ "time_month": "月",
+ "time_months": "月",
+ "time_minutes": "分",
+ "time_allTime": "全期間",
+ "dialog_disconnect": "切断する",
+ "dialog_disconnectConfirm": "本当にこのデバイスとの接続を解除したいですか?",
+ "login_repeaterLogin": "再ログイン",
+ "login_roomLogin": "ルームサーバーへのログイン",
+ "login_password": "パスワード",
+ "login_enterPassword": "パスワードを入力してください",
+ "login_savePassword": "パスワードを保存する",
+ "login_savePasswordSubtitle": "パスワードは、このデバイスに安全に保存されます。",
+ "login_repeaterDescription": "設定やステータスにアクセスするために、リピーターのパスワードを入力してください。",
+ "login_roomDescription": "設定やステータスへのアクセスには、部屋のパスワードを入力してください。",
+ "login_routing": "経路設定",
+ "login_routingMode": "ルーティングモード",
+ "login_autoUseSavedPath": "自動 (保存されたパスを使用)",
+ "login_forceFloodMode": "強制的に洪水モードを起動",
+ "login_managePaths": "パスの管理",
+ "login_login": "ログイン",
+ "login_attempt": "試行回数:{current}/{max}",
+ "@login_attempt": {
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "max": {
+ "type": "int"
+ }
+ }
+ },
+ "login_failed": "ログインに失敗しました:{error}",
+ "@login_failed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "login_failedMessage": "ログインに失敗しました。パスワードが間違っているか、または接続が確立されていません。",
+ "common_reload": "再読み込み",
+ "common_clear": "明確",
+ "path_currentPath": "現在のパス: {path}",
+ "@path_currentPath": {
+ "placeholders": {
+ "path": {
+ "type": "String"
+ }
+ }
+ },
+ "path_usingHopsPath": "{count} {count, plural, =1{ホップ} other{ホップ}}のパスを使用",
+ "@path_usingHopsPath": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "path_enterCustomPath": "カスタムパスを入力",
+ "path_currentPathLabel": "現在の経路",
+ "path_hexPrefixInstructions": "各ホップに対して、2文字の16進数プレフィックスをカンマで区切って入力してください。",
+ "path_hexPrefixExample": "例:A1, F2, 3C (各ノードは、自身の公開鍵の最初のバイトを使用)",
+ "path_labelHexPrefixes": "パス (ヘックスプレフィックス)",
+ "path_helperMaxHops": "最大64個のホップ。各プレフィックスは2つの16進数文字(1バイト)で構成されています。",
+ "path_selectFromContacts": "または、連絡先リストから選択してください:",
+ "path_noRepeatersFound": "繰り返し機能やルームサーバーは見つかりませんでした。",
+ "path_customPathsRequire": "カスタムパスには、メッセージを中継できる中間地点が必要です。",
+ "path_invalidHexPrefixes": "無効な16進数プレフィックス: {prefixes}",
+ "@path_invalidHexPrefixes": {
+ "placeholders": {
+ "prefixes": {
+ "type": "String"
+ }
+ }
+ },
+ "path_tooLong": "経路が長すぎる。最大64回のジャンプのみ許可。",
+ "path_setPath": "パスを設定",
+ "repeater_management": "リピーター管理",
+ "room_management": "ルームサーバーの管理",
+ "repeater_managementTools": "管理ツール",
+ "repeater_status": "ステータス",
+ "repeater_statusSubtitle": "リピーターの状態、統計情報、および隣接するネットワークの情報を表示する",
+ "repeater_telemetry": "テレメトリー",
+ "repeater_telemetrySubtitle": "センサーおよびシステムの状態に関するテレメトリの表示",
+ "repeater_cli": "CLI(コマンドラインインターフェース)",
+ "repeater_cliSubtitle": "リピーターへのコマンドを送信する",
+ "repeater_neighbors": "近隣住民",
+ "repeater_neighborsSubtitle": "ゼロホップの隣接ノードを表示する。",
+ "repeater_settings": "設定",
+ "repeater_settingsSubtitle": "リピーターのパラメータを設定する",
+ "repeater_statusTitle": "再送ステータス",
+ "repeater_routingMode": "ルーティングモード",
+ "repeater_autoUseSavedPath": "自動 (保存されたパスを使用)",
+ "repeater_forceFloodMode": "強制的に洪水モードを起動",
+ "repeater_pathManagement": "経路管理",
+ "repeater_refresh": "リフレッシュ",
+ "repeater_statusRequestTimeout": "ステータス情報の取得に失敗しました。",
+ "repeater_errorLoadingStatus": "ステータス読み込みエラー: {error}",
+ "@repeater_errorLoadingStatus": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_systemInformation": "システム情報",
+ "repeater_battery": "バッテリー",
+ "repeater_clockAtLogin": "ログイン時の時刻表示",
+ "repeater_uptime": "稼働率",
+ "repeater_queueLength": "待ち行列の長さ",
+ "repeater_debugFlags": "デバッグフラグ",
+ "repeater_radioStatistics": "ラジオに関する統計",
+ "repeater_lastRssi": "最後のRSSI",
+ "repeater_lastSnr": "最後のSNR",
+ "repeater_noiseFloor": "ノイズレベル",
+ "repeater_txAirtime": "TXの放送時間",
+ "repeater_rxAirtime": "RX 空き時間",
+ "repeater_packetStatistics": "パケット統計",
+ "repeater_sent": "送信",
+ "repeater_received": "受領",
+ "repeater_duplicates": "重複",
+ "repeater_daysHoursMinsSecs": "{days}日 {hours}時間 {minutes}分 {seconds}秒",
+ "@repeater_daysHoursMinsSecs": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ },
+ "hours": {
+ "type": "int"
+ },
+ "minutes": {
+ "type": "int"
+ },
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_packetTxTotal": "合計: {total}, 洪水: {flood}, 直接: {direct}",
+ "@repeater_packetTxTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ },
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_packetRxTotal": "合計: {total}, 洪水: {flood}, 直接: {direct}",
+ "@repeater_packetRxTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ },
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_duplicatesFloodDirect": "{flood}: {flood}, 直接: {direct}",
+ "@repeater_duplicatesFloodDirect": {
+ "placeholders": {
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_duplicatesTotal": "合計: {total}",
+ "@repeater_duplicatesTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_settingsTitle": "リピーター設定",
+ "repeater_basicSettings": "基本設定",
+ "repeater_repeaterName": "送信装置名",
+ "repeater_repeaterNameHelper": "このリピーターの名前",
+ "repeater_adminPassword": "管理者パスワード",
+ "repeater_adminPasswordHelper": "完全アクセス権のパスワード",
+ "repeater_guestPassword": "ゲスト用のパスワード",
+ "repeater_guestPasswordHelper": "読み取り専用アクセス用のパスワード",
+ "repeater_radioSettings": "ラジオ設定",
+ "repeater_frequencyMhz": "周波数 (MHz)",
+ "repeater_frequencyHelper": "300~2500 MHz",
+ "repeater_txPower": "TXパワー",
+ "repeater_txPowerHelper": "-30~-10 dBm",
+ "repeater_bandwidth": "帯域幅",
+ "repeater_spreadingFactor": "伝播係数",
+ "repeater_codingRate": "コーディング速度",
+ "repeater_locationSettings": "場所設定",
+ "repeater_latitude": "緯度",
+ "repeater_latitudeHelper": "度分表記(例:37.7749)",
+ "repeater_longitude": "経度",
+ "repeater_longitudeHelper": "度分表記(例:-122.4194)",
+ "repeater_features": "特徴",
+ "repeater_packetForwarding": "パケット転送",
+ "repeater_packetForwardingSubtitle": "リピーターがパケットを転送できるように設定する",
+ "repeater_guestAccess": "ゲストへのアクセス",
+ "repeater_guestAccessSubtitle": "ゲストへの読み取り専用アクセスを許可する",
+ "repeater_privacyMode": "プライバシーモード",
+ "repeater_privacyModeSubtitle": "広告に名前/場所を記載しない",
+ "repeater_advertisementSettings": "広告設定",
+ "repeater_localAdvertInterval": "地域広告掲載期間",
+ "repeater_localAdvertIntervalMinutes": "{minutes} 分",
+ "@repeater_localAdvertIntervalMinutes": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_floodAdvertInterval": "洪水に関する広告の表示間隔",
+ "repeater_floodAdvertIntervalHours": "{hours} 時間",
+ "@repeater_floodAdvertIntervalHours": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_encryptedAdvertInterval": "暗号化された広告表示間",
+ "repeater_dangerZone": "危険区域",
+ "repeater_rebootRepeater": "リピーターを再起動する",
+ "repeater_rebootRepeaterSubtitle": "リピーターデバイスを再起動する",
+ "repeater_rebootRepeaterConfirm": "本当にこのリピーターを再起動したいですか?",
+ "repeater_regenerateIdentityKey": "IDキーの再生成",
+ "repeater_regenerateIdentityKeySubtitle": "新しい公開鍵/秘密鍵のペアを生成する",
+ "repeater_regenerateIdentityKeyConfirm": "これにより、リピーターには新しい識別情報が割り当てられます。続行しますか?",
+ "repeater_eraseFileSystem": "ファイルシステムを削除する",
+ "repeater_eraseFileSystemSubtitle": "リピーターファイルシステムをフォーマットする",
+ "repeater_eraseFileSystemConfirm": "警告:この操作により、リピーター内のすべてのデータが消去されます。この操作は元に戻すことができません!",
+ "repeater_eraseSerialOnly": "Erase機能は、シリアルコンソール経由でのみ利用可能です。",
+ "repeater_commandSent": "送信されたコマンド: {command}",
+ "@repeater_commandSent": {
+ "placeholders": {
+ "command": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_errorSendingCommand": "コマンド送信エラー:{error}",
+ "@repeater_errorSendingCommand": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_confirm": "確認",
+ "repeater_settingsSaved": "設定が正常に保存されました",
+ "repeater_errorSavingSettings": "設定の保存に失敗しました:{error}",
+ "@repeater_errorSavingSettings": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_refreshBasicSettings": "基本設定をリセットする",
+ "repeater_refreshRadioSettings": "ラジオ設定をリセットする",
+ "repeater_refreshTxPower": "TX の電力レベルをリセットする",
+ "repeater_refreshLocationSettings": "場所設定をリセットする",
+ "repeater_refreshPacketForwarding": "パケット転送の刷新",
+ "repeater_refreshGuestAccess": "ゲストへのアクセスをリフレッシュする",
+ "repeater_refreshPrivacyMode": "プライバシーモードをリセットする",
+ "repeater_refreshAdvertisementSettings": "広告設定のリセット",
+ "repeater_refreshed": "{label} が更新されました",
+ "@repeater_refreshed": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_errorRefreshing": "{label} の更新に失敗しました",
+ "@repeater_errorRefreshing": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_cliTitle": "リピーターのコマンドラインインターフェース",
+ "repeater_debugNextCommand": "次のコマンドのデバッグ",
+ "repeater_commandHelp": "コマンドヘルプ",
+ "repeater_clearHistory": "明確な歴史",
+ "repeater_noCommandsSent": "まだコマンドは送信されていません",
+ "repeater_typeCommandOrUseQuick": "以下のコマンドを入力するか、クイックコマンドを使用してください。",
+ "repeater_enterCommandHint": "コマンドを入力してください...",
+ "repeater_previousCommand": "直前の指示",
+ "repeater_nextCommand": "次の指示",
+ "repeater_enterCommandFirst": "まず、コマンドを入力してください。",
+ "repeater_cliCommandFrameTitle": "CLI コマンドフレーム",
+ "repeater_cliCommandError": "エラー:{error}",
+ "@repeater_cliCommandError": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_cliQuickGetName": "名前を取得する",
+ "repeater_cliQuickGetRadio": "ラジオを聴く",
+ "repeater_cliQuickGetTx": "TXを入手する",
+ "repeater_cliQuickNeighbors": "近隣住民",
+ "repeater_cliQuickVersion": "バージョン",
+ "repeater_cliQuickAdvertise": "広告",
+ "repeater_cliQuickClock": "時計",
+ "repeater_cliHelpAdvert": "広告用資料を送る",
+ "repeater_cliHelpReboot": "デバイスを再起動します。(注:通常は「タイムアウト」が表示されますが、これは正常です)",
+ "repeater_cliHelpClock": "各デバイスの時計で現在の時刻を表示します。",
+ "repeater_cliHelpPassword": "デバイス用の新しい管理者パスワードを設定します。",
+ "repeater_cliHelpVersion": "デバイスのバージョンとファームウェアのビルド日を表示します。",
+ "repeater_cliHelpClearStats": "さまざまな統計カウンターをゼロにリセットする。",
+ "repeater_cliHelpSetAf": "空き時間係数を設定します。",
+ "repeater_cliHelpSetTx": "LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)",
+ "repeater_cliHelpSetRepeat": "このノードに対するリピーターの役割を有効化または無効化します。",
+ "repeater_cliHelpSetAllowReadOnly": "(ルームサーバー設定)「オン」に設定した場合、空白のパスワードでのログインは可能ですが、ルームへの投稿はできません。(閲覧のみ)",
+ "repeater_cliHelpSetFloodMax": "インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。",
+ "repeater_cliHelpSetIntThresh": "干渉閾値を設定します(dB単位)。デフォルト値は14です。0に設定すると、チャンネル間の干渉を検出する機能を無効にします。",
+ "repeater_cliHelpSetAgcResetInterval": "オートゲインコントローラーのリセット間隔を設定します。 0 に設定すると無効化されます。",
+ "repeater_cliHelpSetMultiAcks": "「ダブルACK」機能の有効化または無効化を可能にします。",
+ "repeater_cliHelpSetAdvertInterval": "ローカル(ホップなし)の広告パケットを送信する間隔を分単位で設定します。 0 に設定すると、機能を無効にします。",
+ "repeater_cliHelpSetFloodAdvertInterval": "洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。",
+ "repeater_cliHelpSetGuestPassword": "ゲストのパスワードを設定/更新します。(繰り返し利用の場合、ゲストのログインは「統計情報を取得」のリクエストを送信できます)",
+ "repeater_cliHelpSetName": "広告の名前を設定します。",
+ "repeater_cliHelpSetLat": "広告表示の地図の緯度を設定します。(度分秒表記)",
+ "repeater_cliHelpSetLon": "広告表示の地図の経度を設定します。(度数、分)",
+ "repeater_cliHelpSetRadio": "完全に新しいラジオパラメータを設定し、設定として保存します。適用するには、「再起動」コマンドが必要です。",
+ "repeater_cliHelpSetRxDelay": "(実験用)遅延時間を設定するためのベース(1以上の値に設定する必要)\n受信パケットに対して、信号強度/スコアに基づいてわずかな遅延を適用します。 0に設定すると無効化されます。",
+ "repeater_cliHelpSetTxDelay": "時間経過に応じた「フラッシュモード」パケットの送信遅延を設定します。この遅延は、ランダムなスロットシステムと組み合わせて使用され、パケットの衝突を減らすことを目的としています。",
+ "repeater_cliHelpSetDirectTxDelay": "txdelayと同様ですが、ダイレクトモードのパケット転送にランダムな遅延を適用する場合に使用します。",
+ "repeater_cliHelpSetBridgeEnabled": "ブリッジを有効化/無効化",
+ "repeater_cliHelpSetBridgeDelay": "パケットを再送信する前に、遅延を設定する。",
+ "repeater_cliHelpSetBridgeSource": "橋が受信したパケットを再送信するか、送信したパケットを再送信するかどうかを選択してください。",
+ "repeater_cliHelpSetBridgeBaud": "RS232 橋渡しに使用するシリアルリンクのボーレートを設定する。",
+ "repeater_cliHelpSetBridgeSecret": "ESPNow 橋の秘密設定",
+ "repeater_cliHelpSetAdcMultiplier": "特定のボードでのみサポートされている、報告されるバッテリー電圧を調整するためのカスタムファクタを設定できます。",
+ "repeater_cliHelpTempRadio": "指定された時間(分単位)に対して、一時的にラジオパラメータを設定し、その後元のラジオパラメータに戻します。(設定を保存しません)。",
+ "repeater_cliHelpSetPerm": "ACL を変更します。「permissions」が 0 の場合、対応するエントリ(pubkey のプレフィックスで識別)を削除します。pubkey-hex が有効な長さで、かつ ACL に現在存在しない場合に、新しいエントリを追加します。pubkey のプレフィックスと一致するエントリを更新します。権限ビットはファームウェアの役割によって異なり、下位 2 ビットは以下のとおりです:0 (ゲスト)、1 (読み取り専用)、2 (読み書き)、3 (管理者)",
+ "repeater_cliHelpGetBridgeType": "ブリッジ機能なし、RS232、ESPNow",
+ "repeater_cliHelpLogStart": "パケットのログ記録を開始し、ファイルシステムに保存する。",
+ "repeater_cliHelpLogStop": "ファイルシステムへのパケットログの記録を停止する。",
+ "repeater_cliHelpLogErase": "ファイルシステムからパケットログを削除する。",
+ "repeater_cliHelpNeighbors": "ゼロホップ広告を通じて受信した他のリピーターノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。",
+ "repeater_cliHelpNeighborRemove": "隣接リストから、最初に一致するエントリ(pubkeyプレフィックス(16進数)で特定)を削除します。",
+ "repeater_cliHelpRegion": "(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。",
+ "repeater_cliHelpRegionLoad": "注:これは特殊な複数コマンドの呼び出しです。その後の各コマンドは、地域名であり(スペースを使用して親階層を示し、少なくとも1つのスペースが必要です)、空行/コマンドで終了します。",
+ "repeater_cliHelpRegionGet": "指定された名前のプレフィックスを持つ地域を検索します(または、グローバルな範囲の場合は「*」)。結果として、「region-name (parent-name) 'F'」と返答します。",
+ "repeater_cliHelpRegionPut": "指定された名前で、領域の定義を追加または更新します。",
+ "repeater_cliHelpRegionRemove": "指定された名前を持つ領域の定義を削除します。(正確に一致している必要があり、子領域は存在してはなりません)",
+ "repeater_cliHelpRegionAllowf": "指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)",
+ "repeater_cliHelpRegionDenyf": "指定された領域における「FLOOD」権限を削除します。(注:現時点では、グローバル/従来の範囲での使用は推奨されません!)",
+ "repeater_cliHelpRegionHome": "現在の「ホーム」地域に返信します。(まだ適用されていない、将来利用を予定)",
+ "repeater_cliHelpRegionHomeSet": "「ホーム」地域を設定します。",
+ "repeater_cliHelpRegionSave": "領域リスト/マップをストレージに保存する。",
+ "repeater_cliHelpGps": "GPSの状態を表示します。GPSがオフの場合、「オフ」と表示します。オンの場合、「オン」、「ステータス」、「位置情報」、「衛星数」と表示します。",
+ "repeater_cliHelpGpsOnOff": "GPS の電源状態を切り替えます。",
+ "repeater_cliHelpGpsSync": "ノードの時刻をGPSクロックと同期する。",
+ "repeater_cliHelpGpsSetLoc": "ノードの位置をGPS座標に設定し、設定を保存する。",
+ "repeater_cliHelpGpsAdvert": "ノードの広告設定における場所情報の指定:\n- none: 広告に場所情報を含まない\n- share: GPS位置情報を共有 (SensorManagerから取得)\n- prefs: プリファレンスに保存された場所情報を広告",
+ "repeater_cliHelpGpsAdvertSet": "場所に関する広告設定を行います。",
+ "repeater_commandsListTitle": "コマンド一覧",
+ "repeater_commandsListNote": "注:さまざまな「set ...」コマンドには、「get ...」コマンドも存在します。",
+ "repeater_general": "一般的な",
+ "repeater_settingsCategory": "設定",
+ "repeater_bridge": "橋",
+ "repeater_logging": "ログ記録",
+ "repeater_neighborsRepeaterOnly": "近隣住民(リピーターのみ)",
+ "repeater_regionManagementRepeaterOnly": "地域管理(ブロードキャスト用のみ)",
+ "repeater_regionNote": "地域レベルでの管理のため、地域定義と権限の管理を行うための機能が導入されました。",
+ "repeater_gpsManagement": "GPS管理",
+ "repeater_gpsNote": "GPSコマンドは、位置情報に関連するタスクを管理するために導入されました。",
+ "telemetry_receivedData": "受信したテレメトリーデータ",
+ "telemetry_requestTimeout": "テレメトリの要求タイムアウトしました。",
+ "telemetry_errorLoading": "テレメトリの読み込みに失敗しました: {error}",
+ "@telemetry_errorLoading": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_noData": "テレメトリデータは利用できません。",
+ "telemetry_channelTitle": "チャンネル {channel}",
+ "@telemetry_channelTitle": {
+ "placeholders": {
+ "channel": {
+ "type": "int"
+ }
+ }
+ },
+ "telemetry_batteryLabel": "バッテリー",
+ "telemetry_voltageLabel": "電圧",
+ "telemetry_mcuTemperatureLabel": "MCU の温度",
+ "telemetry_temperatureLabel": "温度",
+ "telemetry_currentLabel": "現在",
+ "telemetry_batteryValue": "{percent}% / {volts}V",
+ "@telemetry_batteryValue": {
+ "placeholders": {
+ "percent": {
+ "type": "int"
+ },
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_voltageValue": "{volts}V",
+ "@telemetry_voltageValue": {
+ "placeholders": {
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_currentValue": "{amps}A",
+ "@telemetry_currentValue": {
+ "placeholders": {
+ "amps": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_temperatureValue": "{celsius}℃ / {fahrenheit}°F",
+ "@telemetry_temperatureValue": {
+ "placeholders": {
+ "celsius": {
+ "type": "String"
+ },
+ "fahrenheit": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_receivedData": "近隣住民のデータを受信",
+ "neighbors_requestTimedOut": "近隣住民からの要望:時間制限を設けてください。",
+ "neighbors_errorLoading": "近隣情報の読み込みに失敗: {error}",
+ "@neighbors_errorLoading": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_repeatersNeighbors": "繰り返し送信する、近隣",
+ "neighbors_noData": "近隣のデータは利用できません。",
+ "neighbors_unknownContact": "不明な {pubkey}",
+ "@neighbors_unknownContact": {
+ "placeholders": {
+ "pubkey": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_heardAgo": "聞いたのは、{time} くらい前です",
+ "@neighbors_heardAgo": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_title": "パケットパス",
+ "channelPath_viewMap": "地図を表示する",
+ "channelPath_otherObservedPaths": "観察されたその他の経路",
+ "channelPath_repeaterHops": "ホップの繰り返し",
+ "channelPath_noHopDetails": "このパッケージに関する詳細な情報は提供されていません。",
+ "channelPath_messageDetails": "メッセージの詳細",
+ "channelPath_senderLabel": "送信者",
+ "channelPath_timeLabel": "時間",
+ "channelPath_repeatsLabel": "繰り返し",
+ "channelPath_pathLabel": "{index} 番目の経路",
+ "channelPath_observedLabel": "観察",
+ "channelPath_observedPathTitle": "観察された経路 {index} • {hops}",
+ "@channelPath_observedPathTitle": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ },
+ "hops": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_noLocationData": "場所に関するデータはありません",
+ "channelPath_timeWithDate": "{day}/{month} {time}",
+ "@channelPath_timeWithDate": {
+ "placeholders": {
+ "day": {
+ "type": "int"
+ },
+ "month": {
+ "type": "int"
+ },
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_timeOnly": "{time}",
+ "@channelPath_timeOnly": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_unknownPath": "不明",
+ "channelPath_floodPath": "洪水",
+ "channelPath_directPath": "直接",
+ "channelPath_observedZeroOf": "{total}個のホップ",
+ "@channelPath_observedZeroOf": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_observedSomeOf": "{observed} of {total} hops",
+ "@channelPath_observedSomeOf": {
+ "placeholders": {
+ "observed": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_mapTitle": "経路図",
+ "channelPath_noRepeaterLocations": "この経路には、中継装置の設置場所がありません。",
+ "channelPath_primaryPath": "{index}番目の経路(主要経路)",
+ "@channelPath_primaryPath": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "@channelPath_pathLabel": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_pathLabelTitle": "道",
+ "channelPath_observedPathHeader": "観察された経路",
+ "channelPath_selectedPathLabel": "{label} • {prefixes}",
+ "@channelPath_selectedPathLabel": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ },
+ "prefixes": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_noHopDetailsAvailable": "このパッケージに関する詳細な配送情報は利用できません。",
+ "channelPath_unknownRepeater": "不明な増幅機",
+ "community_title": "地域",
+ "community_create": "コミュニティを構築する",
+ "community_createDesc": "新しいコミュニティを作成し、QRコードで共有する。",
+ "community_join": "参加する",
+ "community_joinTitle": "コミュニティに参加する",
+ "community_joinConfirmation": "{name}さんのようなコミュニティに参加したいですか?",
+ "@community_joinConfirmation": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_scanQr": "コミュニティのQRコードをスキャン",
+ "community_scanInstructions": "カメラを、地域のQRコードを向けて",
+ "community_showQr": "QRコードを表示する",
+ "community_publicChannel": "地域住民向け",
+ "community_hashtagChannel": "コミュニティ用ハッシュタグ",
+ "community_name": "コミュニティ名",
+ "community_enterName": "コミュニティ名を入力してください",
+ "community_created": "コミュニティ「{name}」が作成されました",
+ "@community_created": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_joined": "{name} のコミュニティに参加",
+ "@community_joined": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_qrTitle": "コミュニティ共有",
+ "community_qrInstructions": "このQRコードをスキャンして、{name}に参加してください。",
+ "@community_qrInstructions": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_hashtagPrivacyHint": "コミュニティハッシュタグのチャンネルは、コミュニティのメンバーのみが参加できます。",
+ "community_invalidQrCode": "無効なコミュニティQRコード",
+ "community_alreadyMember": "すでに会員である",
+ "community_alreadyMemberMessage": "あなたはすでに {name} の会員です。",
+ "@community_alreadyMemberMessage": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_addPublicChannel": "コミュニティ用の公開チャンネルを追加",
+ "community_addPublicChannelHint": "このコミュニティの公開チャンネルを自動的に追加する",
+ "community_noCommunities": "まだコミュニティは形成されていません。",
+ "community_scanOrCreate": "QRコードをスキャンするか、コミュニティを作成して開始してください。",
+ "community_manageCommunities": "コミュニティの管理",
+ "community_delete": "コミュニティからの離脱",
+ "community_deleteConfirm": "{name}を辞める?",
+ "@community_deleteConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_deleteChannelsWarning": "これにより、{count} のチャンネルとそのメッセージも削除されます。",
+ "@community_deleteChannelsWarning": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "community_deleted": "コミュニティ「{name}」を離れる",
+ "@community_deleted": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecret": "秘密の復元",
+ "community_regenerateSecretConfirm": "{name} の秘密鍵を再生成しますか? 継続的に通信するため、すべてのメンバーは新しいQRコードをスキャンする必要があります。",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerate": "再生",
+ "community_secretRegenerated": "{name} への秘密が再設定されました",
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_updateSecret": "秘密情報の更新",
+ "community_secretUpdated": "{name} 向けの秘密設定を更新",
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_scanToUpdateSecret": "新しいQRコードをスキャンして、{name}の秘密情報を更新してください。",
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_addHashtagChannel": "コミュニティのハッシュタグを追加",
+ "community_addHashtagChannelDesc": "このコミュニティ用のハッシュタグチャンネルを追加する",
+ "community_selectCommunity": "コミュニティを選択",
+ "community_regularHashtag": "定期的なハッシュタグ",
+ "community_regularHashtagDesc": "一般のハッシュタグ(誰でも参加可能)",
+ "community_communityHashtag": "コミュニティ用ハッシュタグ",
+ "community_communityHashtagDesc": "コミュニティメンバーのみへの限定",
+ "community_forCommunity": "{name} 様",
+ "@community_forCommunity": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "listFilter_tooltip": "フィルタリングと並べ替え",
+ "listFilter_sortBy": "並び替え",
+ "listFilter_latestMessages": "最新のメッセージ",
+ "listFilter_heardRecently": "最近、聞いた",
+ "listFilter_az": "AからZ",
+ "listFilter_filters": "フィルター",
+ "listFilter_all": "すべて",
+ "listFilter_favorites": "お気に入り",
+ "listFilter_addToFavorites": "お気に入りに追加",
+ "listFilter_removeFromFavorites": "お気に入りから削除",
+ "listFilter_users": "利用者",
+ "listFilter_repeaters": "繰り返し送信装置",
+ "listFilter_roomServers": "ルーム用サーバー",
+ "listFilter_unreadOnly": "未読のみ",
+ "listFilter_newGroup": "新しいグループ",
+ "pathTrace_you": "あなた",
+ "pathTrace_failed": "パスの追跡に失敗しました。",
+ "pathTrace_notAvailable": "パスの追跡機能は利用できません。",
+ "pathTrace_refreshTooltip": "パスの追跡をリフレッシュする。",
+ "pathTrace_someHopsNoLocation": "ホップの1つまたは複数について、場所が特定されていません。",
+ "pathTrace_clearTooltip": "明確な道筋。",
+ "losSelectStartEnd": "LOS の開始ノードと終了ノードを選択してください。",
+ "losRunFailed": "視界確認に失敗: {error}",
+ "@losRunFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "losClearAllPoints": "すべての項目をクリア",
+ "losRunToViewElevationProfile": "LOS(レーザー測距)を使用して、標高プロファイルを表示する",
+ "losMenuTitle": "LOS メニュー",
+ "losMenuSubtitle": "特定の場所をタップするか、地図を長押ししてカスタムポイントを作成する。",
+ "losShowDisplayNodes": "表示ノードを表示する",
+ "losCustomPoints": "カスタマイズ可能なポイント",
+ "losCustomPointLabel": "カスタマイズ {index}",
+ "@losCustomPointLabel": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "losPointA": "ポイントA",
+ "losPointB": "ポイントB",
+ "losAntennaA": "アンテナ A: {value} {unit}",
+ "@losAntennaA": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "unit": {
+ "type": "String"
+ }
+ }
+ },
+ "losAntennaB": "アンテナ B: {value} {unit}",
+ "@losAntennaB": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "unit": {
+ "type": "String"
+ }
+ }
+ },
+ "losRun": "LOS(レーティングシステム)を使用する",
+ "losNoElevationData": "標高データは含まれていません",
+ "losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}",
+ "@losProfileClear": {
+ "placeholders": {
+ "distance": {
+ "type": "String"
+ },
+ "distanceUnit": {
+ "type": "String"
+ },
+ "clearance": {
+ "type": "String"
+ },
+ "heightUnit": {
+ "type": "String"
+ }
+ }
+ },
+ "losProfileBlocked": "{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}",
+ "@losProfileBlocked": {
+ "placeholders": {
+ "distance": {
+ "type": "String"
+ },
+ "distanceUnit": {
+ "type": "String"
+ },
+ "obstruction": {
+ "type": "String"
+ },
+ "heightUnit": {
+ "type": "String"
+ }
+ }
+ },
+ "losStatusChecking": "LOS:確認中…",
+ "losStatusNoData": "LOS: データの欠如",
+ "losStatusSummary": "LOS: {clear}/{total} clear, {blocked} blocked, {unknown} unknown",
+ "@losStatusSummary": {
+ "placeholders": {
+ "clear": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "blocked": {
+ "type": "int"
+ },
+ "unknown": {
+ "type": "int"
+ }
+ }
+ },
+ "losErrorElevationUnavailable": "あるサンプルまたは複数のサンプルについて、標高データが利用できません。",
+ "losErrorInvalidInput": "LOS(レーダー)計算に必要な、無効な点/標高データ。",
+ "losRenameCustomPoint": "カスタムポイントの名前を変更する",
+ "losPointName": "項目名",
+ "losShowPanelTooltip": "LOSパネルを表示する",
+ "losHidePanelTooltip": "LOSパネルを隠す",
+ "losElevationAttribution": "標高データ:Open-Meteo (CC BY 4.0)",
+ "losLegendRadioHorizon": "ラジオ・ホライゾン",
+ "losLegendLosBeam": "LOS ビーミング",
+ "losLegendTerrain": "地形",
+ "losFrequencyLabel": "周波数",
+ "losFrequencyInfoTooltip": "計算の詳細を見る",
+ "losFrequencyDialogTitle": "ラジオによる水平線計算",
+ "losFrequencyDialogDescription": "k={baselineK} ( {baselineFreq} MHz) から開始し、現在の {frequencyMHz} MHz の帯域に対して k の値を調整します。これにより、曲面状の無線通信範囲の限界が定義されます。",
+ "@losFrequencyDialogDescription": {
+ "description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
+ "placeholders": {
+ "baselineK": {
+ "type": "double"
+ },
+ "baselineFreq": {
+ "type": "double"
+ },
+ "frequencyMHz": {
+ "type": "double"
+ },
+ "kFactor": {
+ "type": "double"
+ }
+ }
+ },
+ "contacts_pathTrace": "経路追跡",
+ "contacts_ping": "パング",
+ "contacts_repeaterPathTrace": "リピーターまでの経路を追跡する",
+ "contacts_repeaterPing": "PING 繰り返し",
+ "contacts_roomPathTrace": "部屋のサーバーへの経路を追跡する",
+ "contacts_roomPing": "ピンルーム用サーバー",
+ "contacts_chatTraceRoute": "経路の追跡ルート",
+ "contacts_pathTraceTo": "{name} への経路を追跡する",
+ "@contacts_pathTraceTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_clipboardEmpty": "クリップボードは空です。",
+ "contacts_invalidAdvertFormat": "無効な連絡先情報",
+ "contacts_contactImported": "連絡先が登録されました。",
+ "contacts_contactImportFailed": "連絡先のインポートに失敗しました。",
+ "contacts_zeroHopAdvert": "ゼロホップ広告",
+ "contacts_floodAdvert": "洪水に関する広告",
+ "contacts_copyAdvertToClipboard": "広告をクリップボードにコピー",
+ "contacts_addContactFromClipboard": "クリップボードから連絡先を追加する",
+ "contacts_ShareContact": "連絡先をクリップボードにコピー",
+ "contacts_ShareContactZeroHop": "広告を通じて連絡先を共有する",
+ "contacts_zeroHopContactAdvertSent": "広告を通じて連絡先を得た。",
+ "contacts_zeroHopContactAdvertFailed": "連絡を送信できませんでした。",
+ "contacts_contactAdvertCopied": "広告がクリップボードにコピーされました。",
+ "contacts_contactAdvertCopyFailed": "広告のコピーがクリップボードにコピーできませんでした。",
+ "notification_activityTitle": "メッシュコアの活動",
+ "notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}",
+ "@notification_messagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_channelMessagesCount": "{count} {count, plural, =1{チャンネルメッセージ} other{チャンネルメッセージ}}",
+ "@notification_channelMessagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_newNodesCount": "{count} {count, plural, =1{新しいノード} other{新しいノード}}",
+ "@notification_newNodesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_newTypeDiscovered": "新たに {contactType} が発見されました",
+ "@notification_newTypeDiscovered": {
+ "placeholders": {
+ "contactType": {
+ "type": "String"
+ }
+ }
+ },
+ "notification_receivedNewMessage": "新しいメッセージを受信",
+ "settings_gpxExportRepeaters": "GPX へのエクスポート用リピーター/ルームサーバー",
+ "settings_gpxExportRepeatersSubtitle": "GPXファイルに場所情報を付加した、レピーター/ルームサーバーのエクスポート",
+ "settings_gpxExportContacts": "GPX 形式へのエクスポート",
+ "settings_gpxExportContactsSubtitle": "GPXファイルに位置情報を保存して、他の人と共有する。",
+ "settings_gpxExportAll": "すべての連絡先をGPX形式でエクスポートする",
+ "settings_gpxExportAllSubtitle": "すべての連絡先を、場所情報付きのGPXファイルにエクスポートする。",
+ "settings_gpxExportSuccess": "GPXファイルの正常なエクスポートが完了しました。",
+ "settings_gpxExportNoContacts": "エクスポートする連絡先は存在しません。",
+ "settings_gpxExportNotAvailable": "このデバイス/OSではサポートされていません",
+ "settings_gpxExportError": "エクスポート時にエラーが発生しました。",
+ "settings_gpxExportRepeatersRoom": "中継装置およびルームサーバーの設置場所",
+ "settings_gpxExportChat": "関連施設",
+ "settings_gpxExportAllContacts": "すべての連絡先場所",
+ "settings_gpxExportShareText": "meshcore-openからエクスポートされた地図データ",
+ "settings_gpxExportShareSubject": "meshcore-open GPX形式の地図データのエクスポート",
+ "snrIndicator_nearByRepeaters": "近くの電波中継局",
+ "snrIndicator_lastSeen": "最後に確認された場所",
+ "contactsSettings_title": "連絡先設定",
+ "contactsSettings_autoAddTitle": "自動検出",
+ "contactsSettings_otherTitle": "その他の連絡に関する設定",
+ "contactsSettings_autoAddUsersTitle": "自動でユーザーを追加する",
+ "contactsSettings_autoAddUsersSubtitle": "利用者が自動的に発見したユーザーを追加できるようにする。",
+ "contactsSettings_autoAddRepeatersTitle": "自動で繰り返し設定",
+ "contactsSettings_autoAddRepeatersSubtitle": "発見した中継局を、自動的に追加できるようにする。",
+ "contactsSettings_autoAddRoomServersTitle": "自動でルームサーバーを追加",
+ "contactsSettings_autoAddRoomServersSubtitle": "利用者が、発見した部屋のサーバーを自動的に追加できるようにする。",
+ "contactsSettings_autoAddSensorsTitle": "自動でセンサーを追加",
+ "contactsSettings_autoAddSensorsSubtitle": "利用者が、発見したセンサーを自動的に追加できるようにする。",
+ "contactsSettings_overwriteOldestTitle": "最も古いものを上書きする",
+ "contactsSettings_overwriteOldestSubtitle": "連絡先リストが満杯になった場合、最も古いかつ「お気に入り」ではない連絡先が削除されます。",
+ "discoveredContacts_Title": "連絡先が見つかった",
+ "discoveredContacts_noMatching": "一致する連絡先が見つかりませんでした",
+ "discoveredContacts_searchHint": "発見された連絡先を検索する",
+ "discoveredContacts_contactAdded": "連絡先を追加",
+ "discoveredContacts_addContact": "連絡先を追加",
+ "discoveredContacts_copyContact": "連絡先をクリップボードにコピー",
+ "discoveredContacts_deleteContact": "発見された連絡先を削除",
+ "discoveredContacts_deleteContactAll": "発見されたすべての連絡先を削除",
+ "discoveredContacts_deleteContactAllContent": "本当に、見つけたすべての連絡先を削除してもよろしいですか?",
+ "chat_sendCooldown": "再度送信する前に、しばらくお待ちください。",
+ "appSettings_jumpToOldestUnread": "最も古い未読のメッセージへ移動",
+ "appSettings_jumpToOldestUnreadSubtitle": "未読メッセージがあるチャットを開く際、「最新のメッセージ」ではなく、最初に未読のメッセージまでスクロールしてください。",
+ "appSettings_languageHu": "ハンガリー語",
+ "appSettings_languageJa": "日本語",
+ "appSettings_languageKo": "韓国語",
+ "radioStats_tooltip": "ラジオおよびメッシュに関する統計",
+ "radioStats_screenTitle": "ラジオの統計",
+ "radioStats_notConnected": "ラジオの統計情報を表示するために、デバイスに接続してください。",
+ "radioStats_firmwareTooOld": "ラジオの統計機能を使用するには、v8またはそれ以降のファームウェアが必要です。",
+ "radioStats_waiting": "データ待ち…",
+ "radioStats_noiseFloor": "ノイズレベル: {noiseDbm} dBm",
+ "@radioStats_noiseFloor": {
+ "placeholders": {
+ "noiseDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_lastRssi": "最後のRSSI: {rssiDbm} dBm",
+ "@radioStats_lastRssi": {
+ "placeholders": {
+ "rssiDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_lastSnr": "最終SNR: {snr} dB",
+ "@radioStats_lastSnr": {
+ "placeholders": {
+ "snr": {
+ "type": "String"
+ }
+ }
+ },
+ "radioStats_txAir": "TX 放送時間(合計):{seconds} 秒",
+ "@radioStats_txAir": {
+ "placeholders": {
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_rxAir": "RX 放送時間(合計):{seconds} 秒",
+ "@radioStats_rxAir": {
+ "placeholders": {
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_chartCaption": "最近のサンプルのノイズレベル(dBm)。",
+ "radioStats_stripNoise": "ノイズレベル: {noiseDbm} dBm",
+ "@radioStats_stripNoise": {
+ "placeholders": {
+ "noiseDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_stripWaiting": "ラジオの統計情報を取得中…",
+ "radioStats_settingsTile": "ラジオの統計",
+ "radioStats_settingsSubtitle": "ノイズレベル、RSSI、SNR、および通信時間",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "プライバシー設定",
+ "settings_privacySubtitle": "共有する情報の内容を管理する。",
+ "settings_denyAll": "すべてを否定",
+ "settings_privacySettingsDescription": "自分のデバイスが他の人に共有する情報を選択してください。",
+ "settings_allowByContact": "連絡先を明示するオプション",
+ "settings_allowAll": "すべて許可",
+ "settings_telemetryBaseMode": "テレメトリ基本モード",
+ "settings_telemetryLocationMode": "テレメトリ位置特定モード",
+ "settings_telemetryEnvironmentMode": "テレメトリ環境モード",
+ "settings_advertLocation": "広告掲載場所",
+ "settings_advertLocationSubtitle": "広告に場所を記載してください。",
+ "settings_multiAck": "複数のACK:{value}",
+ "settings_telemetryModeUpdated": "テレメトリモードが更新されました",
+ "contact_info": "連絡先",
+ "contact_settings": "連絡設定",
+ "contact_telemetry": "テレメトリー",
+ "contact_lastSeen": "最後に確認された場所",
+ "contact_clearChat": "チャットのクリア",
+ "contact_teleBase": "テレメトリ基地",
+ "contact_teleBaseSubtitle": "バッテリー残量と基本的なテレメトリーの共有を許可する",
+ "contact_teleLoc": "テレメトリの場所",
+ "contact_teleLocSubtitle": "位置情報共有を許可する",
+ "contact_teleEnv": "テレメトリ環境",
+ "contact_teleEnvSubtitle": "環境センサーのデータを共有することを許可する",
+ "map_showOverlaps": "リピーターキーの重複",
+ "map_runTraceWithReturnPath": "元の経路に戻る。",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "PINを表示",
+ "scanner_linuxPairingHidePin": "PINを非表示",
+ "scanner_linuxPairingPinTitle": "Bluetooth ペアリング PIN",
+ "scanner_linuxPairingPinPrompt": "{deviceName}のPINを入力してください(なしの場合は空欄のまま)。"
+}
diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb
new file mode 100644
index 0000000..6bccc19
--- /dev/null
+++ b/lib/l10n/app_ko.arb
@@ -0,0 +1,2059 @@
+{
+ "@@locale": "ko",
+ "appTitle": "MeshCore Open",
+ "nav_contacts": "연락처",
+ "nav_channels": "채널",
+ "nav_map": "지도",
+ "common_cancel": "취소",
+ "common_ok": "알겠습니다",
+ "common_connect": "연결",
+ "common_unknownDevice": "알 수 없는 장치",
+ "common_save": "저장",
+ "common_delete": "삭제",
+ "common_deleteAll": "모두 삭제",
+ "common_close": "닫기",
+ "common_edit": "수정",
+ "common_add": "추가",
+ "common_settings": "설정",
+ "common_disconnect": "연결 해제",
+ "common_connected": "연결된",
+ "common_disconnected": "단절",
+ "common_create": "만들다",
+ "common_continue": "계속",
+ "common_share": "공유",
+ "common_copy": "복사",
+ "common_retry": "다시 시도",
+ "common_hide": "숨기다",
+ "common_remove": "제거",
+ "common_enable": "활성화",
+ "common_disable": "비활성화",
+ "common_reboot": "재부팅",
+ "common_loading": "로딩 중...",
+ "common_notAvailable": "—",
+ "common_voltageValue": "{volts} V",
+ "@common_voltageValue": {
+ "placeholders": {
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "common_percentValue": "{percent}%",
+ "@common_percentValue": {
+ "placeholders": {
+ "percent": {
+ "type": "int"
+ }
+ }
+ },
+ "scanner_title": "MeshCore 공개",
+ "connectionChoiceUsbLabel": "USB",
+ "connectionChoiceBluetoothLabel": "블루투스",
+ "connectionChoiceTcpLabel": "TCP",
+ "tcpScreenTitle": "TCP를 통해 연결",
+ "tcpHostLabel": "IP 주소",
+ "tcpHostHint": "192.168.40.10",
+ "tcpPortLabel": "항",
+ "tcpPortHint": "5000",
+ "tcpStatus_notConnected": "목적지 주소 입력 후 연결",
+ "tcpStatus_connectingTo": "{endpoint}에 연결 중...",
+ "@tcpStatus_connectingTo": {
+ "placeholders": {
+ "endpoint": {
+ "type": "String"
+ }
+ }
+ },
+ "tcpErrorHostRequired": "IP 주소가 필요합니다.",
+ "tcpErrorPortInvalid": "포트 번호는 1에서 65535 사이여야 합니다.",
+ "tcpErrorUnsupported": "이 플랫폼에서는 TCP 트랜스포트를 지원하지 않습니다.",
+ "tcpErrorTimedOut": "TCP 연결이 시간 초과되었습니다.",
+ "tcpConnectionFailed": "TCP 연결 실패: {error}",
+ "@tcpConnectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "usbScreenTitle": "USB를 통해 연결",
+ "usbScreenSubtitle": "감지된 시리얼 장치를 선택하고 MeshCore 노드에 직접 연결하십시오.",
+ "usbScreenStatus": "USB 장치를 선택합니다.",
+ "usbScreenNote": "USB 직렬 통신은 지원되는 안드로이드 장치 및 데스크톱 플랫폼에서 활성화됩니다.",
+ "usbScreenEmptyState": "USB 장치가 탐지되지 않았습니다. USB 장치를 연결하고 다시 시도해 보세요.",
+ "usbErrorPermissionDenied": "USB 접근 권한이 거부되었습니다.",
+ "usbErrorDeviceMissing": "선택한 USB 장치는 더 이상 사용 불가능합니다.",
+ "usbErrorInvalidPort": "유효한 USB 장치를 선택하세요.",
+ "usbErrorBusy": "또 다른 USB 연결 요청이 이미 진행 중입니다.",
+ "usbErrorNotConnected": "USB 장치가 연결되지 않았습니다.",
+ "usbErrorOpenFailed": "선택한 USB 장치를 열 수 없습니다.",
+ "usbErrorConnectFailed": "선택한 USB 장치에 연결에 실패했습니다.",
+ "usbErrorUnsupported": "이 플랫폼에서는 USB 직렬 통신을 지원하지 않습니다.",
+ "usbErrorAlreadyActive": "USB 연결이 이미 활성화되어 있습니다.",
+ "usbErrorNoDeviceSelected": "USB 장치가 선택되지 않았습니다.",
+ "usbErrorPortClosed": "USB 연결이 활성화되지 않았습니다.",
+ "usbErrorConnectTimedOut": "연결이 시간 초과되었습니다. 장치가 USB Companion 펌웨어를 가지고 있는지 확인해 주세요.",
+ "usbFallbackDeviceName": "웹 시리얼 장치",
+ "usbStatus_notConnected": "USB 장치를 선택합니다.",
+ "usbStatus_connecting": "USB 장치에 연결 중...",
+ "usbStatus_searching": "USB 장치 검색 중...",
+ "usbConnectionFailed": "USB 연결 실패: {error}",
+ "@usbConnectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_scanning": "장치 검색 중...",
+ "scanner_connecting": "연결 중...",
+ "scanner_disconnecting": "연결 해제 중...",
+ "scanner_notConnected": "연결되지 않음",
+ "scanner_connectedTo": "{deviceName}에 연결됨",
+ "@scanner_connectedTo": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_searchingDevices": "MeshCore 장치를 검색 중...",
+ "scanner_tapToScan": "MeshCore 장치를 찾기 위해 스캔 버튼을 누르세요.",
+ "scanner_connectionFailed": "연결 실패: {error}",
+ "@scanner_connectionFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_stop": "멈춰",
+ "scanner_scan": "스캔",
+ "scanner_bluetoothOff": "블루투스는 꺼져 있습니다.",
+ "scanner_bluetoothOffMessage": "블루투스를 켜서 장치를 검색해주세요.",
+ "scanner_chromeRequired": "크롬 브라우저 필요",
+ "scanner_chromeRequiredMessage": "이 웹 애플리케이션은 블루투드 지원을 위해 Google Chrome 또는 Chromium 기반 브라우저가 필요합니다.",
+ "scanner_enableBluetooth": "블루투스 활성화",
+ "device_quickSwitch": "빠른 전환",
+ "device_meshcore": "메쉬코어",
+ "settings_title": "설정",
+ "settings_deviceInfo": "장치 정보",
+ "settings_appSettings": "앱 설정",
+ "settings_appSettingsSubtitle": "알림, 메시징, 지도 설정",
+ "settings_nodeSettings": "노드 설정",
+ "settings_nodeName": "노드 이름",
+ "settings_nodeNameNotSet": "설정되지 않음",
+ "settings_nodeNameHint": "노드 이름을 입력하세요",
+ "settings_nodeNameUpdated": "이름 변경",
+ "settings_radioSettings": "라디오 설정",
+ "settings_radioSettingsSubtitle": "주파수, 전력, 스펙트럼",
+ "settings_radioSettingsUpdated": "라디오 설정이 업데이트되었습니다.",
+ "settings_location": "위치",
+ "settings_locationSubtitle": "GPS 좌표",
+ "settings_locationUpdated": "위치 및 GPS 설정이 업데이트되었습니다.",
+ "settings_locationBothRequired": "위도와 경도를 모두 입력하세요.",
+ "settings_locationInvalid": "유효하지 않은 위도 또는 경도.",
+ "settings_locationGPSEnable": "GPS 활성화",
+ "settings_locationGPSEnableSubtitle": "GPS를 사용하여 위치 정보를 자동으로 업데이트할 수 있도록 합니다.",
+ "settings_locationIntervalSec": "GPS 간격 (초)",
+ "settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.",
+ "settings_latitude": "위도",
+ "settings_longitude": "경도",
+ "settings_contactSettings": "연락처 설정",
+ "settings_contactSettingsSubtitle": "연락처 추가 방식 설정",
+ "settings_privacyMode": "개인 정보 보호 모드",
+ "settings_privacyModeSubtitle": "광고에 이름/위치 정보 숨기기",
+ "settings_privacyModeToggle": "광고에 자신의 이름과 위치를 숨기기 위해 개인 정보 보호 모드를 켜거나 끄십시오.",
+ "settings_privacyModeEnabled": "개인 정보 보호 모드 활성화",
+ "settings_privacyModeDisabled": "개인 정보 보호 모드 비활성화",
+ "settings_actions": "행동",
+ "settings_sendAdvertisement": "광고 전송",
+ "settings_sendAdvertisementSubtitle": "방송 활동",
+ "settings_advertisementSent": "광고 전송",
+ "settings_syncTime": "동기화 시간",
+ "settings_syncTimeSubtitle": "장치 시계를 휴대폰 시간으로 설정",
+ "settings_timeSynchronized": "시간 동기화",
+ "settings_refreshContacts": "연락처 갱신",
+ "settings_refreshContactsSubtitle": "장치에서 연락처 목록을 다시 불러오기",
+ "settings_rebootDevice": "장치 재부팅",
+ "settings_rebootDeviceSubtitle": "MeshCore 장치를 재부팅하세요.",
+ "settings_rebootDeviceConfirm": "정말로 장치를 재부팅하시겠습니까? 이 경우 연결이 끊어집니다.",
+ "settings_debug": "디버깅",
+ "settings_bleDebugLog": "BLE 디버그 로그",
+ "settings_bleDebugLogSubtitle": "BLE 명령어, 응답 및 원시 데이터",
+ "settings_appDebugLog": "앱 디버깅 로그",
+ "settings_appDebugLogSubtitle": "애플리케이션 디버깅 메시지",
+ "settings_about": "소개",
+ "settings_aboutVersion": "MeshCore Open {version} 버전",
+ "@settings_aboutVersion": {
+ "placeholders": {
+ "version": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_aboutLegalese": "2026년 MeshCore 오픈 소스 프로젝트",
+ "settings_aboutDescription": "MeshCore LoRa 메시 네트워크 장치를 위한 오픈 소스 Flutter 클라이언트.",
+ "settings_aboutOpenMeteoAttribution": "LOS 고도 데이터: Open-Meteo (CC BY 4.0)",
+ "settings_infoName": "이름",
+ "settings_infoId": "ID",
+ "settings_infoStatus": "상태",
+ "settings_infoBattery": "배터리",
+ "settings_infoPublicKey": "공개 키",
+ "settings_infoContactsCount": "연락처 수",
+ "settings_infoChannelCount": "채널 수",
+ "settings_presets": "기본 설정",
+ "settings_frequency": "주파수 (MHz)",
+ "settings_frequencyHelper": "300.0 - 2500.0",
+ "settings_frequencyInvalid": "유효하지 않은 주파수 (300-2500 MHz)",
+ "settings_bandwidth": "대역폭",
+ "settings_spreadingFactor": "분산 계수",
+ "settings_codingRate": "코딩 속도",
+ "settings_txPower": "TX 전력 (dBm)",
+ "settings_txPowerHelper": "0 - 22",
+ "settings_txPowerInvalid": "유효하지 않은 TX 전력 (0-22 dBm)",
+ "settings_clientRepeat": "오프그리드 반복",
+ "settings_clientRepeatSubtitle": "이 장치가 다른 사람들을 위해 메시 패킷을 반복하도록 허용합니다.",
+ "settings_clientRepeatFreqWarning": "오프그리드(무전력) 시스템 재연결에는 433MHz, 869MHz, 또는 918MHz 주파수가 필요합니다.",
+ "settings_error": "오류: {message}",
+ "@settings_error": {
+ "placeholders": {
+ "message": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_title": "앱 설정",
+ "appSettings_appearance": "외관",
+ "appSettings_theme": "주제",
+ "appSettings_themeSystem": "기본 설정",
+ "appSettings_themeLight": "빛",
+ "appSettings_themeDark": "어둡다",
+ "appSettings_language": "언어",
+ "appSettings_languageSystem": "기본 설정",
+ "appSettings_languageEn": "영어",
+ "appSettings_languageFr": "프랑스어",
+ "appSettings_languageEs": "스페인어",
+ "appSettings_languageDe": "독일어",
+ "appSettings_languagePl": "폴란드",
+ "appSettings_languageSl": "슬로베니아어",
+ "appSettings_languagePt": "포르투갈어",
+ "appSettings_languageIt": "이탈리아어",
+ "appSettings_languageZh": "중국어",
+ "appSettings_languageSv": "스웨덴어",
+ "appSettings_languageNl": "네덜란드어",
+ "appSettings_languageSk": "슬로베니아어",
+ "appSettings_languageBg": "불가리",
+ "appSettings_languageRu": "러시아어",
+ "appSettings_languageUk": "우크라이나",
+ "appSettings_enableMessageTracing": "메시지 추적 기능 활성화",
+ "appSettings_enableMessageTracingSubtitle": "메시지에 대한 상세한 경로 및 시간 정보를 표시",
+ "appSettings_notifications": "알림",
+ "appSettings_enableNotifications": "알림 활성화",
+ "appSettings_enableNotificationsSubtitle": "메시지와 광고에 대한 알림을 받으세요.",
+ "appSettings_notificationPermissionDenied": "알림 권한 거부",
+ "appSettings_notificationsEnabled": "알림 기능 활성화",
+ "appSettings_notificationsDisabled": "알림 기능 끄기",
+ "appSettings_messageNotifications": "메시지 알림",
+ "appSettings_messageNotificationsSubtitle": "새로운 메시지를 받을 때 알림 표시",
+ "appSettings_channelMessageNotifications": "채널 메시지 알림",
+ "appSettings_channelMessageNotificationsSubtitle": "채널 메시지를 수신할 때 알림 표시",
+ "appSettings_advertisementNotifications": "광고 알림",
+ "appSettings_advertisementNotificationsSubtitle": "새 노드가 발견되었을 때 알림 표시",
+ "appSettings_messaging": "메시징",
+ "appSettings_clearPathOnMaxRetry": "Max 재시도 시 경로 명확하게 설정",
+ "appSettings_clearPathOnMaxRetrySubtitle": "5번의 전송 시도가 실패하면 연락 경로를 재설정",
+ "appSettings_pathsWillBeCleared": "5번의 시도 실패 후, 해당 경로가 확보될 것입니다.",
+ "appSettings_pathsWillNotBeCleared": "경로는 자동으로 정리되지 않습니다.",
+ "appSettings_autoRouteRotation": "자동 경로 순환",
+ "appSettings_autoRouteRotationSubtitle": "최적 경로와 방수 모드 사이를 전환",
+ "appSettings_autoRouteRotationEnabled": "자동 경로 순환 기능 활성화",
+ "appSettings_autoRouteRotationDisabled": "자동 경로 순환 기능 비활성화",
+ "appSettings_maxRouteWeight": "최대 경로 무게",
+ "appSettings_maxRouteWeightSubtitle": "한 경로가 성공적인 배송을 통해 누적할 수 있는 최대 무게",
+ "appSettings_initialRouteWeight": "초기 경로 가중치",
+ "appSettings_initialRouteWeightSubtitle": "새롭게 발견된 경로의 초기 무게",
+ "appSettings_routeWeightSuccessIncrement": "성공 횟수 증가",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "성공적으로 배송된 경로에 추가된 무게",
+ "appSettings_routeWeightFailureDecrement": "오류 가중치 감소",
+ "appSettings_routeWeightFailureDecrementSubtitle": "배송 실패 후 경로에서 제거된 무게",
+ "appSettings_maxMessageRetries": "최대 메시지 재시도 횟수",
+ "appSettings_maxMessageRetriesSubtitle": "메시지를 실패로 처리하기 전 시도 횟수",
+ "path_routeWeight": "{weight}/{max}",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_battery": "배터리",
+ "appSettings_batteryChemistry": "배터리 화학",
+ "appSettings_batteryChemistryPerDevice": "{deviceName} 당분간",
+ "@appSettings_batteryChemistryPerDevice": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_batteryChemistryConnectFirst": "장치를 선택하기 위해 연결",
+ "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)",
+ "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)",
+ "appSettings_batteryLipo": "리튬 폴리머 (3.0-4.2V)",
+ "appSettings_mapDisplay": "지도 표시",
+ "appSettings_showRepeaters": "반복 기능 표시",
+ "appSettings_showRepeatersSubtitle": "지도에 반복자 노드를 표시",
+ "appSettings_showChatNodes": "채팅 노드 표시",
+ "appSettings_showChatNodesSubtitle": "지도에 채팅 노드를 표시",
+ "appSettings_showOtherNodes": "다른 노드 표시",
+ "appSettings_showOtherNodesSubtitle": "지도에서 다른 노드 유형을 표시",
+ "appSettings_timeFilter": "시간 필터",
+ "appSettings_timeFilterShowAll": "모든 노드 표시",
+ "appSettings_timeFilterShowLast": "지난 {hours} 시간 동안의 노드 표시",
+ "@appSettings_timeFilterShowLast": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "appSettings_mapTimeFilter": "지도 필터",
+ "appSettings_showNodesDiscoveredWithin": "다음 내역에서 발견된 노드 표시:",
+ "appSettings_allTime": "모든 시간",
+ "appSettings_lastHour": "지난 시간",
+ "appSettings_last6Hours": "지난 6시간",
+ "appSettings_last24Hours": "지난 24시간",
+ "appSettings_lastWeek": "지난 주",
+ "appSettings_offlineMapCache": "오프라인 지도 캐시",
+ "appSettings_unitsTitle": "단위",
+ "appSettings_unitsMetric": "단위 (m / km)",
+ "appSettings_unitsImperial": "제국 (피트/마일)",
+ "appSettings_noAreaSelected": "선택된 영역 없음",
+ "appSettings_areaSelectedZoom": "선택된 영역 (줌 레벨: {minZoom} - {maxZoom})",
+ "@appSettings_areaSelectedZoom": {
+ "placeholders": {
+ "minZoom": {
+ "type": "int"
+ },
+ "maxZoom": {
+ "type": "int"
+ }
+ }
+ },
+ "appSettings_debugCard": "디버깅",
+ "appSettings_appDebugLogging": "앱 디버깅 로깅",
+ "appSettings_appDebugLoggingSubtitle": "로그 앱 디버깅 메시지 (문제 해결을 위한)",
+ "appSettings_appDebugLoggingEnabled": "앱 디버깅 로깅 활성화",
+ "appSettings_appDebugLoggingDisabled": "앱 디버깅 로깅 비활성화",
+ "contacts_title": "연락처",
+ "contacts_noContacts": "아직 연락처는 없습니다.",
+ "contacts_contactsWillAppear": "장치가 광고를 할 때, 연락처 정보가 표시됩니다.",
+ "contacts_unread": "읽지 않음",
+ "contacts_searchContactsNoNumber": "연락처 검색...",
+ "contacts_searchContacts": "{number} {str} 연락처 검색...",
+ "@contacts_searchContacts": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchFavorites": "{number} {str} 검색 결과 보기...",
+ "@contacts_searchFavorites": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchUsers": "{number} {str} 사용자 검색...",
+ "@contacts_searchUsers": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchRepeaters": "{number} {str} 검색 결과 반복기 검색",
+ "@contacts_searchRepeaters": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_searchRoomServers": "{number} {str} 방 서버 검색",
+ "@contacts_searchRoomServers": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_noUnreadContacts": "읽지 않은 연락처가 없습니다.",
+ "contacts_noContactsFound": "연락처 또는 그룹이 검색되지 않았습니다.",
+ "contacts_deleteContact": "연락처 삭제",
+ "contacts_removeConfirm": "{contactName}를 연락처 목록에서 제거하시겠습니까?",
+ "@contacts_removeConfirm": {
+ "placeholders": {
+ "contactName": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_manageRepeater": "리피터 관리",
+ "contacts_manageRoom": "방 서버 관리",
+ "contacts_roomLogin": "방 서버 로그인",
+ "contacts_openChat": "자유로운 대화",
+ "contacts_editGroup": "편집 그룹",
+ "contacts_deleteGroup": "그룹 삭제",
+ "contacts_deleteGroupConfirm": "{groupName} 삭제?",
+ "@contacts_deleteGroupConfirm": {
+ "placeholders": {
+ "groupName": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_newGroup": "새로운 그룹",
+ "contacts_groupName": "그룹 이름",
+ "contacts_groupNameRequired": "그룹 이름이 필요합니다",
+ "contacts_groupNameReserved": "이 그룹 이름은 이미 사용 중입니다.",
+ "contacts_groupAlreadyExists": "그룹 \"{name}\"은 이미 존재합니다.",
+ "@contacts_groupAlreadyExists": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_filterContacts": "연락처 필터링...",
+ "contacts_noContactsMatchFilter": "입력하신 검색 조건과 일치하는 연락처가 없습니다.",
+ "contacts_noMembers": "회원 없음",
+ "contacts_lastSeenNow": "최근",
+ "contacts_lastSeenMinsAgo": "~ {minutes} min.",
+ "@contacts_lastSeenMinsAgo": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "contacts_lastSeenHourAgo": "약 1시간",
+ "contacts_lastSeenHoursAgo": "~ {hours} hours",
+ "@contacts_lastSeenHoursAgo": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "contacts_lastSeenDayAgo": "~ 1일",
+ "contacts_lastSeenDaysAgo": "~ {days}일",
+ "@contacts_lastSeenDaysAgo": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_title": "채널",
+ "channels_noChannelsConfigured": "구성된 채널이 없습니다.",
+ "channels_addPublicChannel": "공개 채널 추가",
+ "channels_searchChannels": "검색 채널...",
+ "channels_noChannelsFound": "채널을 찾을 수 없습니다.",
+ "channels_channelIndex": "채널 {index}",
+ "@channels_channelIndex": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_hashtagChannel": "해시태그 채널",
+ "channels_public": "대중의",
+ "channels_private": "사립",
+ "channels_publicChannel": "공개 채널",
+ "channels_privateChannel": "개인 채널",
+ "channels_editChannel": "채널 편집",
+ "channels_muteChannel": "음소거 채널",
+ "channels_unmuteChannel": "채널 음소거 해제",
+ "channels_deleteChannel": "채널 삭제",
+ "channels_deleteChannelConfirm": "{name} 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.",
+ "@channels_deleteChannelConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_channelDeleteFailed": "채널 \"{name}\" 삭제에 실패했습니다.",
+ "@channels_channelDeleteFailed": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_channelDeleted": "채널 \"{name}\" 삭제",
+ "@channels_channelDeleted": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_addChannel": "채널 추가",
+ "channels_channelIndexLabel": "채널 인덱스",
+ "channels_channelName": "채널 이름",
+ "channels_usePublicChannel": "공개 채널 사용",
+ "channels_standardPublicPsk": "표준 공공 PSK",
+ "channels_pskHex": "PSK (헥스)",
+ "channels_generateRandomPsk": "임의의 PSK 생성",
+ "channels_enterChannelName": "채널 이름을 입력해 주세요.",
+ "channels_pskMustBe32Hex": "PSK(개인식별키)는 32자리 16진수 문자여야 합니다.",
+ "channels_channelAdded": "채널 \"{name}\" 추가",
+ "@channels_channelAdded": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_editChannelTitle": "채널 {index} 편집",
+ "@channels_editChannelTitle": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channels_smazCompression": "SMAZ 압축",
+ "channels_channelUpdated": "채널 \"{name}\"이 업데이트되었습니다.",
+ "@channels_channelUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "channels_publicChannelAdded": "공개 채널 추가",
+ "channels_sortBy": "정렬 기준 선택",
+ "channels_sortManual": "사용 설명서",
+ "channels_sortAZ": "A부터 Z까지",
+ "channels_sortLatestMessages": "최신 메시지",
+ "channels_sortUnread": "읽지 않음",
+ "channels_createPrivateChannel": "개인 채널 만들기",
+ "channels_createPrivateChannelDesc": "비밀 키로 암호화되어 있습니다.",
+ "channels_joinPrivateChannel": "개인 채널에 참여하기",
+ "channels_joinPrivateChannelDesc": "비밀 키를 수동으로 입력합니다.",
+ "channels_joinPublicChannel": "공개 채널에 참여하세요",
+ "channels_joinPublicChannelDesc": "누구나 이 채널에 참여할 수 있습니다.",
+ "channels_joinHashtagChannel": "해시태그 채널에 참여하세요",
+ "channels_joinHashtagChannelDesc": "누구나 해시태그 채널에 참여할 수 있습니다.",
+ "channels_scanQrCode": "QR 코드를 스캔",
+ "channels_scanQrCodeComingSoon": "곧 출시",
+ "channels_enterHashtag": "해시태그 입력",
+ "channels_hashtagHint": "예: #팀",
+ "chat_noMessages": "아직 메시지가 없습니다.",
+ "chat_sendMessageToStart": "시작하려면 메시지를 보내세요.",
+ "chat_originalMessageNotFound": "원래 메시지를 찾을 수 없음",
+ "chat_replyingTo": "{name}에게 답변",
+ "@chat_replyingTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_replyTo": "{name}님께 회신",
+ "@chat_replyTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_location": "위치",
+ "chat_sendMessageTo": "{contactName}에게 메시지를 보내",
+ "@chat_sendMessageTo": {
+ "placeholders": {
+ "contactName": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_typeMessage": "메시지를 입력하세요...",
+ "chat_messageTooLong": "메시지가 너무 길어서 (최대 {maxBytes} 바이트).",
+ "@chat_messageTooLong": {
+ "placeholders": {
+ "maxBytes": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_messageCopied": "메시지가 복사되었습니다",
+ "chat_messageDeleted": "메시지가 삭제되었습니다.",
+ "chat_retryingMessage": "재시도 메시지",
+ "chat_retryCount": "{current}/{max} 시도",
+ "@chat_retryCount": {
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "max": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_sendGif": "GIF 보내기",
+ "chat_reply": "답변",
+ "chat_addReaction": "댓글 추가",
+ "chat_me": "나",
+ "emojiCategorySmileys": "이모티콘",
+ "emojiCategoryGestures": "제스처",
+ "emojiCategoryHearts": "심장",
+ "emojiCategoryObjects": "대상",
+ "gifPicker_title": "GIF 선택",
+ "gifPicker_searchHint": "GIF 검색...",
+ "gifPicker_poweredBy": "GIPHY에서 제공",
+ "gifPicker_noGifsFound": "GIF 파일이 없습니다.",
+ "gifPicker_failedLoad": "GIF 파일 로딩 실패",
+ "gifPicker_failedSearch": "GIF 검색에 실패했습니다.",
+ "gifPicker_noInternet": "인터넷 연결 없음",
+ "debugLog_appTitle": "앱 디버깅 로그",
+ "debugLog_bleTitle": "BLE 디버그 로그",
+ "debugLog_copyLog": "로그 기록",
+ "debugLog_clearLog": "명확한 로그",
+ "debugLog_copied": "디버깅 로그 복사",
+ "debugLog_bleCopied": "BLE 로그 복사",
+ "debugLog_noEntries": "현재 디버깅 로그는 생성되지 않았습니다.",
+ "debugLog_enableInSettings": "설정에서 앱 디버깅 로깅을 활성화합니다.",
+ "debugLog_frames": "프레임",
+ "debugLog_rawLogRx": "원시 로그-RX",
+ "debugLog_noBleActivity": "현재 BLE 관련 활동은 없습니다.",
+ "debugFrame_length": "프레임 길이: {count} 바이트",
+ "@debugFrame_length": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "debugFrame_command": "명령: 0x{value}",
+ "@debugFrame_command": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textMessageHeader": "텍스트 메시지 프레임:",
+ "debugFrame_destinationPubKey": "- 목적지 공개 키: {pubKey}",
+ "@debugFrame_destinationPubKey": {
+ "placeholders": {
+ "pubKey": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_timestamp": "- 시간: {timestamp}",
+ "@debugFrame_timestamp": {
+ "placeholders": {
+ "timestamp": {
+ "type": "int"
+ }
+ }
+ },
+ "debugFrame_flags": "- 플래그: 0x{value}",
+ "@debugFrame_flags": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textType": "- 텍스트 유형: {type} ({label})",
+ "@debugFrame_textType": {
+ "placeholders": {
+ "type": {
+ "type": "int"
+ },
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_textTypeCli": "명령줄 인터페이스 (CLI)",
+ "debugFrame_textTypePlain": "단순한",
+ "debugFrame_text": "- 텍스트: \"{text}\"",
+ "@debugFrame_text": {
+ "placeholders": {
+ "text": {
+ "type": "String"
+ }
+ }
+ },
+ "debugFrame_hexDump": "헥스 덤프:",
+ "chat_pathManagement": "경로 관리",
+ "chat_ShowAllPaths": "모든 경로 표시",
+ "chat_routingMode": "라우팅 방식",
+ "chat_autoUseSavedPath": "자동 (저장된 경로 사용)",
+ "chat_forceFloodMode": "강수 모드 활성화",
+ "chat_recentAckPaths": "최근 사용한 ACK 경로 (사용하려면 탭):",
+ "chat_pathHistoryFull": "이력 기록은 이미 가득 차 있습니다. 항목을 삭제하여 새로운 항목을 추가할 수 있습니다.",
+ "chat_hopSingular": "점프",
+ "chat_hopPlural": "홉",
+ "chat_hopsCount": "{count} {count, plural, =1{홉} other{홉}}",
+ "@chat_hopsCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_successes": "성공 사례",
+ "chat_removePath": "경로 제거",
+ "chat_noPathHistoryYet": "아직 경로 기록이 없습니다.\n경로를 찾기 위해 메시지를 보내세요.",
+ "chat_pathActions": "경로 작업:",
+ "chat_setCustomPath": "사용자 지정 경로 설정",
+ "chat_setCustomPathSubtitle": "수동으로 경로를 지정",
+ "chat_clearPath": "명확한 길",
+ "chat_clearPathSubtitle": "다음 전송 시, 강제 재전송 설정",
+ "chat_pathCleared": "경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.",
+ "chat_floodModeSubtitle": "앱 바에서 라우팅 스위치를 사용",
+ "chat_floodModeEnabled": "홍수 모드 활성화됨. 앱 바의 경로 아이콘을 사용하여 다시 전환할 수 있습니다.",
+ "chat_fullPath": "전체 경로",
+ "chat_pathDetailsNotAvailable": "경로 정보는 아직 제공되지 않습니다. 메시지를 보내어 다시 시도해 보세요.",
+ "chat_pathSetHops": "Path set: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
+ "@chat_pathSetHops": {
+ "placeholders": {
+ "hopCount": {
+ "type": "int"
+ },
+ "status": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_pathSavedLocally": "로컬에 저장. 동기화 연결",
+ "chat_pathDeviceConfirmed": "장치 확인 완료.",
+ "chat_pathDeviceNotConfirmed": "기기가 아직 확인되지 않았습니다.",
+ "chat_type": "종류",
+ "chat_path": "경로",
+ "chat_publicKey": "공개 키",
+ "chat_compressOutgoingMessages": "전송되는 메시지 압축",
+ "chat_floodForced": "홍수 (강제)",
+ "chat_directForced": "직접적인 (강제적인)",
+ "chat_hopsForced": "{count}번 띄우기 (강제)",
+ "@chat_hopsForced": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_floodAuto": "홍수 (자동)",
+ "chat_direct": "직접",
+ "chat_poiShared": "공유된 POI",
+ "chat_unread": "읽지 않음: {count}",
+ "@chat_unread": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "chat_openLink": "링크를 열기?",
+ "chat_openLinkConfirmation": "이 링크를 브라우저에서 열고 싶으신가요?",
+ "chat_open": "열기",
+ "chat_couldNotOpenLink": "링크를 열 수 없습니다: {url}",
+ "@chat_couldNotOpenLink": {
+ "placeholders": {
+ "url": {
+ "type": "String"
+ }
+ }
+ },
+ "chat_invalidLink": "유효하지 않은 링크 형식",
+ "map_title": "노드 매핑",
+ "map_lineOfSight": "시야",
+ "map_losScreenTitle": "시야",
+ "map_noNodesWithLocation": "위치 정보가 있는 노드가 없습니다.",
+ "map_nodesNeedGps": "노드는 지도에 표시되려면 GPS 좌표를 공유해야 합니다.",
+ "map_nodesCount": "노드: {count}",
+ "@map_nodesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "map_pinsCount": "핀: {count}",
+ "@map_pinsCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "map_chat": "채팅",
+ "map_repeater": "반복기",
+ "map_room": "방",
+ "map_sensor": "센서",
+ "map_pinDm": "핀 (DM)",
+ "map_pinPrivate": "개인 계정",
+ "map_pinPublic": "공개 (일반 공개)",
+ "map_lastSeen": "마지막으로 목격",
+ "map_disconnectConfirm": "이 장치와의 연결을 해제하시겠습니까?",
+ "map_from": "~부터",
+ "map_source": "출처",
+ "map_flags": "깃발",
+ "map_shareMarkerHere": "여기에서 마커 공유",
+ "map_setAsMyLocation": "내 위치로 설정",
+ "map_pinLabel": "핀 라벨",
+ "map_label": "레이블",
+ "map_pointOfInterest": "관심 지점",
+ "map_sendToContact": "연락처로 보내기",
+ "map_sendToChannel": "채널로 전송",
+ "map_noChannelsAvailable": "사용 가능한 채널이 없습니다.",
+ "map_publicLocationShare": "공개 장소 공유",
+ "map_publicLocationShareConfirm": "현재 {channelLabel} 채널에서 위치 정보를 공유하려고 합니다. 이 채널은 공개되어 있으며, PSK를 가진 모든 사용자가 이 위치 정보를 볼 수 있습니다.",
+ "@map_publicLocationShareConfirm": {
+ "placeholders": {
+ "channelLabel": {
+ "type": "String"
+ }
+ }
+ },
+ "map_connectToShareMarkers": "장치를 연결하여 마커를 공유",
+ "map_filterNodes": "필터 노드",
+ "map_nodeTypes": "노드 유형",
+ "map_chatNodes": "채팅 노드",
+ "map_repeaters": "다시 보내는 장치",
+ "map_otherNodes": "다른 노드",
+ "map_keyPrefix": "핵심 접두사",
+ "map_filterByKeyPrefix": "주요 접두사 기준으로 필터링",
+ "map_publicKeyPrefix": "공개 키 접두사",
+ "map_markers": "마커",
+ "map_showSharedMarkers": "공통 마커 표시",
+ "map_showGuessedLocations": "추정된 노드 위치 표시",
+ "map_showDiscoveryContacts": "디스커버리 담당자 연락처 보기",
+ "map_guessedLocation": "추측된 위치",
+ "map_lastSeenTime": "마지막으로 확인된 시간",
+ "map_sharedPin": "공유 비밀번호",
+ "map_joinRoom": "방에 참여",
+ "map_manageRepeater": "리피터 관리",
+ "map_tapToAdd": "노드에 클릭하여 경로에 추가합니다.",
+ "map_runTrace": "경로 추적",
+ "map_removeLast": "마지막 항목 삭제",
+ "map_pathTraceCancelled": "경로 추적 기능이 취소되었습니다.",
+ "mapCache_title": "오프라인 지도 캐시",
+ "mapCache_selectAreaFirst": "캐시할 영역을 먼저 선택하세요",
+ "mapCache_noTilesToDownload": "이 지역에 다운로드할 타일이 없습니다.",
+ "mapCache_downloadTilesTitle": "타일 다운로드",
+ "mapCache_downloadTilesPrompt": "{count}개의 타일을 오프라인 사용을 위해 다운로드하시겠습니까?",
+ "@mapCache_downloadTilesPrompt": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadAction": "다운로드",
+ "mapCache_cachedTiles": "{count} 개의 타일 캐시",
+ "@mapCache_cachedTiles": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_cachedTilesWithFailed": "Cached {downloaded} tiles ({failed} failed)",
+ "@mapCache_cachedTilesWithFailed": {
+ "placeholders": {
+ "downloaded": {
+ "type": "int"
+ },
+ "failed": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_clearOfflineCacheTitle": "오프라인 캐시 삭제",
+ "mapCache_clearOfflineCachePrompt": "모든 캐시된 지도 템플릿을 삭제하시겠습니까?",
+ "mapCache_offlineCacheCleared": "오프라인 캐시 삭제",
+ "mapCache_noAreaSelected": "선택된 영역 없음",
+ "mapCache_cacheArea": "캐시 영역",
+ "mapCache_useCurrentView": "현재 보기 유지",
+ "mapCache_zoomRange": "줌 기능 범위",
+ "mapCache_estimatedTiles": "예상되는 타일 개수: {count}",
+ "@mapCache_estimatedTiles": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadedTiles": "Downloaded {completed} / {total}",
+ "@mapCache_downloadedTiles": {
+ "placeholders": {
+ "completed": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_downloadTilesButton": "타일 다운로드",
+ "mapCache_clearCacheButton": "캐시 삭제",
+ "mapCache_failedDownloads": "실패한 다운로드: {count}",
+ "@mapCache_failedDownloads": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
+ "@mapCache_boundsLabel": {
+ "placeholders": {
+ "north": {
+ "type": "String"
+ },
+ "south": {
+ "type": "String"
+ },
+ "east": {
+ "type": "String"
+ },
+ "west": {
+ "type": "String"
+ }
+ }
+ },
+ "time_justNow": "방금",
+ "time_minutesAgo": "{minutes}분 전",
+ "@time_minutesAgo": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "time_hoursAgo": "{hours}h ago",
+ "@time_hoursAgo": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "time_daysAgo": "{days}일 전",
+ "@time_daysAgo": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ }
+ }
+ },
+ "time_hour": "시간",
+ "time_hours": "시간",
+ "time_day": "하루",
+ "time_days": "일",
+ "time_week": "주",
+ "time_weeks": "몇 주",
+ "time_month": "달",
+ "time_months": "개월",
+ "time_minutes": "분",
+ "time_allTime": "모든 시간",
+ "dialog_disconnect": "연결 해제",
+ "dialog_disconnectConfirm": "이 장치와의 연결을 해제하시겠습니까?",
+ "login_repeaterLogin": "다시 로그인",
+ "login_roomLogin": "방 서버 로그인",
+ "login_password": "비밀번호",
+ "login_enterPassword": "비밀번호를 입력하세요",
+ "login_savePassword": "비밀번호 저장",
+ "login_savePasswordSubtitle": "비밀번호는 이 장치에 안전하게 저장됩니다.",
+ "login_repeaterDescription": "반복기 비밀번호를 입력하여 설정 및 상태를 확인하십시오.",
+ "login_roomDescription": "설정 및 상태에 액세스하려면 방 비밀번호를 입력하세요.",
+ "login_routing": "라우팅",
+ "login_routingMode": "라우팅 모드",
+ "login_autoUseSavedPath": "자동 (저장된 경로 사용)",
+ "login_forceFloodMode": "강수 모드 활성화",
+ "login_managePaths": "경로 관리",
+ "login_login": "로그인",
+ "login_attempt": "시도 {current}/{max}",
+ "@login_attempt": {
+ "placeholders": {
+ "current": {
+ "type": "int"
+ },
+ "max": {
+ "type": "int"
+ }
+ }
+ },
+ "login_failed": "로그인 실패: {error}",
+ "@login_failed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "login_failedMessage": "로그인에 실패했습니다. 비밀번호가 잘못되었거나, 연결이 되지 않는 것 같습니다.",
+ "common_reload": "다시 로드",
+ "common_clear": "명확하게",
+ "path_currentPath": "현재 경로: {path}",
+ "@path_currentPath": {
+ "placeholders": {
+ "path": {
+ "type": "String"
+ }
+ }
+ },
+ "path_usingHopsPath": "Using {count} {count, plural, =1{hop} other{hops}} path",
+ "@path_usingHopsPath": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "path_enterCustomPath": "사용자 지정 경로 입력",
+ "path_currentPathLabel": "현재 경로",
+ "path_hexPrefixInstructions": "각 단계에 대한 2자리 헥사데진 접두사를 쉼표로 구분하여 입력하세요.",
+ "path_hexPrefixExample": "예시: A1, F2, 3C (각 노드는 자신의 공개 키의 첫 번째 바이트를 사용)",
+ "path_labelHexPrefixes": "경로 (헥스 접두사)",
+ "path_helperMaxHops": "최대 64개의 홉. 각 접두사는 2개의 16진수 문자(1바이트)로 구성됩니다.",
+ "path_selectFromContacts": "또 연락처 목록에서 선택:",
+ "path_noRepeatersFound": "반복 장치 또는 서버는 찾을 수 없습니다.",
+ "path_customPathsRequire": "사용자 정의 경로에는 메시지를 전달할 수 있는 중간 경로가 필요합니다.",
+ "path_invalidHexPrefixes": "유효하지 않은 16진수 접두사: {prefixes}",
+ "@path_invalidHexPrefixes": {
+ "placeholders": {
+ "prefixes": {
+ "type": "String"
+ }
+ }
+ },
+ "path_tooLong": "경로가 너무 길어. 최대 64개의 연결만 허용됩니다.",
+ "path_setPath": "경로 설정",
+ "repeater_management": "리피터 관리",
+ "room_management": "방 서버 관리",
+ "repeater_managementTools": "관리 도구",
+ "repeater_status": "상태",
+ "repeater_statusSubtitle": "반복 장비의 상태, 통계, 및 이웃 장비 목록 보기",
+ "repeater_telemetry": "텔레메트리",
+ "repeater_telemetrySubtitle": "센서 및 시스템 상태에 대한 통신 데이터를 확인",
+ "repeater_cli": "명령줄 인터페이스 (CLI)",
+ "repeater_cliSubtitle": "리피터에 명령을 전송",
+ "repeater_neighbors": "이웃",
+ "repeater_neighborsSubtitle": "0홉 이웃 노드를 확인합니다.",
+ "repeater_settings": "설정",
+ "repeater_settingsSubtitle": "리피터 파라미터 설정",
+ "repeater_statusTitle": "반복 장치 상태",
+ "repeater_routingMode": "라우팅 방식",
+ "repeater_autoUseSavedPath": "자동 (저장된 경로 사용)",
+ "repeater_forceFloodMode": "강수 모드 활성화",
+ "repeater_pathManagement": "경로 관리",
+ "repeater_refresh": "새롭게",
+ "repeater_statusRequestTimeout": "상태 확인 요청이 시간 초과되었습니다.",
+ "repeater_errorLoadingStatus": "상태 로딩 오류: {error}",
+ "@repeater_errorLoadingStatus": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_systemInformation": "시스템 정보",
+ "repeater_battery": "배터리",
+ "repeater_clockAtLogin": "로그인 시 시간 표시",
+ "repeater_uptime": "가동 시간",
+ "repeater_queueLength": "대기 줄의 길이",
+ "repeater_debugFlags": "디버깅 플래그",
+ "repeater_radioStatistics": "라디오 통계",
+ "repeater_lastRssi": "마지막 RSSI 값",
+ "repeater_lastSnr": "마지막 SNR",
+ "repeater_noiseFloor": "잡음 수준",
+ "repeater_txAirtime": "TX 에어타임",
+ "repeater_rxAirtime": "RX 에어타임",
+ "repeater_packetStatistics": "패킷 통계",
+ "repeater_sent": "발송",
+ "repeater_received": "수신",
+ "repeater_duplicates": "중복",
+ "repeater_daysHoursMinsSecs": "{days}일 {hours}시간 {minutes}분 {seconds}초",
+ "@repeater_daysHoursMinsSecs": {
+ "placeholders": {
+ "days": {
+ "type": "int"
+ },
+ "hours": {
+ "type": "int"
+ },
+ "minutes": {
+ "type": "int"
+ },
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_packetTxTotal": "총: {total}, 홍수: {flood}, 직접: {direct}",
+ "@repeater_packetTxTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ },
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_packetRxTotal": "총: {total}, 홍수: {flood}, 직접: {direct}",
+ "@repeater_packetRxTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ },
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_duplicatesFloodDirect": "홍수: {flood}, 직접: {direct}",
+ "@repeater_duplicatesFloodDirect": {
+ "placeholders": {
+ "flood": {
+ "type": "String"
+ },
+ "direct": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_duplicatesTotal": "총: {total}",
+ "@repeater_duplicatesTotal": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_settingsTitle": "리피터 설정",
+ "repeater_basicSettings": "기본 설정",
+ "repeater_repeaterName": "반복 장비 이름",
+ "repeater_repeaterNameHelper": "이 반복기용 표시 이름",
+ "repeater_adminPassword": "관리자 비밀번호",
+ "repeater_adminPasswordHelper": "전체 접근 권한 비밀번호",
+ "repeater_guestPassword": "게스트 비밀번호",
+ "repeater_guestPasswordHelper": "읽기 전용 접근 비밀번호",
+ "repeater_radioSettings": "라디오 설정",
+ "repeater_frequencyMhz": "주파수 (MHz)",
+ "repeater_frequencyHelper": "300-2500 MHz",
+ "repeater_txPower": "TX 파워",
+ "repeater_txPowerHelper": "1~30 dBm",
+ "repeater_bandwidth": "대역폭",
+ "repeater_spreadingFactor": "분산 계수",
+ "repeater_codingRate": "코딩 속도",
+ "repeater_locationSettings": "위치 설정",
+ "repeater_latitude": "위도",
+ "repeater_latitudeHelper": "십진법 위도 (예: 37.7749)",
+ "repeater_longitude": "경도",
+ "repeater_longitudeHelper": "십진법 위도 (예: -122.4194)",
+ "repeater_features": "특징",
+ "repeater_packetForwarding": "패킷 전송",
+ "repeater_packetForwardingSubtitle": "리피터가 패킷을 전달하도록 설정",
+ "repeater_guestAccess": "게스트 접근",
+ "repeater_guestAccessSubtitle": "게스트의 읽기 전용 접근 권한 허용",
+ "repeater_privacyMode": "개인 정보 보호 모드",
+ "repeater_privacyModeSubtitle": "광고에 이름/위치 정보 숨기기",
+ "repeater_advertisementSettings": "광고 설정",
+ "repeater_localAdvertInterval": "지역 광고 시간 간격",
+ "repeater_localAdvertIntervalMinutes": "{minutes} 분",
+ "@repeater_localAdvertIntervalMinutes": {
+ "placeholders": {
+ "minutes": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_floodAdvertInterval": "홍수 광고 간격",
+ "repeater_floodAdvertIntervalHours": "{hours} 시간",
+ "@repeater_floodAdvertIntervalHours": {
+ "placeholders": {
+ "hours": {
+ "type": "int"
+ }
+ }
+ },
+ "repeater_encryptedAdvertInterval": "암호화된 광고 간격",
+ "repeater_dangerZone": "위험 구역",
+ "repeater_rebootRepeater": "리부트 반복",
+ "repeater_rebootRepeaterSubtitle": "리피터 장치를 재시작하세요.",
+ "repeater_rebootRepeaterConfirm": "반복기를 재부팅하시려는 것이 맞으신가요?",
+ "repeater_regenerateIdentityKey": "아이디 키 재 생성",
+ "repeater_regenerateIdentityKeySubtitle": "새로운 공개/개인 키 쌍 생성",
+ "repeater_regenerateIdentityKeyConfirm": "이를 통해 리피터에 새로운 식별자를 할당합니다. 계속 진행하시겠습니까?",
+ "repeater_eraseFileSystem": "파일 시스템 삭제",
+ "repeater_eraseFileSystemSubtitle": "리피터 파일 시스템을 포맷합니다.",
+ "repeater_eraseFileSystemConfirm": "경고: 이 작업은 리피터에 있는 모든 데이터를 삭제합니다. 이 작업을 되돌릴 수 없습니다!",
+ "repeater_eraseSerialOnly": "'Erase' 기능은 시리얼 콘솔을 통해서만 사용할 수 있습니다.",
+ "repeater_commandSent": "명령 전송: {command}",
+ "@repeater_commandSent": {
+ "placeholders": {
+ "command": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_errorSendingCommand": "명령 전송 오류: {error}",
+ "@repeater_errorSendingCommand": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_confirm": "확인",
+ "repeater_settingsSaved": "설정이 성공적으로 저장되었습니다.",
+ "repeater_errorSavingSettings": "설정 저장 오류: {error}",
+ "@repeater_errorSavingSettings": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_refreshBasicSettings": "기본 설정 초기화",
+ "repeater_refreshRadioSettings": "라디오 설정 초기화",
+ "repeater_refreshTxPower": "TX 전원 재설정",
+ "repeater_refreshLocationSettings": "위치 설정 초기화",
+ "repeater_refreshPacketForwarding": "패킷 전송 재시작",
+ "repeater_refreshGuestAccess": "게스트 접근 권한 갱신",
+ "repeater_refreshPrivacyMode": "개인 정보 보호 모드 재설정",
+ "repeater_refreshAdvertisementSettings": "광고 설정 재설정",
+ "repeater_refreshed": "{label}가 갱신됨",
+ "@repeater_refreshed": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_errorRefreshing": "{label}를 새로 고침 중 오류 발생",
+ "@repeater_errorRefreshing": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_cliTitle": "리피터 CLI",
+ "repeater_debugNextCommand": "다음 명령 디버깅",
+ "repeater_commandHelp": "명령 도움",
+ "repeater_clearHistory": "명확한 역사",
+ "repeater_noCommandsSent": "아직 명령이 전송되지 않았습니다.",
+ "repeater_typeCommandOrUseQuick": "아래에 명령어를 입력하거나, 빠른 명령어를 사용하세요.",
+ "repeater_enterCommandHint": "명령어를 입력하세요...",
+ "repeater_previousCommand": "이전 명령어",
+ "repeater_nextCommand": "다음 명령어",
+ "repeater_enterCommandFirst": "먼저 명령어를 입력하세요",
+ "repeater_cliCommandFrameTitle": "CLI 명령어 프레임",
+ "repeater_cliCommandError": "오류: {error}",
+ "@repeater_cliCommandError": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "repeater_cliQuickGetName": "이름을 알려주세요",
+ "repeater_cliQuickGetRadio": "라디오 듣기",
+ "repeater_cliQuickGetTx": "TX 획득",
+ "repeater_cliQuickNeighbors": "이웃",
+ "repeater_cliQuickVersion": "버전",
+ "repeater_cliQuickAdvertise": "광고",
+ "repeater_cliQuickClock": "시계",
+ "repeater_cliHelpAdvert": "광고 패킷을 발송",
+ "repeater_cliHelpReboot": "장치를 재부팅합니다. (참고: '시간 초과' 오류가 발생할 수 있으며, 이는 정상적인 현상입니다)",
+ "repeater_cliHelpClock": "각 기기의 시계에 표시되는 현재 시간",
+ "repeater_cliHelpPassword": "장치에 새로운 관리자 비밀번호를 설정합니다.",
+ "repeater_cliHelpVersion": "장치 버전 및 펌웨어 빌드 날짜를 표시합니다.",
+ "repeater_cliHelpClearStats": "다양한 통계 지표를 0으로 초기화합니다.",
+ "repeater_cliHelpSetAf": "에어 타임 요소를 설정합니다.",
+ "repeater_cliHelpSetTx": "LoRa 전송 전력을 dBm 단위로 설정합니다. (설정을 적용하려면 재부팅 필요)",
+ "repeater_cliHelpSetRepeat": "이 노드에 대한 리피터 역할을 활성화하거나 비활성화합니다.",
+ "repeater_cliHelpSetAllowReadOnly": "(방 서버) '켜짐' 상태인 경우, 빈 비밀번호로 로그인할 수 있지만, 방에 게시할 수는 없습니다 (단, 읽기만 가능).",
+ "repeater_cliHelpSetFloodMax": "들어오는 플러드 패킷의 최대 홉 수를 설정합니다 (최대 홉 수보다 크거나 같으면 패킷은 전달되지 않습니다).",
+ "repeater_cliHelpSetIntThresh": "간섭 임계값을 설정합니다 (dB 단위). 기본값은 14입니다. 0으로 설정하면 채널 간섭 감지 기능을 비활성화합니다.",
+ "repeater_cliHelpSetAgcResetInterval": "자동 게인 제어기를 재설정하는 간격을 설정합니다. 0으로 설정하면 비활성화됩니다.",
+ "repeater_cliHelpSetMultiAcks": "'더블 ACK' 기능을 활성화하거나 비활성화할 수 있습니다.",
+ "repeater_cliHelpSetAdvertInterval": "로컬 (제로 홉) 광고 패킷을 전송하는 간격 (분 단위)을 설정합니다. 0으로 설정하면 비활성화됩니다.",
+ "repeater_cliHelpSetFloodAdvertInterval": "시간 단위로 광고 패킷을 전송하는 간격을 설정합니다. 0으로 설정하면 비활성화됩니다.",
+ "repeater_cliHelpSetGuestPassword": "게스트 비밀번호를 설정하거나 업데이트합니다. (반복 사용자, 게스트 로그인 시 \"통계 가져오기\" 요청을 보낼 수 있음)",
+ "repeater_cliHelpSetName": "광고 이름을 설정합니다.",
+ "repeater_cliHelpSetLat": "광고 지도의 위도를 설정합니다. (십진법 단위)",
+ "repeater_cliHelpSetLon": "광고 지도의 경도를 설정합니다. (십진도)",
+ "repeater_cliHelpSetRadio": "완전히 새로운 라디오 파라미터를 설정하고, 선호 사항에 저장합니다. 적용하려면 \"재부팅\" 명령이 필요합니다.",
+ "repeater_cliHelpSetRxDelay": "(실험용) 기본 설정 (최소 1이어야 함)으로, 수신된 패킷에 약간의 지연을 적용하며, 신호 강도/점수를 기준으로 설정합니다. 0으로 설정하면 비활성화됩니다.",
+ "repeater_cliHelpSetTxDelay": "공통 패킷의 전송 지연 시간을 설정하며, 시간-공기 시간과 무작위 슬롯 시스템을 곱하여 충돌 가능성을 줄입니다.",
+ "repeater_cliHelpSetDirectTxDelay": "txdelay와 동일하게, 하지만 직접 모드 패킷 전송 시 무작위 지연을 적용하는 경우",
+ "repeater_cliHelpSetBridgeEnabled": "브리지 활성화/비활성화",
+ "repeater_cliHelpSetBridgeDelay": "패킷 재전송 전에 지연 시간을 설정합니다.",
+ "repeater_cliHelpSetBridgeSource": "브리지가 수신된 패킷을 다시 전송할지, 아니면 전송된 패킷을 다시 전송할지 선택하십시오.",
+ "repeater_cliHelpSetBridgeBaud": "rs232 브리지에 대한 직렬 통신 속도(baud rate)를 설정합니다.",
+ "repeater_cliHelpSetBridgeSecret": "ESPNow 브리지에 대한 비밀 설정",
+ "repeater_cliHelpSetAdcMultiplier": "특정 보드에서만 지원되는 방식으로, 보고되는 배터리 전압을 조정하기 위한 사용자 정의 요소를 설정할 수 있습니다.",
+ "repeater_cliHelpTempRadio": "주어진 시간(분) 동안 임시 라디오 파라미터를 설정하고, 이후 원래 라디오 파라미터로 되돌립니다. (설정을 저장하지 않습니다).",
+ "repeater_cliHelpSetPerm": "ACL을 수정합니다. \"permissions\" 값이 0인 경우, 일치하는 항목(pubkey 접두사)을 제거합니다. pubkey-hex 길이가 완전하고 현재 ACL에 없는 경우 새로운 항목을 추가합니다. pubkey 접두사를 기준으로 항목을 업데이트합니다. 권한 비트는 펌웨어 역할에 따라 다르지만, 하위 2비트는 다음과 같습니다: 0 (게스트), 1 (읽기 전용), 2 (읽기/쓰기), 3 (관리자)",
+ "repeater_cliHelpGetBridgeType": "브리지형, RS232, ESPNOW 지원",
+ "repeater_cliHelpLogStart": "패킷 로깅을 파일 시스템으로 시작합니다.",
+ "repeater_cliHelpLogStop": "패킷 로깅을 파일 시스템으로 저장하는 것을 중단합니다.",
+ "repeater_cliHelpLogErase": "파일 시스템에서 패킷 로그를 삭제합니다.",
+ "repeater_cliHelpNeighbors": "제로 홉 광고를 통해 수신된 다른 리피터 노드 목록을 보여줍니다. 각 줄은 ID-프리픽스-16진수:타임스탬프:SNR-횟수-4 형식입니다.",
+ "repeater_cliHelpNeighborRemove": "이 함수는 지정된 pubkey 접두사(16진수)와 일치하는 첫 번째 항목을 이웃 목록에서 제거합니다.",
+ "repeater_cliHelpRegion": "(단일 시리즈) 정의된 모든 지역과 현재 홍수 허가 정보를 나열합니다.",
+ "repeater_cliHelpRegionLoad": "참고: 이는 여러 명령을 한 번에 실행하는 특별한 방식입니다. 각 후속 명령은 영역 이름이며 (부모 계층 구조를 나타내기 위해 공백으로 들여쓰기하며, 최소 1개의 공백을 사용) 공백으로 끝나는 줄 또는 명령을 보내어 종료합니다.",
+ "repeater_cliHelpRegionGet": "주어진 이름 접두사(또는 전역 검색을 위한 \"\\*\" 사용)를 사용하여 특정 지역을 검색합니다. 결과를 \"-> 지역 이름 (상위 지역 이름) 'F'\" 형태로 반환합니다.",
+ "repeater_cliHelpRegionPut": "주어진 이름으로 지역 정의를 추가하거나 업데이트합니다.",
+ "repeater_cliHelpRegionRemove": "지정된 이름으로 특정 영역 정의를 제거합니다. (정확히 일치해야 하며, 하위 영역은 존재하지 않아야 합니다)",
+ "repeater_cliHelpRegionAllowf": "지정된 영역에 대한 '물' 접근 권한을 설정합니다. ('*'는 전역/기존 범위에 해당)",
+ "repeater_cliHelpRegionDenyf": "지정된 영역에 대해 'Flood' 권한을 제거합니다. (참고: 현재 단계에서는 전역/기존 범위에서 이 기능을 사용하지 않는 것이 좋습니다!!)",
+ "repeater_cliHelpRegionHome": "현재 '홈' 지역으로 응답합니다. (아직 적용되지 않았으며, 향후 사용을 위해 예약됨)",
+ "repeater_cliHelpRegionHomeSet": "'홈' 지역을 설정합니다.",
+ "repeater_cliHelpRegionSave": "지역 목록/지도를 저장에 유지합니다.",
+ "repeater_cliHelpGps": "GPS 상태를 표시합니다. GPS가 꺼져 있으면 \"꺼짐\"이라고 표시하고, 켜져 있으면 \"켜짐\", 상태, 위치 정보, 위성 수 등을 표시합니다.",
+ "repeater_cliHelpGpsOnOff": "GPS 전원 상태를 켜고 끄는 기능.",
+ "repeater_cliHelpGpsSync": "노드 시간을 GPS 시계와 동기화합니다.",
+ "repeater_cliHelpGpsSetLoc": "노드의 위치를 GPS 좌표로 설정하고, 설정을 저장합니다.",
+ "repeater_cliHelpGpsAdvert": "노드의 위치 광고 설정:\n- none: 광고에 위치 정보를 포함하지 않음\n- share: GPS 위치 정보를 공유 (SensorManager에서 가져옴)\n- prefs: 설정에 저장된 위치를 광고",
+ "repeater_cliHelpGpsAdvertSet": "위치 기반 광고 설정 구성",
+ "repeater_commandsListTitle": "명령 목록",
+ "repeater_commandsListNote": "참고: 다양한 \"set...\" 명령과 함께 \"get...\" 명령도 존재합니다.",
+ "repeater_general": "일반",
+ "repeater_settingsCategory": "설정",
+ "repeater_bridge": "다리",
+ "repeater_logging": "로깅",
+ "repeater_neighborsRepeaterOnly": "이웃 (단방향 통신만 지원)",
+ "repeater_regionManagementRepeaterOnly": "지역 관리 (단, 중계 기능만 사용)",
+ "repeater_regionNote": "지역별 관리 기능을 도입하여 지역 정의 및 권한 관리를 수행할 수 있습니다.",
+ "repeater_gpsManagement": "GPS 관리",
+ "repeater_gpsNote": "GPS 명령이 위치 관련 주제를 관리하기 위해 도입되었습니다.",
+ "telemetry_receivedData": "수신된 통신 데이터",
+ "telemetry_requestTimeout": "원격 모니터링 요청이 시간 초과되었습니다.",
+ "telemetry_errorLoading": "{error} 오류로 인해 통신 데이터를 로드하지 못했습니다.",
+ "@telemetry_errorLoading": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_noData": "텔레메트리 데이터는 제공되지 않습니다.",
+ "telemetry_channelTitle": "채널 {channel}",
+ "@telemetry_channelTitle": {
+ "placeholders": {
+ "channel": {
+ "type": "int"
+ }
+ }
+ },
+ "telemetry_batteryLabel": "배터리",
+ "telemetry_voltageLabel": "전압",
+ "telemetry_mcuTemperatureLabel": "MCU의 온도",
+ "telemetry_temperatureLabel": "온도",
+ "telemetry_currentLabel": "현재",
+ "telemetry_batteryValue": "{percent}% / {volts}V",
+ "@telemetry_batteryValue": {
+ "placeholders": {
+ "percent": {
+ "type": "int"
+ },
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_voltageValue": "{volts}V",
+ "@telemetry_voltageValue": {
+ "placeholders": {
+ "volts": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_currentValue": "{amps}A",
+ "@telemetry_currentValue": {
+ "placeholders": {
+ "amps": {
+ "type": "String"
+ }
+ }
+ },
+ "telemetry_temperatureValue": "{celsius}°C / {fahrenheit}°F",
+ "@telemetry_temperatureValue": {
+ "placeholders": {
+ "celsius": {
+ "type": "String"
+ },
+ "fahrenheit": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_receivedData": "이웃 정보 수집",
+ "neighbors_requestTimedOut": "이웃들이 시간 제한을 요청하고 있습니다.",
+ "neighbors_errorLoading": "이웃 정보 로딩 중 오류: {error}",
+ "@neighbors_errorLoading": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_repeatersNeighbors": "반복기, 이웃",
+ "neighbors_noData": "이웃 정보는 없습니다.",
+ "neighbors_unknownContact": "알 수 없는 {pubkey}",
+ "@neighbors_unknownContact": {
+ "placeholders": {
+ "pubkey": {
+ "type": "String"
+ }
+ }
+ },
+ "neighbors_heardAgo": "Heard: {time} ago",
+ "@neighbors_heardAgo": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_title": "패킷 경로",
+ "channelPath_viewMap": "지도 보기",
+ "channelPath_otherObservedPaths": "관찰된 다른 경로",
+ "channelPath_repeaterHops": "반복 홉",
+ "channelPath_noHopDetails": "이 패키지에 대한 자세한 정보는 제공되지 않습니다.",
+ "channelPath_messageDetails": "메시지 세부 정보",
+ "channelPath_senderLabel": "발신자",
+ "channelPath_timeLabel": "시간",
+ "channelPath_repeatsLabel": "반복",
+ "channelPath_pathLabel": "경로 {index}",
+ "channelPath_observedLabel": "관찰",
+ "channelPath_observedPathTitle": "관찰된 경로 {index} • {hops}",
+ "@channelPath_observedPathTitle": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ },
+ "hops": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_noLocationData": "위치 정보 없음",
+ "channelPath_timeWithDate": "{day}/{month} {time}",
+ "@channelPath_timeWithDate": {
+ "placeholders": {
+ "day": {
+ "type": "int"
+ },
+ "month": {
+ "type": "int"
+ },
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_timeOnly": "{time}",
+ "@channelPath_timeOnly": {
+ "placeholders": {
+ "time": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_unknownPath": "알 수 없음",
+ "channelPath_floodPath": "홍수",
+ "channelPath_directPath": "직접",
+ "channelPath_observedZeroOf": "{total} 중 0개",
+ "@channelPath_observedZeroOf": {
+ "placeholders": {
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_observedSomeOf": "{observed} of {total} hops",
+ "@channelPath_observedSomeOf": {
+ "placeholders": {
+ "observed": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_mapTitle": "경로 지도",
+ "channelPath_noRepeaterLocations": "이 경로에 대한 중계기 설치 위치는 없습니다.",
+ "channelPath_primaryPath": "경로 {index} (주 경로)",
+ "@channelPath_primaryPath": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "@channelPath_pathLabel": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "channelPath_pathLabelTitle": "경로",
+ "channelPath_observedPathHeader": "관찰된 경로",
+ "channelPath_selectedPathLabel": "{label} • {prefixes}",
+ "@channelPath_selectedPathLabel": {
+ "placeholders": {
+ "label": {
+ "type": "String"
+ },
+ "prefixes": {
+ "type": "String"
+ }
+ }
+ },
+ "channelPath_noHopDetailsAvailable": "이 패킷에 대한 이동 정보는 제공되지 않습니다.",
+ "channelPath_unknownRepeater": "알 수 없는 중계기",
+ "community_title": "지역 사회",
+ "community_create": "커뮤니티 만들기",
+ "community_createDesc": "새로운 커뮤니티를 만들고 QR 코드를 통해 공유하세요.",
+ "community_join": "참여하기",
+ "community_joinTitle": "커뮤니티에 참여하기",
+ "community_joinConfirmation": "{name}님, 커뮤니티에 참여하고 싶으신가요?",
+ "@community_joinConfirmation": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_scanQr": "커뮤니티 QR 스캔",
+ "community_scanInstructions": "카메라를 커뮤니티 QR 코드 방향으로 향하게 하세요.",
+ "community_showQr": "QR 코드 표시",
+ "community_publicChannel": "지역 사회 대상",
+ "community_hashtagChannel": "커뮤니티 해시태그",
+ "community_name": "지역 이름",
+ "community_enterName": "커뮤니티 이름을 입력하세요",
+ "community_created": "커뮤니티 \"{name}\"이 생성되었습니다.",
+ "@community_created": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_joined": "\"{name}\" 커뮤니티에 가입",
+ "@community_joined": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_qrTitle": "커뮤니티 공유",
+ "community_qrInstructions": "이 QR 코드를 스캔하여 \"{name}\"에 가입하세요.",
+ "@community_qrInstructions": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_hashtagPrivacyHint": "커뮤니티 해시태그 채널은 커뮤니티 구성원만 가입할 수 있습니다.",
+ "community_invalidQrCode": "유효하지 않은 커뮤니티 QR 코드",
+ "community_alreadyMember": "이미 회원인 경우",
+ "community_alreadyMemberMessage": "이미 {name}의 회원입니다.",
+ "@community_alreadyMemberMessage": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_addPublicChannel": "커뮤니티 공개 채널 추가",
+ "community_addPublicChannelHint": "이 커뮤니티에 공개 채널을 자동으로 추가합니다.",
+ "community_noCommunities": "아직 어느 커뮤니티도 가입하지 않았습니다.",
+ "community_scanOrCreate": "QR 코드를 스캔하거나 커뮤니티를 만들어 시작하세요.",
+ "community_manageCommunities": "커뮤니티 관리",
+ "community_delete": "커뮤니티 떠나기",
+ "community_deleteConfirm": "{name}을 묻어두나요?",
+ "@community_deleteConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_deleteChannelsWarning": "또한, 이 기능은 {count}개의 채널과 그에 해당하는 메시지를 삭제합니다.",
+ "@community_deleteChannelsWarning": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "community_deleted": "지역 커뮤니티 \"{name}\"",
+ "@community_deleted": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerateSecret": "비밀 복원",
+ "community_regenerateSecretConfirm": "{name}의 비밀 키를 재생성하시겠습니까? 모든 회원은 계속 통신을 위해 새로운 QR 코드를 스캔해야 합니다.",
+ "@community_regenerateSecretConfirm": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_regenerate": "재생",
+ "community_secretRegenerated": "{name}을 위한 비밀 정보가 복원되었습니다.",
+ "@community_secretRegenerated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_updateSecret": "비밀 업데이트",
+ "community_secretUpdated": "{name}을 위한 비밀 정보 업데이트",
+ "@community_secretUpdated": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_scanToUpdateSecret": "새로운 QR 코드를 스캔하여 {name}의 비밀번호를 업데이트하세요.",
+ "@community_scanToUpdateSecret": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "community_addHashtagChannel": "커뮤니티 해시태그 추가",
+ "community_addHashtagChannelDesc": "이 커뮤니티를 위한 해시태그 채널을 추가하세요.",
+ "community_selectCommunity": "커뮤니티 선택",
+ "community_regularHashtag": "일반 해시태그",
+ "community_regularHashtagDesc": "공개 해시태그 (누구나 참여 가능)",
+ "community_communityHashtag": "커뮤니티 해시태그",
+ "community_communityHashtagDesc": "지역 주민을 위한",
+ "community_forCommunity": "{name} 님께",
+ "@community_forCommunity": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "listFilter_tooltip": "필터링 및 정렬",
+ "listFilter_sortBy": "정렬 기준 선택",
+ "listFilter_latestMessages": "최신 메시지",
+ "listFilter_heardRecently": "최근에 들었습니다",
+ "listFilter_az": "A부터 Z까지",
+ "listFilter_filters": "필터",
+ "listFilter_all": "모든",
+ "listFilter_favorites": "관심 목록",
+ "listFilter_addToFavorites": "즐겨찾으로 추가",
+ "listFilter_removeFromFavorites": "즐겨찾에서 제거",
+ "listFilter_users": "사용자",
+ "listFilter_repeaters": "다시 보내는 장치",
+ "listFilter_roomServers": "방 내 서버",
+ "listFilter_unreadOnly": "읽지 않은 항목만",
+ "listFilter_newGroup": "새로운 그룹",
+ "pathTrace_you": "당신",
+ "pathTrace_failed": "경로 추적 실패.",
+ "pathTrace_notAvailable": "경로 추적 기능은 제공되지 않습니다.",
+ "pathTrace_refreshTooltip": "경로 추적 재시작",
+ "pathTrace_someHopsNoLocation": "홉 중 하나 또는 여러 개에 위치 정보가 누락되었습니다!",
+ "pathTrace_clearTooltip": "명확한 경로.",
+ "losSelectStartEnd": "LOS(최소 거리 경로)의 시작 및 종료 노드를 선택합니다.",
+ "losRunFailed": "시야 확인 실패: {error}",
+ "@losRunFailed": {
+ "placeholders": {
+ "error": {
+ "type": "String"
+ }
+ }
+ },
+ "losClearAllPoints": "모든 사항을 명확히 합니다.",
+ "losRunToViewElevationProfile": "LOS(Line of Sight)를 사용하여 고도 프로필을 확인합니다.",
+ "losMenuTitle": "LOS 메뉴",
+ "losMenuSubtitle": "사용자 지정 지점을 추가하려면, 노드를 탭하거나 맵을 길게 눌러 주세요.",
+ "losShowDisplayNodes": "노드 표시",
+ "losCustomPoints": "사용자 지정 포인트",
+ "losCustomPointLabel": "맞춤형 {index}",
+ "@losCustomPointLabel": {
+ "placeholders": {
+ "index": {
+ "type": "int"
+ }
+ }
+ },
+ "losPointA": "A 지점",
+ "losPointB": "점 B",
+ "losAntennaA": "안테나 A: {value} {unit}",
+ "@losAntennaA": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "unit": {
+ "type": "String"
+ }
+ }
+ },
+ "losAntennaB": "Antenna B: {value} {unit}",
+ "@losAntennaB": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ },
+ "unit": {
+ "type": "String"
+ }
+ }
+ },
+ "losRun": "LOS (Loss of Signal) 상태로 전환",
+ "losNoElevationData": "고도 정보 없음",
+ "losProfileClear": "{distance} {distanceUnit}, clear LOS, min clearance {clearance} {heightUnit}",
+ "@losProfileClear": {
+ "placeholders": {
+ "distance": {
+ "type": "String"
+ },
+ "distanceUnit": {
+ "type": "String"
+ },
+ "clearance": {
+ "type": "String"
+ },
+ "heightUnit": {
+ "type": "String"
+ }
+ }
+ },
+ "losProfileBlocked": "{distance} {distanceUnit}, blocked by {obstruction} {heightUnit}",
+ "@losProfileBlocked": {
+ "placeholders": {
+ "distance": {
+ "type": "String"
+ },
+ "distanceUnit": {
+ "type": "String"
+ },
+ "obstruction": {
+ "type": "String"
+ },
+ "heightUnit": {
+ "type": "String"
+ }
+ }
+ },
+ "losStatusChecking": "LOS: 확인 중...",
+ "losStatusNoData": "LOS: 데이터 없음",
+ "losStatusSummary": "LOS: {clear}/{total} 개, {blocked} 개, {unknown} 개",
+ "@losStatusSummary": {
+ "placeholders": {
+ "clear": {
+ "type": "int"
+ },
+ "total": {
+ "type": "int"
+ },
+ "blocked": {
+ "type": "int"
+ },
+ "unknown": {
+ "type": "int"
+ }
+ }
+ },
+ "losErrorElevationUnavailable": "샘플 중 하나 이상에 대한 고도 데이터가 없습니다.",
+ "losErrorInvalidInput": "LOS 계산에 사용되는 부정확한 지점/고도 데이터.",
+ "losRenameCustomPoint": "사용자 지정된 지점의 이름을 변경",
+ "losPointName": "항목 이름",
+ "losShowPanelTooltip": "LOS 패널 표시",
+ "losHidePanelTooltip": "LOS 패널 숨기기",
+ "losElevationAttribution": "고도 데이터: Open-Meteo (CC BY 4.0)",
+ "losLegendRadioHorizon": "라디오 호라이즌",
+ "losLegendLosBeam": "LOS 빔",
+ "losLegendTerrain": "지형",
+ "losFrequencyLabel": "빈도",
+ "losFrequencyInfoTooltip": "계산 내역 보기",
+ "losFrequencyDialogTitle": "라디오 수신 가능 범위 계산",
+ "losFrequencyDialogDescription": "{baselineK}에서 시작하여 {baselineFreq} MHz의 주파수에서 계산을 시작하면, 현재 {frequencyMHz} MHz 대역에 대한 k-값을 조정하여, 이는 곡선형 라디오 지평선 상한선을 정의합니다.",
+ "@losFrequencyDialogDescription": {
+ "description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
+ "placeholders": {
+ "baselineK": {
+ "type": "double"
+ },
+ "baselineFreq": {
+ "type": "double"
+ },
+ "frequencyMHz": {
+ "type": "double"
+ },
+ "kFactor": {
+ "type": "double"
+ }
+ }
+ },
+ "contacts_pathTrace": "경로 추적",
+ "contacts_ping": "핑",
+ "contacts_repeaterPathTrace": "리피터로 가는 경로",
+ "contacts_repeaterPing": "핑 반복",
+ "contacts_roomPathTrace": "방 서버로의 경로 추적",
+ "contacts_roomPing": "피нг 룸 서버",
+ "contacts_chatTraceRoute": "경로 추적 경로",
+ "contacts_pathTraceTo": "{name}까지의 경로 추적",
+ "@contacts_pathTraceTo": {
+ "placeholders": {
+ "name": {
+ "type": "String"
+ }
+ }
+ },
+ "contacts_clipboardEmpty": "클립보드가 비어 있습니다.",
+ "contacts_invalidAdvertFormat": "유효하지 않은 연락 정보",
+ "contacts_contactImported": "연락이 수신되었습니다.",
+ "contacts_contactImportFailed": "연락처를 가져오지 못했습니다.",
+ "contacts_zeroHopAdvert": "제로 홉 광고",
+ "contacts_floodAdvert": "홍수 광고",
+ "contacts_copyAdvertToClipboard": "광고 텍스트를 클립보드에 복사",
+ "contacts_addContactFromClipboard": "복사본에서 연락처 추가",
+ "contacts_ShareContact": "연락처를 복사",
+ "contacts_ShareContactZeroHop": "광고를 통해 연락처 공유",
+ "contacts_zeroHopContactAdvertSent": "광고를 통해 연락처를 받았습니다.",
+ "contacts_zeroHopContactAdvertFailed": "연락처 전송에 실패했습니다.",
+ "contacts_contactAdvertCopied": "광고 내용이 복사되었습니다.",
+ "contacts_contactAdvertCopyFailed": "광고를 클립보드에 복사하는 데 실패했습니다.",
+ "notification_activityTitle": "메쉬코어 활동",
+ "notification_messagesCount": "{count} {count, plural, =1{메시지} other{메시지들}}",
+ "@notification_messagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_channelMessagesCount": "{count} {count, plural, =1{채널 메시지} other{채널 메시지}}",
+ "@notification_channelMessagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_newNodesCount": "{count} {count, plural, =1{새 노드} other{새 노드들}}",
+ "@notification_newNodesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
+ "notification_newTypeDiscovered": "새로운 {contactType} 발견",
+ "@notification_newTypeDiscovered": {
+ "placeholders": {
+ "contactType": {
+ "type": "String"
+ }
+ }
+ },
+ "notification_receivedNewMessage": "새로운 메시지를 받았습니다",
+ "settings_gpxExportRepeaters": "GPX로 전송/방 관리 서버",
+ "settings_gpxExportRepeatersSubtitle": "GPX 파일에 위치 정보를 포함하여 반복자/룸 서버를 내보냅니다.",
+ "settings_gpxExportContacts": "GPX 형식으로 내보내기",
+ "settings_gpxExportContactsSubtitle": "GPX 파일에 위치 정보를 포함하여 동행하는 기능을 내보냅니다.",
+ "settings_gpxExportAll": "모든 연락처를 GPX 형식으로 내보내기",
+ "settings_gpxExportAllSubtitle": "위치 정보가 있는 모든 연락처를 GPX 파일로 내보냅니다.",
+ "settings_gpxExportSuccess": "GPX 파일이 성공적으로 내보내졌습니다.",
+ "settings_gpxExportNoContacts": "수출할 연락처가 없습니다.",
+ "settings_gpxExportNotAvailable": "귀하의 장치/운영체제에서는 지원되지 않습니다.",
+ "settings_gpxExportError": "데이터 내보내기 과정에서 오류가 발생했습니다.",
+ "settings_gpxExportRepeatersRoom": "중계 장치 및 서버 위치",
+ "settings_gpxExportChat": "함께 방문할 장소",
+ "settings_gpxExportAllContacts": "모든 연락처 위치",
+ "settings_gpxExportShareText": "meshcore-open에서 추출한 지도 데이터",
+ "settings_gpxExportShareSubject": "meshcore-open GPX 지도 데이터 내보내기",
+ "snrIndicator_nearByRepeaters": "주변의 중계기",
+ "snrIndicator_lastSeen": "마지막으로 목격",
+ "contactsSettings_title": "연락처 설정",
+ "contactsSettings_autoAddTitle": "자동 검색",
+ "contactsSettings_otherTitle": "다른 연락 관련 설정",
+ "contactsSettings_autoAddUsersTitle": "자동으로 사용자 추가",
+ "contactsSettings_autoAddUsersSubtitle": "동반자가 자동으로 발견한 사용자를 추가할 수 있도록 합니다.",
+ "contactsSettings_autoAddRepeatersTitle": "자동으로 중계기 추가",
+ "contactsSettings_autoAddRepeatersSubtitle": "애완동물이 발견한 무선 라디오를 자동으로 추가할 수 있도록 설정합니다.",
+ "contactsSettings_autoAddRoomServersTitle": "자동으로 방 서버 추가",
+ "contactsSettings_autoAddRoomServersSubtitle": "애완동물이 발견한 방 서버를 자동으로 추가할 수 있도록 설정합니다.",
+ "contactsSettings_autoAddSensorsTitle": "자동으로 센서 추가",
+ "contactsSettings_autoAddSensorsSubtitle": "애완동물이 발견한 센서를 자동으로 추가할 수 있도록 설정합니다.",
+ "contactsSettings_overwriteOldestTitle": "가장 오래된 것을 덮어쓰기",
+ "contactsSettings_overwriteOldestSubtitle": "연락처 목록이 가득 차면, 가장 오래된 (선호하지 않은) 연락처가 대체됩니다.",
+ "discoveredContacts_Title": "연락처 찾기",
+ "discoveredContacts_noMatching": "일치하는 연락처가 없습니다.",
+ "discoveredContacts_searchHint": "발견된 연락처 검색",
+ "discoveredContacts_contactAdded": "연락처 추가",
+ "discoveredContacts_addContact": "연락처 추가",
+ "discoveredContacts_copyContact": "복사",
+ "discoveredContacts_deleteContact": "발견된 연락처 삭제",
+ "discoveredContacts_deleteContactAll": "발견된 모든 연락처 삭제",
+ "discoveredContacts_deleteContactAllContent": "정말로 모든 검색된 연락처를 삭제하시겠습니까?",
+ "chat_sendCooldown": "다시 보내기 전에 잠시 기다려 주시기 바랍니다.",
+ "appSettings_jumpToOldestUnread": "가장 오래된, 아직 읽지 않은 항목으로 이동",
+ "appSettings_jumpToOldestUnreadSubtitle": "새로운 메시지가 없는 채팅을 열 때, 최신 메시지가 아닌 첫 번째 읽지 않은 메시지로 스크롤하세요.",
+ "appSettings_languageHu": "헝가리",
+ "appSettings_languageJa": "일본어",
+ "appSettings_languageKo": "한국어",
+ "radioStats_tooltip": "라디오 및 메시 통계",
+ "radioStats_screenTitle": "라디오 통계",
+ "radioStats_notConnected": "라디오 통계를 확인하기 위해 장치에 연결합니다.",
+ "radioStats_firmwareTooOld": "무선 통계 기능을 사용하려면 v8 또는 그 이상의 호환 펌웨어가 필요합니다.",
+ "radioStats_waiting": "데이터를 기다리는 중…",
+ "radioStats_noiseFloor": "잡음 수준: {noiseDbm} dBm",
+ "@radioStats_noiseFloor": {
+ "placeholders": {
+ "noiseDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_lastRssi": "마지막 RSSI: {rssiDbm} dBm",
+ "@radioStats_lastRssi": {
+ "placeholders": {
+ "rssiDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_lastSnr": "마지막 SNR: {snr} dB",
+ "@radioStats_lastSnr": {
+ "placeholders": {
+ "snr": {
+ "type": "String"
+ }
+ }
+ },
+ "radioStats_txAir": "TX 방송 시간 (총): {seconds} 초",
+ "@radioStats_txAir": {
+ "placeholders": {
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_rxAir": "RX 사용 시간 (총): {seconds} 초",
+ "@radioStats_rxAir": {
+ "placeholders": {
+ "seconds": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_chartCaption": "최근 샘플의 잡음 수준 (dBm)",
+ "radioStats_stripNoise": "잡음 수준: {noiseDbm} dBm",
+ "@radioStats_stripNoise": {
+ "placeholders": {
+ "noiseDbm": {
+ "type": "int"
+ }
+ }
+ },
+ "radioStats_stripWaiting": "라디오 통계 가져오기…",
+ "radioStats_settingsTile": "라디오 통계",
+ "radioStats_settingsSubtitle": "잡음 수준, RSSI, 신호 대 잡음비, 통신 시간",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "개인 정보 설정",
+ "settings_privacySubtitle": "어떤 정보를 공유할지 통제하세요.",
+ "settings_privacySettingsDescription": "어떤 정보를 기기가 다른 사람들과 공유할지 선택하세요.",
+ "settings_denyAll": "모든 것을 부정",
+ "settings_allowByContact": "연락처 표시 기능 활성화",
+ "settings_allowAll": "모든 것을 허용",
+ "settings_telemetryBaseMode": "원격 모니터링 기본 설정",
+ "settings_telemetryLocationMode": "텔레메트리 위치 모드",
+ "settings_telemetryEnvironmentMode": "텔레메트리 환경 모드",
+ "settings_advertLocation": "광고 위치",
+ "settings_advertLocationSubtitle": "광고에 위치 정보를 포함하세요.",
+ "settings_multiAck": "다중 ACK: {value}",
+ "settings_telemetryModeUpdated": "텔레메트리 모드 업데이트 완료",
+ "contact_info": "연락처",
+ "contact_settings": "연락처 설정",
+ "contact_telemetry": "텔레메트리",
+ "contact_lastSeen": "마지막으로 목격",
+ "contact_clearChat": "명확한 대화",
+ "contact_teleBase": "텔레메트리 기반",
+ "contact_teleBaseSubtitle": "배터리 잔량 및 기본적인 통신 데이터를 공유할 수 있도록 허용",
+ "contact_teleLoc": "텔레메트리 위치",
+ "contact_teleLocSubtitle": "위치 정보 공유 허용",
+ "contact_teleEnv": "텔레메트리 환경",
+ "contact_teleEnvSubtitle": "환경 센서 데이터를 공유하도록 허용",
+ "map_showOverlaps": "반복 키 중복",
+ "map_runTraceWithReturnPath": "원래 경로로 돌아가세요.",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "PIN 표시",
+ "scanner_linuxPairingPinTitle": "블루투스 페어링 PIN",
+ "scanner_linuxPairingHidePin": "PIN 숨기기",
+ "scanner_linuxPairingPinPrompt": "{deviceName}에 대한 PIN을 입력하세요 (없으면 비워두세요)."
+}
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index b38c08f..db787b3 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -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'),
@@ -826,6 +832,84 @@ abstract class AppLocalizations {
/// **'Privacy mode disabled'**
String get settings_privacyModeDisabled;
+ /// No description provided for @settings_privacy.
+ ///
+ /// In en, this message translates to:
+ /// **'Privacy Settings'**
+ String get settings_privacy;
+
+ /// No description provided for @settings_privacySubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Control what information is shared.'**
+ String get settings_privacySubtitle;
+
+ /// No description provided for @settings_privacySettingsDescription.
+ ///
+ /// In en, this message translates to:
+ /// **'Choose what information your device shares with others.'**
+ String get settings_privacySettingsDescription;
+
+ /// No description provided for @settings_denyAll.
+ ///
+ /// In en, this message translates to:
+ /// **'Deny all'**
+ String get settings_denyAll;
+
+ /// No description provided for @settings_allowByContact.
+ ///
+ /// In en, this message translates to:
+ /// **'Allow by contact flags'**
+ String get settings_allowByContact;
+
+ /// No description provided for @settings_allowAll.
+ ///
+ /// In en, this message translates to:
+ /// **'Allow all'**
+ String get settings_allowAll;
+
+ /// No description provided for @settings_telemetryBaseMode.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry Base Mode'**
+ String get settings_telemetryBaseMode;
+
+ /// No description provided for @settings_telemetryLocationMode.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry Location Mode'**
+ String get settings_telemetryLocationMode;
+
+ /// No description provided for @settings_telemetryEnvironmentMode.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry Environment Mode'**
+ String get settings_telemetryEnvironmentMode;
+
+ /// No description provided for @settings_advertLocation.
+ ///
+ /// In en, this message translates to:
+ /// **'Advert Location'**
+ String get settings_advertLocation;
+
+ /// No description provided for @settings_advertLocationSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Include location in advert.'**
+ String get settings_advertLocationSubtitle;
+
+ /// No description provided for @settings_multiAck.
+ ///
+ /// In en, this message translates to:
+ /// **'Multi-ACKs: {value}'**
+ String settings_multiAck(String value);
+
+ /// No description provided for @settings_telemetryModeUpdated.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry mode updated'**
+ String get settings_telemetryModeUpdated;
+
/// No description provided for @settings_actions.
///
/// In en, this message translates to:
@@ -1360,6 +1444,72 @@ abstract class AppLocalizations {
/// **'Auto route rotation disabled'**
String get appSettings_autoRouteRotationDisabled;
+ /// No description provided for @appSettings_maxRouteWeight.
+ ///
+ /// In en, this message translates to:
+ /// **'Max Route Weight'**
+ String get appSettings_maxRouteWeight;
+
+ /// No description provided for @appSettings_maxRouteWeightSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Maximum weight a path can accumulate from successful deliveries'**
+ String get appSettings_maxRouteWeightSubtitle;
+
+ /// No description provided for @appSettings_initialRouteWeight.
+ ///
+ /// In en, this message translates to:
+ /// **'Initial Route Weight'**
+ String get appSettings_initialRouteWeight;
+
+ /// No description provided for @appSettings_initialRouteWeightSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Starting weight for newly discovered paths'**
+ String get appSettings_initialRouteWeightSubtitle;
+
+ /// No description provided for @appSettings_routeWeightSuccessIncrement.
+ ///
+ /// In en, this message translates to:
+ /// **'Success Weight Increment'**
+ String get appSettings_routeWeightSuccessIncrement;
+
+ /// No description provided for @appSettings_routeWeightSuccessIncrementSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Weight added to a path after successful delivery'**
+ String get appSettings_routeWeightSuccessIncrementSubtitle;
+
+ /// No description provided for @appSettings_routeWeightFailureDecrement.
+ ///
+ /// In en, this message translates to:
+ /// **'Failure Weight Decrement'**
+ String get appSettings_routeWeightFailureDecrement;
+
+ /// No description provided for @appSettings_routeWeightFailureDecrementSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Weight removed from a path after failed delivery'**
+ String get appSettings_routeWeightFailureDecrementSubtitle;
+
+ /// No description provided for @appSettings_maxMessageRetries.
+ ///
+ /// In en, this message translates to:
+ /// **'Max Message Retries'**
+ String get appSettings_maxMessageRetries;
+
+ /// No description provided for @appSettings_maxMessageRetriesSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Number of retry attempts before marking a message as failed'**
+ String get appSettings_maxMessageRetriesSubtitle;
+
+ /// No description provided for @path_routeWeight.
+ ///
+ /// In en, this message translates to:
+ /// **'{weight}/{max}'**
+ String path_routeWeight(String weight, String max);
+
/// No description provided for @appSettings_battery.
///
/// In en, this message translates to:
@@ -1780,6 +1930,72 @@ abstract class AppLocalizations {
/// **'~ {days} days'**
String contacts_lastSeenDaysAgo(int days);
+ /// No description provided for @contact_info.
+ ///
+ /// In en, this message translates to:
+ /// **'Contact Info'**
+ String get contact_info;
+
+ /// No description provided for @contact_settings.
+ ///
+ /// In en, this message translates to:
+ /// **'Contact Settings'**
+ String get contact_settings;
+
+ /// No description provided for @contact_telemetry.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry'**
+ String get contact_telemetry;
+
+ /// No description provided for @contact_lastSeen.
+ ///
+ /// In en, this message translates to:
+ /// **'Last seen'**
+ String get contact_lastSeen;
+
+ /// No description provided for @contact_clearChat.
+ ///
+ /// In en, this message translates to:
+ /// **'Clear Chat'**
+ String get contact_clearChat;
+
+ /// No description provided for @contact_teleBase.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry Base'**
+ String get contact_teleBase;
+
+ /// No description provided for @contact_teleBaseSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Allow sharing battery level and basic telemetry'**
+ String get contact_teleBaseSubtitle;
+
+ /// No description provided for @contact_teleLoc.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry Location'**
+ String get contact_teleLoc;
+
+ /// No description provided for @contact_teleLocSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Allow sharing location data'**
+ String get contact_teleLocSubtitle;
+
+ /// No description provided for @contact_teleEnv.
+ ///
+ /// In en, this message translates to:
+ /// **'Telemetry Environment'**
+ String get contact_teleEnv;
+
+ /// No description provided for @contact_teleEnvSubtitle.
+ ///
+ /// In en, this message translates to:
+ /// **'Allow sharing environment sensor data'**
+ String get contact_teleEnvSubtitle;
+
/// No description provided for @channels_title.
///
/// In en, this message translates to:
@@ -2842,6 +3058,12 @@ abstract class AppLocalizations {
/// **'Other Nodes'**
String get map_otherNodes;
+ /// No description provided for @map_showOverlaps.
+ ///
+ /// In en, this message translates to:
+ /// **'Repeater Key Overlaps'**
+ String get map_showOverlaps;
+
/// No description provided for @map_keyPrefix.
///
/// In en, this message translates to:
@@ -2923,9 +3145,15 @@ abstract class AppLocalizations {
/// No description provided for @map_runTrace.
///
/// In en, this message translates to:
- /// **'Run Path Trace'**
+ /// **'Run path trace'**
String get map_runTrace;
+ /// No description provided for @map_runTraceWithReturnPath.
+ ///
+ /// In en, this message translates to:
+ /// **'Return back on the same path.'**
+ String get map_runTraceWithReturnPath;
+
/// No description provided for @map_removeLast.
///
/// In en, this message translates to:
@@ -5794,6 +6022,156 @@ 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 @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);
}
class _AppLocalizationsDelegate
@@ -5812,7 +6190,10 @@ class _AppLocalizationsDelegate
'en',
'es',
'fr',
+ 'hu',
'it',
+ 'ja',
+ 'ko',
'nl',
'pl',
'pt',
@@ -5841,8 +6222,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':
diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart
index 96b67d8..2909278 100644
--- a/lib/l10n/app_localizations_bg.dart
+++ b/lib/l10n/app_localizations_bg.dart
@@ -398,6 +398,52 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Режим на поверителност е деактивиран';
+ @override
+ String get settings_privacy => 'Настройки на поверителността';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Контролирайте каква информация се споделя.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Изберете каква информация устройството ви споделя с другите.';
+
+ @override
+ String get settings_denyAll => 'Откажи всичко';
+
+ @override
+ String get settings_allowByContact => 'Позволи по флагове за контакт';
+
+ @override
+ String get settings_allowAll => 'Позволи всичко';
+
+ @override
+ String get settings_telemetryBaseMode => 'Базов режим на телеметрия';
+
+ @override
+ String get settings_telemetryLocationMode =>
+ 'Режим на местоположение на телеметрията';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Режим на средата на телеметрията';
+
+ @override
+ String get settings_advertLocation => 'Място на обявата';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Включи местоположение в обявата';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Мулти-потвърди: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Режим на телеметрията е обновен';
+
@override
String get settings_actions => 'Действия';
@@ -695,6 +741,51 @@ class AppLocalizationsBg extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Автоматично маршрутизирането е деактивирано';
+ @override
+ String get appSettings_maxRouteWeight =>
+ 'Максимално допустимо тегло на маршрута';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Максималното тегло, което един маршрут може да събере от успешни доставки.';
+
+ @override
+ String get appSettings_initialRouteWeight =>
+ 'Първоначална тежест на маршрута';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Начално тегло за новооткрити маршрути';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Увеличение на теглото за успех';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Тегло, добавено към път след успешно доставяне.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Намаляване на теглото, свързано с неуспех';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Тегло, което е било премахнато от пътя след неуспешен опит за доставка.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Максимален брой опити за изпращане на съобщение';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Брой опити за повторно изпращане, преди съобщението да бъде маркирано като неуспешно.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Батерия';
@@ -944,6 +1035,42 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Последно видян $days дни преди.';
}
+ @override
+ String get contact_info => 'Контактна информация';
+
+ @override
+ String get contact_settings => 'Настройки за контакти';
+
+ @override
+ String get contact_telemetry => 'Телеметрия';
+
+ @override
+ String get contact_lastSeen => 'Последно видян';
+
+ @override
+ String get contact_clearChat => 'Изчисти чата';
+
+ @override
+ String get contact_teleBase => 'Базата данни за телеметрия';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Позволи споделяне на ниво на батерията и основна телеметрия';
+
+ @override
+ String get contact_teleLoc => 'Местоположение на телеметрията';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Позволи споделяне на данни за местоположение';
+
+ @override
+ String get contact_teleEnv => 'Среда на телеметрия';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Позволи споделяне на данни от средносферните датчици';
+
@override
String get channels_title => 'Канали';
@@ -1562,6 +1689,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_otherNodes => 'Други възли';
+ @override
+ String get map_showOverlaps => 'Покриване на ключа на повтаряча';
+
@override
String get map_keyPrefix => 'Префикс на ключа';
@@ -1606,6 +1736,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get map_runTrace => 'Изпълни Път на Следване';
+ @override
+ String get map_runTraceWithReturnPath => 'Върни се по същия път.';
+
@override
String get map_removeLast => 'Премахни Последно';
@@ -3351,4 +3484,102 @@ 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 scanner_linuxPairingShowPin => 'Покажи PIN';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'Скрий ПИН';
+
+ @override
+ String get scanner_linuxPairingPinTitle =>
+ 'PIN код за сдвояване на Bluetooth';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Въведете ПИН за $deviceName (оставете празно, ако няма).';
+ }
}
diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart
index dcbcd3f..4afefde 100644
--- a/lib/l10n/app_localizations_de.dart
+++ b/lib/l10n/app_localizations_de.dart
@@ -398,6 +398,50 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Datenschutzmodus deaktiviert';
+ @override
+ String get settings_privacy => 'Datenschutzeinstellungen';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Steuern Sie die Informationen, die freigegeben werden.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Wählen Sie die Informationen, die Ihr Gerät mit anderen teilt.';
+
+ @override
+ String get settings_denyAll => 'Alle ablehnen';
+
+ @override
+ String get settings_allowByContact => 'Zulassen durch Kontaktflaggen';
+
+ @override
+ String get settings_allowAll => 'Alles zulassen';
+
+ @override
+ String get settings_telemetryBaseMode => 'Telemetrie-Basismodus';
+
+ @override
+ String get settings_telemetryLocationMode => 'Telemetrie-Ortsmodus';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'Telemetrie-Umgebungsmodus';
+
+ @override
+ String get settings_advertLocation => 'Anzeigenort';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Ort in der Anzeige einbeziehen';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Mehrfach-Bestätigungen: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Telemetriemodus aktualisiert';
+
@override
String get settings_actions => 'Aktionen';
@@ -695,6 +739,49 @@ class AppLocalizationsDe extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatische Routenrotation deaktiviert';
+ @override
+ String get appSettings_maxRouteWeight => 'Maximale Gesamtstreckenlänge';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Maximales Gewicht, das ein Weg durch erfolgreiche Lieferungen erreichen kann.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Anfangs-Streckengewicht';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Ausgangsgewicht für neu entdeckte Pfade';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Erhöhung des Erfolgsgewichts';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Gewicht, das einem Pfad nach erfolgreicher Lieferung hinzugefügt wird.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Reduzierung des Gewichts bei Fehlern';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Gewicht, das nach einem fehlgeschlagenen Versand von einem Weg entfernt wurde';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Maximale Anzahl an Wiederholungsversuchen';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Anzahl der Versuche, eine Nachricht erneut zu senden, bevor sie als fehlgeschlagen markiert wird.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Akku';
@@ -944,6 +1031,41 @@ class AppLocalizationsDe extends AppLocalizations {
return '~ $days Tage';
}
+ @override
+ String get contact_info => 'Kontaktinformationen';
+
+ @override
+ String get contact_settings => 'Kontakteinstellungen';
+
+ @override
+ String get contact_telemetry => 'Telemetrie';
+
+ @override
+ String get contact_lastSeen => 'Zuletzt gesehen';
+
+ @override
+ String get contact_clearChat => 'Chat löschen';
+
+ @override
+ String get contact_teleBase => 'Telemetriebasis';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Erlauben des Freigebens des Batteriestands und der grundlegenden Telemetrie';
+
+ @override
+ String get contact_teleLoc => 'Telemetrieort';
+
+ @override
+ String get contact_teleLocSubtitle => 'Teilen von Standortdaten zulassen';
+
+ @override
+ String get contact_teleEnv => 'Telemetrieumgebung';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Teilen von Umgebungsensordaten zulassen';
+
@override
String get channels_title => 'Kanäle';
@@ -1564,6 +1686,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Knoten';
+ @override
+ String get map_showOverlaps => 'Überlappungen der Repeater-Taste';
+
@override
String get map_keyPrefix => 'Schlüsselpräfix';
@@ -1608,6 +1733,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get map_runTrace => 'Pfadverlauf ausführen';
+ @override
+ String get map_runTraceWithReturnPath =>
+ 'Auf dem gleichen Pfad zurückkehren.';
+
@override
String get map_removeLast => 'Letztes Entfernen';
@@ -3365,4 +3494,100 @@ 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 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).';
+ }
}
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 01127c6..a420a55 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -392,6 +392,48 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privacy mode disabled';
+ @override
+ String get settings_privacy => 'Privacy Settings';
+
+ @override
+ String get settings_privacySubtitle => 'Control what information is shared.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Choose what information your device shares with others.';
+
+ @override
+ String get settings_denyAll => 'Deny all';
+
+ @override
+ String get settings_allowByContact => 'Allow by contact flags';
+
+ @override
+ String get settings_allowAll => 'Allow all';
+
+ @override
+ String get settings_telemetryBaseMode => 'Telemetry Base Mode';
+
+ @override
+ String get settings_telemetryLocationMode => 'Telemetry Location Mode';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'Telemetry Environment Mode';
+
+ @override
+ String get settings_advertLocation => 'Advert Location';
+
+ @override
+ String get settings_advertLocationSubtitle => 'Include location in advert.';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Multi-ACKs: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Telemetry mode updated';
+
@override
String get settings_actions => 'Actions';
@@ -684,6 +726,48 @@ class AppLocalizationsEn extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Auto route rotation disabled';
+ @override
+ String get appSettings_maxRouteWeight => 'Max Route Weight';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Maximum weight a path can accumulate from successful deliveries';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Initial Route Weight';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Starting weight for newly discovered paths';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Success Weight Increment';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Weight added to a path after successful delivery';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Failure Weight Decrement';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Weight removed from a path after failed delivery';
+
+ @override
+ String get appSettings_maxMessageRetries => 'Max Message Retries';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Number of retry attempts before marking a message as failed';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Battery';
@@ -930,6 +1014,40 @@ class AppLocalizationsEn extends AppLocalizations {
return '~ $days days';
}
+ @override
+ String get contact_info => 'Contact Info';
+
+ @override
+ String get contact_settings => 'Contact Settings';
+
+ @override
+ String get contact_telemetry => 'Telemetry';
+
+ @override
+ String get contact_lastSeen => 'Last seen';
+
+ @override
+ String get contact_clearChat => 'Clear Chat';
+
+ @override
+ String get contact_teleBase => 'Telemetry Base';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Allow sharing battery level and basic telemetry';
+
+ @override
+ String get contact_teleLoc => 'Telemetry Location';
+
+ @override
+ String get contact_teleLocSubtitle => 'Allow sharing location data';
+
+ @override
+ String get contact_teleEnv => 'Telemetry Environment';
+
+ @override
+ String get contact_teleEnvSubtitle => 'Allow sharing environment sensor data';
+
@override
String get channels_title => 'Channels';
@@ -1538,6 +1656,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get map_otherNodes => 'Other Nodes';
+ @override
+ String get map_showOverlaps => 'Repeater Key Overlaps';
+
@override
String get map_keyPrefix => 'Key Prefix';
@@ -1578,7 +1699,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
@override
- String get map_runTrace => 'Run Path Trace';
+ String get map_runTrace => 'Run path trace';
+
+ @override
+ String get map_runTraceWithReturnPath => 'Return back on the same path.';
@override
String get map_removeLast => 'Remove Last';
@@ -3297,4 +3421,98 @@ 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 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).';
+ }
}
diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart
index fac431e..93a8bc9 100644
--- a/lib/l10n/app_localizations_es.dart
+++ b/lib/l10n/app_localizations_es.dart
@@ -396,6 +396,51 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modo de privacidad desactivado';
+ @override
+ String get settings_privacy => 'Configuración de privacidad';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Controlar qué información se comparte.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Elige qué información comparte tu dispositivo con otros.';
+
+ @override
+ String get settings_denyAll => 'Denegar todo';
+
+ @override
+ String get settings_allowByContact => 'Permitir por banderas de contacto';
+
+ @override
+ String get settings_allowAll => 'Permitir todo';
+
+ @override
+ String get settings_telemetryBaseMode => 'Modo base de telemetría';
+
+ @override
+ String get settings_telemetryLocationMode =>
+ 'Modo de ubicación de telemetría';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Modo de entorno de telemetría';
+
+ @override
+ String get settings_advertLocation => 'Ubicación de anuncio';
+
+ @override
+ String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Multi-ACKs: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Modo de telemetría actualizado';
+
@override
String get settings_actions => 'Acciones';
@@ -694,6 +739,49 @@ class AppLocalizationsEs extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotación de ruta automática desactivada';
+ @override
+ String get appSettings_maxRouteWeight => 'Peso máximo permitido para la ruta';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Peso máximo que una ruta puede acumular gracias a entregas exitosas.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Peso inicial de la ruta';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Peso inicial para rutas recién descubiertas';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Incremento de peso para el éxito';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Peso añadido a una ruta después de una entrega exitosa.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Reducción del peso asociado al fallo';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Peso retirado de un camino después de un intento de entrega fallido.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Número máximo de reintentos de envío de mensajes';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Número de intentos de reintento antes de marcar un mensaje como fallido.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Batería';
@@ -944,6 +1032,42 @@ class AppLocalizationsEs extends AppLocalizations {
return '~ $days días';
}
+ @override
+ String get contact_info => 'Información de contacto';
+
+ @override
+ String get contact_settings => 'Configuración de contacto';
+
+ @override
+ String get contact_telemetry => 'Telemetría';
+
+ @override
+ String get contact_lastSeen => 'Visto por última vez';
+
+ @override
+ String get contact_clearChat => 'Borrar chat';
+
+ @override
+ String get contact_teleBase => 'Base de Telemetría';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Permitir el intercambio de nivel de batería y telemetría básica';
+
+ @override
+ String get contact_teleLoc => 'Ubicación de telemetría';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Permitir el intercambio de datos de ubicación';
+
+ @override
+ String get contact_teleEnv => 'Entorno de Telemetría';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Permitir el intercambio de datos de sensores de entorno';
+
@override
String get channels_title => 'Canales';
@@ -1561,6 +1685,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_otherNodes => 'Otros Nodos';
+ @override
+ String get map_showOverlaps => 'Superposiciones de tecla repetidora';
+
@override
String get map_keyPrefix => 'Prefijo de clave';
@@ -1604,6 +1731,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
+ @override
+ String get map_runTraceWithReturnPath => 'Volver atrás por el mismo camino.';
+
@override
String get map_removeLast => 'Eliminar último';
@@ -3357,4 +3487,100 @@ 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 scanner_linuxPairingShowPin => 'Mostrar PIN';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'Ocultar PIN';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'PIN de emparejamiento Bluetooth';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Introduzca el PIN para $deviceName (déjelo en blanco si no hay ninguno).';
+ }
}
diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart
index 6932437..9912542 100644
--- a/lib/l10n/app_localizations_fr.dart
+++ b/lib/l10n/app_localizations_fr.dart
@@ -400,6 +400,52 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Mode de confidentialité désactivé';
+ @override
+ String get settings_privacy => 'Paramètres de confidentialité';
+
+ @override
+ String get settings_privacySubtitle => 'Contrôlez les informations partagées';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Choisissez les informations que votre appareil partage avec les autres.';
+
+ @override
+ String get settings_denyAll => 'Refuser tout';
+
+ @override
+ String get settings_allowByContact => 'Autoriser par drapeaux de contact';
+
+ @override
+ String get settings_allowAll => 'Autoriser tout';
+
+ @override
+ String get settings_telemetryBaseMode => 'Mode de base Télémétrie';
+
+ @override
+ String get settings_telemetryLocationMode =>
+ 'Mode d\'emplacement de télémétrie';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Mode d\'environnement de télémétrie';
+
+ @override
+ String get settings_advertLocation => 'Emplacement de l\'annonce';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Inclure l\'emplacement dans l\'annonce';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Multi-ACKs : $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated =>
+ 'Le mode télémétrie a été mis à jour';
+
@override
String get settings_actions => 'Actions';
@@ -698,6 +744,50 @@ class AppLocalizationsFr extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotation de l\'itinéraire automatique désactivée';
+ @override
+ String get appSettings_maxRouteWeight =>
+ 'Poids maximal autorisé pour le trajet';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Poids maximal qu\'un itinéraire peut accumuler grâce à des livraisons réussies.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Poids initial de l\'itinéraire';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Poids de départ pour les nouveaux chemins découverts';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Augmentation du poids de réussite';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Poids ajouté à un itinéraire après une livraison réussie.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Réduction du poids de pénalité';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Poids retiré d\'un itinéraire après une tentative de livraison infructueuse.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Nombre maximal de tentatives de récupération de messages';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Nombre de tentatives de relance avant de marquer un message comme ayant échoué.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Batterie';
@@ -947,6 +1037,42 @@ class AppLocalizationsFr extends AppLocalizations {
return '~ $days jours';
}
+ @override
+ String get contact_info => 'Informations de contact';
+
+ @override
+ String get contact_settings => 'Paramètres de contact';
+
+ @override
+ String get contact_telemetry => 'Télémétrie';
+
+ @override
+ String get contact_lastSeen => 'Dernière fois vu';
+
+ @override
+ String get contact_clearChat => 'Effacer la conversation';
+
+ @override
+ String get contact_teleBase => 'Base de télémétrie';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Autoriser le partage du niveau de batterie et de la télémétrie de base';
+
+ @override
+ String get contact_teleLoc => 'Emplacement de télémétrie';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Autoriser le partage des données de localisation';
+
+ @override
+ String get contact_teleEnv => 'Environnement Télémétrie';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Autoriser le partage des données des capteurs d\'environnement';
+
@override
String get channels_title => 'Canaux';
@@ -1569,6 +1695,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_otherNodes => 'Autres nœuds';
+ @override
+ String get map_showOverlaps => 'Chevauchement de la touche répétitive';
+
@override
String get map_keyPrefix => 'Préfixe clé';
@@ -1613,6 +1742,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get map_runTrace => 'Exécuter la traçage de chemin';
+ @override
+ String get map_runTraceWithReturnPath => 'Revenir sur le même chemin.';
+
@override
String get map_removeLast => 'Supprimer le dernier';
@@ -3379,4 +3511,102 @@ 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 scanner_linuxPairingShowPin => 'Afficher le code PIN';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'Masquer le code PIN';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'Code PIN d’appairage Bluetooth';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Entrez le code PIN pour $deviceName (laissez vide si aucun).';
+ }
}
diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart
new file mode 100644
index 0000000..dc6374a
--- /dev/null
+++ b/lib/l10n/app_localizations_hu.dart
@@ -0,0 +1,3603 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Hungarian (`hu`).
+class AppLocalizationsHu extends AppLocalizations {
+ AppLocalizationsHu([String locale = 'hu']) : super(locale);
+
+ @override
+ String get appTitle => 'MeshCore Open';
+
+ @override
+ String get nav_contacts => 'Kapcsolatok';
+
+ @override
+ String get nav_channels => 'Csatornák';
+
+ @override
+ String get nav_map => 'Térkép';
+
+ @override
+ String get common_cancel => 'Át kell venni';
+
+ @override
+ String get common_ok => 'Rendben';
+
+ @override
+ String get common_connect => 'Kapcsolódj';
+
+ @override
+ String get common_unknownDevice => 'Tudatlan eszköz';
+
+ @override
+ String get common_save => 'Mentés';
+
+ @override
+ String get common_delete => 'Töröl';
+
+ @override
+ String get common_deleteAll => 'Minden törlés';
+
+ @override
+ String get common_close => 'Bezárás';
+
+ @override
+ String get common_edit => 'Szerkesztés';
+
+ @override
+ String get common_add => 'Hozzáad';
+
+ @override
+ String get common_settings => 'Beállítások';
+
+ @override
+ String get common_disconnect => 'Csatlakozást megszakasztani';
+
+ @override
+ String get common_connected => 'Kapcsolódó';
+
+ @override
+ String get common_disconnected => 'Elválasztva';
+
+ @override
+ String get common_create => 'Készítsd';
+
+ @override
+ String get common_continue => 'Folytatás';
+
+ @override
+ String get common_share => 'Ossza meg';
+
+ @override
+ String get common_copy => 'Másolat';
+
+ @override
+ String get common_retry => 'Újrapróbálja';
+
+ @override
+ String get common_hide => 'Elrejt';
+
+ @override
+ String get common_remove => 'Eltávolít';
+
+ @override
+ String get common_enable => 'Engedélyezés';
+
+ @override
+ String get common_disable => 'Leteteszt';
+
+ @override
+ String get common_reboot => 'Újraindítás';
+
+ @override
+ String get common_loading => 'Betöltés...';
+
+ @override
+ String get common_notAvailable => '—';
+
+ @override
+ String common_voltageValue(String volts) {
+ return '$volts V';
+ }
+
+ @override
+ String common_percentValue(int percent) {
+ return '$percent%';
+ }
+
+ @override
+ String get scanner_title => 'MeshCore nyitott';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'Bluetooth';
+
+ @override
+ String get connectionChoiceTcpLabel => 'TCP';
+
+ @override
+ String get tcpScreenTitle => 'TCP-n keresztül kapcsolódjon';
+
+ @override
+ String get tcpHostLabel => 'IP-cím';
+
+ @override
+ String get tcpHostHint => '192.168.40.10';
+
+ @override
+ String get tcpPortLabel => 'Múzeum';
+
+ @override
+ String get tcpPortHint => '5000';
+
+ @override
+ String get tcpStatus_notConnected =>
+ 'Adja meg a célpontot, majd kapcsolja össze.';
+
+ @override
+ String tcpStatus_connectingTo(String endpoint) {
+ return 'Kapcsolat a $endpoint-hez...';
+ }
+
+ @override
+ String get tcpErrorHostRequired => 'Az IP-címet meg kell adni.';
+
+ @override
+ String get tcpErrorPortInvalid => 'Az érték 1 és 65535 között kell lennie.';
+
+ @override
+ String get tcpErrorUnsupported =>
+ 'A TCP-protokoll nem támogatott ez a platformon.';
+
+ @override
+ String get tcpErrorTimedOut => 'A TCP-kapcsolat időtúllépett.';
+
+ @override
+ String tcpConnectionFailed(String error) {
+ return 'A TCP-kapcsolat sikertelen: $error';
+ }
+
+ @override
+ String get usbScreenTitle => 'USB-en keresztül csatlakoztassuk';
+
+ @override
+ String get usbScreenSubtitle =>
+ 'Válasszon egy azonosított soros eszközt, és közvetlenül csatlakoztassa a MeshCore-hoz.';
+
+ @override
+ String get usbScreenStatus => 'Válasszon egy USB-es eszközt';
+
+ @override
+ String get usbScreenNote =>
+ 'Az USB-es soros kommunikáció a támogatott Android eszközökön és asztali rendszereken is elérhető.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'Nincs USB eszköz megtalálva. Csatlakoztasson egyet, majd frissítse a rendszert.';
+
+ @override
+ String get usbErrorPermissionDenied => 'A USB-es hozzáférés megtagadva.';
+
+ @override
+ String get usbErrorDeviceMissing =>
+ 'Az kiválasztott USB eszköz már nem elérhető.';
+
+ @override
+ String get usbErrorInvalidPort => 'Válasszon egy érvényes USB-eszközt.';
+
+ @override
+ String get usbErrorBusy =>
+ 'Egy másik USB-csatlakozás kérése már folyamatban van.';
+
+ @override
+ String get usbErrorNotConnected => 'Nincs csatlakoztatott USB eszköz.';
+
+ @override
+ String get usbErrorOpenFailed =>
+ 'Nem sikerült megnyitni a kiválasztott USB-eszközöt.';
+
+ @override
+ String get usbErrorConnectFailed =>
+ 'Nem sikerült kapcsolatot létesíteni a kiválasztott USB-eszközhöz.';
+
+ @override
+ String get usbErrorUnsupported =>
+ 'Ez a platform nem támogat USB-es soros kommunikációt.';
+
+ @override
+ String get usbErrorAlreadyActive => 'Az USB-kapcsolat már be van állítva.';
+
+ @override
+ String get usbErrorNoDeviceSelected => 'Nincs kiválasztva USB eszköz.';
+
+ @override
+ String get usbErrorPortClosed => 'Az USB-kapcsolat nem aktív.';
+
+ @override
+ String get usbErrorConnectTimedOut =>
+ 'Kapcsolódás sikertelen. Ellenőrizze, hogy a eszköz rendelkezik-e USB-hez tartozó firmware-rel.';
+
+ @override
+ String get usbFallbackDeviceName => 'Web-szériás eszköz';
+
+ @override
+ String get usbStatus_notConnected => 'Válasszon egy USB-es eszközt';
+
+ @override
+ String get usbStatus_connecting => 'USB eszközhez való csatlakozás...';
+
+ @override
+ String get usbStatus_searching => 'USB eszközök keresése...';
+
+ @override
+ String usbConnectionFailed(String error) {
+ return 'USB-kapcsolat sikertelen: $error';
+ }
+
+ @override
+ String get scanner_scanning => 'Készülékek keresése...';
+
+ @override
+ String get scanner_connecting => 'Kapcsolódás...';
+
+ @override
+ String get scanner_disconnecting => 'Kapcsolat megszakad...';
+
+ @override
+ String get scanner_notConnected => 'Nem csatlakozva';
+
+ @override
+ String scanner_connectedTo(String deviceName) {
+ return 'Kapcsolódik a $deviceName-hez';
+ }
+
+ @override
+ String get scanner_searchingDevices => 'MeshCore eszközök keresése...';
+
+ @override
+ String get scanner_tapToScan =>
+ 'A Tap Scan funkció segítségével kereshet MeshCore eszközöket.';
+
+ @override
+ String scanner_connectionFailed(String error) {
+ return 'Kapcsolódás sikertelen: $error';
+ }
+
+ @override
+ String get scanner_stop => 'Megállj';
+
+ @override
+ String get scanner_scan => 'Szkenálás';
+
+ @override
+ String get scanner_bluetoothOff => 'A Bluetooth kikapcsolva';
+
+ @override
+ String get scanner_bluetoothOffMessage =>
+ 'Kérjük, kapcsolja be a Bluetooth-ot, hogy eszközök keresése lehessen.';
+
+ @override
+ String get scanner_chromeRequired => 'Chrome böngésző szükséges';
+
+ @override
+ String get scanner_chromeRequiredMessage =>
+ 'Ez az alkalmazás a Bluetooth funkcióhoz Google Chrome-ot vagy Chromium alapú böngészőt igényel.';
+
+ @override
+ String get scanner_enableBluetooth => 'Engedje be a Bluetooth funkciót';
+
+ @override
+ String get device_quickSwitch => 'Gyors váltás';
+
+ @override
+ String get device_meshcore => 'MeshCore';
+
+ @override
+ String get settings_title => 'Beállítások';
+
+ @override
+ String get settings_deviceInfo => 'A készülék információi';
+
+ @override
+ String get settings_appSettings => 'Alkalmazási beállítások';
+
+ @override
+ String get settings_appSettingsSubtitle =>
+ 'Értesítések, üzenetküldés és térképi beállítások';
+
+ @override
+ String get settings_nodeSettings => 'Műközép beállítások';
+
+ @override
+ String get settings_nodeName => 'Vonal neve';
+
+ @override
+ String get settings_nodeNameNotSet => 'Nem megállapított';
+
+ @override
+ String get settings_nodeNameHint => 'Adja meg a csomópont nevét';
+
+ @override
+ String get settings_nodeNameUpdated => 'Neve frissítve';
+
+ @override
+ String get settings_radioSettings => 'Rádióbeállítások';
+
+ @override
+ String get settings_radioSettingsSubtitle =>
+ 'Frekvencia, teljesítmény, szélesítési tényező';
+
+ @override
+ String get settings_radioSettingsUpdated => 'A rádió beállítások frissítve';
+
+ @override
+ String get settings_location => 'Helyszín';
+
+ @override
+ String get settings_locationSubtitle => 'GPS koordináták';
+
+ @override
+ String get settings_locationUpdated =>
+ 'A helyzet és a GPS beállítások frissítve';
+
+ @override
+ String get settings_locationBothRequired =>
+ 'Kérjük, adja meg a földrajzi szélességet és hosszúságot.';
+
+ @override
+ String get settings_locationInvalid =>
+ 'Érvénytelen szélesszög vagy hosszszög.';
+
+ @override
+ String get settings_locationGPSEnable => 'GPS engedélyezve';
+
+ @override
+ String get settings_locationGPSEnableSubtitle =>
+ 'Lehetővé teszi, hogy a GPS automatikusan frissítse a helyzetet.';
+
+ @override
+ String get settings_locationIntervalSec =>
+ 'GPS-számolási intervallum (másodpercek)';
+
+ @override
+ String get settings_locationIntervalInvalid =>
+ 'Az intervallum legalább 60 másodpercnek, de legfeljebb 86400 másodpercnak kell lennie.';
+
+ @override
+ String get settings_latitude => 'Nyugat-–––––––––––––––––––––––––––––––';
+
+ @override
+ String get settings_longitude => 'hosszúság';
+
+ @override
+ String get settings_contactSettings => 'Kapcsolat beállítások';
+
+ @override
+ String get settings_contactSettingsSubtitle =>
+ 'Beállítások, amelyek meghatározzák, hogyan lehet új kapcsolatokat hozzáadni.';
+
+ @override
+ String get settings_privacyMode => 'Adatvédelem mód';
+
+ @override
+ String get settings_privacyModeSubtitle =>
+ 'Elrejtsük a nevét/a helyszínt az űrianyagokban';
+
+ @override
+ String get settings_privacyModeToggle =>
+ 'Engedje be a privát üzemmódot, hogy elrejtse a nevét és a helyét az online hirdetésekben.';
+
+ @override
+ String get settings_privacyModeEnabled => 'Adatvédelem mód beállítva';
+
+ @override
+ String get settings_privacyModeDisabled => 'Adatvédelem mód kikapcsolva';
+
+ @override
+ String get settings_privacy => 'Adatvédelem beállítások';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Ellenőrizd, hogy milyen információkat osztanak meg.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Válassza ki, hogy az eszközének melyik információkat oszt meg másokkal.';
+
+ @override
+ String get settings_denyAll => 'Elutasítom';
+
+ @override
+ String get settings_allowByContact =>
+ 'Lehetővé teszi a kapcsolatok kezelését';
+
+ @override
+ String get settings_allowAll => 'Engedje meg mindent';
+
+ @override
+ String get settings_telemetryBaseMode => 'Adatkapcsolati alapállapot';
+
+ @override
+ String get settings_telemetryLocationMode => 'Adatkapcsolási helyszín mód';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Adatkapcsolati környezeti mód';
+
+ @override
+ String get settings_advertLocation => 'Reklám megjelenési hely';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'A hirdetés tartalmazza a helyszínt.';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Többszöri visszaigazolások: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'A telemetriamód frissítve';
+
+ @override
+ String get settings_actions => 'Tevékenységek';
+
+ @override
+ String get settings_sendAdvertisement => 'Hirdetés küldése';
+
+ @override
+ String get settings_sendAdvertisementSubtitle => 'A nyilvános megjelenés';
+
+ @override
+ String get settings_advertisementSent => 'Hirdetés elküldve';
+
+ @override
+ String get settings_syncTime => 'Szinkronizációs idő';
+
+ @override
+ String get settings_syncTimeSubtitle =>
+ 'Állítsa a készülék időzítését a telefon időjére';
+
+ @override
+ String get settings_timeSynchronized => 'Időben szinkronizált';
+
+ @override
+ String get settings_refreshContacts => 'Újraindítsd a kapcsolatok listát';
+
+ @override
+ String get settings_refreshContactsSubtitle =>
+ 'Újra töltse a kontaktlista-adatokat a készülékről';
+
+ @override
+ String get settings_rebootDevice => 'Újraindítás';
+
+ @override
+ String get settings_rebootDeviceSubtitle =>
+ 'Indítsa újra a MeshCore eszközt.';
+
+ @override
+ String get settings_rebootDeviceConfirm =>
+ 'Biztosan szeretné újraindítani a készüléket? Ebben az esetben a kapcsolat megszűnik.';
+
+ @override
+ String get settings_debug => 'Hibakeresés';
+
+ @override
+ String get settings_bleDebugLog => 'BLE hibaelhárítási napló';
+
+ @override
+ String get settings_bleDebugLogSubtitle =>
+ 'BLE parancsok, válaszok és alapvető adatok';
+
+ @override
+ String get settings_appDebugLog => 'App-debug log';
+
+ @override
+ String get settings_appDebugLogSubtitle => 'Programozási hibajelzések';
+
+ @override
+ String get settings_about => 'Ról';
+
+ @override
+ String settings_aboutVersion(String version) {
+ return 'MeshCore Open $version verzió';
+ }
+
+ @override
+ String get settings_aboutLegalese =>
+ '2026-os MeshCore nyílt forráskódú projekt';
+
+ @override
+ String get settings_aboutDescription =>
+ 'Egy nyílt forráskódú Flutter kliens a MeshCore LoRa hálózati eszközök számára.';
+
+ @override
+ String get settings_aboutOpenMeteoAttribution =>
+ 'LOS magassági adatok: Open-Meteo (CC BY 4.0)';
+
+ @override
+ String get settings_infoName => 'Név';
+
+ @override
+ String get settings_infoId => 'Az azonosító';
+
+ @override
+ String get settings_infoStatus => 'Állapot';
+
+ @override
+ String get settings_infoBattery => 'Akku';
+
+ @override
+ String get settings_infoPublicKey => 'Nyelvkönyv';
+
+ @override
+ String get settings_infoContactsCount => 'Kapcsolatok száma';
+
+ @override
+ String get settings_infoChannelCount => 'Csatorna száma';
+
+ @override
+ String get settings_presets => 'Előre beállított beállítások';
+
+ @override
+ String get settings_frequency => 'Frekvencia (MHz)';
+
+ @override
+ String get settings_frequencyHelper => '300,0 – 2500,0';
+
+ @override
+ String get settings_frequencyInvalid =>
+ 'Érvénytelen frekvencia (300-2500 MHz)';
+
+ @override
+ String get settings_bandwidth => 'Kapacitás';
+
+ @override
+ String get settings_spreadingFactor => 'Terjesztési tényező';
+
+ @override
+ String get settings_codingRate => 'Kódolási sebesség';
+
+ @override
+ String get settings_txPower => 'TX teljesítmény (dBm)';
+
+ @override
+ String get settings_txPowerHelper => '0 – 22';
+
+ @override
+ String get settings_txPowerInvalid =>
+ 'Érvénytelen TX teljesítmény (0-22 dBm)';
+
+ @override
+ String get settings_clientRepeat => 'Autonóm rendszer újra';
+
+ @override
+ String get settings_clientRepeatSubtitle =>
+ 'Engedje, hogy ez a eszköz mások számára is ismételje a hálózati csomagokat.';
+
+ @override
+ String get settings_clientRepeatFreqWarning =>
+ 'A hálózat nélküli kommunikációhoz 433, 869 vagy 918 MHz frekvenciát igényel.';
+
+ @override
+ String settings_error(String message) {
+ return 'Hiba: $message';
+ }
+
+ @override
+ String get appSettings_title => 'Alkalmazási beállítások';
+
+ @override
+ String get appSettings_appearance => 'Megjelenés';
+
+ @override
+ String get appSettings_theme => 'Téma';
+
+ @override
+ String get appSettings_themeSystem => 'Alapértékek';
+
+ @override
+ String get appSettings_themeLight => 'Világítás';
+
+ @override
+ String get appSettings_themeDark => 'Sötét';
+
+ @override
+ String get appSettings_language => 'Nyelv';
+
+ @override
+ String get appSettings_languageSystem => 'Alapértékek';
+
+ @override
+ String get appSettings_languageEn => 'Angol';
+
+ @override
+ String get appSettings_languageFr => 'Francia';
+
+ @override
+ String get appSettings_languageEs => 'Spanyol';
+
+ @override
+ String get appSettings_languageDe => 'Német';
+
+ @override
+ String get appSettings_languagePl => 'Lengyel';
+
+ @override
+ String get appSettings_languageSl => 'szlovén nyelv';
+
+ @override
+ String get appSettings_languagePt => 'Portugál';
+
+ @override
+ String get appSettings_languageIt => 'Olasz';
+
+ @override
+ String get appSettings_languageZh => 'Kínai';
+
+ @override
+ String get appSettings_languageSv => 'Svéd';
+
+ @override
+ String get appSettings_languageNl => 'Hollandi';
+
+ @override
+ String get appSettings_languageSk => 'Szlovén nyelvre fordítás';
+
+ @override
+ String get appSettings_languageBg => 'Bulgár';
+
+ @override
+ String get appSettings_languageRu => 'Orosz';
+
+ @override
+ String get appSettings_languageUk => 'Украинский';
+
+ @override
+ String get appSettings_enableMessageTracing =>
+ 'Engedje meg a üzenetek nyomon követését';
+
+ @override
+ String get appSettings_enableMessageTracingSubtitle =>
+ 'Adja meg a üzenetek részletes útvonal- és időzítési adatokat.';
+
+ @override
+ String get appSettings_notifications => 'Értesítések';
+
+ @override
+ String get appSettings_enableNotifications => 'Engedélyezze az értesítéseket';
+
+ @override
+ String get appSettings_enableNotificationsSubtitle =>
+ 'Kapjon értesítéseket üzenetekről és hirdetésekről.';
+
+ @override
+ String get appSettings_notificationPermissionDenied =>
+ 'A értesítési engedély megtagadva';
+
+ @override
+ String get appSettings_notificationsEnabled =>
+ 'A figyelmeztetések engedélyezve';
+
+ @override
+ String get appSettings_notificationsDisabled =>
+ 'A figyelmeztetések kikapcsolva';
+
+ @override
+ String get appSettings_messageNotifications => 'Üzenet értesítések';
+
+ @override
+ String get appSettings_messageNotificationsSubtitle =>
+ 'A figyelmeztetést megjelenítve, amikor új üzenet érkezik';
+
+ @override
+ String get appSettings_channelMessageNotifications =>
+ 'Csatorna-üzenetek értesítése';
+
+ @override
+ String get appSettings_channelMessageNotificationsSubtitle =>
+ 'A figyelmeztetést megjelenítve, amikor új üzenet érkezik a csatornáról';
+
+ @override
+ String get appSettings_advertisementNotifications => 'Reklám értesítések';
+
+ @override
+ String get appSettings_advertisementNotificationsSubtitle =>
+ 'A figyelmeztetést megjelenítve, amikor új csomópontok kerülnek felfedezésre.';
+
+ @override
+ String get appSettings_messaging => 'Üzenetek küldése';
+
+ @override
+ String get appSettings_clearPathOnMaxRetry =>
+ 'Egyértelmű út a Max Retry funkció használatával';
+
+ @override
+ String get appSettings_clearPathOnMaxRetrySubtitle =>
+ 'A kapcsolat visszaállítás 5 sikertelen továbbítás után';
+
+ @override
+ String get appSettings_pathsWillBeCleared =>
+ 'Ha 5-szer sikertelenül próbálunk, a útvonalat automatikusan tisztítjuk.';
+
+ @override
+ String get appSettings_pathsWillNotBeCleared =>
+ 'A utak automatikusan nem tisztítódnak.';
+
+ @override
+ String get appSettings_autoRouteRotation => 'Autóútok forgása';
+
+ @override
+ String get appSettings_autoRouteRotationSubtitle =>
+ 'Válasszon a legjobb útvonalak között, vagy válassza a vízözön-módot.';
+
+ @override
+ String get appSettings_autoRouteRotationEnabled =>
+ 'Az automatikus útvonalváltás engedélyezve';
+
+ @override
+ String get appSettings_autoRouteRotationDisabled =>
+ 'Az automatikus útvonal-választás funkció kikapcsolva.';
+
+ @override
+ String get appSettings_maxRouteWeight => 'Maximális útvonal súly';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'A lehető legnagyobb súly, amit egy útvonal sikeres szállítmányok során összegyűjthet.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'A kezdeti útvonal súlya';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Az új, felfedezett útvonalakhoz tartozó kezdeti súly';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Sikerhez vezető növelés';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'A sikeresen teljesített útvonalhoz hozzáadott súly.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement => 'Hibás súly csökkenése';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'A jártatásból eltávolított súly, ami a sikertelen szállítás következménye.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Maximális üzenetek újraküldési próbálkozások';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'A próbálkozások száma, mielőtt egy üzenetet hibásnak jelölünk.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
+ @override
+ String get appSettings_battery => 'Akku';
+
+ @override
+ String get appSettings_batteryChemistry => 'Aakkum töltés kémia';
+
+ @override
+ String appSettings_batteryChemistryPerDevice(String deviceName) {
+ return 'Beállítások $deviceName-hez';
+ }
+
+ @override
+ String get appSettings_batteryChemistryConnectFirst =>
+ 'Csatlakozzon egy eszközhez, hogy kiválassza';
+
+ @override
+ String get appSettings_batteryNmc => '18650 NMC (3,0-4,2 V)';
+
+ @override
+ String get appSettings_batteryLifepo4 => 'LiFePO4 (2,6–3,65 V)';
+
+ @override
+ String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)';
+
+ @override
+ String get appSettings_mapDisplay => 'Térkép megjelenítése';
+
+ @override
+ String get appSettings_showRepeaters => 'Megismétlés';
+
+ @override
+ String get appSettings_showRepeatersSubtitle =>
+ 'A térképen megjelenítsük a repeater-eket.';
+
+ @override
+ String get appSettings_showChatNodes => 'Megjeleníts kommunikációs pontokat';
+
+ @override
+ String get appSettings_showChatNodesSubtitle =>
+ 'A chat-szobákat megjelenítsük a térképen';
+
+ @override
+ String get appSettings_showOtherNodes => 'Mutasson további csomópontokat';
+
+ @override
+ String get appSettings_showOtherNodesSubtitle =>
+ 'Mutassa meg a többi hálózati elemet a térképen';
+
+ @override
+ String get appSettings_timeFilter => 'Időbeli szűrés';
+
+ @override
+ String get appSettings_timeFilterShowAll =>
+ 'Mutassa meg az összes csomópontot';
+
+ @override
+ String appSettings_timeFilterShowLast(int hours) {
+ return 'Mutasson az utolsó $hours órából származó adatokat.';
+ }
+
+ @override
+ String get appSettings_mapTimeFilter => 'Térkép időszűrő';
+
+ @override
+ String get appSettings_showNodesDiscoveredWithin =>
+ 'Megjeleníts olyan węzveket, amelyek a következő területen lettek felfedezve:';
+
+ @override
+ String get appSettings_allTime => 'Minden időpont';
+
+ @override
+ String get appSettings_lastHour => 'Az utolsó óra';
+
+ @override
+ String get appSettings_last6Hours => 'Az utóban 6 óra';
+
+ @override
+ String get appSettings_last24Hours => 'Az utóbbi 24 óra';
+
+ @override
+ String get appSettings_lastWeek => 'A múlt héten';
+
+ @override
+ String get appSettings_offlineMapCache => 'Offline térkép tárolás';
+
+ @override
+ String get appSettings_unitsTitle => 'Egységek';
+
+ @override
+ String get appSettings_unitsMetric => 'Méter (m / kilométer)';
+
+ @override
+ String get appSettings_unitsImperial => 'Királyi (láb / mérföld)';
+
+ @override
+ String get appSettings_noAreaSelected => 'Nincs kiválasztott terület.';
+
+ @override
+ String appSettings_areaSelectedZoom(int minZoom, int maxZoom) {
+ return 'Kiválasztott terület (zoom: $minZoom-$maxZoom)';
+ }
+
+ @override
+ String get appSettings_debugCard => 'Hibakeresés';
+
+ @override
+ String get appSettings_appDebugLogging =>
+ 'App-ban történő hibakereséshez használt naplózás';
+
+ @override
+ String get appSettings_appDebugLoggingSubtitle =>
+ 'Log alkalmazás hibaelhárítási üzenetek';
+
+ @override
+ String get appSettings_appDebugLoggingEnabled =>
+ 'Az alkalmazás hibaelhárítási naplózás engedélyezve';
+
+ @override
+ String get appSettings_appDebugLoggingDisabled =>
+ 'Az alkalmazás hibaelhárítási naplózatának bekapcsolása kiküszöbölve';
+
+ @override
+ String get contacts_title => 'Kapcsolatok';
+
+ @override
+ String get contacts_noContacts => 'Jelenleg még nincs kapcsolat.';
+
+ @override
+ String get contacts_contactsWillAppear =>
+ 'A kapcsolatok megjelennek, amikor a eszközök hirdetnek.';
+
+ @override
+ String get contacts_unread => 'Olvasatlan';
+
+ @override
+ String get contacts_searchContactsNoNumber => 'Kapcsolatok keresése...';
+
+ @override
+ String contacts_searchContacts(int number, String str) {
+ return 'Keresés $number-ban $str…';
+ }
+
+ @override
+ String contacts_searchFavorites(int number, String str) {
+ return 'Keresés $number$str... Kedvencek';
+ }
+
+ @override
+ String contacts_searchUsers(int number, String str) {
+ return 'Search $number$str Users...';
+ }
+
+ @override
+ String contacts_searchRepeaters(int number, String str) {
+ return 'Keresés $number-on, $str típusú adóállomások között...';
+ }
+
+ @override
+ String contacts_searchRoomServers(int number, String str) {
+ return 'Keresés $number-ban $str...';
+ }
+
+ @override
+ String get contacts_noUnreadContacts => 'Nincs olvasatlan üzenetek';
+
+ @override
+ String get contacts_noContactsFound =>
+ 'Nincs megtalálva semmilyen kapcsolat vagy csoport.';
+
+ @override
+ String get contacts_deleteContact => 'Kapcsolattól töröl';
+
+ @override
+ String contacts_removeConfirm(String contactName) {
+ return 'Hogy töröljem a $contactName nevű személyt a kontaktlistából?';
+ }
+
+ @override
+ String get contacts_manageRepeater => 'Ellenőriző eszköz kezelése';
+
+ @override
+ String get contacts_manageRoom => 'A szobai szerver kezelése';
+
+ @override
+ String get contacts_roomLogin => 'Szoba szerverbe való bejelentkezés';
+
+ @override
+ String get contacts_openChat => 'Nyitott beszélgetés';
+
+ @override
+ String get contacts_editGroup => 'Edit csoport';
+
+ @override
+ String get contacts_deleteGroup => 'Csoport törlése';
+
+ @override
+ String contacts_deleteGroupConfirm(String groupName) {
+ return 'Hogy töröljem a \"$groupName\"-t?';
+ }
+
+ @override
+ String get contacts_newGroup => 'Új csoport';
+
+ @override
+ String get contacts_groupName => 'Csoport neve';
+
+ @override
+ String get contacts_groupNameRequired =>
+ 'A csoportnak meg kell adni a nevét.';
+
+ @override
+ String get contacts_groupNameReserved => 'Ez a csoportnév foglalt';
+
+ @override
+ String contacts_groupAlreadyExists(String name) {
+ return 'A \"$name\" nevű csoport már létezik.';
+ }
+
+ @override
+ String get contacts_filterContacts => 'Szűrj kontaktokat...';
+
+ @override
+ String get contacts_noContactsMatchFilter =>
+ 'Nincs találat a megadott szűrés alapján.';
+
+ @override
+ String get contacts_noMembers => 'Nincsenek tagok';
+
+ @override
+ String get contacts_lastSeenNow => 'utóbbi időben';
+
+ @override
+ String contacts_lastSeenMinsAgo(int minutes) {
+ return '~ $minutes perc';
+ }
+
+ @override
+ String get contacts_lastSeenHourAgo => 'Kb. 1 óra';
+
+ @override
+ String contacts_lastSeenHoursAgo(int hours) {
+ return '~ $hours óra';
+ }
+
+ @override
+ String get contacts_lastSeenDayAgo => 'Kb. 1 nap';
+
+ @override
+ String contacts_lastSeenDaysAgo(int days) {
+ return '~ $days days';
+ }
+
+ @override
+ String get contact_info => 'Kapcsolattartási információk';
+
+ @override
+ String get contact_settings => 'Kapcsolat beállítások';
+
+ @override
+ String get contact_telemetry => 'Adatvisszaadás';
+
+ @override
+ String get contact_lastSeen => 'Utoljára, amikor látták';
+
+ @override
+ String get contact_clearChat => 'Tiszta beszélgetés';
+
+ @override
+ String get contact_teleBase => 'Adatgyűjtő központ';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Engedje meg a akkumulátor töltöttségi szintjének és alapvető adatoknak megosztását.';
+
+ @override
+ String get contact_teleLoc => 'Adatkapcsolati helyszín';
+
+ @override
+ String get contact_teleLocSubtitle => 'Engedje meg a helyadatok megosztását';
+
+ @override
+ String get contact_teleEnv => 'Adatkapcsolati környezet';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Engedje meg az érzékelő adatok megosztását';
+
+ @override
+ String get channels_title => 'Csatornák';
+
+ @override
+ String get channels_noChannelsConfigured => 'Nincs konfigurált csatorna.';
+
+ @override
+ String get channels_addPublicChannel => 'Hozzon létre nyilvános csatornát';
+
+ @override
+ String get channels_searchChannels => 'Keresési opciók...';
+
+ @override
+ String get channels_noChannelsFound => 'Nincs megtalálható csatorna';
+
+ @override
+ String channels_channelIndex(int index) {
+ return '$index-os csatorna';
+ }
+
+ @override
+ String get channels_hashtagChannel => 'Hashtag-ok közössége';
+
+ @override
+ String get channels_public => 'A nyilvánosság számára';
+
+ @override
+ String get channels_private => 'Személyes';
+
+ @override
+ String get channels_publicChannel => 'Össztávos csatorna';
+
+ @override
+ String get channels_privateChannel => 'Személyes csatorna';
+
+ @override
+ String get channels_editChannel => 'Csatorna szerkesztése';
+
+ @override
+ String get channels_muteChannel => 'Csendes csatorna';
+
+ @override
+ String get channels_unmuteChannel => 'Engedje be a hangot';
+
+ @override
+ String get channels_deleteChannel => 'Mozdony törlése';
+
+ @override
+ String channels_deleteChannelConfirm(String name) {
+ return 'Törlés $name? Ez nem visszafordítható.';
+ }
+
+ @override
+ String channels_channelDeleteFailed(String name) {
+ return 'Nem sikerült törölni a \"$name\" nevű csatornát.';
+ }
+
+ @override
+ String channels_channelDeleted(String name) {
+ return 'A \"$name\" nevű csatorna törölve';
+ }
+
+ @override
+ String get channels_addChannel => 'Csatorna hozzáadása';
+
+ @override
+ String get channels_channelIndexLabel => 'Csatorna index';
+
+ @override
+ String get channels_channelName => 'Csatorna neve';
+
+ @override
+ String get channels_usePublicChannel => 'Használja a nyilvános csatornát';
+
+ @override
+ String get channels_standardPublicPsk =>
+ 'Általános, állami által finanszírozott PSK';
+
+ @override
+ String get channels_pskHex => 'PSK (Hexadecimális kód)';
+
+ @override
+ String get channels_generateRandomPsk => 'Véletlenszerűen generáljon PSK-t';
+
+ @override
+ String get channels_enterChannelName => 'Kérjük, adja meg egy csatorna nevét';
+
+ @override
+ String get channels_pskMustBe32Hex =>
+ 'A PSK 32-bázisú hexadecimális karakterből áll.';
+
+ @override
+ String channels_channelAdded(String name) {
+ return 'A \"$name\" csatorna hozzáadva';
+ }
+
+ @override
+ String channels_editChannelTitle(int index) {
+ return 'Módosítsd a csatornát $index';
+ }
+
+ @override
+ String get channels_smazCompression => 'SMAZ kompresszió';
+
+ @override
+ String channels_channelUpdated(String name) {
+ return 'A $name csatorna frissítve';
+ }
+
+ @override
+ String get channels_publicChannelAdded => 'A nyilvános csatorna hozzáadva';
+
+ @override
+ String get channels_sortBy => 'Szűrés';
+
+ @override
+ String get channels_sortManual => 'Használati útmutató';
+
+ @override
+ String get channels_sortAZ => 'A-Z';
+
+ @override
+ String get channels_sortLatestMessages => 'Legfrissebb üzenetek';
+
+ @override
+ String get channels_sortUnread => 'Olvasatlan';
+
+ @override
+ String get channels_createPrivateChannel => 'Létrehoz egy privát csatornát';
+
+ @override
+ String get channels_createPrivateChannelDesc =>
+ 'Titkos kulcs segítségével védelem.';
+
+ @override
+ String get channels_joinPrivateChannel =>
+ 'Csatlakozzon egy privát csatornához';
+
+ @override
+ String get channels_joinPrivateChannelDesc =>
+ 'Kézzel adja meg a titkos kulcsot.';
+
+ @override
+ String get channels_joinPublicChannel =>
+ 'Csatlakozzon a nyilvános csatornához';
+
+ @override
+ String get channels_joinPublicChannelDesc =>
+ 'Bárki csatlakozhat ehhez a csatornához.';
+
+ @override
+ String get channels_joinHashtagChannel =>
+ 'Csatlakozzon egy hashtage-os csatornához';
+
+ @override
+ String get channels_joinHashtagChannelDesc =>
+ 'Bárkinek lehet csatlakoznia a hashtagekhez tartozó csatornához.';
+
+ @override
+ String get channels_scanQrCode => 'Scanned egy QR-kódot';
+
+ @override
+ String get channels_scanQrCodeComingSoon => 'Hamarosan';
+
+ @override
+ String get channels_enterHashtag => 'Írja be a hashtaget';
+
+ @override
+ String get channels_hashtagHint => 'pl. #csapat';
+
+ @override
+ String get chat_noMessages => 'Még nincs üzenet.';
+
+ @override
+ String get chat_sendMessageToStart => 'Küldj egy üzenetet, hogy elindulj!';
+
+ @override
+ String get chat_originalMessageNotFound => 'A eredeti üzenet nem található.';
+
+ @override
+ String chat_replyingTo(String name) {
+ return 'Replying to $name';
+ }
+
+ @override
+ String chat_replyTo(String name) {
+ return 'Reply to $name';
+ }
+
+ @override
+ String get chat_location => 'Helyszín';
+
+ @override
+ String chat_sendMessageTo(String contactName) {
+ return 'Küldj üzenetet $contactName-nek';
+ }
+
+ @override
+ String get chat_typeMessage => 'Írjon üzenetet...';
+
+ @override
+ String chat_messageTooLong(int maxBytes) {
+ return 'A üzenet túl hosszú (a maximális $maxBytes bájt).';
+ }
+
+ @override
+ String get chat_messageCopied => 'Üzenet másolva';
+
+ @override
+ String get chat_messageDeleted => 'Üzenet törölve';
+
+ @override
+ String get chat_retryingMessage => 'Újrapróbálási üzenet';
+
+ @override
+ String chat_retryCount(int current, int max) {
+ return 'Újrapróbál $current/$max';
+ }
+
+ @override
+ String get chat_sendGif => 'Küldj GIF-ot';
+
+ @override
+ String get chat_reply => 'Válasz';
+
+ @override
+ String get chat_addReaction => 'Hozzon létre reakciót';
+
+ @override
+ String get chat_me => 'Én';
+
+ @override
+ String get emojiCategorySmileys => 'Emoji';
+
+ @override
+ String get emojiCategoryGestures => 'Testmozgások';
+
+ @override
+ String get emojiCategoryHearts => 'Szívak';
+
+ @override
+ String get emojiCategoryObjects => 'Tárgyak';
+
+ @override
+ String get gifPicker_title => 'Válasszon egy GIF-et';
+
+ @override
+ String get gifPicker_searchHint => 'GIF-ek keresése...';
+
+ @override
+ String get gifPicker_poweredBy => 'Forrás: GIPHY';
+
+ @override
+ String get gifPicker_noGifsFound => 'Nincsenek GIF-ek megtalálva.';
+
+ @override
+ String get gifPicker_failedLoad => 'Nem sikerült betölteni a GIF-fájlokat.';
+
+ @override
+ String get gifPicker_failedSearch => 'Nem sikerült a GIF-eket megtalálni.';
+
+ @override
+ String get gifPicker_noInternet => 'Nincs internetkapcsolat.';
+
+ @override
+ String get debugLog_appTitle => 'App-debug log';
+
+ @override
+ String get debugLog_bleTitle => 'BLE hibajelentő napló';
+
+ @override
+ String get debugLog_copyLog => 'Másolat napló';
+
+ @override
+ String get debugLog_clearLog => 'Jelzett napló';
+
+ @override
+ String get debugLog_copied => 'Hibajelentő napló másolva';
+
+ @override
+ String get debugLog_bleCopied => 'BLE-log másolva';
+
+ @override
+ String get debugLog_noEntries =>
+ 'Jelenleg még nem léteznek hibaelhárítási naplókat.';
+
+ @override
+ String get debugLog_enableInSettings =>
+ 'Engedje be az alkalmazás hibaelhárítási naplózását a beállítások menüben.';
+
+ @override
+ String get debugLog_frames => 'Keretek';
+
+ @override
+ String get debugLog_rawLogRx => 'Az eredeti Log-RX';
+
+ @override
+ String get debugLog_noBleActivity =>
+ 'Jelenleg nincs BLE-hez kapcsolódó tevékenység.';
+
+ @override
+ String debugFrame_length(int count) {
+ return 'Keret hossza: $count bájt';
+ }
+
+ @override
+ String debugFrame_command(String value) {
+ return 'Parancs: 0x$value';
+ }
+
+ @override
+ String get debugFrame_textMessageHeader => 'Címzett:';
+
+ @override
+ String debugFrame_destinationPubKey(String pubKey) {
+ return '- Célhely: $pubKey';
+ }
+
+ @override
+ String debugFrame_timestamp(int timestamp) {
+ return '- Időbélyeg: $timestamp';
+ }
+
+ @override
+ String debugFrame_flags(String value) {
+ return '- Jelvények: 0x$value';
+ }
+
+ @override
+ String debugFrame_textType(int type, String label) {
+ return '- Tartalom típusa: $type ($label)';
+ }
+
+ @override
+ String get debugFrame_textTypeCli => 'Parancssori felület (CLI)';
+
+ @override
+ String get debugFrame_textTypePlain => 'Egyszerű, alap, hagyományos';
+
+ @override
+ String debugFrame_text(String text) {
+ return '- Tartalom: \"$text\"';
+ }
+
+ @override
+ String get debugFrame_hexDump => 'Hex-dump:';
+
+ @override
+ String get chat_pathManagement => 'Útvonal-kezelés';
+
+ @override
+ String get chat_ShowAllPaths => 'Mutasson meg minden útvonalat';
+
+ @override
+ String get chat_routingMode => 'Útvonal-kezelési mód';
+
+ @override
+ String get chat_autoUseSavedPath =>
+ 'Automatikus (az eddigi útvonal használata)';
+
+ @override
+ String get chat_forceFloodMode => 'Erőforrás-alapú áramlás mód';
+
+ @override
+ String get chat_recentAckPaths =>
+ 'Legutóbbi használt útvonalak (gombra kattintva):';
+
+ @override
+ String get chat_pathHistoryFull =>
+ 'Az előző lépések listája teljes. Törölj ki a bejegyzéseket, hogy újokat hozzáadhatsd.';
+
+ @override
+ String get chat_hopSingular => 'ugor';
+
+ @override
+ String get chat_hopPlural => 'babér';
+
+ @override
+ String chat_hopsCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'ugrások',
+ one: 'ugrás',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String get chat_successes => 'sikerek';
+
+ @override
+ String get chat_removePath => 'Törölje a elérési útvonalat';
+
+ @override
+ String get chat_noPathHistoryYet =>
+ 'Még nincs útvonal-történet.\nKüldjön egy üzenetet, hogy megtudja a lehetséges útvonalakat.';
+
+ @override
+ String get chat_pathActions => 'Céltúrások:';
+
+ @override
+ String get chat_setCustomPath => 'Beállítsd a saját útvonalat';
+
+ @override
+ String get chat_setCustomPathSubtitle => 'Kézzel megadott útvonal';
+
+ @override
+ String get chat_clearPath => 'Egyértelmű út';
+
+ @override
+ String get chat_clearPathSubtitle =>
+ 'A parancs új küldéskor újra kell aktivizálnia.';
+
+ @override
+ String get chat_pathCleared =>
+ 'Útvonal cleared. A következő üzenet újból feltérképezheti az útvonalat.';
+
+ @override
+ String get chat_floodModeSubtitle =>
+ 'Használja a \"útvonal\" kapcsolót az alkalmazás sávjában.';
+
+ @override
+ String get chat_floodModeEnabled =>
+ 'Árvízvédelmi mód bekapcsolva. A visszaállítás a alkalmazásban található útvonal ikon segítségével.';
+
+ @override
+ String get chat_fullPath => 'Teljes elérési út';
+
+ @override
+ String get chat_pathDetailsNotAvailable =>
+ 'Az útvonal részletei még nem elérhetők. Próbálja meg küldeni egy üzenetet, hogy frissítse az információkat.';
+
+ @override
+ String chat_pathSetHops(int hopCount, String status) {
+ String _temp0 = intl.Intl.pluralLogic(
+ hopCount,
+ locale: localeName,
+ other: 'hops',
+ one: 'hop',
+ );
+ return 'Path set: $hopCount $_temp0 - $status';
+ }
+
+ @override
+ String get chat_pathSavedLocally =>
+ 'Helyileg mentve. Kapcsolódjon a szinkronizáláshoz.';
+
+ @override
+ String get chat_pathDeviceConfirmed => 'A készülék megvan.';
+
+ @override
+ String get chat_pathDeviceNotConfirmed => 'A készülék még nem bizonyított.';
+
+ @override
+ String get chat_type => 'Típus';
+
+ @override
+ String get chat_path => 'Út';
+
+ @override
+ String get chat_publicKey => 'Nyelvkönyv';
+
+ @override
+ String get chat_compressOutgoingMessages => 'A küldött üzenetek tömörítése';
+
+ @override
+ String get chat_floodForced => 'Áradás (kényszerített)';
+
+ @override
+ String get chat_directForced => 'Közvetlen (erélyes)';
+
+ @override
+ String chat_hopsForced(int count) {
+ return '$count ánusz (erővel)';
+ }
+
+ @override
+ String get chat_floodAuto => 'Vízosztás (autó)';
+
+ @override
+ String get chat_direct => 'Közvetlen';
+
+ @override
+ String get chat_poiShared => 'Közös erőforrás';
+
+ @override
+ String chat_unread(int count) {
+ return 'Olvasatlan: $count';
+ }
+
+ @override
+ String get chat_openLink => 'Nyisd meg a linket?';
+
+ @override
+ String get chat_openLinkConfirmation =>
+ 'Szeretnéd megnyitni ezt a linket a böngésződben?';
+
+ @override
+ String get chat_open => 'Nyitott';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'Nem sikerült megnyitni a hivat: $url';
+ }
+
+ @override
+ String get chat_invalidLink => 'Érvénytelen hivatkozás formátum';
+
+ @override
+ String get map_title => 'Grafikus ábrázás';
+
+ @override
+ String get map_lineOfSight => 'Látási vonal';
+
+ @override
+ String get map_losScreenTitle => 'Látási vonal';
+
+ @override
+ String get map_noNodesWithLocation =>
+ 'Nincs olyan adatpont, amelyhez helyszín-információk tartoznak.';
+
+ @override
+ String get map_nodesNeedGps =>
+ 'A pontoknak meg kell osztaniuk GPS koordinátáikat, hogy megjelenjenek a térképen.';
+
+ @override
+ String map_nodesCount(int count) {
+ return 'Csúcsok: $count';
+ }
+
+ @override
+ String map_pinsCount(int count) {
+ return 'Csapok: $count';
+ }
+
+ @override
+ String get map_chat => 'Csevegés';
+
+ @override
+ String get map_repeater => 'Ismétlő';
+
+ @override
+ String get map_room => 'szoba';
+
+ @override
+ String get map_sensor => 'Érzékelő';
+
+ @override
+ String get map_pinDm => 'Jel (DM)';
+
+ @override
+ String get map_pinPrivate => 'Titkos (privát)';
+
+ @override
+ String get map_pinPublic => 'Jelmez (nyilvános)';
+
+ @override
+ String get map_lastSeen => 'Utoljára látva';
+
+ @override
+ String get map_disconnectConfirm =>
+ 'Biztosan szeretné kiírni ezt a készüléket?';
+
+ @override
+ String get map_from => 'Attól';
+
+ @override
+ String get map_source => 'Forrás';
+
+ @override
+ String get map_flags => 'Zászló';
+
+ @override
+ String get map_shareMarkerHere => 'Osztja ezt a tartalmat itt';
+
+ @override
+ String get map_setAsMyLocation => 'Állítsa be a jelenlegi helyzetemként';
+
+ @override
+ String get map_pinLabel => 'Címkét ragasztani';
+
+ @override
+ String get map_label => 'Címke';
+
+ @override
+ String get map_pointOfInterest => 'Érdekes hely';
+
+ @override
+ String get map_sendToContact => 'Kapcsolatfelvételi űrlap';
+
+ @override
+ String get map_sendToChannel => 'Küldés a csatornán';
+
+ @override
+ String get map_noChannelsAvailable => 'Nincs elérhető csatorna.';
+
+ @override
+ String get map_publicLocationShare => 'Térköz, nyilvános hely';
+
+ @override
+ String map_publicLocationShareConfirm(String channelLabel) {
+ return 'Most egy helyszínt megosztasz a $channelLabel csatornán. Ez a csatorna nyilvános, és bárki, aki rendelkezik a PSK-val, megtekintheti.';
+ }
+
+ @override
+ String get map_connectToShareMarkers =>
+ 'Kapcsolódjon egy eszközhöz, hogy megoszthassa a vonalzókat.';
+
+ @override
+ String get map_filterNodes => 'Szűrési pontok';
+
+ @override
+ String get map_nodeTypes => 'Vonalak típusai';
+
+ @override
+ String get map_chatNodes => 'Csevegési pontok';
+
+ @override
+ String get map_repeaters => 'Újraküldők';
+
+ @override
+ String get map_otherNodes => 'Egyéb csomópontok';
+
+ @override
+ String get map_showOverlaps => 'Az ismétlő kulcsok ütköznek';
+
+ @override
+ String get map_keyPrefix => 'Kulcsfontosságú előtag';
+
+ @override
+ String get map_filterByKeyPrefix => 'Szűrj a kulcsos előtér szerint';
+
+ @override
+ String get map_publicKeyPrefix => 'Névfelhasználó kulc-prefix';
+
+ @override
+ String get map_markers => 'Jelölők';
+
+ @override
+ String get map_showSharedMarkers => 'Mutassa meg a közös jeleket';
+
+ @override
+ String get map_showGuessedLocations =>
+ 'Megjelenítsa a megjósolt csomópontok helyét';
+
+ @override
+ String get map_showDiscoveryContacts =>
+ 'Megjelenítse a Discovery-nál elérhet kontaktokat';
+
+ @override
+ String get map_guessedLocation => 'Tippolt hely';
+
+ @override
+ String get map_lastSeenTime => 'Utoljára megjelent idő';
+
+ @override
+ String get map_sharedPin => 'Gemeinsames PIN-kód';
+
+ @override
+ String get map_joinRoom => 'Csatlakozás a szobához';
+
+ @override
+ String get map_manageRepeater => 'Ellenőriző eszköz kezelése';
+
+ @override
+ String get map_tapToAdd =>
+ 'Nyomj meg a csomópontokhoz, hogy hozzáadd őket az útvonalhoz.';
+
+ @override
+ String get map_runTrace => 'Útvonal követés';
+
+ @override
+ String get map_runTraceWithReturnPath => 'Visszaforduljon az eredeti úton.';
+
+ @override
+ String get map_removeLast => 'Törölj utolsó';
+
+ @override
+ String get map_pathTraceCancelled => 'Az útvonal követés megszakadt.';
+
+ @override
+ String get mapCache_title => 'Offline térkép tárolás';
+
+ @override
+ String get mapCache_selectAreaFirst =>
+ 'Válasszon egy területet, amelyet először cache-oljon.';
+
+ @override
+ String get mapCache_noTilesToDownload =>
+ 'Nincsenek letölthető tile-ok ebben a területben.';
+
+ @override
+ String get mapCache_downloadTilesTitle => 'Letöltsd a tile-okat';
+
+ @override
+ String mapCache_downloadTilesPrompt(int count) {
+ return 'Töltse le $count darab tile-t offline használatra?';
+ }
+
+ @override
+ String get mapCache_downloadAction => 'Letöltés';
+
+ @override
+ String mapCache_cachedTiles(int count) {
+ return 'Tárolt $count darab';
+ }
+
+ @override
+ String mapCache_cachedTilesWithFailed(int downloaded, int failed) {
+ return 'Cached $downloaded tiles ($failed failed)';
+ }
+
+ @override
+ String get mapCache_clearOfflineCacheTitle => 'Tiszta offline tárhely';
+
+ @override
+ String get mapCache_clearOfflineCachePrompt =>
+ 'Távolítsa el az összes tárolt térképmegjelenítőt?';
+
+ @override
+ String get mapCache_offlineCacheCleared => 'A helyi memóriát töröltük.';
+
+ @override
+ String get mapCache_noAreaSelected => 'Nincs kiválasztott terület.';
+
+ @override
+ String get mapCache_cacheArea => 'Tároló terület';
+
+ @override
+ String get mapCache_useCurrentView => 'Használja a jelenlegi nézetet';
+
+ @override
+ String get mapCache_zoomRange => 'Zoom tartomány';
+
+ @override
+ String mapCache_estimatedTiles(int count) {
+ return 'Becsült kerámiák: $count';
+ }
+
+ @override
+ String mapCache_downloadedTiles(int completed, int total) {
+ return 'Letöltve $completed / $total';
+ }
+
+ @override
+ String get mapCache_downloadTilesButton => 'Letöltsd a tile-okat';
+
+ @override
+ String get mapCache_clearCacheButton => 'Ósztótt adatokat';
+
+ @override
+ String mapCache_failedDownloads(int count) {
+ return 'Sikertelen letöltések: $count';
+ }
+
+ @override
+ String mapCache_boundsLabel(
+ String north,
+ String south,
+ String east,
+ String west,
+ ) {
+ return 'N $north, S $south, E $east, W $west';
+ }
+
+ @override
+ String get time_justNow => 'Most';
+
+ @override
+ String time_minutesAgo(int minutes) {
+ return '$minutes perckel ezelőtt';
+ }
+
+ @override
+ String time_hoursAgo(int hours) {
+ return '$hours óva';
+ }
+
+ @override
+ String time_daysAgo(int days) {
+ return '${days}d ago';
+ }
+
+ @override
+ String get time_hour => 'óra';
+
+ @override
+ String get time_hours => 'órák';
+
+ @override
+ String get time_day => 'nap';
+
+ @override
+ String get time_days => 'napok';
+
+ @override
+ String get time_week => 'het';
+
+ @override
+ String get time_weeks => 'het, hetek';
+
+ @override
+ String get time_month => 'hónap';
+
+ @override
+ String get time_months => 'hónapok';
+
+ @override
+ String get time_minutes => 'percek';
+
+ @override
+ String get time_allTime => 'Bármely időpont';
+
+ @override
+ String get dialog_disconnect => 'Csatlakozást megszakasztani';
+
+ @override
+ String get dialog_disconnectConfirm =>
+ 'Biztosan szeretné kiírni ezt a készüléket?';
+
+ @override
+ String get login_repeaterLogin => 'Ismételt bejelentkezés';
+
+ @override
+ String get login_roomLogin => 'Szoba szerverbe való bejelentkezés';
+
+ @override
+ String get login_password => 'Jelszó';
+
+ @override
+ String get login_enterPassword => 'Adja meg a jelszót';
+
+ @override
+ String get login_savePassword => 'Mentse el a jelszót';
+
+ @override
+ String get login_savePasswordSubtitle =>
+ 'A jelszó biztonságosan tárolódik ezen a készüléken.';
+
+ @override
+ String get login_repeaterDescription =>
+ 'Adja meg a repeater (ismétítő) jelszót, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.';
+
+ @override
+ String get login_roomDescription =>
+ 'Adja meg a belépési kódot, hogy hozzáférhessen a beállításokhoz és az állapot információkhoz.';
+
+ @override
+ String get login_routing => 'Útvonal meghatározás';
+
+ @override
+ String get login_routingMode => 'Útvonal-kezelési mód';
+
+ @override
+ String get login_autoUseSavedPath =>
+ 'Automatikus (az eddigi útvonal használata)';
+
+ @override
+ String get login_forceFloodMode => 'Erőforrás-alapú áramlás mód';
+
+ @override
+ String get login_managePaths => 'Útvonalak kezelése';
+
+ @override
+ String get login_login => 'Bejelentkezés';
+
+ @override
+ String login_attempt(int current, int max) {
+ return 'Megpróbálás $current/$max-adik';
+ }
+
+ @override
+ String login_failed(String error) {
+ return 'Belépés sikertelen: $error';
+ }
+
+ @override
+ String get login_failedMessage =>
+ 'Belépés sikertelen. Vagy a jelszó helytelen, vagy a hálózati kapcsolat nem létesül.';
+
+ @override
+ String get common_reload => 'Újra töltés';
+
+ @override
+ String get common_clear => 'Egyértelmű';
+
+ @override
+ String path_currentPath(String path) {
+ return 'Jelenlegi útvonal: $path';
+ }
+
+ @override
+ String path_usingHopsPath(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'ugrások',
+ one: 'ugrás',
+ );
+ return '$count $_temp0 útvonal használata';
+ }
+
+ @override
+ String get path_enterCustomPath => 'Adja meg a saját elérési útvonalat';
+
+ @override
+ String get path_currentPathLabel => 'Jelenlegi útvonal';
+
+ @override
+ String get path_hexPrefixInstructions =>
+ 'Adja meg a 2 karakteres hexadecimális előtagokat minden lépéshez, tagolva kommával.';
+
+ @override
+ String get path_hexPrefixExample =>
+ 'Példa: A1, F2, 3C (minden csomó az első részét használja a nyilvános kulcsából)';
+
+ @override
+ String get path_labelHexPrefixes => 'Út (hex-prefixek)';
+
+ @override
+ String get path_helperMaxHops =>
+ 'A maximális hossz 64 karakter. Minden előző rész 2 hatos számjegyből áll (1 bájt).';
+
+ @override
+ String get path_selectFromContacts =>
+ 'Válasszon a kontaktlista elembek közül:';
+
+ @override
+ String get path_noRepeatersFound =>
+ 'Nincs megtalálva semmilyen ismétlődő vagy helyiség-szolgáltató szervert.';
+
+ @override
+ String get path_customPathsRequire =>
+ 'Az egyedi útvonalaknak szükségük van átjáró pontokra, amelyek képesek üzeneteket továbbítani.';
+
+ @override
+ String path_invalidHexPrefixes(String prefixes) {
+ return 'Érvénytelen hexadecimális előtagok: $prefixes';
+ }
+
+ @override
+ String get path_tooLong =>
+ 'Az út túl hosszú. A maximális engedélyezett lépések száma 64.';
+
+ @override
+ String get path_setPath => 'Útvonal meghatározása';
+
+ @override
+ String get repeater_management => 'Adatkapcsolás kezelése';
+
+ @override
+ String get room_management => 'Szoba-szerver kezelés';
+
+ @override
+ String get repeater_managementTools => 'Menedzsmentes eszközök';
+
+ @override
+ String get repeater_status => 'Állapot';
+
+ @override
+ String get repeater_statusSubtitle =>
+ 'Megtekintheted a repeater állapotát, statisztikáit és a környező eszközök adatait.';
+
+ @override
+ String get repeater_telemetry => 'Adatvisszaadás';
+
+ @override
+ String get repeater_telemetrySubtitle =>
+ 'Tekintsük a szenzorok és a rendszer állapotának adatát';
+
+ @override
+ String get repeater_cli => 'Parancssori felület (CLI)';
+
+ @override
+ String get repeater_cliSubtitle => 'Küldj parancsokat a repeaternek.';
+
+ @override
+ String get repeater_neighbors => 'Szomszédok';
+
+ @override
+ String get repeater_neighborsSubtitle =>
+ 'Tekintsük a nullás lépésű szomszédokat.';
+
+ @override
+ String get repeater_settings => 'Beállítások';
+
+ @override
+ String get repeater_settingsSubtitle => 'Állítsa be a repeater paramétereket';
+
+ @override
+ String get repeater_statusTitle => 'Adatkapcsolódás állapot';
+
+ @override
+ String get repeater_routingMode => 'Útvonal-kezelési mód';
+
+ @override
+ String get repeater_autoUseSavedPath =>
+ 'Automatikus (az eddigi útvonal használata)';
+
+ @override
+ String get repeater_forceFloodMode => 'Erőforrás-alapú áramlás mód';
+
+ @override
+ String get repeater_pathManagement => 'Útvonal-kezelés';
+
+ @override
+ String get repeater_refresh => 'Újrafriszol';
+
+ @override
+ String get repeater_statusRequestTimeout => 'Az állapotkérés időtúlt.';
+
+ @override
+ String repeater_errorLoadingStatus(String error) {
+ return 'Hiba a státusz betöltés közben: $error';
+ }
+
+ @override
+ String get repeater_systemInformation => 'Rendszerinformációk';
+
+ @override
+ String get repeater_battery => 'Akku';
+
+ @override
+ String get repeater_clockAtLogin => 'Óra (bejelentkezéskor)';
+
+ @override
+ String get repeater_uptime => 'A rendszer elérhetősége';
+
+ @override
+ String get repeater_queueLength => 'Várakozási sor hossza';
+
+ @override
+ String get repeater_debugFlags => 'Hibakeresési beállítások';
+
+ @override
+ String get repeater_radioStatistics => 'Rádió statisztika';
+
+ @override
+ String get repeater_lastRssi => 'Utolsó RSSI érték';
+
+ @override
+ String get repeater_lastSnr => 'Utolsó SNR';
+
+ @override
+ String get repeater_noiseFloor => 'Háttérzaj szint';
+
+ @override
+ String get repeater_txAirtime => 'TX Airtime';
+
+ @override
+ String get repeater_rxAirtime => 'RX Airtime';
+
+ @override
+ String get repeater_packetStatistics => 'Csomagok statisztikája';
+
+ @override
+ String get repeater_sent => 'Elküldve';
+
+ @override
+ String get repeater_received => 'Megérkezett';
+
+ @override
+ String get repeater_duplicates => 'Duplák';
+
+ @override
+ String repeater_daysHoursMinsSecs(
+ int days,
+ int hours,
+ int minutes,
+ int seconds,
+ ) {
+ return '$days days ${hours}h ${minutes}m ${seconds}s';
+ }
+
+ @override
+ String repeater_packetTxTotal(int total, String flood, String direct) {
+ return 'Összesen: $total, Árvíz: $flood, Közvetlen: $direct';
+ }
+
+ @override
+ String repeater_packetRxTotal(int total, String flood, String direct) {
+ return 'Összesen: $total, Árvíz: $flood, Közvetlen: $direct';
+ }
+
+ @override
+ String repeater_duplicatesFloodDirect(String flood, String direct) {
+ return 'Áradás: $flood, Közvetlen: $direct';
+ }
+
+ @override
+ String repeater_duplicatesTotal(int total) {
+ return 'Összesen: $total';
+ }
+
+ @override
+ String get repeater_settingsTitle => 'Adatátvisszaadási beállítások';
+
+ @override
+ String get repeater_basicSettings => 'Alapbeállítások';
+
+ @override
+ String get repeater_repeaterName => 'Adóállomás neve';
+
+ @override
+ String get repeater_repeaterNameHelper => 'Ez a repeater neve';
+
+ @override
+ String get repeater_adminPassword => 'Adminisztrátori jelszó';
+
+ @override
+ String get repeater_adminPasswordHelper => 'Teljes jogosultságú jelszó';
+
+ @override
+ String get repeater_guestPassword => 'Vendég felhasználói név/jelszó';
+
+ @override
+ String get repeater_guestPasswordHelper =>
+ 'Csak olvasási jogosítást biztosító jelszó';
+
+ @override
+ String get repeater_radioSettings => 'Rádióbeállítások';
+
+ @override
+ String get repeater_frequencyMhz => 'Frekvencia (MHz)';
+
+ @override
+ String get repeater_frequencyHelper => '300–2500 MHz';
+
+ @override
+ String get repeater_txPower => 'TX Power';
+
+ @override
+ String get repeater_txPowerHelper => '1-30 dBm';
+
+ @override
+ String get repeater_bandwidth => 'Adatkapacitás';
+
+ @override
+ String get repeater_spreadingFactor => 'Terjesztési tényező';
+
+ @override
+ String get repeater_codingRate => 'Kódolási sebesség';
+
+ @override
+ String get repeater_locationSettings => 'Helyszínbeállítások';
+
+ @override
+ String get repeater_latitude => 'Nyugat–keleti szélesség';
+
+ @override
+ String get repeater_latitudeHelper => 'Desztes fokok (pl. 37,7749)';
+
+ @override
+ String get repeater_longitude => 'hosszúság';
+
+ @override
+ String get repeater_longitudeHelper => 'Desztes fokok (pl. -122.4194)';
+
+ @override
+ String get repeater_features => 'Jellemzők';
+
+ @override
+ String get repeater_packetForwarding => 'Csomagok továbbítás';
+
+ @override
+ String get repeater_packetForwardingSubtitle =>
+ 'Engedje, hogy a repeater továbbítsa a csomagokat.';
+
+ @override
+ String get repeater_guestAccess => 'Vendégek számára elérhető';
+
+ @override
+ String get repeater_guestAccessSubtitle =>
+ 'Engedje meg a vendégek számára, hogy csak olvassák a tartalmat';
+
+ @override
+ String get repeater_privacyMode => 'Adatvédelem mód';
+
+ @override
+ String get repeater_privacyModeSubtitle =>
+ 'Elrejtse a nevét/a helyszínt az űrlapon';
+
+ @override
+ String get repeater_advertisementSettings => 'Reklámbeállítások';
+
+ @override
+ String get repeater_localAdvertInterval => 'Helyi hirdetés időtartama';
+
+ @override
+ String repeater_localAdvertIntervalMinutes(int minutes) {
+ return '$minutes perc';
+ }
+
+ @override
+ String get repeater_floodAdvertInterval => 'Vízosztály-hirdetés időtartama';
+
+ @override
+ String repeater_floodAdvertIntervalHours(int hours) {
+ return '$hours óra';
+ }
+
+ @override
+ String get repeater_encryptedAdvertInterval => 'Kódolt hirdetés-szünet';
+
+ @override
+ String get repeater_dangerZone => 'Veszélyzóna';
+
+ @override
+ String get repeater_rebootRepeater => 'Újraindítás';
+
+ @override
+ String get repeater_rebootRepeaterSubtitle => 'Indítsa újra a repeater-t.';
+
+ @override
+ String get repeater_rebootRepeaterConfirm =>
+ 'Biztosan szeretné újraindítani ezt a repeatert?';
+
+ @override
+ String get repeater_regenerateIdentityKey =>
+ 'Újra generálja az azonosító kulcsot';
+
+ @override
+ String get repeater_regenerateIdentityKeySubtitle =>
+ 'Új nyilvános/személyes kulcs-párt generáljon';
+
+ @override
+ String get repeater_regenerateIdentityKeyConfirm =>
+ 'Ez új azonosítást fog létrehozni a repeater számára. Folytatni?';
+
+ @override
+ String get repeater_eraseFileSystem => 'Törölje a fájlrendszert';
+
+ @override
+ String get repeater_eraseFileSystemSubtitle =>
+ 'Formázza a duplázó fájlrendszert.';
+
+ @override
+ String get repeater_eraseFileSystemConfirm =>
+ 'FIGYELEM: Ez törli az összes adatot a repeater-en. Ez nem visszafordítható!';
+
+ @override
+ String get repeater_eraseSerialOnly =>
+ 'Az Erase funkció csak a soros konzolon érhető el.';
+
+ @override
+ String repeater_commandSent(String command) {
+ return 'Parancs elküldve: $command';
+ }
+
+ @override
+ String repeater_errorSendingCommand(String error) {
+ return 'Hibás parancs küldés: $error';
+ }
+
+ @override
+ String get repeater_confirm => 'Beküldve';
+
+ @override
+ String get repeater_settingsSaved => 'Beállítások sikeresen mentve';
+
+ @override
+ String repeater_errorSavingSettings(String error) {
+ return 'Hibás beállítások mentése: $error';
+ }
+
+ @override
+ String get repeater_refreshBasicSettings => 'Visszaállítás az alapértékekre';
+
+ @override
+ String get repeater_refreshRadioSettings => 'Frissítse a rádió beállításait';
+
+ @override
+ String get repeater_refreshTxPower => 'Újraindítás TX-támogatással';
+
+ @override
+ String get repeater_refreshLocationSettings =>
+ 'Újraindítás helyszín beállításokkal';
+
+ @override
+ String get repeater_refreshPacketForwarding =>
+ 'Csomagok továbbításának frissítése';
+
+ @override
+ String get repeater_refreshGuestAccess => 'Újraindítás vendégHozzáférés';
+
+ @override
+ String get repeater_refreshPrivacyMode =>
+ 'Visszaállítás a magánéletvédő módra';
+
+ @override
+ String get repeater_refreshAdvertisementSettings =>
+ 'Újraindítás hirdetés beállítások';
+
+ @override
+ String repeater_refreshed(String label) {
+ return '$label frissítve';
+ }
+
+ @override
+ String repeater_errorRefreshing(String label) {
+ return 'Hiba a $label frissítés közben';
+ }
+
+ @override
+ String get repeater_cliTitle => 'CLI (parancssori felület)';
+
+ @override
+ String get repeater_debugNextCommand => 'Hibakeresés, következő parancs';
+
+ @override
+ String get repeater_commandHelp => 'Segítség';
+
+ @override
+ String get repeater_clearHistory => 'Egyértelmű történet';
+
+ @override
+ String get repeater_noCommandsSent => 'Még egyik parancsot sem küldtünk.';
+
+ @override
+ String get repeater_typeCommandOrUseQuick =>
+ 'Írja be a parancsot alább, vagy használja a gyors parancsokat.';
+
+ @override
+ String get repeater_enterCommandHint => 'Írja be a parancsot...';
+
+ @override
+ String get repeater_previousCommand => 'Előző parancs';
+
+ @override
+ String get repeater_nextCommand => 'Következő parancs';
+
+ @override
+ String get repeater_enterCommandFirst => 'Add meg először egy parancsot';
+
+ @override
+ String get repeater_cliCommandFrameTitle => 'CLI parancssor felépítése';
+
+ @override
+ String repeater_cliCommandError(String error) {
+ return 'Hiba: $error';
+ }
+
+ @override
+ String get repeater_cliQuickGetName => 'Kapcsold össze a nevet';
+
+ @override
+ String get repeater_cliQuickGetRadio => 'Szerezd a rádiót';
+
+ @override
+ String get repeater_cliQuickGetTx => 'Szerezd a TX-t';
+
+ @override
+ String get repeater_cliQuickNeighbors => 'Szomszédok';
+
+ @override
+ String get repeater_cliQuickVersion => 'Verzió';
+
+ @override
+ String get repeater_cliQuickAdvertise => 'Hirdetés';
+
+ @override
+ String get repeater_cliQuickClock => 'óra';
+
+ @override
+ String get repeater_cliHelpAdvert => 'Elküldi egy hirdetési csomagot';
+
+ @override
+ String get repeater_cliHelpReboot =>
+ 'Újraindítja a készüléket. (Kérjük, vegye figyelembe, hogy valószínűleg \"Időhiba\" üzenetet fog kapni, ami normális)';
+
+ @override
+ String get repeater_cliHelpClock =>
+ 'A jelenlegi időt mutatja az egyes eszközök karórája alapján.';
+
+ @override
+ String get repeater_cliHelpPassword =>
+ 'Új adminisztrációs jelszót állít be a eszköz számára.';
+
+ @override
+ String get repeater_cliHelpVersion =>
+ 'Megjeleníti a készülék verzióját és a szoftver verziószámát.';
+
+ @override
+ String get repeater_cliHelpClearStats =>
+ 'Visszaállítja a különböző statisztikai mérőszámokat a nullára.';
+
+ @override
+ String get repeater_cliHelpSetAf => 'Beállítja az idő-szabályozási tényezőt.';
+
+ @override
+ String get repeater_cliHelpSetTx =>
+ 'Beállítja a LoRa átviteli teljesítményt dBm-ben (a rendszer újraindításával alkalmazható).';
+
+ @override
+ String get repeater_cliHelpSetRepeat =>
+ 'Engedélyezi vagy tiltja meg a repeater szerepet ezen a csomón.';
+
+ @override
+ String get repeater_cliHelpSetAllowReadOnly =>
+ '(Szoba szerver) Ha \"igen\", akkor üres jelszóval történő bejelentkezés engedélyezett lesz, de nem lehet üzeneteket küldeni a szobában. (Csak olvasási funkció)';
+
+ @override
+ String get repeater_cliHelpSetFloodMax =>
+ 'Beállítja a bejövő adatcsomagok maximális számát (ha ez a érték nagyobb vagy egyenlő a maximális értékkel, a csomag nem továbbítódik).';
+
+ @override
+ String get repeater_cliHelpSetIntThresh =>
+ 'Beállítja az interferencia határértéket (dB-ben). Az alapérték 14. Ha 0-ra állítja, kiküntheti a csatornák közötti interferencia detektálást.';
+
+ @override
+ String get repeater_cliHelpSetAgcResetInterval =>
+ 'Beállítja az intervallumot, amely a \"Automatikus gain\" szabályozó újraindításához szükséges. Beállítás értéke 0, ha a funkciót le kell tiltani.';
+
+ @override
+ String get repeater_cliHelpSetMultiAcks =>
+ 'Engedélyezi vagy kikapcsolja a „dupla visszaigazolás” funkciót.';
+
+ @override
+ String get repeater_cliHelpSetAdvertInterval =>
+ 'Beállítja az időzítő intervallumot percenként, hogy egy helyi (nincs átjáró) hirdetési csomagot küldjen. Beállítás értéke 0, ha a funkciót le szeretné tiltani.';
+
+ @override
+ String get repeater_cliHelpSetFloodAdvertInterval =>
+ 'Beállítja az időzítő intervallumot órában, hogy egy \"áramló\" hirdetési üzenetet küldjön. Beállítás értéke 0, ha a funkciót kikapcsolni kell.';
+
+ @override
+ String get repeater_cliHelpSetGuestPassword =>
+ 'Beállítja/frissíti a vendég felhasználói fiókot. (Ez lehetővé teszi a visszatérő felhasználók számára, hogy a \"Statistika lekérdezése\" kérést elküldjék)';
+
+ @override
+ String get repeater_cliHelpSetName => 'Megadja az űrlap neve.';
+
+ @override
+ String get repeater_cliHelpSetLat =>
+ 'Beállítja az hirdetés térképen megjelenő pont koordinátájának (tizedes fokokban) a latitude-ját.';
+
+ @override
+ String get repeater_cliHelpSetLon =>
+ 'Beállítja az hirdetés térképen megjelenő hosszúság koordinátát (tizedes fokokban).';
+
+ @override
+ String get repeater_cliHelpSetRadio =>
+ 'Teljesen új rádióparamétereket állít be, és azokat a beállításokba menti. Az alkalmazásához \"újraindítás\" parancs szükséges.';
+
+ @override
+ String get repeater_cliHelpSetRxDelay =>
+ 'Beállítások (kísérleti): Alapérték (legalább 1 értékre kell állítani, hogy hatás legyen), amely alapján a fogadott csomagokhoz enyhe késést alkalmazunk, a jelet ereje/pontszám alapján. 0-ra állítva a funkciót lekapcsoljuk.';
+
+ @override
+ String get repeater_cliHelpSetTxDelay =>
+ 'Beállítja egy tényezőt, amely a légköri idővel szorozva, egy áramlás-üzem módú csomaghoz, valamint egy véletlenszerű slot-rendszerhez, hogy késleltesse a továbbítását. (az ütközések valószínűségének csökkentése érdekében)';
+
+ @override
+ String get repeater_cliHelpSetDirectTxDelay =>
+ 'Hasonló a txdelay-hez, de ebben az esetben egy véletlenszerű késést alkalmazunk a közvetlen módú csomagok továbbításakor.';
+
+ @override
+ String get repeater_cliHelpSetBridgeEnabled =>
+ 'Engedélyez/Tiltás a híd funkciójának.';
+
+ @override
+ String get repeater_cliHelpSetBridgeDelay =>
+ 'Állíts be egy késleztatást a csomagok újbóli továbbításakor.';
+
+ @override
+ String get repeater_cliHelpSetBridgeSource =>
+ 'Döntse el, hogy a híd fogadott vagy elküldött csomagokat fogja-e továbbítani.';
+
+ @override
+ String get repeater_cliHelpSetBridgeBaud =>
+ 'Állítsa be a soros kommunikáció sebességét az RS232 hídok számára.';
+
+ @override
+ String get repeater_cliHelpSetBridgeSecret =>
+ 'Állítsa be a titkos kapcsolatot az ESPNOW hídokhoz.';
+
+ @override
+ String get repeater_cliHelpSetAdcMultiplier =>
+ 'Lehetővé teszi a felhasználónak, hogy egyedi tényezőt állíts be a riportolt akkumulátor feszültségének módosításához (ez csak bizonyos alkatrészeken támogatott).';
+
+ @override
+ String get repeater_cliHelpTempRadio =>
+ 'Időjárás szerinti rádióparamétereket állít be a megadott időtartamra, majd visszaállítja az eredeti beállításokat. (Nem menti a beállításokat a beállítások részben).';
+
+ @override
+ String get repeater_cliHelpSetPerm =>
+ 'A ACL-t módosítja. Ha a \"permissions\" érték 0, akkor eltávolítja a megfelelő bejegyzést (a pubkey előtag alapján). Új bejegyzést hoz létre, ha a pubkey-hex teljes hossza, és jelenleg nem szerepel az ACL-ben. A bejegyzést frissíti a megfelelő pubkey előtag alapján. A engedélyek különbözőek a különböző firmware szerepek között, de az alsó 2 bit a következő értékeket képviseli: 0 (Vendég), 1 (Csak olvasás), 2 (Olvasás és írás), 3 (Adminisztrátor)';
+
+ @override
+ String get repeater_cliHelpGetBridgeType =>
+ 'Kapcsolatok: hid típusú, RS232, ESPNOW';
+
+ @override
+ String get repeater_cliHelpLogStart =>
+ 'Elindítja a csomagok naplózását a fájlrendszerbe.';
+
+ @override
+ String get repeater_cliHelpLogStop =>
+ 'Megállítja a csomagok naplózását a fájlrendszerbe.';
+
+ @override
+ String get repeater_cliHelpLogErase =>
+ 'Törli a fájlrendszerből a csomagok log-fájljait.';
+
+ @override
+ String get repeater_cliHelpNeighbors =>
+ 'Mutat egy listát, amely tartalmazza a más repeater-ek által hallott adatok listáját, amelyek 0-hop hirdetések révén érhetők el. Minden sor az alábbi formát követi: id-prefix-hex:timestamp:snr-times-4';
+
+ @override
+ String get repeater_cliHelpNeighborRemove =>
+ 'Törli az első, a megadott kulcs-prefix (hexadecimális formában) alapján megegyező bejegyzést a szomszédok listájából.';
+
+ @override
+ String get repeater_cliHelpRegion =>
+ '(sorozat) Lista az összes meghatározott területet és a jelenlegi árvízvédelmi engedélyeket.';
+
+ @override
+ String get repeater_cliHelpRegionLoad =>
+ 'FIGYELEM: ez egy speciális, több parancsot tartalmazó futtatás. Minden következő parancs egy területtel kapcsolatos, amely egyenletes szóközökkel (a szülő-gyermek kapcsolatot jelző) megkülönböztethető. A parancs végrehajtása egy üres sor/parancs küldésével történik.';
+
+ @override
+ String get repeater_cliHelpRegionGet =>
+ 'Keresések egy adott név előtérrel (vagy \"*\" globális hatókörre). Válasz: \"-> region-név (szülő-név) \'F\'\"';
+
+ @override
+ String get repeater_cliHelpRegionPut =>
+ 'Hozzáad vagy frissíti egy régió definíciót megadott néven.';
+
+ @override
+ String get repeater_cliHelpRegionRemove =>
+ 'Eltávolítja a megadott nevet használó régió-definíciót. (pontosan meg kell egyeznie, és nem lehet gyermekrégiója)';
+
+ @override
+ String get repeater_cliHelpRegionAllowf =>
+ 'Beállítja a megadott területre vonatkozó \"víz\" jogosultságot. (A globális/régi beállítások esetén a \"*\" jelölő)';
+
+ @override
+ String get repeater_cliHelpRegionDenyf =>
+ 'Eltávolítja a megadott területre vonatkozó \"F\"lood (víz) engedélyt. (FIGYELEM: jelenleg nem javasolt ezt a globális/régi verzióban használni!!)';
+
+ @override
+ String get repeater_cliHelpRegionHome =>
+ 'Visszaállítja a jelenlegi „otthoni” régiót. (Ez a beállítás még nem került alkalmazásra, csak jövőbeli használatra fenyelve)';
+
+ @override
+ String get repeater_cliHelpRegionHomeSet => 'Beállítja a \"házi\" régiót.';
+
+ @override
+ String get repeater_cliHelpRegionSave =>
+ 'Megőrzi a régió listát/térképet a tárolóban.';
+
+ @override
+ String get repeater_cliHelpGps =>
+ 'Megadja a GPS állapotát. Ha a GPS kikapcsolva van, akkor csak \"ki\" választot ad, ha be van, akkor \"be\", \"állapot\", \"pozíció\", \"satellitok száma\" értékeket ad.';
+
+ @override
+ String get repeater_cliHelpGpsOnOff => 'Engedi a GPS működés állapotát.';
+
+ @override
+ String get repeater_cliHelpGpsSync =>
+ 'A hálózati időt az GPS óra időjével szinkronizálja.';
+
+ @override
+ String get repeater_cliHelpGpsSetLoc =>
+ 'Beállítja a węsz pozícióját GPS koordináták alapján, és menti a beállításokat.';
+
+ @override
+ String get repeater_cliHelpGpsAdvert =>
+ 'Adja meg a hirdetés konfigurációjának helyszín-információját:\n- none: ne tartalmazza a helyszínt a hirdetésekben\n- share: megosztja a GPS-helyszínt (SensorManager-ből)\n- prefs: hirdeti a beállításokban tárolt helyszínt';
+
+ @override
+ String get repeater_cliHelpGpsAdvertSet =>
+ 'Beállítja a hirdetés helyszín-specifikus beállításait.';
+
+ @override
+ String get repeater_commandsListTitle => 'Parancsok listája';
+
+ @override
+ String get repeater_commandsListNote =>
+ 'FIGYELEM: a különböző \"set ...\" parancsok mellett létezik egy \"get ...\" parancs is.';
+
+ @override
+ String get repeater_general => 'Általános';
+
+ @override
+ String get repeater_settingsCategory => 'Beállítások';
+
+ @override
+ String get repeater_bridge => 'Híd';
+
+ @override
+ String get repeater_logging => 'Naplózás';
+
+ @override
+ String get repeater_neighborsRepeaterOnly =>
+ 'Szomszédok (Csak ismétlő funkció)';
+
+ @override
+ String get repeater_regionManagementRepeaterOnly =>
+ 'Regionális menedzsment (Csak egyirányú kommunikáció)';
+
+ @override
+ String get repeater_regionNote =>
+ 'Region-specifikus parancsokat vezettek be a régiók definiálására és a hozzájuk tartozó engedélyek kezelésére.';
+
+ @override
+ String get repeater_gpsManagement => 'GPS-vezérlés';
+
+ @override
+ String get repeater_gpsNote =>
+ 'Az GPS-al kapcsolatos funkciók lehetővé teszik a helyszín-személyesítéssel kapcsolatos feladatok kezelését.';
+
+ @override
+ String get telemetry_receivedData => 'Kapott adatokat a szenzorokról';
+
+ @override
+ String get telemetry_requestTimeout => 'Az adatkapcsolati kérés sikertelen.';
+
+ @override
+ String telemetry_errorLoading(String error) {
+ return 'Hiba az adatok begyűjtésében: $error';
+ }
+
+ @override
+ String get telemetry_noData => 'Nincsenek elérhető telemetriadatok.';
+
+ @override
+ String telemetry_channelTitle(int channel) {
+ return '$channel csatorna';
+ }
+
+ @override
+ String get telemetry_batteryLabel => 'Akku';
+
+ @override
+ String get telemetry_voltageLabel => 'Feszültség';
+
+ @override
+ String get telemetry_mcuTemperatureLabel => 'MCU hőmérséklet';
+
+ @override
+ String get telemetry_temperatureLabel => 'Hőmérséklet';
+
+ @override
+ String get telemetry_currentLabel => 'Jelenlegi';
+
+ @override
+ String telemetry_batteryValue(int percent, String volts) {
+ return '$percent% / ${volts}V';
+ }
+
+ @override
+ String telemetry_voltageValue(String volts) {
+ return '${volts}V';
+ }
+
+ @override
+ String telemetry_currentValue(String amps) {
+ return '${amps}A';
+ }
+
+ @override
+ String telemetry_temperatureValue(String celsius, String fahrenheit) {
+ return '$celsius °C / $fahrenheit °F';
+ }
+
+ @override
+ String get neighbors_receivedData => 'Kapott szomszédok adatait';
+
+ @override
+ String get neighbors_requestTimedOut =>
+ 'A szomszédok kérik, hogy tiltsák le a kamerát.';
+
+ @override
+ String neighbors_errorLoading(String error) {
+ return 'Hiba a szomszédok betöltésében: $error';
+ }
+
+ @override
+ String get neighbors_repeatersNeighbors => 'Ismétlő eszközök, szomszédok';
+
+ @override
+ String get neighbors_noData => 'Nincsenek elérhető szomszédokról adatok.';
+
+ @override
+ String neighbors_unknownContact(String pubkey) {
+ return 'Tudatlan $pubkey';
+ }
+
+ @override
+ String neighbors_heardAgo(String time) {
+ return 'Értsd: $time sitten';
+ }
+
+ @override
+ String get channelPath_title => 'Csomagok útvonala';
+
+ @override
+ String get channelPath_viewMap => 'Megtekinthető térkép';
+
+ @override
+ String get channelPath_otherObservedPaths => 'Egyéb megfigyelt utak';
+
+ @override
+ String get channelPath_repeaterHops => 'Adat továbbító lépések';
+
+ @override
+ String get channelPath_noHopDetails =>
+ 'Ez a csomag nem tartalmaz részletes információkat a \"hop\" (vagy más hasonló) szót használó kifejezésekről.';
+
+ @override
+ String get channelPath_messageDetails => 'Üzenet részletei';
+
+ @override
+ String get channelPath_senderLabel => 'Megküldő';
+
+ @override
+ String get channelPath_timeLabel => 'Idő';
+
+ @override
+ String get channelPath_repeatsLabel => 'Ismétli';
+
+ @override
+ String channelPath_pathLabel(int index) {
+ return 'Útvonal $index';
+ }
+
+ @override
+ String get channelPath_observedLabel => 'Megfigyelt';
+
+ @override
+ String channelPath_observedPathTitle(int index, String hops) {
+ return 'Megfigyelt útvonal: $index • $hops';
+ }
+
+ @override
+ String get channelPath_noLocationData => 'Nincs helyszínadat.';
+
+ @override
+ String channelPath_timeWithDate(int day, int month, String time) {
+ return '$day/$month $time';
+ }
+
+ @override
+ String channelPath_timeOnly(String time) {
+ return '$time';
+ }
+
+ @override
+ String get channelPath_unknownPath => 'Megfejt';
+
+ @override
+ String get channelPath_floodPath => 'Árvíz';
+
+ @override
+ String get channelPath_directPath => 'Közvetlen';
+
+ @override
+ String channelPath_observedZeroOf(int total) {
+ return '0-ból $total';
+ }
+
+ @override
+ String channelPath_observedSomeOf(int observed, int total) {
+ return '$observed of $total hops';
+ }
+
+ @override
+ String get channelPath_mapTitle => 'Útvonal térkép';
+
+ @override
+ String get channelPath_noRepeaterLocations =>
+ 'Ez a útvonal nem támogat repeater-t.';
+
+ @override
+ String channelPath_primaryPath(int index) {
+ return 'Útvonal $index (Elsődleges)';
+ }
+
+ @override
+ String get channelPath_pathLabelTitle => 'Út';
+
+ @override
+ String get channelPath_observedPathHeader => 'Megfigyelt útvonal';
+
+ @override
+ String channelPath_selectedPathLabel(String label, String prefixes) {
+ return '$label • $prefixes';
+ }
+
+ @override
+ String get channelPath_noHopDetailsAvailable =>
+ 'Ez a csomag nem tartalmaz részletes információkat a szállításhoz.';
+
+ @override
+ String get channelPath_unknownRepeater => 'Tudatlan erősítő';
+
+ @override
+ String get community_title => 'Helyi közösség';
+
+ @override
+ String get community_create => 'Teremtsd meg a közösséget';
+
+ @override
+ String get community_createDesc =>
+ 'Légyon létre egy új közösséget, és osszák meg QR-kód segítségével.';
+
+ @override
+ String get community_join => 'Csatlakozjon';
+
+ @override
+ String get community_joinTitle => 'Csatlakozzon a közösséghez';
+
+ @override
+ String community_joinConfirmation(String name) {
+ return 'Szeretne csatlakozni a közösséghez, $name?';
+ }
+
+ @override
+ String get community_scanQr => 'QR-kód olvasó a közösség számára';
+
+ @override
+ String get community_scanInstructions =>
+ 'Fordítsa a kamerát egy közösségi QR-kód irányába.';
+
+ @override
+ String get community_showQr => 'Megjelenítse a QR-kódot';
+
+ @override
+ String get community_publicChannel => 'Összetartó, közösségi';
+
+ @override
+ String get community_hashtagChannel => 'Helyi hashtaget';
+
+ @override
+ String get community_name => 'Helyi közösség neve';
+
+ @override
+ String get community_enterName => 'Kérjük, a közösség nevét írja be.';
+
+ @override
+ String community_created(String name) {
+ return 'A \"$name\" nevű közösség létrehozva';
+ }
+
+ @override
+ String community_joined(String name) {
+ return 'Csatlakozott a $name közösséghez';
+ }
+
+ @override
+ String get community_qrTitle => 'Osszpontosítás a közösségben';
+
+ @override
+ String community_qrInstructions(String name) {
+ return 'Scanned this QR-kódot, hogy csatlakozhat a $name csoporthoz.';
+ }
+
+ @override
+ String get community_hashtagPrivacyHint =>
+ 'A közösségi hashtagekhez tartozó csatornák csak a közösség tagjai számára érhetők el.';
+
+ @override
+ String get community_invalidQrCode => 'Érvénytelen közösségi QR-kód';
+
+ @override
+ String get community_alreadyMember => 'Már tag vagy';
+
+ @override
+ String community_alreadyMemberMessage(String name) {
+ return 'Már tagja $name-nek.';
+ }
+
+ @override
+ String get community_addPublicChannel =>
+ 'Hozzon létre egy közösségi nyilvános csatornát';
+
+ @override
+ String get community_addPublicChannelHint =>
+ 'Automatikusan hozzon létre ezt a csatornát a közösség számára.';
+
+ @override
+ String get community_noCommunities => 'Még egyik közösség sem csatlakozott.';
+
+ @override
+ String get community_scanOrCreate =>
+ 'Scelle egy QR-kódot, vagy hozzon létre egy közösséget, hogy elinduljon.';
+
+ @override
+ String get community_manageCommunities => 'Közösségek kezelése';
+
+ @override
+ String get community_delete => 'Hagyományos közösségi élet';
+
+ @override
+ String community_deleteConfirm(String name) {
+ return 'Hagyom $name-et?';
+ }
+
+ @override
+ String community_deleteChannelsWarning(int count) {
+ return 'Ezem törli is $count csatornát és a hozzá tartozó üzeneteket.';
+ }
+
+ @override
+ String community_deleted(String name) {
+ return 'A közösség, amely $name';
+ }
+
+ @override
+ String get community_regenerateSecret => 'Titkos visszaállítás';
+
+ @override
+ String community_regenerateSecretConfirm(String name) {
+ return 'Újra kell generálni a titkos kulcsot $name számára? Minden tagnak be kell szkennelnie az új QR-kódot, hogy továbbra is kommunikálhasson.';
+ }
+
+ @override
+ String get community_regenerate => 'Újraalakítás';
+
+ @override
+ String community_secretRegenerated(String name) {
+ return 'Titkos kulcs megújult $name számára.';
+ }
+
+ @override
+ String get community_updateSecret => 'Frissítési titok';
+
+ @override
+ String community_secretUpdated(String name) {
+ return 'Titkos információ frissítve $name számára';
+ }
+
+ @override
+ String community_scanToUpdateSecret(String name) {
+ return 'Scanned a új QR-kódot, hogy frissítsük a $name számára megőrzött titkos információt.';
+ }
+
+ @override
+ String get community_addHashtagChannel => 'Adjon egy közösségi hashtaget';
+
+ @override
+ String get community_addHashtagChannelDesc =>
+ 'Hozz létre egy hashtage-os csatornát ennek a közösségnek';
+
+ @override
+ String get community_selectCommunity => 'Válasszon közösséget';
+
+ @override
+ String get community_regularHashtag => 'Rendszeres hashtag';
+
+ @override
+ String get community_regularHashtagDesc =>
+ 'Önmagas szintű hashtaget (bárki csatlakozhat)';
+
+ @override
+ String get community_communityHashtag => 'Helyi hashtaget';
+
+ @override
+ String get community_communityHashtagDesc => 'Csak a közösség tagjai számára';
+
+ @override
+ String community_forCommunity(String name) {
+ return '$name számára';
+ }
+
+ @override
+ String get listFilter_tooltip => 'Szűrés és rendezés';
+
+ @override
+ String get listFilter_sortBy => 'Szűrés';
+
+ @override
+ String get listFilter_latestMessages => 'Legfrissebb üzenetek';
+
+ @override
+ String get listFilter_heardRecently => 'Úgy hallottam, hogy...';
+
+ @override
+ String get listFilter_az => 'A-Z';
+
+ @override
+ String get listFilter_filters => 'Szűrők';
+
+ @override
+ String get listFilter_all => 'Mind';
+
+ @override
+ String get listFilter_favorites => 'Kedvencek';
+
+ @override
+ String get listFilter_addToFavorites => 'Megerősítés kívánságlistára';
+
+ @override
+ String get listFilter_removeFromFavorites => 'Törölj a kedvencekből';
+
+ @override
+ String get listFilter_users => 'Felhasználók';
+
+ @override
+ String get listFilter_repeaters => 'Újraküldők';
+
+ @override
+ String get listFilter_roomServers => 'Szoba-szolgálatok';
+
+ @override
+ String get listFilter_unreadOnly => 'Csak olvasatlan';
+
+ @override
+ String get listFilter_newGroup => 'Új csoport';
+
+ @override
+ String get pathTrace_you => 'Te';
+
+ @override
+ String get pathTrace_failed => 'A útvonal követése sikertelen.';
+
+ @override
+ String get pathTrace_notAvailable =>
+ 'Az útvonal követési funkció nem elérhető.';
+
+ @override
+ String get pathTrace_refreshTooltip => 'Út mentesség frissítése.';
+
+ @override
+ String get pathTrace_someHopsNoLocation =>
+ 'Egy vagy több búzavirág hiányozik a helyszínéről!';
+
+ @override
+ String get pathTrace_clearTooltip => 'Egyértelmű út.';
+
+ @override
+ String get losSelectStartEnd =>
+ 'Válassza ki a kezdő és a végpontokat a LOS-hoz.';
+
+ @override
+ String losRunFailed(String error) {
+ return 'A látószög ellenőrzése sikertelen: $error';
+ }
+
+ @override
+ String get losClearAllPoints => 'Teljesen tisztázzuk az összes pontot';
+
+ @override
+ String get losRunToViewElevationProfile =>
+ 'Használja a LOS-t, hogy megtekinthesse a magasságkülönbségek diagramját.';
+
+ @override
+ String get losMenuTitle => 'LOS menü';
+
+ @override
+ String get losMenuSubtitle =>
+ 'A térképen található pontok kiválasztására vagy a térképen hosszúra nyomva, hogy egyedi pontokat definiálhassunk.';
+
+ @override
+ String get losShowDisplayNodes => 'Megjelenítsen a megjelenítési egységeket';
+
+ @override
+ String get losCustomPoints => 'Egyedi pontok';
+
+ @override
+ String losCustomPointLabel(int index) {
+ return 'Egyedi $index';
+ }
+
+ @override
+ String get losPointA => 'A pont A';
+
+ @override
+ String get losPointB => 'Pont B';
+
+ @override
+ String losAntennaA(String value, String unit) {
+ return 'Antenna A: $value $unit';
+ }
+
+ @override
+ String losAntennaB(String value, String unit) {
+ return 'Antenna B: $value $unit';
+ }
+
+ @override
+ String get losRun => 'Futtass a LOS-on';
+
+ @override
+ String get losNoElevationData => 'Nincsenek emelkedési adatok.';
+
+ @override
+ String losProfileClear(
+ String distance,
+ String distanceUnit,
+ String clearance,
+ String heightUnit,
+ ) {
+ return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
+ }
+
+ @override
+ String losProfileBlocked(
+ String distance,
+ String distanceUnit,
+ String obstruction,
+ String heightUnit,
+ ) {
+ return '$distance $distanceUnit, amelyet $obstruction akadályoz meg $heightUnit-ban';
+ }
+
+ @override
+ String get losStatusChecking => 'LOS: ellenőrzés...';
+
+ @override
+ String get losStatusNoData => 'LOS: nincs adat';
+
+ @override
+ String losStatusSummary(int clear, int total, int blocked, int unknown) {
+ return 'LOS: $clear/$total tisztított, $blocked blokkolt, $unknown ismeretlen';
+ }
+
+ @override
+ String get losErrorElevationUnavailable =>
+ 'Az alábbi minták esetében nem áll rendelkezésre magasságadat.';
+
+ @override
+ String get losErrorInvalidInput =>
+ 'Hibás vagy hiányos táblázatok a LOS (Loss of Signal) számításához.';
+
+ @override
+ String get losRenameCustomPoint => 'Állítsa meg a saját pont nevét';
+
+ @override
+ String get losPointName => 'Pont neve';
+
+ @override
+ String get losShowPanelTooltip => 'Megjelenítse a LOS paneelt';
+
+ @override
+ String get losHidePanelTooltip => 'Rejtse el a LOS paneelt';
+
+ @override
+ String get losElevationAttribution =>
+ 'Magasságadatok: Open-Meteo (CC BY 4.0)';
+
+ @override
+ String get losLegendRadioHorizon => 'Radio Horizont';
+
+ @override
+ String get losLegendLosBeam => 'LOS jelzés';
+
+ @override
+ String get losLegendTerrain => 'Terület';
+
+ @override
+ String get losFrequencyLabel => 'Hatósság';
+
+ @override
+ String get losFrequencyInfoTooltip => 'Lásd a számítás részleteit';
+
+ @override
+ String get losFrequencyDialogTitle =>
+ 'A rádióhullámok hatótávolságának kiszámítása';
+
+ @override
+ String losFrequencyDialogDescription(
+ double baselineK,
+ double baselineFreq,
+ double frequencyMHz,
+ double kFactor,
+ ) {
+ return 'A $baselineK értékből kezdve, $baselineFreq MHz-os frekvencián, a számítás az aktuális $frequencyMHz MHz-os sávhoz igazítja a k-tényezőt, amely meghatározza a görbös rádióhatótávolság határát.';
+ }
+
+ @override
+ String get contacts_pathTrace => 'Útvonal követése';
+
+ @override
+ String get contacts_ping => 'Ping';
+
+ @override
+ String get contacts_repeaterPathTrace => 'Az útvonal követése a repeaterig';
+
+ @override
+ String get contacts_repeaterPing => 'Ping-szinkronizáló';
+
+ @override
+ String get contacts_roomPathTrace => 'Kapcsolat a szobai szerverrel';
+
+ @override
+ String get contacts_roomPing => 'Ping-szolgáló szerver';
+
+ @override
+ String get contacts_chatTraceRoute => 'Útvonal meghatározása';
+
+ @override
+ String contacts_pathTraceTo(String name) {
+ return 'Keresse meg a $name címét.';
+ }
+
+ @override
+ String get contacts_clipboardEmpty => 'A kiválasztott szöveg üres.';
+
+ @override
+ String get contacts_invalidAdvertFormat => 'Érvénytelen kontaktinformáció';
+
+ @override
+ String get contacts_contactImported => 'Kapcsolat létrejött.';
+
+ @override
+ String get contacts_contactImportFailed =>
+ 'Nem sikerült a kapcsolatot importálni.';
+
+ @override
+ String get contacts_zeroHopAdvert => 'Zero Hop reklám';
+
+ @override
+ String get contacts_floodAdvert => 'Árvízre vonatkozó hirdetés';
+
+ @override
+ String get contacts_copyAdvertToClipboard =>
+ 'Másolja a hirdetést a kiválasztási ablakba';
+
+ @override
+ String get contacts_addContactFromClipboard =>
+ 'Adjon hozzá egy kapcsolatot a kiválasztott listából';
+
+ @override
+ String get contacts_ShareContact => 'Másolja a kapcsolatot a kiválasztóba';
+
+ @override
+ String get contacts_ShareContactZeroHop =>
+ 'Ossza meg a kapcsolatot hirdetés segítségével';
+
+ @override
+ String get contacts_zeroHopContactAdvertSent =>
+ 'Kapcsolatot a hirdetésen keresztül.';
+
+ @override
+ String get contacts_zeroHopContactAdvertFailed =>
+ 'Nem sikerült a kapcsolatot elküldeni.';
+
+ @override
+ String get contacts_contactAdvertCopied => 'A hirdetés másolva a vágólapra.';
+
+ @override
+ String get contacts_contactAdvertCopyFailed =>
+ 'Az hirdetés másolása a vágólapra sikertelen.';
+
+ @override
+ String get notification_activityTitle => 'MeshCore tevékenységek';
+
+ @override
+ String notification_messagesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'üzenetek',
+ one: 'üzenet',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_channelMessagesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'csatornaüzenetek',
+ one: 'csatornaüzenet',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_newNodesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'új csomópontok',
+ one: 'új csomópont',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_newTypeDiscovered(String contactType) {
+ return 'Új $contactType megtalálva';
+ }
+
+ @override
+ String get notification_receivedNewMessage => 'Új üzenetet kaptam';
+
+ @override
+ String get settings_gpxExportRepeaters =>
+ 'Külső eszközök / helyi szerver a GPX formátumba';
+
+ @override
+ String get settings_gpxExportRepeatersSubtitle =>
+ 'Exportálható repeater/szobaterm-szerver, amely egy GPX fájlban tárolja a helyzetet.';
+
+ @override
+ String get settings_gpxExportContacts => 'GPX export funkciók';
+
+ @override
+ String get settings_gpxExportContactsSubtitle =>
+ 'Az export funkció lehetővé teszi, hogy a GPS fájlban megadott helyszínen is megőrizzük az útvonalat.';
+
+ @override
+ String get settings_gpxExportAll =>
+ 'Exportálja az összes kapcsolatot GPX formátumban.';
+
+ @override
+ String get settings_gpxExportAllSubtitle =>
+ 'Az összes elérhetőséget, amelyekhez egy helyszín tartozik, egy GPX fájlba exportálja.';
+
+ @override
+ String get settings_gpxExportSuccess =>
+ 'A GPX fájl sikeresen exportálva lett.';
+
+ @override
+ String get settings_gpxExportNoContacts => 'Nincs exportálható kapcsolatok.';
+
+ @override
+ String get settings_gpxExportNotAvailable =>
+ 'Nem támogatott a jelenlegi eszközön/rendszeren.';
+
+ @override
+ String get settings_gpxExportError => 'Hiba történt az export során.';
+
+ @override
+ String get settings_gpxExportRepeatersRoom =>
+ 'Adatátvisszaadó eszközök és helyiségi szerverek helyei';
+
+ @override
+ String get settings_gpxExportChat => 'Kapcsolódó helyszínek';
+
+ @override
+ String get settings_gpxExportAllContacts => 'Az összes kapcsolat helyszíne';
+
+ @override
+ String get settings_gpxExportShareText =>
+ 'A meshcore-open-ból exportált térkéadatumok';
+
+ @override
+ String get settings_gpxExportShareSubject =>
+ 'meshcore-open GPX formátumú térképi adatok export';
+
+ @override
+ String get snrIndicator_nearByRepeaters => 'Helyszíni erősítők';
+
+ @override
+ String get snrIndicator_lastSeen => 'Utoljára, amikor látták';
+
+ @override
+ String get contactsSettings_title => 'Kapcsolatok beállításai';
+
+ @override
+ String get contactsSettings_autoAddTitle => 'Automatikus felfedezés';
+
+ @override
+ String get contactsSettings_otherTitle =>
+ 'Egyéb kapcsolattal kapcsolatos beállítások';
+
+ @override
+ String get contactsSettings_autoAddUsersTitle =>
+ 'Automatikus felhasználói hozzáadás';
+
+ @override
+ String get contactsSettings_autoAddUsersSubtitle =>
+ 'Engedje, hogy a segítő automatikusan hozzáadja az új felhasználókat.';
+
+ @override
+ String get contactsSettings_autoAddRepeatersTitle =>
+ 'Automatikus visszatöltés';
+
+ @override
+ String get contactsSettings_autoAddRepeatersSubtitle =>
+ 'Engedje, hogy a segítő eszköz automatikusan hozzáadja az új, megtalált jelzőállomásokat.';
+
+ @override
+ String get contactsSettings_autoAddRoomServersTitle =>
+ 'Automatikus szobák szerverek hozzáadása';
+
+ @override
+ String get contactsSettings_autoAddRoomServersSubtitle =>
+ 'Engedje, hogy a segítő automatikusan hozzáadja az új, megtalált hálózati szervereket.';
+
+ @override
+ String get contactsSettings_autoAddSensorsTitle =>
+ 'Automatikus érzékelők hozzáadása';
+
+ @override
+ String get contactsSettings_autoAddSensorsSubtitle =>
+ 'Engedje, hogy a kísérő automatikusan hozzáadja az új, megtalált szenzorokat.';
+
+ @override
+ String get contactsSettings_overwriteOldestTitle => 'Felülírja a legrégebbet';
+
+ @override
+ String get contactsSettings_overwriteOldestSubtitle =>
+ 'Amikor a névsor telítődik, a legidősebb, de még nem kedvencként jelölt személyt helyettesíti egy újabb.';
+
+ @override
+ String get discoveredContacts_Title => 'Megtalált kapcsolatok';
+
+ @override
+ String get discoveredContacts_noMatching => 'Nincs megegyező kapcsolat.';
+
+ @override
+ String get discoveredContacts_searchHint => 'Keress új kapcsolatokat';
+
+ @override
+ String get discoveredContacts_contactAdded => 'Kapcsolat hozzáadva';
+
+ @override
+ String get discoveredContacts_addContact => 'Adjon személyhez';
+
+ @override
+ String get discoveredContacts_copyContact =>
+ 'Másolja a kapcsolatot a vágólapra';
+
+ @override
+ String get discoveredContacts_deleteContact =>
+ 'Törölj a feltalált kapcsolatot';
+
+ @override
+ String get discoveredContacts_deleteContactAll =>
+ 'Törölj minden megtalált kapcsolatot';
+
+ @override
+ String get discoveredContacts_deleteContactAllContent =>
+ 'Biztos, hogy szeretné törölni az összes eddig megtalált kapcsolatot?';
+
+ @override
+ String get chat_sendCooldown =>
+ 'Kérjük, várjon egy pillanatot, mielőtt újra elküldené.';
+
+ @override
+ String get appSettings_jumpToOldestUnread =>
+ 'Jelentkezzen az legörebb, olvasatlan üzenetre';
+
+ @override
+ String get appSettings_jumpToOldestUnreadSubtitle =>
+ 'Amikor egy új csevet indítunk, amelyben vannak olvashatatlan üzenetek, görgessük a listát, hogy a legelső, olvashatatlan üzenet megjelenjen, nem pedig az utolsó.';
+
+ @override
+ String get appSettings_languageHu => 'Magyar';
+
+ @override
+ String get appSettings_languageJa => 'Japán';
+
+ @override
+ String get appSettings_languageKo => 'Koreai';
+
+ @override
+ String get radioStats_tooltip => 'Rádió és hálózati statisztikák';
+
+ @override
+ String get radioStats_screenTitle => 'Rádió statisztikák';
+
+ @override
+ String get radioStats_notConnected =>
+ 'Csatlakozzon egy eszközhöz, hogy megtekinthesse a rádió adatok statisztikáit.';
+
+ @override
+ String get radioStats_firmwareTooOld =>
+ 'A rádió statisztikákhoz v8 vagy újabb verziójú szoftver szükséges.';
+
+ @override
+ String get radioStats_waiting => 'Adatokra vár…';
+
+ @override
+ String radioStats_noiseFloor(int noiseDbm) {
+ return 'Háttérzaj szint: $noiseDbm dBm';
+ }
+
+ @override
+ String radioStats_lastRssi(int rssiDbm) {
+ return 'Utolsó RSSI érték: $rssiDbm dBm';
+ }
+
+ @override
+ String radioStats_lastSnr(String snr) {
+ return 'Utolsó SNR: $snr dB';
+ }
+
+ @override
+ String radioStats_txAir(int seconds) {
+ return 'TX-es idő (összesen): $seconds másodperc';
+ }
+
+ @override
+ String radioStats_rxAir(int seconds) {
+ return 'RX használat időtartama (összesen): $seconds s';
+ }
+
+ @override
+ String get radioStats_chartCaption =>
+ 'Háttérzaj szint (dBm) a legutóbbi minták alapján.';
+
+ @override
+ String radioStats_stripNoise(int noiseDbm) {
+ return 'Háttérzaj szint: $noiseDbm dBm';
+ }
+
+ @override
+ String get radioStats_stripWaiting => 'Rádió adatok begyűjtése…';
+
+ @override
+ String get radioStats_settingsTile => 'Rádió statisztikák';
+
+ @override
+ String get radioStats_settingsSubtitle =>
+ 'Háttérzaj, RSSI, zaj-sűrűség, és a használat időtartama';
+
+ @override
+ String get scanner_linuxPairingShowPin => 'PIN megjelenítése';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'PIN elrejtése';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'Bluetooth párosítási PIN';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Adja meg a(z) $deviceName PIN-kódját (hagyja üresen, ha nincs).';
+ }
+}
diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart
index 68c2af3..3fc5e56 100644
--- a/lib/l10n/app_localizations_it.dart
+++ b/lib/l10n/app_localizations_it.dart
@@ -398,6 +398,52 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modalità privacy disabilitata';
+ @override
+ String get settings_privacy => 'Impostazioni sulla privacy';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Controlla le informazioni che vengono condivise.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Scegli le informazioni che il tuo dispositivo condivide con gli altri.';
+
+ @override
+ String get settings_denyAll => 'Negare tutto';
+
+ @override
+ String get settings_allowByContact => 'Consenti in base ai flag di contatto';
+
+ @override
+ String get settings_allowAll => 'Consenti tutto';
+
+ @override
+ String get settings_telemetryBaseMode => 'Modalità di base di telemetria';
+
+ @override
+ String get settings_telemetryLocationMode =>
+ 'Modalità di posizionamento telemetrico';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Modalità di ambiente di telemetria';
+
+ @override
+ String get settings_advertLocation => 'Posizione dell\'annuncio';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Includi la posizione nell\'annuncio';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Multi-ACKs: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Modalità telemetria aggiornata';
+
@override
String get settings_actions => 'Azioni';
@@ -695,6 +741,50 @@ class AppLocalizationsIt extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotazione del percorso automatico disabilitata';
+ @override
+ String get appSettings_maxRouteWeight =>
+ 'Massimo peso consentito per il percorso';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Il peso massimo che un percorso può accumulare grazie a consegne di successo.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Peso iniziale del percorso';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Peso di partenza per nuovi percorsi';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Aumento del peso del successo';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Peso aggiunto a un percorso dopo una consegna riuscita.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Riduzione del peso associato al fallimento';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Peso rimosso da un percorso dopo un tentativo di consegna fallito.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Numero massimo di tentativi di invio del messaggio';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Numero di tentativi di riprova prima di considerare un messaggio come fallito.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Batteria';
@@ -943,6 +1033,42 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Ultimo visto $days giorni fa';
}
+ @override
+ String get contact_info => 'Informazioni di Contatto';
+
+ @override
+ String get contact_settings => 'Impostazioni di contatto';
+
+ @override
+ String get contact_telemetry => 'Telemetria';
+
+ @override
+ String get contact_lastSeen => 'Ultimo accesso';
+
+ @override
+ String get contact_clearChat => 'Cancella chat';
+
+ @override
+ String get contact_teleBase => 'Base di telemetria';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Consenti la condivisione del livello della batteria e della telemetria di base';
+
+ @override
+ String get contact_teleLoc => 'Posizione telemetria';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Consenti la condivisione dei dati di posizione';
+
+ @override
+ String get contact_teleEnv => 'Ambiente di telemetria';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Consenti la condivisione dei dati del sensore ambientale';
+
@override
String get channels_title => 'Canali';
@@ -1561,6 +1687,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_otherNodes => 'Altri Nodi';
+ @override
+ String get map_showOverlaps => 'Sovrapposizioni della chiave ripetitore';
+
@override
String get map_keyPrefix => 'Prefisso Chiave';
@@ -1603,6 +1732,10 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get map_runTrace => 'Esegui Path Trace';
+ @override
+ String get map_runTraceWithReturnPath =>
+ 'Tornare indietro sullo stesso percorso';
+
@override
String get map_removeLast => 'Rimuovi ultimo';
@@ -3358,4 +3491,100 @@ 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 scanner_linuxPairingShowPin => 'Mostra PIN';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'Nascondi PIN';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'PIN di associazione Bluetooth';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Inserisci il PIN per $deviceName (lascia vuoto se non ce n\'è).';
+ }
}
diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart
new file mode 100644
index 0000000..03d70d4
--- /dev/null
+++ b/lib/l10n/app_localizations_ja.dart
@@ -0,0 +1,3418 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Japanese (`ja`).
+class AppLocalizationsJa extends AppLocalizations {
+ AppLocalizationsJa([String locale = 'ja']) : super(locale);
+
+ @override
+ String get appTitle => 'MeshCore Open';
+
+ @override
+ String get nav_contacts => '連絡先';
+
+ @override
+ String get nav_channels => 'チャンネル';
+
+ @override
+ String get nav_map => '地図';
+
+ @override
+ String get common_cancel => 'キャンセル';
+
+ @override
+ String get common_ok => '了解';
+
+ @override
+ String get common_connect => '接続する';
+
+ @override
+ String get common_unknownDevice => '不明なデバイス';
+
+ @override
+ String get common_save => '保存';
+
+ @override
+ String get common_delete => '削除';
+
+ @override
+ String get common_deleteAll => 'すべて削除';
+
+ @override
+ String get common_close => '閉じる';
+
+ @override
+ String get common_edit => '編集';
+
+ @override
+ String get common_add => '追加';
+
+ @override
+ String get common_settings => '設定';
+
+ @override
+ String get common_disconnect => '切断する';
+
+ @override
+ String get common_connected => '接続されている';
+
+ @override
+ String get common_disconnected => '切断';
+
+ @override
+ String get common_create => '作成する';
+
+ @override
+ String get common_continue => '続き';
+
+ @override
+ String get common_share => '共有する';
+
+ @override
+ String get common_copy => 'コピー';
+
+ @override
+ String get common_retry => '再試';
+
+ @override
+ String get common_hide => '隠す';
+
+ @override
+ String get common_remove => '削除';
+
+ @override
+ String get common_enable => '有効化する';
+
+ @override
+ String get common_disable => '無効化する';
+
+ @override
+ String get common_reboot => '再起動';
+
+ @override
+ String get common_loading => '読み込み中...';
+
+ @override
+ String get common_notAvailable => '—';
+
+ @override
+ String common_voltageValue(String volts) {
+ return '$volts V';
+ }
+
+ @override
+ String common_percentValue(int percent) {
+ return '$percent%';
+ }
+
+ @override
+ String get scanner_title => 'MeshCore オープン';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => 'ブルートゥース';
+
+ @override
+ String get connectionChoiceTcpLabel => 'TCP';
+
+ @override
+ String get tcpScreenTitle => 'TCP を使用して接続';
+
+ @override
+ String get tcpHostLabel => 'IPアドレス';
+
+ @override
+ String get tcpHostHint => '192.168.40.10';
+
+ @override
+ String get tcpPortLabel => '港';
+
+ @override
+ String get tcpPortHint => '5000';
+
+ @override
+ String get tcpStatus_notConnected => 'エンドポイントを入力し、接続する';
+
+ @override
+ String tcpStatus_connectingTo(String endpoint) {
+ return '$endpoint への接続中...';
+ }
+
+ @override
+ String get tcpErrorHostRequired => 'IPアドレスが必要です。';
+
+ @override
+ String get tcpErrorPortInvalid => 'ポート番号は1から65535の範囲で指定してください。';
+
+ @override
+ String get tcpErrorUnsupported => 'このプラットフォームでは、TCP 転送はサポートされていません。';
+
+ @override
+ String get tcpErrorTimedOut => 'TCP 接続がタイムアウトしました。';
+
+ @override
+ String tcpConnectionFailed(String error) {
+ return 'TCP接続に失敗しました:$error';
+ }
+
+ @override
+ String get usbScreenTitle => 'USB経由で接続';
+
+ @override
+ String get usbScreenSubtitle => '検出されたシリアルデバイスを選択し、MeshCoreノードに直接接続してください。';
+
+ @override
+ String get usbScreenStatus => 'USBデバイスを選択する';
+
+ @override
+ String get usbScreenNote =>
+ 'USBシリアルポートは、サポートされているAndroidデバイスおよびデスクトッププラットフォームで利用可能です。';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'USBデバイスが見つかりませんでした。「別のUSBデバイスを接続して、再度確認してください。」';
+
+ @override
+ String get usbErrorPermissionDenied => 'USBへのアクセス許可が拒否されました。';
+
+ @override
+ String get usbErrorDeviceMissing => '選択されたUSBデバイスは、もう利用できません。';
+
+ @override
+ String get usbErrorInvalidPort => '有効なUSBデバイスを選択してください。';
+
+ @override
+ String get usbErrorBusy => '別のUSB接続の要求がすでに処理中です。';
+
+ @override
+ String get usbErrorNotConnected => 'USBデバイスは接続されていません。';
+
+ @override
+ String get usbErrorOpenFailed => '選択したUSBデバイスを開くことができません。';
+
+ @override
+ String get usbErrorConnectFailed => '選択したUSBデバイスへの接続に失敗しました。';
+
+ @override
+ String get usbErrorUnsupported => 'このプラットフォームでは、USBシリアル通信はサポートされていません。';
+
+ @override
+ String get usbErrorAlreadyActive => 'USB接続はすでに確立されています。';
+
+ @override
+ String get usbErrorNoDeviceSelected => 'USBデバイスは選択されていません。';
+
+ @override
+ String get usbErrorPortClosed => 'USB接続は確立されていません。';
+
+ @override
+ String get usbErrorConnectTimedOut =>
+ '接続がタイムアウトしました。デバイスにUSBコンパニオンファームウェアがインストールされていることを確認してください。';
+
+ @override
+ String get usbFallbackDeviceName => 'ウェブシリアルデバイス';
+
+ @override
+ String get usbStatus_notConnected => 'USBデバイスを選択する';
+
+ @override
+ String get usbStatus_connecting => 'USBデバイスへの接続中...';
+
+ @override
+ String get usbStatus_searching => 'USBデバイスを検索中...';
+
+ @override
+ String usbConnectionFailed(String error) {
+ return 'USB接続に失敗しました:$error';
+ }
+
+ @override
+ String get scanner_scanning => 'デバイスをスキャン中...';
+
+ @override
+ String get scanner_connecting => '接続中...';
+
+ @override
+ String get scanner_disconnecting => '切断...';
+
+ @override
+ String get scanner_notConnected => '接続されていない';
+
+ @override
+ String scanner_connectedTo(String deviceName) {
+ return '$deviceName に接続';
+ }
+
+ @override
+ String get scanner_searchingDevices => 'MeshCoreデバイスの検索';
+
+ @override
+ String get scanner_tapToScan => 'MeshCore デバイスを検索するには、「スキャン」ボタンをタップしてください。';
+
+ @override
+ String scanner_connectionFailed(String error) {
+ return '接続に失敗しました:$error';
+ }
+
+ @override
+ String get scanner_stop => '停止';
+
+ @override
+ String get scanner_scan => 'スキャン';
+
+ @override
+ String get scanner_bluetoothOff => 'Bluetooth はオフになっています';
+
+ @override
+ String get scanner_bluetoothOffMessage => 'Bluetoothを有効にして、デバイスを検索してください。';
+
+ @override
+ String get scanner_chromeRequired => 'Chrome ブラウザが必須です';
+
+ @override
+ String get scanner_chromeRequiredMessage =>
+ 'このWebアプリケーションは、Bluetooth機能を利用するために、Google ChromeまたはChromiumベースのブラウザが必要です。';
+
+ @override
+ String get scanner_enableBluetooth => 'Bluetoothを有効にする';
+
+ @override
+ String get device_quickSwitch => '素早い切り替え';
+
+ @override
+ String get device_meshcore => 'メッシュコア';
+
+ @override
+ String get settings_title => '設定';
+
+ @override
+ String get settings_deviceInfo => 'デバイス情報';
+
+ @override
+ String get settings_appSettings => 'アプリ設定';
+
+ @override
+ String get settings_appSettingsSubtitle => '通知、メッセージング、および地図の表示設定';
+
+ @override
+ String get settings_nodeSettings => 'ノード設定';
+
+ @override
+ String get settings_nodeName => 'ノード名';
+
+ @override
+ String get settings_nodeNameNotSet => '設定されていない';
+
+ @override
+ String get settings_nodeNameHint => 'ノード名を入力してください';
+
+ @override
+ String get settings_nodeNameUpdated => '氏名変更';
+
+ @override
+ String get settings_radioSettings => 'ラジオ設定';
+
+ @override
+ String get settings_radioSettingsSubtitle => '周波数、電力、スプレッドファクター';
+
+ @override
+ String get settings_radioSettingsUpdated => 'ラジオの設定が更新されました';
+
+ @override
+ String get settings_location => '場所';
+
+ @override
+ String get settings_locationSubtitle => 'GPS 座標';
+
+ @override
+ String get settings_locationUpdated => '場所とGPS設定が更新されました';
+
+ @override
+ String get settings_locationBothRequired => '緯度と経度をそれぞれ入力してください。';
+
+ @override
+ String get settings_locationInvalid => '無効な緯度または経度。';
+
+ @override
+ String get settings_locationGPSEnable => 'GPS機能有効';
+
+ @override
+ String get settings_locationGPSEnableSubtitle => 'GPSが自動的に位置情報を更新できるようにする。';
+
+ @override
+ String get settings_locationIntervalSec => 'GPS データの取得間隔(秒)';
+
+ @override
+ String get settings_locationIntervalInvalid =>
+ '間隔は少なくとも60秒で、86400秒未満でなければなりません。';
+
+ @override
+ String get settings_latitude => '緯度';
+
+ @override
+ String get settings_longitude => '経度';
+
+ @override
+ String get settings_contactSettings => '連絡設定';
+
+ @override
+ String get settings_contactSettingsSubtitle => '連絡先を追加する設定';
+
+ @override
+ String get settings_privacyMode => 'プライバシーモード';
+
+ @override
+ String get settings_privacyModeSubtitle => '広告に名前/場所を記載しない';
+
+ @override
+ String get settings_privacyModeToggle =>
+ 'プライバシーモードをオンにして、広告に表示される名前や場所を非表示にします。';
+
+ @override
+ String get settings_privacyModeEnabled => 'プライバシーモードが有効になっています';
+
+ @override
+ String get settings_privacyModeDisabled => 'プライバシーモードは無効化されています';
+
+ @override
+ String get settings_privacy => 'プライバシー設定';
+
+ @override
+ String get settings_privacySubtitle => '共有する情報の内容を管理する。';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ '自分のデバイスが他の人に共有する情報を選択してください。';
+
+ @override
+ String get settings_denyAll => 'すべてを否定';
+
+ @override
+ String get settings_allowByContact => '連絡先を明示するオプション';
+
+ @override
+ String get settings_allowAll => 'すべて許可';
+
+ @override
+ String get settings_telemetryBaseMode => 'テレメトリ基本モード';
+
+ @override
+ String get settings_telemetryLocationMode => 'テレメトリ位置特定モード';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'テレメトリ環境モード';
+
+ @override
+ String get settings_advertLocation => '広告掲載場所';
+
+ @override
+ String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
+
+ @override
+ String settings_multiAck(String value) {
+ return '複数のACK:$value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'テレメトリモードが更新されました';
+
+ @override
+ String get settings_actions => '行動';
+
+ @override
+ String get settings_sendAdvertisement => '広告を送信する';
+
+ @override
+ String get settings_sendAdvertisementSubtitle => '現在、放送での活動';
+
+ @override
+ String get settings_advertisementSent => '広告が送信されました';
+
+ @override
+ String get settings_syncTime => '同期時間';
+
+ @override
+ String get settings_syncTimeSubtitle => 'デバイスの時刻を、携帯電話の時刻に合わせる';
+
+ @override
+ String get settings_timeSynchronized => '時間同期';
+
+ @override
+ String get settings_refreshContacts => '連絡先を更新する';
+
+ @override
+ String get settings_refreshContactsSubtitle => 'デバイスから連絡先リストを再読み込みする';
+
+ @override
+ String get settings_rebootDevice => 'デバイスを再起動する';
+
+ @override
+ String get settings_rebootDeviceSubtitle => 'MeshCore デバイスを再起動する';
+
+ @override
+ String get settings_rebootDeviceConfirm =>
+ '本当にデバイスを再起動したいですか? その場合、接続が切断されます。';
+
+ @override
+ String get settings_debug => 'デバッグ';
+
+ @override
+ String get settings_bleDebugLog => 'BLE デバッグログ';
+
+ @override
+ String get settings_bleDebugLogSubtitle => 'BLEコマンド、応答、および生のデータ';
+
+ @override
+ String get settings_appDebugLog => 'アプリケーションのデバッグログ';
+
+ @override
+ String get settings_appDebugLogSubtitle => 'アプリケーションのデバッグメッセージ';
+
+ @override
+ String get settings_about => '概要';
+
+ @override
+ String settings_aboutVersion(String version) {
+ return 'MeshCore Open $version版';
+ }
+
+ @override
+ String get settings_aboutLegalese => '2026年のMeshCoreオープンソースプロジェクト';
+
+ @override
+ String get settings_aboutDescription =>
+ 'MeshCore LoRaメッシュネットワークデバイス用の、オープンソースのFlutterクライアント。';
+
+ @override
+ String get settings_aboutOpenMeteoAttribution =>
+ 'LOS 標高データ:Open-Meteo (CC BY 4.0)';
+
+ @override
+ String get settings_infoName => '名前';
+
+ @override
+ String get settings_infoId => 'ID';
+
+ @override
+ String get settings_infoStatus => 'ステータス';
+
+ @override
+ String get settings_infoBattery => 'バッテリー';
+
+ @override
+ String get settings_infoPublicKey => '公開鍵';
+
+ @override
+ String get settings_infoContactsCount => '連絡先数';
+
+ @override
+ String get settings_infoChannelCount => 'チャンネル数';
+
+ @override
+ String get settings_presets => 'プリセット';
+
+ @override
+ String get settings_frequency => '周波数 (MHz)';
+
+ @override
+ String get settings_frequencyHelper => '300.0 - 2500.0';
+
+ @override
+ String get settings_frequencyInvalid => '無効な周波数 (300-2500 MHz)';
+
+ @override
+ String get settings_bandwidth => '帯域幅';
+
+ @override
+ String get settings_spreadingFactor => '伝播係数';
+
+ @override
+ String get settings_codingRate => 'コーディング速度';
+
+ @override
+ String get settings_txPower => 'TX 信号電力 (dBm)';
+
+ @override
+ String get settings_txPowerHelper => '0 - 22';
+
+ @override
+ String get settings_txPowerInvalid => '無効な送信電力 (0-22 dBm)';
+
+ @override
+ String get settings_clientRepeat => 'オフグリッド(電力網から孤立した状態)の繰り返し';
+
+ @override
+ String get settings_clientRepeatSubtitle =>
+ 'このデバイスが、他のデバイスに対してメッシュパケットを繰り返し送信できるようにする。';
+
+ @override
+ String get settings_clientRepeatFreqWarning =>
+ 'オフグリッドでの再送には、433MHz、869MHz、または918MHzの周波数が必要です。';
+
+ @override
+ String settings_error(String message) {
+ return 'エラー:$message';
+ }
+
+ @override
+ String get appSettings_title => 'アプリ設定';
+
+ @override
+ String get appSettings_appearance => '外観';
+
+ @override
+ String get appSettings_theme => 'テーマ';
+
+ @override
+ String get appSettings_themeSystem => 'システムデフォルト';
+
+ @override
+ String get appSettings_themeLight => '光';
+
+ @override
+ String get appSettings_themeDark => '暗い';
+
+ @override
+ String get appSettings_language => '言語';
+
+ @override
+ String get appSettings_languageSystem => 'システムデフォルト';
+
+ @override
+ String get appSettings_languageEn => '英語';
+
+ @override
+ String get appSettings_languageFr => 'フランス語';
+
+ @override
+ String get appSettings_languageEs => 'スペイン語';
+
+ @override
+ String get appSettings_languageDe => 'ドイツ語';
+
+ @override
+ String get appSettings_languagePl => 'ポーランド語';
+
+ @override
+ String get appSettings_languageSl => 'スロベニア語';
+
+ @override
+ String get appSettings_languagePt => 'ポルトガル語';
+
+ @override
+ String get appSettings_languageIt => 'イタリア語';
+
+ @override
+ String get appSettings_languageZh => '中国語';
+
+ @override
+ String get appSettings_languageSv => 'スウェーデン語';
+
+ @override
+ String get appSettings_languageNl => 'オランダ語';
+
+ @override
+ String get appSettings_languageSk => 'スロベニア語';
+
+ @override
+ String get appSettings_languageBg => 'ブルガリア語';
+
+ @override
+ String get appSettings_languageRu => 'ロシア語';
+
+ @override
+ String get appSettings_languageUk => 'ウクライナ語';
+
+ @override
+ String get appSettings_enableMessageTracing => 'メッセージ追跡機能を有効にする';
+
+ @override
+ String get appSettings_enableMessageTracingSubtitle =>
+ 'メッセージに関する詳細な経路およびタイミングに関するメタデータを表示する';
+
+ @override
+ String get appSettings_notifications => '通知';
+
+ @override
+ String get appSettings_enableNotifications => '通知を有効にする';
+
+ @override
+ String get appSettings_enableNotificationsSubtitle => 'メッセージや広告に関する通知を受け取る';
+
+ @override
+ String get appSettings_notificationPermissionDenied => '通知の許可が拒否されました';
+
+ @override
+ String get appSettings_notificationsEnabled => '通知機能が有効になっています';
+
+ @override
+ String get appSettings_notificationsDisabled => '通知が無効化されています';
+
+ @override
+ String get appSettings_messageNotifications => 'メッセージ通知';
+
+ @override
+ String get appSettings_messageNotificationsSubtitle =>
+ '新しいメッセージを受信した際に、通知を表示する';
+
+ @override
+ String get appSettings_channelMessageNotifications => 'チャネルメッセージの通知';
+
+ @override
+ String get appSettings_channelMessageNotificationsSubtitle =>
+ 'チャンネルからのメッセージを受信した際に、通知を表示する';
+
+ @override
+ String get appSettings_advertisementNotifications => '広告通知';
+
+ @override
+ String get appSettings_advertisementNotificationsSubtitle =>
+ '新しいノードが発見された場合に通知を表示する';
+
+ @override
+ String get appSettings_messaging => 'メッセージング';
+
+ @override
+ String get appSettings_clearPathOnMaxRetry => 'マックスリトライでの明確な手順';
+
+ @override
+ String get appSettings_clearPathOnMaxRetrySubtitle =>
+ '5回送信に失敗した場合、連絡経路をリセットする';
+
+ @override
+ String get appSettings_pathsWillBeCleared => '5回失敗した後、経路が再開されます。';
+
+ @override
+ String get appSettings_pathsWillNotBeCleared => 'パスは自動で削除されません。';
+
+ @override
+ String get appSettings_autoRouteRotation => '自動ルートの切り替え';
+
+ @override
+ String get appSettings_autoRouteRotationSubtitle => '最適なルートと、洪水モードを切り替える';
+
+ @override
+ String get appSettings_autoRouteRotationEnabled => '自動ルートの切り替え機能が有効になっています';
+
+ @override
+ String get appSettings_autoRouteRotationDisabled => '自動ルートの変更機能が無効になっています。';
+
+ @override
+ String get appSettings_maxRouteWeight => '最大ルート重量';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'ある経路が、成功裏に配送された場合に、積み上げられる最大重量';
+
+ @override
+ String get appSettings_initialRouteWeight => '初期ルートの重み';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle => '新たに発見された経路の初期重量';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement => '成功時の重み増加';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ '配送が成功した場合に、経路に追加される重量';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement => '失敗時の重み減少';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ '配送に失敗した際に、経路から取り除かれた重量';
+
+ @override
+ String get appSettings_maxMessageRetries => '最大メッセージ再試行回数';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'メッセージを「失敗」とマークするまでの、再試行回数';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
+ @override
+ String get appSettings_battery => 'バッテリー';
+
+ @override
+ String get appSettings_batteryChemistry => '電池の化学';
+
+ @override
+ String appSettings_batteryChemistryPerDevice(String deviceName) {
+ return '$deviceName 単位';
+ }
+
+ @override
+ String get appSettings_batteryChemistryConnectFirst => 'デバイスを選択するために接続する';
+
+ @override
+ String get appSettings_batteryNmc => '18650型 NMC (3.0-4.2V)';
+
+ @override
+ String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65V)';
+
+ @override
+ String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)';
+
+ @override
+ String get appSettings_mapDisplay => '地図の表示';
+
+ @override
+ String get appSettings_showRepeaters => '繰り返し再生機能';
+
+ @override
+ String get appSettings_showRepeatersSubtitle => '地図上にリピーターノードを表示する';
+
+ @override
+ String get appSettings_showChatNodes => 'チャットノードの表示';
+
+ @override
+ String get appSettings_showChatNodesSubtitle => '地図上にチャットノードを表示する';
+
+ @override
+ String get appSettings_showOtherNodes => '他のノードを表示する';
+
+ @override
+ String get appSettings_showOtherNodesSubtitle => '地図上に、他のノードの種類を表示する';
+
+ @override
+ String get appSettings_timeFilter => '時間フィルター';
+
+ @override
+ String get appSettings_timeFilterShowAll => 'すべてのノードを表示する';
+
+ @override
+ String appSettings_timeFilterShowLast(int hours) {
+ return '過去 $hours 時間のノードを表示する';
+ }
+
+ @override
+ String get appSettings_mapTimeFilter => '地図の表示期間を絞り込む';
+
+ @override
+ String get appSettings_showNodesDiscoveredWithin => '以下の範囲内で発見されたノードを表示する:';
+
+ @override
+ String get appSettings_allTime => 'すべての期間';
+
+ @override
+ String get appSettings_lastHour => '直前の';
+
+ @override
+ String get appSettings_last6Hours => '過去6時間';
+
+ @override
+ String get appSettings_last24Hours => '過去24時間';
+
+ @override
+ String get appSettings_lastWeek => '先週';
+
+ @override
+ String get appSettings_offlineMapCache => 'オフライン用地図キャッシュ';
+
+ @override
+ String get appSettings_unitsTitle => '単位';
+
+ @override
+ String get appSettings_unitsMetric => 'メートル (m) / キロメートル (km)';
+
+ @override
+ String get appSettings_unitsImperial => '帝国 (フィート / マイル)';
+
+ @override
+ String get appSettings_noAreaSelected => '選択されたエリアはありません';
+
+ @override
+ String appSettings_areaSelectedZoom(int minZoom, int maxZoom) {
+ return '選択された範囲(ズームレベル:$minZoom~$maxZoom)';
+ }
+
+ @override
+ String get appSettings_debugCard => 'デバッグ';
+
+ @override
+ String get appSettings_appDebugLogging => 'アプリケーションのデバッグ用ログ';
+
+ @override
+ String get appSettings_appDebugLoggingSubtitle =>
+ 'ログアプリのデバッグメッセージ(トラブルシューティング用)';
+
+ @override
+ String get appSettings_appDebugLoggingEnabled =>
+ 'アプリケーションのデバッグ用ログ機能が有効になっています。';
+
+ @override
+ String get appSettings_appDebugLoggingDisabled =>
+ 'アプリケーションのデバッグログが無効化されています。';
+
+ @override
+ String get contacts_title => '連絡先';
+
+ @override
+ String get contacts_noContacts => '現時点では、連絡先はまだありません。';
+
+ @override
+ String get contacts_contactsWillAppear => 'デバイスが広告を行う際に、連絡先が表示されます。';
+
+ @override
+ String get contacts_unread => '未読';
+
+ @override
+ String get contacts_searchContactsNoNumber => '連絡先を検索...';
+
+ @override
+ String contacts_searchContacts(int number, String str) {
+ return '$number件の$strに関する連絡先を検索...';
+ }
+
+ @override
+ String contacts_searchFavorites(int number, String str) {
+ return '$number件の$strを検索...';
+ }
+
+ @override
+ String contacts_searchUsers(int number, String str) {
+ return '$number件の$strに関するユーザーを検索する...';
+ }
+
+ @override
+ String contacts_searchRepeaters(int number, String str) {
+ return '$number $str までの検索...';
+ }
+
+ @override
+ String contacts_searchRoomServers(int number, String str) {
+ return '$number $str 部屋のサーバーを検索する...';
+ }
+
+ @override
+ String get contacts_noUnreadContacts => '未読の連絡先はありません';
+
+ @override
+ String get contacts_noContactsFound => '連絡先またはグループは見つかりませんでした。';
+
+ @override
+ String get contacts_deleteContact => '連絡先を削除';
+
+ @override
+ String contacts_removeConfirm(String contactName) {
+ return '$contactName を連絡先から削除しますか?';
+ }
+
+ @override
+ String get contacts_manageRepeater => 'リピーターの管理';
+
+ @override
+ String get contacts_manageRoom => 'ルームサーバーの管理';
+
+ @override
+ String get contacts_roomLogin => 'ルームサーバーへのログイン';
+
+ @override
+ String get contacts_openChat => '自由な会話';
+
+ @override
+ String get contacts_editGroup => '編集グループ';
+
+ @override
+ String get contacts_deleteGroup => 'グループを削除';
+
+ @override
+ String contacts_deleteGroupConfirm(String groupName) {
+ return '$groupName を削除しますか?';
+ }
+
+ @override
+ String get contacts_newGroup => '新しいグループ';
+
+ @override
+ String get contacts_groupName => 'グループ名';
+
+ @override
+ String get contacts_groupNameRequired => 'グループ名が必須です';
+
+ @override
+ String get contacts_groupNameReserved => 'このグループ名はすでに使用されています。';
+
+ @override
+ String contacts_groupAlreadyExists(String name) {
+ return 'グループ「$name」はすでに存在しています';
+ }
+
+ @override
+ String get contacts_filterContacts => '連絡先をフィルタリングする…';
+
+ @override
+ String get contacts_noContactsMatchFilter => '指定された条件に合致する連絡先は見つかりませんでした。';
+
+ @override
+ String get contacts_noMembers => 'メンバーはいない';
+
+ @override
+ String get contacts_lastSeenNow => '最近';
+
+ @override
+ String contacts_lastSeenMinsAgo(int minutes) {
+ return '~$minutes 分';
+ }
+
+ @override
+ String get contacts_lastSeenHourAgo => '約1時間';
+
+ @override
+ String contacts_lastSeenHoursAgo(int hours) {
+ return '~ $hours 時間';
+ }
+
+ @override
+ String get contacts_lastSeenDayAgo => '~1日';
+
+ @override
+ String contacts_lastSeenDaysAgo(int days) {
+ return '~$days日間';
+ }
+
+ @override
+ String get contact_info => '連絡先';
+
+ @override
+ String get contact_settings => '連絡設定';
+
+ @override
+ String get contact_telemetry => 'テレメトリー';
+
+ @override
+ String get contact_lastSeen => '最後に確認された場所';
+
+ @override
+ String get contact_clearChat => 'チャットのクリア';
+
+ @override
+ String get contact_teleBase => 'テレメトリ基地';
+
+ @override
+ String get contact_teleBaseSubtitle => 'バッテリー残量と基本的なテレメトリーの共有を許可する';
+
+ @override
+ String get contact_teleLoc => 'テレメトリの場所';
+
+ @override
+ String get contact_teleLocSubtitle => '位置情報共有を許可する';
+
+ @override
+ String get contact_teleEnv => 'テレメトリ環境';
+
+ @override
+ String get contact_teleEnvSubtitle => '環境センサーのデータを共有することを許可する';
+
+ @override
+ String get channels_title => 'チャンネル';
+
+ @override
+ String get channels_noChannelsConfigured => '設定されたチャンネルがありません';
+
+ @override
+ String get channels_addPublicChannel => 'パブリックチャンネルを追加する';
+
+ @override
+ String get channels_searchChannels => '検索オプション...';
+
+ @override
+ String get channels_noChannelsFound => 'チャンネルが見つかりませんでした';
+
+ @override
+ String channels_channelIndex(int index) {
+ return 'チャンネル $index';
+ }
+
+ @override
+ String get channels_hashtagChannel => 'ハッシュタグチャンネル';
+
+ @override
+ String get channels_public => '一般の人々';
+
+ @override
+ String get channels_private => '個人の';
+
+ @override
+ String get channels_publicChannel => '一般チャンネル';
+
+ @override
+ String get channels_privateChannel => 'プライベートチャンネル';
+
+ @override
+ String get channels_editChannel => 'チャンネルを編集する';
+
+ @override
+ String get channels_muteChannel => 'ミュート機能';
+
+ @override
+ String get channels_unmuteChannel => 'ミュートを解除する';
+
+ @override
+ String get channels_deleteChannel => 'チャンネルを削除する';
+
+ @override
+ String channels_deleteChannelConfirm(String name) {
+ return '$name を削除しますか? これは取り消すことができません。';
+ }
+
+ @override
+ String channels_channelDeleteFailed(String name) {
+ return 'チャンネル「$name」の削除に失敗しました。';
+ }
+
+ @override
+ String channels_channelDeleted(String name) {
+ return 'チャンネル「$name」が削除されました';
+ }
+
+ @override
+ String get channels_addChannel => 'チャンネルを追加';
+
+ @override
+ String get channels_channelIndexLabel => 'チャンネルインデックス';
+
+ @override
+ String get channels_channelName => 'チャンネル名';
+
+ @override
+ String get channels_usePublicChannel => 'パブリックチャンネルを使用する';
+
+ @override
+ String get channels_standardPublicPsk => '標準的な公用 PSK';
+
+ @override
+ String get channels_pskHex => 'PSK (ヘックス)';
+
+ @override
+ String get channels_generateRandomPsk => 'ランダムなPSK(正交符号分割変調)を生成する';
+
+ @override
+ String get channels_enterChannelName => 'チャンネル名を入力してください';
+
+ @override
+ String get channels_pskMustBe32Hex => 'PSKは32桁の16進数で構成されている必要があります。';
+
+ @override
+ String channels_channelAdded(String name) {
+ return 'チャンネル「$name」を追加';
+ }
+
+ @override
+ String channels_editChannelTitle(int index) {
+ return 'チャンネル $index の編集';
+ }
+
+ @override
+ String get channels_smazCompression => 'SMAZ 圧縮';
+
+ @override
+ String channels_channelUpdated(String name) {
+ return 'チャンネル「$name」が更新されました';
+ }
+
+ @override
+ String get channels_publicChannelAdded => 'パブリックチャンネルが追加されました';
+
+ @override
+ String get channels_sortBy => '並び替え';
+
+ @override
+ String get channels_sortManual => 'マニュアル';
+
+ @override
+ String get channels_sortAZ => 'AからZ';
+
+ @override
+ String get channels_sortLatestMessages => '最新のメッセージ';
+
+ @override
+ String get channels_sortUnread => '未読';
+
+ @override
+ String get channels_createPrivateChannel => 'プライベートチャンネルを作成する';
+
+ @override
+ String get channels_createPrivateChannelDesc => '秘密鍵を使用して保護されています。';
+
+ @override
+ String get channels_joinPrivateChannel => 'プライベートチャンネルに参加する';
+
+ @override
+ String get channels_joinPrivateChannelDesc => '手動で秘密のキーを入力する。';
+
+ @override
+ String get channels_joinPublicChannel => '公開チャンネルに参加する';
+
+ @override
+ String get channels_joinPublicChannelDesc => 'このチャンネルには、誰でも参加できます。';
+
+ @override
+ String get channels_joinHashtagChannel => 'ハッシュタグチャンネルに参加する';
+
+ @override
+ String get channels_joinHashtagChannelDesc => '誰でもハッシュタグチャンネルに参加できます。';
+
+ @override
+ String get channels_scanQrCode => 'QRコードをスキャンする';
+
+ @override
+ String get channels_scanQrCodeComingSoon => '近日公開';
+
+ @override
+ String get channels_enterHashtag => 'ハッシュタグを入力してください';
+
+ @override
+ String get channels_hashtagHint => '例:#チーム';
+
+ @override
+ String get chat_noMessages => 'まだメッセージは届いていません';
+
+ @override
+ String get chat_sendMessageToStart => '開始するためにメッセージを送信してください';
+
+ @override
+ String get chat_originalMessageNotFound => '元のメッセージが見つかりませんでした';
+
+ @override
+ String chat_replyingTo(String name) {
+ return '$name への返信';
+ }
+
+ @override
+ String chat_replyTo(String name) {
+ return '$nameへの返信';
+ }
+
+ @override
+ String get chat_location => '場所';
+
+ @override
+ String chat_sendMessageTo(String contactName) {
+ return '$contactName へのメッセージを送信する';
+ }
+
+ @override
+ String get chat_typeMessage => 'メッセージを入力してください…';
+
+ @override
+ String chat_messageTooLong(int maxBytes) {
+ return 'メッセージが長すぎる($maxBytes バイトを超える)。';
+ }
+
+ @override
+ String get chat_messageCopied => 'メッセージがコピーされました';
+
+ @override
+ String get chat_messageDeleted => 'メッセージは削除されました';
+
+ @override
+ String get chat_retryingMessage => '再試行メッセージ';
+
+ @override
+ String chat_retryCount(int current, int max) {
+ return '$current / $max 回目';
+ }
+
+ @override
+ String get chat_sendGif => 'GIFを送信する';
+
+ @override
+ String get chat_reply => '返信';
+
+ @override
+ String get chat_addReaction => '反応を追加';
+
+ @override
+ String get chat_me => '私';
+
+ @override
+ String get emojiCategorySmileys => '笑顔の絵文字';
+
+ @override
+ String get emojiCategoryGestures => '身振り、動作';
+
+ @override
+ String get emojiCategoryHearts => '心';
+
+ @override
+ String get emojiCategoryObjects => '対象物';
+
+ @override
+ String get gifPicker_title => 'GIF を選択してください';
+
+ @override
+ String get gifPicker_searchHint => 'GIFの検索...';
+
+ @override
+ String get gifPicker_poweredBy => 'GIPHYによる提供';
+
+ @override
+ String get gifPicker_noGifsFound => 'GIF形式のファイルは見つかりませんでした';
+
+ @override
+ String get gifPicker_failedLoad => 'GIFファイルの読み込みに失敗しました';
+
+ @override
+ String get gifPicker_failedSearch => 'GIFファイルの検索に失敗しました';
+
+ @override
+ String get gifPicker_noInternet => 'インターネット接続なし';
+
+ @override
+ String get debugLog_appTitle => 'アプリケーションのデバッグログ';
+
+ @override
+ String get debugLog_bleTitle => 'BLE デバッグログ';
+
+ @override
+ String get debugLog_copyLog => '記録';
+
+ @override
+ String get debugLog_clearLog => '詳細なログ';
+
+ @override
+ String get debugLog_copied => 'デバッグログをコピー';
+
+ @override
+ String get debugLog_bleCopied => 'BLEログのコピー';
+
+ @override
+ String get debugLog_noEntries => 'デバッグログはまだ生成されていません';
+
+ @override
+ String get debugLog_enableInSettings => 'アプリのデバッグログを有効にするには、設定から操作してください。';
+
+ @override
+ String get debugLog_frames => 'フレーム';
+
+ @override
+ String get debugLog_rawLogRx => '生のログ-RX';
+
+ @override
+ String get debugLog_noBleActivity => '現時点では、BLE関連の活動は行われていません。';
+
+ @override
+ String debugFrame_length(int count) {
+ return 'フレーム長: $count バイト';
+ }
+
+ @override
+ String debugFrame_command(String value) {
+ return 'コマンド: 0x$value';
+ }
+
+ @override
+ String get debugFrame_textMessageHeader => 'テキストメッセージ用フレーム:';
+
+ @override
+ String debugFrame_destinationPubKey(String pubKey) {
+ return '- 宛先公開鍵: $pubKey';
+ }
+
+ @override
+ String debugFrame_timestamp(int timestamp) {
+ return '- タイムスタンプ: $timestamp';
+ }
+
+ @override
+ String debugFrame_flags(String value) {
+ return '- フラグ: 0x$value';
+ }
+
+ @override
+ String debugFrame_textType(int type, String label) {
+ return '- テキストの種類: $type ($label)';
+ }
+
+ @override
+ String get debugFrame_textTypeCli => 'CLI(コマンドラインインターフェース)';
+
+ @override
+ String get debugFrame_textTypePlain => 'シンプルな';
+
+ @override
+ String debugFrame_text(String text) {
+ return '- テキスト:「$text」';
+ }
+
+ @override
+ String get debugFrame_hexDump => 'ヘックスダンプ:';
+
+ @override
+ String get chat_pathManagement => '経路管理';
+
+ @override
+ String get chat_ShowAllPaths => 'すべての経路を表示';
+
+ @override
+ String get chat_routingMode => 'ルーティングモード';
+
+ @override
+ String get chat_autoUseSavedPath => '自動 (保存されたパスを使用)';
+
+ @override
+ String get chat_forceFloodMode => '強制的に洪水モードを起動';
+
+ @override
+ String get chat_recentAckPaths => '最近使用したACKパス(タップして使用):';
+
+ @override
+ String get chat_pathHistoryFull => 'パスの履歴は完全です。エントリを削除して、新しいものを追加できます。';
+
+ @override
+ String get chat_hopSingular => 'ジャンプ';
+
+ @override
+ String get chat_hopPlural => 'ホップ';
+
+ @override
+ String chat_hopsCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'ホップ',
+ one: 'ホップ',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String get chat_successes => '成功事例';
+
+ @override
+ String get chat_removePath => 'パスを削除する';
+
+ @override
+ String get chat_noPathHistoryYet => 'まだ履歴はありません。\nパスを特定するためにメッセージを送信してください。';
+
+ @override
+ String get chat_pathActions => 'パスの操作:';
+
+ @override
+ String get chat_setCustomPath => 'カスタムパスを設定';
+
+ @override
+ String get chat_setCustomPathSubtitle => '手動で経路を指定する';
+
+ @override
+ String get chat_clearPath => '明確な道';
+
+ @override
+ String get chat_clearPathSubtitle => '次回送信時に、以前の情報を再取得する';
+
+ @override
+ String get chat_pathCleared => '経路が確保されました。次のメッセージでルートを再確認します。';
+
+ @override
+ String get chat_floodModeSubtitle => 'アプリのバーにあるルーティング切り替え機能を使用する';
+
+ @override
+ String get chat_floodModeEnabled =>
+ '洪水モードが有効になっています。アプリのメニューバーにあるルートアイコンを使用して、モードを切り替えることができます。';
+
+ @override
+ String get chat_fullPath => 'フルパス';
+
+ @override
+ String get chat_pathDetailsNotAvailable =>
+ '経路の詳細については、まだ情報がありません。「リフレッシュ」ボタンを押して、再度お試しください。';
+
+ @override
+ String chat_pathSetHops(int hopCount, String status) {
+ String _temp0 = intl.Intl.pluralLogic(
+ hopCount,
+ locale: localeName,
+ other: 'hops',
+ one: 'hop',
+ );
+ return 'Path set: $hopCount $_temp0 - $status';
+ }
+
+ @override
+ String get chat_pathSavedLocally => 'ローカルで保存。同期のために接続する。';
+
+ @override
+ String get chat_pathDeviceConfirmed => 'デバイスの確認済み。';
+
+ @override
+ String get chat_pathDeviceNotConfirmed => 'デバイスの確認はまだできていません。';
+
+ @override
+ String get chat_type => '種類';
+
+ @override
+ String get chat_path => '道';
+
+ @override
+ String get chat_publicKey => '公開鍵';
+
+ @override
+ String get chat_compressOutgoingMessages => '送信されるメッセージを圧縮する';
+
+ @override
+ String get chat_floodForced => '洪水(強制的な)';
+
+ @override
+ String get chat_directForced => '直接的な(強制的な)';
+
+ @override
+ String chat_hopsForced(int count) {
+ return '$count 本のホップ(強制的に採取)';
+ }
+
+ @override
+ String get chat_floodAuto => '洪水 (自動)';
+
+ @override
+ String get chat_direct => '直接';
+
+ @override
+ String get chat_poiShared => '共有されたPOI';
+
+ @override
+ String chat_unread(int count) {
+ return '未読: $count';
+ }
+
+ @override
+ String get chat_openLink => 'リンクを開く?';
+
+ @override
+ String get chat_openLinkConfirmation => 'このリンクをブラウザで開くことはご希望ですか?';
+
+ @override
+ String get chat_open => '開く';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return 'リンクを開けられませんでした: $url';
+ }
+
+ @override
+ String get chat_invalidLink => '無効なリンク形式';
+
+ @override
+ String get map_title => 'ノードマップ';
+
+ @override
+ String get map_lineOfSight => '視界';
+
+ @override
+ String get map_losScreenTitle => '視界';
+
+ @override
+ String get map_noNodesWithLocation => '位置情報データを持つノードは存在しません';
+
+ @override
+ String get map_nodesNeedGps => 'ノードは、地図上に表示されるために、GPS座標を共有する必要があります。';
+
+ @override
+ String map_nodesCount(int count) {
+ return 'ノード:$count';
+ }
+
+ @override
+ String map_pinsCount(int count) {
+ return 'ピン:$count個';
+ }
+
+ @override
+ String get map_chat => 'チャット';
+
+ @override
+ String get map_repeater => '繰り返し送信装置';
+
+ @override
+ String get map_room => '部屋';
+
+ @override
+ String get map_sensor => 'センサー';
+
+ @override
+ String get map_pinDm => 'ピン(DM)';
+
+ @override
+ String get map_pinPrivate => 'プライベート(非公開)';
+
+ @override
+ String get map_pinPublic => '公開 (一般公開)';
+
+ @override
+ String get map_lastSeen => '最後に確認された場所';
+
+ @override
+ String get map_disconnectConfirm => '本当にこのデバイスとの接続を解除したいですか?';
+
+ @override
+ String get map_from => '~から';
+
+ @override
+ String get map_source => '出典';
+
+ @override
+ String get map_flags => '旗';
+
+ @override
+ String get map_shareMarkerHere => 'この場所でシェア';
+
+ @override
+ String get map_setAsMyLocation => '現在地として設定';
+
+ @override
+ String get map_pinLabel => 'ピンラベル';
+
+ @override
+ String get map_label => 'ラベル';
+
+ @override
+ String get map_pointOfInterest => '注目すべき点';
+
+ @override
+ String get map_sendToContact => '連絡先へ送信';
+
+ @override
+ String get map_sendToChannel => '特定のチャンネルに送信する';
+
+ @override
+ String get map_noChannelsAvailable => '利用可能なチャンネルはありません';
+
+ @override
+ String get map_publicLocationShare => '公共スペースの共有';
+
+ @override
+ String map_publicLocationShareConfirm(String channelLabel) {
+ return '現在、$channelLabel で位置情報を共有する準備をしています。このチャンネルは公開されており、PSK を持つ誰でも閲覧できます。';
+ }
+
+ @override
+ String get map_connectToShareMarkers => '他のデバイスと接続して、マーカーを共有する';
+
+ @override
+ String get map_filterNodes => 'フィルタノード';
+
+ @override
+ String get map_nodeTypes => 'ノードの種類';
+
+ @override
+ String get map_chatNodes => 'チャットノード';
+
+ @override
+ String get map_repeaters => '繰り返し送信装置';
+
+ @override
+ String get map_otherNodes => 'その他のノード';
+
+ @override
+ String get map_showOverlaps => 'リピーターキーの重複';
+
+ @override
+ String get map_keyPrefix => '主要なプレフィックス';
+
+ @override
+ String get map_filterByKeyPrefix => '主要なプレフィックスでフィルタリングする';
+
+ @override
+ String get map_publicKeyPrefix => '公開鍵のプレフィックス';
+
+ @override
+ String get map_markers => 'マーカー';
+
+ @override
+ String get map_showSharedMarkers => '共有のマーカーを表示する';
+
+ @override
+ String get map_showGuessedLocations => '推測されたノードの位置を表示する';
+
+ @override
+ String get map_showDiscoveryContacts => 'Discovery社の連絡先を表示する';
+
+ @override
+ String get map_guessedLocation => '推測された場所';
+
+ @override
+ String get map_lastSeenTime => '最後に確認された時間';
+
+ @override
+ String get map_sharedPin => '共有パスワード';
+
+ @override
+ String get map_joinRoom => '部屋に参加する';
+
+ @override
+ String get map_manageRepeater => 'リピーターの管理';
+
+ @override
+ String get map_tapToAdd => 'ノードをクリックして、パスに追加します。';
+
+ @override
+ String get map_runTrace => 'パスの追跡を実行';
+
+ @override
+ String get map_runTraceWithReturnPath => '元の経路に戻る。';
+
+ @override
+ String get map_removeLast => '最後のものを削除';
+
+ @override
+ String get map_pathTraceCancelled => 'パスの追跡は中止。';
+
+ @override
+ String get mapCache_title => 'オフライン用地図キャッシュ';
+
+ @override
+ String get mapCache_selectAreaFirst => '最初にキャッシュする領域を選択してください';
+
+ @override
+ String get mapCache_noTilesToDownload => 'この地域にはダウンロードできるタイルは存在しません。';
+
+ @override
+ String get mapCache_downloadTilesTitle => 'タイルをダウンロードする';
+
+ @override
+ String mapCache_downloadTilesPrompt(int count) {
+ return 'オフラインでの使用のために、$count個のタイルをダウンロードしますか?';
+ }
+
+ @override
+ String get mapCache_downloadAction => 'ダウンロード';
+
+ @override
+ String mapCache_cachedTiles(int count) {
+ return '$count 個のタイルをキャッシュ';
+ }
+
+ @override
+ String mapCache_cachedTilesWithFailed(int downloaded, int failed) {
+ return 'Cached $downloaded tiles ($failed failed)';
+ }
+
+ @override
+ String get mapCache_clearOfflineCacheTitle => 'オフラインキャッシュをクリアする';
+
+ @override
+ String get mapCache_clearOfflineCachePrompt => 'キャッシュされた地図のタイルをすべて削除しますか?';
+
+ @override
+ String get mapCache_offlineCacheCleared => 'オフラインキャッシュをクリア';
+
+ @override
+ String get mapCache_noAreaSelected => '選択されたエリアはありません';
+
+ @override
+ String get mapCache_cacheArea => 'キャッシュエリア';
+
+ @override
+ String get mapCache_useCurrentView => '現在表示されている内容を保持する';
+
+ @override
+ String get mapCache_zoomRange => 'ズーム範囲';
+
+ @override
+ String mapCache_estimatedTiles(int count) {
+ return '推定されるタイル数: $count';
+ }
+
+ @override
+ String mapCache_downloadedTiles(int completed, int total) {
+ return 'Downloaded $completed / $total';
+ }
+
+ @override
+ String get mapCache_downloadTilesButton => 'タイルをダウンロードする';
+
+ @override
+ String get mapCache_clearCacheButton => 'キャッシュをクリアする';
+
+ @override
+ String mapCache_failedDownloads(int count) {
+ return '失敗したダウンロード: $count';
+ }
+
+ @override
+ String mapCache_boundsLabel(
+ String north,
+ String south,
+ String east,
+ String west,
+ ) {
+ return 'N $north, S $south, E $east, W $west';
+ }
+
+ @override
+ String get time_justNow => 'まさに今';
+
+ @override
+ String time_minutesAgo(int minutes) {
+ return '$minutes分前';
+ }
+
+ @override
+ String time_hoursAgo(int hours) {
+ return '$hours時間前';
+ }
+
+ @override
+ String time_daysAgo(int days) {
+ return '$days日前';
+ }
+
+ @override
+ String get time_hour => '1時間';
+
+ @override
+ String get time_hours => '時間';
+
+ @override
+ String get time_day => '一日';
+
+ @override
+ String get time_days => '日';
+
+ @override
+ String get time_week => '1週間';
+
+ @override
+ String get time_weeks => '週';
+
+ @override
+ String get time_month => '月';
+
+ @override
+ String get time_months => '月';
+
+ @override
+ String get time_minutes => '分';
+
+ @override
+ String get time_allTime => '全期間';
+
+ @override
+ String get dialog_disconnect => '切断する';
+
+ @override
+ String get dialog_disconnectConfirm => '本当にこのデバイスとの接続を解除したいですか?';
+
+ @override
+ String get login_repeaterLogin => '再ログイン';
+
+ @override
+ String get login_roomLogin => 'ルームサーバーへのログイン';
+
+ @override
+ String get login_password => 'パスワード';
+
+ @override
+ String get login_enterPassword => 'パスワードを入力してください';
+
+ @override
+ String get login_savePassword => 'パスワードを保存する';
+
+ @override
+ String get login_savePasswordSubtitle => 'パスワードは、このデバイスに安全に保存されます。';
+
+ @override
+ String get login_repeaterDescription =>
+ '設定やステータスにアクセスするために、リピーターのパスワードを入力してください。';
+
+ @override
+ String get login_roomDescription => '設定やステータスへのアクセスには、部屋のパスワードを入力してください。';
+
+ @override
+ String get login_routing => '経路設定';
+
+ @override
+ String get login_routingMode => 'ルーティングモード';
+
+ @override
+ String get login_autoUseSavedPath => '自動 (保存されたパスを使用)';
+
+ @override
+ String get login_forceFloodMode => '強制的に洪水モードを起動';
+
+ @override
+ String get login_managePaths => 'パスの管理';
+
+ @override
+ String get login_login => 'ログイン';
+
+ @override
+ String login_attempt(int current, int max) {
+ return '試行回数:$current/$max';
+ }
+
+ @override
+ String login_failed(String error) {
+ return 'ログインに失敗しました:$error';
+ }
+
+ @override
+ String get login_failedMessage =>
+ 'ログインに失敗しました。パスワードが間違っているか、または接続が確立されていません。';
+
+ @override
+ String get common_reload => '再読み込み';
+
+ @override
+ String get common_clear => '明確';
+
+ @override
+ String path_currentPath(String path) {
+ return '現在のパス: $path';
+ }
+
+ @override
+ String path_usingHopsPath(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'ホップ',
+ one: 'ホップ',
+ );
+ return '$count $_temp0のパスを使用';
+ }
+
+ @override
+ String get path_enterCustomPath => 'カスタムパスを入力';
+
+ @override
+ String get path_currentPathLabel => '現在の経路';
+
+ @override
+ String get path_hexPrefixInstructions =>
+ '各ホップに対して、2文字の16進数プレフィックスをカンマで区切って入力してください。';
+
+ @override
+ String get path_hexPrefixExample => '例:A1, F2, 3C (各ノードは、自身の公開鍵の最初のバイトを使用)';
+
+ @override
+ String get path_labelHexPrefixes => 'パス (ヘックスプレフィックス)';
+
+ @override
+ String get path_helperMaxHops =>
+ '最大64個のホップ。各プレフィックスは2つの16進数文字(1バイト)で構成されています。';
+
+ @override
+ String get path_selectFromContacts => 'または、連絡先リストから選択してください:';
+
+ @override
+ String get path_noRepeatersFound => '繰り返し機能やルームサーバーは見つかりませんでした。';
+
+ @override
+ String get path_customPathsRequire => 'カスタムパスには、メッセージを中継できる中間地点が必要です。';
+
+ @override
+ String path_invalidHexPrefixes(String prefixes) {
+ return '無効な16進数プレフィックス: $prefixes';
+ }
+
+ @override
+ String get path_tooLong => '経路が長すぎる。最大64回のジャンプのみ許可。';
+
+ @override
+ String get path_setPath => 'パスを設定';
+
+ @override
+ String get repeater_management => 'リピーター管理';
+
+ @override
+ String get room_management => 'ルームサーバーの管理';
+
+ @override
+ String get repeater_managementTools => '管理ツール';
+
+ @override
+ String get repeater_status => 'ステータス';
+
+ @override
+ String get repeater_statusSubtitle => 'リピーターの状態、統計情報、および隣接するネットワークの情報を表示する';
+
+ @override
+ String get repeater_telemetry => 'テレメトリー';
+
+ @override
+ String get repeater_telemetrySubtitle => 'センサーおよびシステムの状態に関するテレメトリの表示';
+
+ @override
+ String get repeater_cli => 'CLI(コマンドラインインターフェース)';
+
+ @override
+ String get repeater_cliSubtitle => 'リピーターへのコマンドを送信する';
+
+ @override
+ String get repeater_neighbors => '近隣住民';
+
+ @override
+ String get repeater_neighborsSubtitle => 'ゼロホップの隣接ノードを表示する。';
+
+ @override
+ String get repeater_settings => '設定';
+
+ @override
+ String get repeater_settingsSubtitle => 'リピーターのパラメータを設定する';
+
+ @override
+ String get repeater_statusTitle => '再送ステータス';
+
+ @override
+ String get repeater_routingMode => 'ルーティングモード';
+
+ @override
+ String get repeater_autoUseSavedPath => '自動 (保存されたパスを使用)';
+
+ @override
+ String get repeater_forceFloodMode => '強制的に洪水モードを起動';
+
+ @override
+ String get repeater_pathManagement => '経路管理';
+
+ @override
+ String get repeater_refresh => 'リフレッシュ';
+
+ @override
+ String get repeater_statusRequestTimeout => 'ステータス情報の取得に失敗しました。';
+
+ @override
+ String repeater_errorLoadingStatus(String error) {
+ return 'ステータス読み込みエラー: $error';
+ }
+
+ @override
+ String get repeater_systemInformation => 'システム情報';
+
+ @override
+ String get repeater_battery => 'バッテリー';
+
+ @override
+ String get repeater_clockAtLogin => 'ログイン時の時刻表示';
+
+ @override
+ String get repeater_uptime => '稼働率';
+
+ @override
+ String get repeater_queueLength => '待ち行列の長さ';
+
+ @override
+ String get repeater_debugFlags => 'デバッグフラグ';
+
+ @override
+ String get repeater_radioStatistics => 'ラジオに関する統計';
+
+ @override
+ String get repeater_lastRssi => '最後のRSSI';
+
+ @override
+ String get repeater_lastSnr => '最後のSNR';
+
+ @override
+ String get repeater_noiseFloor => 'ノイズレベル';
+
+ @override
+ String get repeater_txAirtime => 'TXの放送時間';
+
+ @override
+ String get repeater_rxAirtime => 'RX 空き時間';
+
+ @override
+ String get repeater_packetStatistics => 'パケット統計';
+
+ @override
+ String get repeater_sent => '送信';
+
+ @override
+ String get repeater_received => '受領';
+
+ @override
+ String get repeater_duplicates => '重複';
+
+ @override
+ String repeater_daysHoursMinsSecs(
+ int days,
+ int hours,
+ int minutes,
+ int seconds,
+ ) {
+ return '$days日 $hours時間 $minutes分 $seconds秒';
+ }
+
+ @override
+ String repeater_packetTxTotal(int total, String flood, String direct) {
+ return '合計: $total, 洪水: $flood, 直接: $direct';
+ }
+
+ @override
+ String repeater_packetRxTotal(int total, String flood, String direct) {
+ return '合計: $total, 洪水: $flood, 直接: $direct';
+ }
+
+ @override
+ String repeater_duplicatesFloodDirect(String flood, String direct) {
+ return '$flood: $flood, 直接: $direct';
+ }
+
+ @override
+ String repeater_duplicatesTotal(int total) {
+ return '合計: $total';
+ }
+
+ @override
+ String get repeater_settingsTitle => 'リピーター設定';
+
+ @override
+ String get repeater_basicSettings => '基本設定';
+
+ @override
+ String get repeater_repeaterName => '送信装置名';
+
+ @override
+ String get repeater_repeaterNameHelper => 'このリピーターの名前';
+
+ @override
+ String get repeater_adminPassword => '管理者パスワード';
+
+ @override
+ String get repeater_adminPasswordHelper => '完全アクセス権のパスワード';
+
+ @override
+ String get repeater_guestPassword => 'ゲスト用のパスワード';
+
+ @override
+ String get repeater_guestPasswordHelper => '読み取り専用アクセス用のパスワード';
+
+ @override
+ String get repeater_radioSettings => 'ラジオ設定';
+
+ @override
+ String get repeater_frequencyMhz => '周波数 (MHz)';
+
+ @override
+ String get repeater_frequencyHelper => '300~2500 MHz';
+
+ @override
+ String get repeater_txPower => 'TXパワー';
+
+ @override
+ String get repeater_txPowerHelper => '-30~-10 dBm';
+
+ @override
+ String get repeater_bandwidth => '帯域幅';
+
+ @override
+ String get repeater_spreadingFactor => '伝播係数';
+
+ @override
+ String get repeater_codingRate => 'コーディング速度';
+
+ @override
+ String get repeater_locationSettings => '場所設定';
+
+ @override
+ String get repeater_latitude => '緯度';
+
+ @override
+ String get repeater_latitudeHelper => '度分表記(例:37.7749)';
+
+ @override
+ String get repeater_longitude => '経度';
+
+ @override
+ String get repeater_longitudeHelper => '度分表記(例:-122.4194)';
+
+ @override
+ String get repeater_features => '特徴';
+
+ @override
+ String get repeater_packetForwarding => 'パケット転送';
+
+ @override
+ String get repeater_packetForwardingSubtitle => 'リピーターがパケットを転送できるように設定する';
+
+ @override
+ String get repeater_guestAccess => 'ゲストへのアクセス';
+
+ @override
+ String get repeater_guestAccessSubtitle => 'ゲストへの読み取り専用アクセスを許可する';
+
+ @override
+ String get repeater_privacyMode => 'プライバシーモード';
+
+ @override
+ String get repeater_privacyModeSubtitle => '広告に名前/場所を記載しない';
+
+ @override
+ String get repeater_advertisementSettings => '広告設定';
+
+ @override
+ String get repeater_localAdvertInterval => '地域広告掲載期間';
+
+ @override
+ String repeater_localAdvertIntervalMinutes(int minutes) {
+ return '$minutes 分';
+ }
+
+ @override
+ String get repeater_floodAdvertInterval => '洪水に関する広告の表示間隔';
+
+ @override
+ String repeater_floodAdvertIntervalHours(int hours) {
+ return '$hours 時間';
+ }
+
+ @override
+ String get repeater_encryptedAdvertInterval => '暗号化された広告表示間';
+
+ @override
+ String get repeater_dangerZone => '危険区域';
+
+ @override
+ String get repeater_rebootRepeater => 'リピーターを再起動する';
+
+ @override
+ String get repeater_rebootRepeaterSubtitle => 'リピーターデバイスを再起動する';
+
+ @override
+ String get repeater_rebootRepeaterConfirm => '本当にこのリピーターを再起動したいですか?';
+
+ @override
+ String get repeater_regenerateIdentityKey => 'IDキーの再生成';
+
+ @override
+ String get repeater_regenerateIdentityKeySubtitle => '新しい公開鍵/秘密鍵のペアを生成する';
+
+ @override
+ String get repeater_regenerateIdentityKeyConfirm =>
+ 'これにより、リピーターには新しい識別情報が割り当てられます。続行しますか?';
+
+ @override
+ String get repeater_eraseFileSystem => 'ファイルシステムを削除する';
+
+ @override
+ String get repeater_eraseFileSystemSubtitle => 'リピーターファイルシステムをフォーマットする';
+
+ @override
+ String get repeater_eraseFileSystemConfirm =>
+ '警告:この操作により、リピーター内のすべてのデータが消去されます。この操作は元に戻すことができません!';
+
+ @override
+ String get repeater_eraseSerialOnly => 'Erase機能は、シリアルコンソール経由でのみ利用可能です。';
+
+ @override
+ String repeater_commandSent(String command) {
+ return '送信されたコマンド: $command';
+ }
+
+ @override
+ String repeater_errorSendingCommand(String error) {
+ return 'コマンド送信エラー:$error';
+ }
+
+ @override
+ String get repeater_confirm => '確認';
+
+ @override
+ String get repeater_settingsSaved => '設定が正常に保存されました';
+
+ @override
+ String repeater_errorSavingSettings(String error) {
+ return '設定の保存に失敗しました:$error';
+ }
+
+ @override
+ String get repeater_refreshBasicSettings => '基本設定をリセットする';
+
+ @override
+ String get repeater_refreshRadioSettings => 'ラジオ設定をリセットする';
+
+ @override
+ String get repeater_refreshTxPower => 'TX の電力レベルをリセットする';
+
+ @override
+ String get repeater_refreshLocationSettings => '場所設定をリセットする';
+
+ @override
+ String get repeater_refreshPacketForwarding => 'パケット転送の刷新';
+
+ @override
+ String get repeater_refreshGuestAccess => 'ゲストへのアクセスをリフレッシュする';
+
+ @override
+ String get repeater_refreshPrivacyMode => 'プライバシーモードをリセットする';
+
+ @override
+ String get repeater_refreshAdvertisementSettings => '広告設定のリセット';
+
+ @override
+ String repeater_refreshed(String label) {
+ return '$label が更新されました';
+ }
+
+ @override
+ String repeater_errorRefreshing(String label) {
+ return '$label の更新に失敗しました';
+ }
+
+ @override
+ String get repeater_cliTitle => 'リピーターのコマンドラインインターフェース';
+
+ @override
+ String get repeater_debugNextCommand => '次のコマンドのデバッグ';
+
+ @override
+ String get repeater_commandHelp => 'コマンドヘルプ';
+
+ @override
+ String get repeater_clearHistory => '明確な歴史';
+
+ @override
+ String get repeater_noCommandsSent => 'まだコマンドは送信されていません';
+
+ @override
+ String get repeater_typeCommandOrUseQuick =>
+ '以下のコマンドを入力するか、クイックコマンドを使用してください。';
+
+ @override
+ String get repeater_enterCommandHint => 'コマンドを入力してください...';
+
+ @override
+ String get repeater_previousCommand => '直前の指示';
+
+ @override
+ String get repeater_nextCommand => '次の指示';
+
+ @override
+ String get repeater_enterCommandFirst => 'まず、コマンドを入力してください。';
+
+ @override
+ String get repeater_cliCommandFrameTitle => 'CLI コマンドフレーム';
+
+ @override
+ String repeater_cliCommandError(String error) {
+ return 'エラー:$error';
+ }
+
+ @override
+ String get repeater_cliQuickGetName => '名前を取得する';
+
+ @override
+ String get repeater_cliQuickGetRadio => 'ラジオを聴く';
+
+ @override
+ String get repeater_cliQuickGetTx => 'TXを入手する';
+
+ @override
+ String get repeater_cliQuickNeighbors => '近隣住民';
+
+ @override
+ String get repeater_cliQuickVersion => 'バージョン';
+
+ @override
+ String get repeater_cliQuickAdvertise => '広告';
+
+ @override
+ String get repeater_cliQuickClock => '時計';
+
+ @override
+ String get repeater_cliHelpAdvert => '広告用資料を送る';
+
+ @override
+ String get repeater_cliHelpReboot =>
+ 'デバイスを再起動します。(注:通常は「タイムアウト」が表示されますが、これは正常です)';
+
+ @override
+ String get repeater_cliHelpClock => '各デバイスの時計で現在の時刻を表示します。';
+
+ @override
+ String get repeater_cliHelpPassword => 'デバイス用の新しい管理者パスワードを設定します。';
+
+ @override
+ String get repeater_cliHelpVersion => 'デバイスのバージョンとファームウェアのビルド日を表示します。';
+
+ @override
+ String get repeater_cliHelpClearStats => 'さまざまな統計カウンターをゼロにリセットする。';
+
+ @override
+ String get repeater_cliHelpSetAf => '空き時間係数を設定します。';
+
+ @override
+ String get repeater_cliHelpSetTx => 'LoRaの送信電力をdBmで設定します。(設定変更後、再起動が必要です)';
+
+ @override
+ String get repeater_cliHelpSetRepeat => 'このノードに対するリピーターの役割を有効化または無効化します。';
+
+ @override
+ String get repeater_cliHelpSetAllowReadOnly =>
+ '(ルームサーバー設定)「オン」に設定した場合、空白のパスワードでのログインは可能ですが、ルームへの投稿はできません。(閲覧のみ)';
+
+ @override
+ String get repeater_cliHelpSetFloodMax =>
+ 'インバウンドフラッパケットの最大ホップ数を設定します(最大値を超えた場合、パケットは転送されません)。';
+
+ @override
+ String get repeater_cliHelpSetIntThresh =>
+ '干渉閾値を設定します(dB単位)。デフォルト値は14です。0に設定すると、チャンネル間の干渉を検出する機能を無効にします。';
+
+ @override
+ String get repeater_cliHelpSetAgcResetInterval =>
+ 'オートゲインコントローラーのリセット間隔を設定します。 0 に設定すると無効化されます。';
+
+ @override
+ String get repeater_cliHelpSetMultiAcks => '「ダブルACK」機能の有効化または無効化を可能にします。';
+
+ @override
+ String get repeater_cliHelpSetAdvertInterval =>
+ 'ローカル(ホップなし)の広告パケットを送信する間隔を分単位で設定します。 0 に設定すると、機能を無効にします。';
+
+ @override
+ String get repeater_cliHelpSetFloodAdvertInterval =>
+ '洪水広告の送信間隔を時間単位で設定します。0に設定すると、送信を停止します。';
+
+ @override
+ String get repeater_cliHelpSetGuestPassword =>
+ 'ゲストのパスワードを設定/更新します。(繰り返し利用の場合、ゲストのログインは「統計情報を取得」のリクエストを送信できます)';
+
+ @override
+ String get repeater_cliHelpSetName => '広告の名前を設定します。';
+
+ @override
+ String get repeater_cliHelpSetLat => '広告表示の地図の緯度を設定します。(度分秒表記)';
+
+ @override
+ String get repeater_cliHelpSetLon => '広告表示の地図の経度を設定します。(度数、分)';
+
+ @override
+ String get repeater_cliHelpSetRadio =>
+ '完全に新しいラジオパラメータを設定し、設定として保存します。適用するには、「再起動」コマンドが必要です。';
+
+ @override
+ String get repeater_cliHelpSetRxDelay =>
+ '(実験用)遅延時間を設定するためのベース(1以上の値に設定する必要)\n受信パケットに対して、信号強度/スコアに基づいてわずかな遅延を適用します。 0に設定すると無効化されます。';
+
+ @override
+ String get repeater_cliHelpSetTxDelay =>
+ '時間経過に応じた「フラッシュモード」パケットの送信遅延を設定します。この遅延は、ランダムなスロットシステムと組み合わせて使用され、パケットの衝突を減らすことを目的としています。';
+
+ @override
+ String get repeater_cliHelpSetDirectTxDelay =>
+ 'txdelayと同様ですが、ダイレクトモードのパケット転送にランダムな遅延を適用する場合に使用します。';
+
+ @override
+ String get repeater_cliHelpSetBridgeEnabled => 'ブリッジを有効化/無効化';
+
+ @override
+ String get repeater_cliHelpSetBridgeDelay => 'パケットを再送信する前に、遅延を設定する。';
+
+ @override
+ String get repeater_cliHelpSetBridgeSource =>
+ '橋が受信したパケットを再送信するか、送信したパケットを再送信するかどうかを選択してください。';
+
+ @override
+ String get repeater_cliHelpSetBridgeBaud =>
+ 'RS232 橋渡しに使用するシリアルリンクのボーレートを設定する。';
+
+ @override
+ String get repeater_cliHelpSetBridgeSecret => 'ESPNow 橋の秘密設定';
+
+ @override
+ String get repeater_cliHelpSetAdcMultiplier =>
+ '特定のボードでのみサポートされている、報告されるバッテリー電圧を調整するためのカスタムファクタを設定できます。';
+
+ @override
+ String get repeater_cliHelpTempRadio =>
+ '指定された時間(分単位)に対して、一時的にラジオパラメータを設定し、その後元のラジオパラメータに戻します。(設定を保存しません)。';
+
+ @override
+ String get repeater_cliHelpSetPerm =>
+ 'ACL を変更します。「permissions」が 0 の場合、対応するエントリ(pubkey のプレフィックスで識別)を削除します。pubkey-hex が有効な長さで、かつ ACL に現在存在しない場合に、新しいエントリを追加します。pubkey のプレフィックスと一致するエントリを更新します。権限ビットはファームウェアの役割によって異なり、下位 2 ビットは以下のとおりです:0 (ゲスト)、1 (読み取り専用)、2 (読み書き)、3 (管理者)';
+
+ @override
+ String get repeater_cliHelpGetBridgeType => 'ブリッジ機能なし、RS232、ESPNow';
+
+ @override
+ String get repeater_cliHelpLogStart => 'パケットのログ記録を開始し、ファイルシステムに保存する。';
+
+ @override
+ String get repeater_cliHelpLogStop => 'ファイルシステムへのパケットログの記録を停止する。';
+
+ @override
+ String get repeater_cliHelpLogErase => 'ファイルシステムからパケットログを削除する。';
+
+ @override
+ String get repeater_cliHelpNeighbors =>
+ 'ゼロホップ広告を通じて受信した他のリピーターノードの一覧を表示します。各行は、IDプレフィックス(16進数)、タイムスタンプ、SNR(シグナル強度)の情報を4つ含みます。';
+
+ @override
+ String get repeater_cliHelpNeighborRemove =>
+ '隣接リストから、最初に一致するエントリ(pubkeyプレフィックス(16進数)で特定)を削除します。';
+
+ @override
+ String get repeater_cliHelpRegion =>
+ '(特定のシリーズのみ)定義されたすべての地域と、現在の洪水許可状況を一覧表示します。';
+
+ @override
+ String get repeater_cliHelpRegionLoad =>
+ '注:これは特殊な複数コマンドの呼び出しです。その後の各コマンドは、地域名であり(スペースを使用して親階層を示し、少なくとも1つのスペースが必要です)、空行/コマンドで終了します。';
+
+ @override
+ String get repeater_cliHelpRegionGet =>
+ '指定された名前のプレフィックスを持つ地域を検索します(または、グローバルな範囲の場合は「*」)。結果として、「region-name (parent-name) \'F\'」と返答します。';
+
+ @override
+ String get repeater_cliHelpRegionPut => '指定された名前で、領域の定義を追加または更新します。';
+
+ @override
+ String get repeater_cliHelpRegionRemove =>
+ '指定された名前を持つ領域の定義を削除します。(正確に一致している必要があり、子領域は存在してはなりません)';
+
+ @override
+ String get repeater_cliHelpRegionAllowf =>
+ '指定された領域に対して、「洪水」アクセス許可を設定します。 (グローバル/従来のスコープには「*」を使用)';
+
+ @override
+ String get repeater_cliHelpRegionDenyf =>
+ '指定された領域における「FLOOD」権限を削除します。(注:現時点では、グローバル/従来の範囲での使用は推奨されません!)';
+
+ @override
+ String get repeater_cliHelpRegionHome =>
+ '現在の「ホーム」地域に返信します。(まだ適用されていない、将来利用を予定)';
+
+ @override
+ String get repeater_cliHelpRegionHomeSet => '「ホーム」地域を設定します。';
+
+ @override
+ String get repeater_cliHelpRegionSave => '領域リスト/マップをストレージに保存する。';
+
+ @override
+ String get repeater_cliHelpGps =>
+ 'GPSの状態を表示します。GPSがオフの場合、「オフ」と表示します。オンの場合、「オン」、「ステータス」、「位置情報」、「衛星数」と表示します。';
+
+ @override
+ String get repeater_cliHelpGpsOnOff => 'GPS の電源状態を切り替えます。';
+
+ @override
+ String get repeater_cliHelpGpsSync => 'ノードの時刻をGPSクロックと同期する。';
+
+ @override
+ String get repeater_cliHelpGpsSetLoc => 'ノードの位置をGPS座標に設定し、設定を保存する。';
+
+ @override
+ String get repeater_cliHelpGpsAdvert =>
+ 'ノードの広告設定における場所情報の指定:\n- none: 広告に場所情報を含まない\n- share: GPS位置情報を共有 (SensorManagerから取得)\n- prefs: プリファレンスに保存された場所情報を広告';
+
+ @override
+ String get repeater_cliHelpGpsAdvertSet => '場所に関する広告設定を行います。';
+
+ @override
+ String get repeater_commandsListTitle => 'コマンド一覧';
+
+ @override
+ String get repeater_commandsListNote =>
+ '注:さまざまな「set ...」コマンドには、「get ...」コマンドも存在します。';
+
+ @override
+ String get repeater_general => '一般的な';
+
+ @override
+ String get repeater_settingsCategory => '設定';
+
+ @override
+ String get repeater_bridge => '橋';
+
+ @override
+ String get repeater_logging => 'ログ記録';
+
+ @override
+ String get repeater_neighborsRepeaterOnly => '近隣住民(リピーターのみ)';
+
+ @override
+ String get repeater_regionManagementRepeaterOnly => '地域管理(ブロードキャスト用のみ)';
+
+ @override
+ String get repeater_regionNote => '地域レベルでの管理のため、地域定義と権限の管理を行うための機能が導入されました。';
+
+ @override
+ String get repeater_gpsManagement => 'GPS管理';
+
+ @override
+ String get repeater_gpsNote => 'GPSコマンドは、位置情報に関連するタスクを管理するために導入されました。';
+
+ @override
+ String get telemetry_receivedData => '受信したテレメトリーデータ';
+
+ @override
+ String get telemetry_requestTimeout => 'テレメトリの要求タイムアウトしました。';
+
+ @override
+ String telemetry_errorLoading(String error) {
+ return 'テレメトリの読み込みに失敗しました: $error';
+ }
+
+ @override
+ String get telemetry_noData => 'テレメトリデータは利用できません。';
+
+ @override
+ String telemetry_channelTitle(int channel) {
+ return 'チャンネル $channel';
+ }
+
+ @override
+ String get telemetry_batteryLabel => 'バッテリー';
+
+ @override
+ String get telemetry_voltageLabel => '電圧';
+
+ @override
+ String get telemetry_mcuTemperatureLabel => 'MCU の温度';
+
+ @override
+ String get telemetry_temperatureLabel => '温度';
+
+ @override
+ String get telemetry_currentLabel => '現在';
+
+ @override
+ String telemetry_batteryValue(int percent, String volts) {
+ return '$percent% / ${volts}V';
+ }
+
+ @override
+ String telemetry_voltageValue(String volts) {
+ return '${volts}V';
+ }
+
+ @override
+ String telemetry_currentValue(String amps) {
+ return '${amps}A';
+ }
+
+ @override
+ String telemetry_temperatureValue(String celsius, String fahrenheit) {
+ return '$celsius℃ / $fahrenheit°F';
+ }
+
+ @override
+ String get neighbors_receivedData => '近隣住民のデータを受信';
+
+ @override
+ String get neighbors_requestTimedOut => '近隣住民からの要望:時間制限を設けてください。';
+
+ @override
+ String neighbors_errorLoading(String error) {
+ return '近隣情報の読み込みに失敗: $error';
+ }
+
+ @override
+ String get neighbors_repeatersNeighbors => '繰り返し送信する、近隣';
+
+ @override
+ String get neighbors_noData => '近隣のデータは利用できません。';
+
+ @override
+ String neighbors_unknownContact(String pubkey) {
+ return '不明な $pubkey';
+ }
+
+ @override
+ String neighbors_heardAgo(String time) {
+ return '聞いたのは、$time くらい前です';
+ }
+
+ @override
+ String get channelPath_title => 'パケットパス';
+
+ @override
+ String get channelPath_viewMap => '地図を表示する';
+
+ @override
+ String get channelPath_otherObservedPaths => '観察されたその他の経路';
+
+ @override
+ String get channelPath_repeaterHops => 'ホップの繰り返し';
+
+ @override
+ String get channelPath_noHopDetails => 'このパッケージに関する詳細な情報は提供されていません。';
+
+ @override
+ String get channelPath_messageDetails => 'メッセージの詳細';
+
+ @override
+ String get channelPath_senderLabel => '送信者';
+
+ @override
+ String get channelPath_timeLabel => '時間';
+
+ @override
+ String get channelPath_repeatsLabel => '繰り返し';
+
+ @override
+ String channelPath_pathLabel(int index) {
+ return '$index 番目の経路';
+ }
+
+ @override
+ String get channelPath_observedLabel => '観察';
+
+ @override
+ String channelPath_observedPathTitle(int index, String hops) {
+ return '観察された経路 $index • $hops';
+ }
+
+ @override
+ String get channelPath_noLocationData => '場所に関するデータはありません';
+
+ @override
+ String channelPath_timeWithDate(int day, int month, String time) {
+ return '$day/$month $time';
+ }
+
+ @override
+ String channelPath_timeOnly(String time) {
+ return '$time';
+ }
+
+ @override
+ String get channelPath_unknownPath => '不明';
+
+ @override
+ String get channelPath_floodPath => '洪水';
+
+ @override
+ String get channelPath_directPath => '直接';
+
+ @override
+ String channelPath_observedZeroOf(int total) {
+ return '$total個のホップ';
+ }
+
+ @override
+ String channelPath_observedSomeOf(int observed, int total) {
+ return '$observed of $total hops';
+ }
+
+ @override
+ String get channelPath_mapTitle => '経路図';
+
+ @override
+ String get channelPath_noRepeaterLocations => 'この経路には、中継装置の設置場所がありません。';
+
+ @override
+ String channelPath_primaryPath(int index) {
+ return '$index番目の経路(主要経路)';
+ }
+
+ @override
+ String get channelPath_pathLabelTitle => '道';
+
+ @override
+ String get channelPath_observedPathHeader => '観察された経路';
+
+ @override
+ String channelPath_selectedPathLabel(String label, String prefixes) {
+ return '$label • $prefixes';
+ }
+
+ @override
+ String get channelPath_noHopDetailsAvailable => 'このパッケージに関する詳細な配送情報は利用できません。';
+
+ @override
+ String get channelPath_unknownRepeater => '不明な増幅機';
+
+ @override
+ String get community_title => '地域';
+
+ @override
+ String get community_create => 'コミュニティを構築する';
+
+ @override
+ String get community_createDesc => '新しいコミュニティを作成し、QRコードで共有する。';
+
+ @override
+ String get community_join => '参加する';
+
+ @override
+ String get community_joinTitle => 'コミュニティに参加する';
+
+ @override
+ String community_joinConfirmation(String name) {
+ return '$nameさんのようなコミュニティに参加したいですか?';
+ }
+
+ @override
+ String get community_scanQr => 'コミュニティのQRコードをスキャン';
+
+ @override
+ String get community_scanInstructions => 'カメラを、地域のQRコードを向けて';
+
+ @override
+ String get community_showQr => 'QRコードを表示する';
+
+ @override
+ String get community_publicChannel => '地域住民向け';
+
+ @override
+ String get community_hashtagChannel => 'コミュニティ用ハッシュタグ';
+
+ @override
+ String get community_name => 'コミュニティ名';
+
+ @override
+ String get community_enterName => 'コミュニティ名を入力してください';
+
+ @override
+ String community_created(String name) {
+ return 'コミュニティ「$name」が作成されました';
+ }
+
+ @override
+ String community_joined(String name) {
+ return '$name のコミュニティに参加';
+ }
+
+ @override
+ String get community_qrTitle => 'コミュニティ共有';
+
+ @override
+ String community_qrInstructions(String name) {
+ return 'このQRコードをスキャンして、$nameに参加してください。';
+ }
+
+ @override
+ String get community_hashtagPrivacyHint =>
+ 'コミュニティハッシュタグのチャンネルは、コミュニティのメンバーのみが参加できます。';
+
+ @override
+ String get community_invalidQrCode => '無効なコミュニティQRコード';
+
+ @override
+ String get community_alreadyMember => 'すでに会員である';
+
+ @override
+ String community_alreadyMemberMessage(String name) {
+ return 'あなたはすでに $name の会員です。';
+ }
+
+ @override
+ String get community_addPublicChannel => 'コミュニティ用の公開チャンネルを追加';
+
+ @override
+ String get community_addPublicChannelHint => 'このコミュニティの公開チャンネルを自動的に追加する';
+
+ @override
+ String get community_noCommunities => 'まだコミュニティは形成されていません。';
+
+ @override
+ String get community_scanOrCreate => 'QRコードをスキャンするか、コミュニティを作成して開始してください。';
+
+ @override
+ String get community_manageCommunities => 'コミュニティの管理';
+
+ @override
+ String get community_delete => 'コミュニティからの離脱';
+
+ @override
+ String community_deleteConfirm(String name) {
+ return '$nameを辞める?';
+ }
+
+ @override
+ String community_deleteChannelsWarning(int count) {
+ return 'これにより、$count のチャンネルとそのメッセージも削除されます。';
+ }
+
+ @override
+ String community_deleted(String name) {
+ return 'コミュニティ「$name」を離れる';
+ }
+
+ @override
+ String get community_regenerateSecret => '秘密の復元';
+
+ @override
+ String community_regenerateSecretConfirm(String name) {
+ return '$name の秘密鍵を再生成しますか? 継続的に通信するため、すべてのメンバーは新しいQRコードをスキャンする必要があります。';
+ }
+
+ @override
+ String get community_regenerate => '再生';
+
+ @override
+ String community_secretRegenerated(String name) {
+ return '$name への秘密が再設定されました';
+ }
+
+ @override
+ String get community_updateSecret => '秘密情報の更新';
+
+ @override
+ String community_secretUpdated(String name) {
+ return '$name 向けの秘密設定を更新';
+ }
+
+ @override
+ String community_scanToUpdateSecret(String name) {
+ return '新しいQRコードをスキャンして、$nameの秘密情報を更新してください。';
+ }
+
+ @override
+ String get community_addHashtagChannel => 'コミュニティのハッシュタグを追加';
+
+ @override
+ String get community_addHashtagChannelDesc => 'このコミュニティ用のハッシュタグチャンネルを追加する';
+
+ @override
+ String get community_selectCommunity => 'コミュニティを選択';
+
+ @override
+ String get community_regularHashtag => '定期的なハッシュタグ';
+
+ @override
+ String get community_regularHashtagDesc => '一般のハッシュタグ(誰でも参加可能)';
+
+ @override
+ String get community_communityHashtag => 'コミュニティ用ハッシュタグ';
+
+ @override
+ String get community_communityHashtagDesc => 'コミュニティメンバーのみへの限定';
+
+ @override
+ String community_forCommunity(String name) {
+ return '$name 様';
+ }
+
+ @override
+ String get listFilter_tooltip => 'フィルタリングと並べ替え';
+
+ @override
+ String get listFilter_sortBy => '並び替え';
+
+ @override
+ String get listFilter_latestMessages => '最新のメッセージ';
+
+ @override
+ String get listFilter_heardRecently => '最近、聞いた';
+
+ @override
+ String get listFilter_az => 'AからZ';
+
+ @override
+ String get listFilter_filters => 'フィルター';
+
+ @override
+ String get listFilter_all => 'すべて';
+
+ @override
+ String get listFilter_favorites => 'お気に入り';
+
+ @override
+ String get listFilter_addToFavorites => 'お気に入りに追加';
+
+ @override
+ String get listFilter_removeFromFavorites => 'お気に入りから削除';
+
+ @override
+ String get listFilter_users => '利用者';
+
+ @override
+ String get listFilter_repeaters => '繰り返し送信装置';
+
+ @override
+ String get listFilter_roomServers => 'ルーム用サーバー';
+
+ @override
+ String get listFilter_unreadOnly => '未読のみ';
+
+ @override
+ String get listFilter_newGroup => '新しいグループ';
+
+ @override
+ String get pathTrace_you => 'あなた';
+
+ @override
+ String get pathTrace_failed => 'パスの追跡に失敗しました。';
+
+ @override
+ String get pathTrace_notAvailable => 'パスの追跡機能は利用できません。';
+
+ @override
+ String get pathTrace_refreshTooltip => 'パスの追跡をリフレッシュする。';
+
+ @override
+ String get pathTrace_someHopsNoLocation => 'ホップの1つまたは複数について、場所が特定されていません。';
+
+ @override
+ String get pathTrace_clearTooltip => '明確な道筋。';
+
+ @override
+ String get losSelectStartEnd => 'LOS の開始ノードと終了ノードを選択してください。';
+
+ @override
+ String losRunFailed(String error) {
+ return '視界確認に失敗: $error';
+ }
+
+ @override
+ String get losClearAllPoints => 'すべての項目をクリア';
+
+ @override
+ String get losRunToViewElevationProfile => 'LOS(レーザー測距)を使用して、標高プロファイルを表示する';
+
+ @override
+ String get losMenuTitle => 'LOS メニュー';
+
+ @override
+ String get losMenuSubtitle => '特定の場所をタップするか、地図を長押ししてカスタムポイントを作成する。';
+
+ @override
+ String get losShowDisplayNodes => '表示ノードを表示する';
+
+ @override
+ String get losCustomPoints => 'カスタマイズ可能なポイント';
+
+ @override
+ String losCustomPointLabel(int index) {
+ return 'カスタマイズ $index';
+ }
+
+ @override
+ String get losPointA => 'ポイントA';
+
+ @override
+ String get losPointB => 'ポイントB';
+
+ @override
+ String losAntennaA(String value, String unit) {
+ return 'アンテナ A: $value $unit';
+ }
+
+ @override
+ String losAntennaB(String value, String unit) {
+ return 'アンテナ B: $value $unit';
+ }
+
+ @override
+ String get losRun => 'LOS(レーティングシステム)を使用する';
+
+ @override
+ String get losNoElevationData => '標高データは含まれていません';
+
+ @override
+ String losProfileClear(
+ String distance,
+ String distanceUnit,
+ String clearance,
+ String heightUnit,
+ ) {
+ return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
+ }
+
+ @override
+ String losProfileBlocked(
+ String distance,
+ String distanceUnit,
+ String obstruction,
+ String heightUnit,
+ ) {
+ return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
+ }
+
+ @override
+ String get losStatusChecking => 'LOS:確認中…';
+
+ @override
+ String get losStatusNoData => 'LOS: データの欠如';
+
+ @override
+ String losStatusSummary(int clear, int total, int blocked, int unknown) {
+ return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown';
+ }
+
+ @override
+ String get losErrorElevationUnavailable =>
+ 'あるサンプルまたは複数のサンプルについて、標高データが利用できません。';
+
+ @override
+ String get losErrorInvalidInput => 'LOS(レーダー)計算に必要な、無効な点/標高データ。';
+
+ @override
+ String get losRenameCustomPoint => 'カスタムポイントの名前を変更する';
+
+ @override
+ String get losPointName => '項目名';
+
+ @override
+ String get losShowPanelTooltip => 'LOSパネルを表示する';
+
+ @override
+ String get losHidePanelTooltip => 'LOSパネルを隠す';
+
+ @override
+ String get losElevationAttribution => '標高データ:Open-Meteo (CC BY 4.0)';
+
+ @override
+ String get losLegendRadioHorizon => 'ラジオ・ホライゾン';
+
+ @override
+ String get losLegendLosBeam => 'LOS ビーミング';
+
+ @override
+ String get losLegendTerrain => '地形';
+
+ @override
+ String get losFrequencyLabel => '周波数';
+
+ @override
+ String get losFrequencyInfoTooltip => '計算の詳細を見る';
+
+ @override
+ String get losFrequencyDialogTitle => 'ラジオによる水平線計算';
+
+ @override
+ String losFrequencyDialogDescription(
+ double baselineK,
+ double baselineFreq,
+ double frequencyMHz,
+ double kFactor,
+ ) {
+ return 'k=$baselineK ( $baselineFreq MHz) から開始し、現在の $frequencyMHz MHz の帯域に対して k の値を調整します。これにより、曲面状の無線通信範囲の限界が定義されます。';
+ }
+
+ @override
+ String get contacts_pathTrace => '経路追跡';
+
+ @override
+ String get contacts_ping => 'パング';
+
+ @override
+ String get contacts_repeaterPathTrace => 'リピーターまでの経路を追跡する';
+
+ @override
+ String get contacts_repeaterPing => 'PING 繰り返し';
+
+ @override
+ String get contacts_roomPathTrace => '部屋のサーバーへの経路を追跡する';
+
+ @override
+ String get contacts_roomPing => 'ピンルーム用サーバー';
+
+ @override
+ String get contacts_chatTraceRoute => '経路の追跡ルート';
+
+ @override
+ String contacts_pathTraceTo(String name) {
+ return '$name への経路を追跡する';
+ }
+
+ @override
+ String get contacts_clipboardEmpty => 'クリップボードは空です。';
+
+ @override
+ String get contacts_invalidAdvertFormat => '無効な連絡先情報';
+
+ @override
+ String get contacts_contactImported => '連絡先が登録されました。';
+
+ @override
+ String get contacts_contactImportFailed => '連絡先のインポートに失敗しました。';
+
+ @override
+ String get contacts_zeroHopAdvert => 'ゼロホップ広告';
+
+ @override
+ String get contacts_floodAdvert => '洪水に関する広告';
+
+ @override
+ String get contacts_copyAdvertToClipboard => '広告をクリップボードにコピー';
+
+ @override
+ String get contacts_addContactFromClipboard => 'クリップボードから連絡先を追加する';
+
+ @override
+ String get contacts_ShareContact => '連絡先をクリップボードにコピー';
+
+ @override
+ String get contacts_ShareContactZeroHop => '広告を通じて連絡先を共有する';
+
+ @override
+ String get contacts_zeroHopContactAdvertSent => '広告を通じて連絡先を得た。';
+
+ @override
+ String get contacts_zeroHopContactAdvertFailed => '連絡を送信できませんでした。';
+
+ @override
+ String get contacts_contactAdvertCopied => '広告がクリップボードにコピーされました。';
+
+ @override
+ String get contacts_contactAdvertCopyFailed => '広告のコピーがクリップボードにコピーできませんでした。';
+
+ @override
+ String get notification_activityTitle => 'メッシュコアの活動';
+
+ @override
+ String notification_messagesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'messages',
+ one: 'message',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_channelMessagesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'チャンネルメッセージ',
+ one: 'チャンネルメッセージ',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_newNodesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '新しいノード',
+ one: '新しいノード',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_newTypeDiscovered(String contactType) {
+ return '新たに $contactType が発見されました';
+ }
+
+ @override
+ String get notification_receivedNewMessage => '新しいメッセージを受信';
+
+ @override
+ String get settings_gpxExportRepeaters => 'GPX へのエクスポート用リピーター/ルームサーバー';
+
+ @override
+ String get settings_gpxExportRepeatersSubtitle =>
+ 'GPXファイルに場所情報を付加した、レピーター/ルームサーバーのエクスポート';
+
+ @override
+ String get settings_gpxExportContacts => 'GPX 形式へのエクスポート';
+
+ @override
+ String get settings_gpxExportContactsSubtitle =>
+ 'GPXファイルに位置情報を保存して、他の人と共有する。';
+
+ @override
+ String get settings_gpxExportAll => 'すべての連絡先をGPX形式でエクスポートする';
+
+ @override
+ String get settings_gpxExportAllSubtitle =>
+ 'すべての連絡先を、場所情報付きのGPXファイルにエクスポートする。';
+
+ @override
+ String get settings_gpxExportSuccess => 'GPXファイルの正常なエクスポートが完了しました。';
+
+ @override
+ String get settings_gpxExportNoContacts => 'エクスポートする連絡先は存在しません。';
+
+ @override
+ String get settings_gpxExportNotAvailable => 'このデバイス/OSではサポートされていません';
+
+ @override
+ String get settings_gpxExportError => 'エクスポート時にエラーが発生しました。';
+
+ @override
+ String get settings_gpxExportRepeatersRoom => '中継装置およびルームサーバーの設置場所';
+
+ @override
+ String get settings_gpxExportChat => '関連施設';
+
+ @override
+ String get settings_gpxExportAllContacts => 'すべての連絡先場所';
+
+ @override
+ String get settings_gpxExportShareText => 'meshcore-openからエクスポートされた地図データ';
+
+ @override
+ String get settings_gpxExportShareSubject =>
+ 'meshcore-open GPX形式の地図データのエクスポート';
+
+ @override
+ String get snrIndicator_nearByRepeaters => '近くの電波中継局';
+
+ @override
+ String get snrIndicator_lastSeen => '最後に確認された場所';
+
+ @override
+ String get contactsSettings_title => '連絡先設定';
+
+ @override
+ String get contactsSettings_autoAddTitle => '自動検出';
+
+ @override
+ String get contactsSettings_otherTitle => 'その他の連絡に関する設定';
+
+ @override
+ String get contactsSettings_autoAddUsersTitle => '自動でユーザーを追加する';
+
+ @override
+ String get contactsSettings_autoAddUsersSubtitle =>
+ '利用者が自動的に発見したユーザーを追加できるようにする。';
+
+ @override
+ String get contactsSettings_autoAddRepeatersTitle => '自動で繰り返し設定';
+
+ @override
+ String get contactsSettings_autoAddRepeatersSubtitle =>
+ '発見した中継局を、自動的に追加できるようにする。';
+
+ @override
+ String get contactsSettings_autoAddRoomServersTitle => '自動でルームサーバーを追加';
+
+ @override
+ String get contactsSettings_autoAddRoomServersSubtitle =>
+ '利用者が、発見した部屋のサーバーを自動的に追加できるようにする。';
+
+ @override
+ String get contactsSettings_autoAddSensorsTitle => '自動でセンサーを追加';
+
+ @override
+ String get contactsSettings_autoAddSensorsSubtitle =>
+ '利用者が、発見したセンサーを自動的に追加できるようにする。';
+
+ @override
+ String get contactsSettings_overwriteOldestTitle => '最も古いものを上書きする';
+
+ @override
+ String get contactsSettings_overwriteOldestSubtitle =>
+ '連絡先リストが満杯になった場合、最も古いかつ「お気に入り」ではない連絡先が削除されます。';
+
+ @override
+ String get discoveredContacts_Title => '連絡先が見つかった';
+
+ @override
+ String get discoveredContacts_noMatching => '一致する連絡先が見つかりませんでした';
+
+ @override
+ String get discoveredContacts_searchHint => '発見された連絡先を検索する';
+
+ @override
+ String get discoveredContacts_contactAdded => '連絡先を追加';
+
+ @override
+ String get discoveredContacts_addContact => '連絡先を追加';
+
+ @override
+ String get discoveredContacts_copyContact => '連絡先をクリップボードにコピー';
+
+ @override
+ String get discoveredContacts_deleteContact => '発見された連絡先を削除';
+
+ @override
+ String get discoveredContacts_deleteContactAll => '発見されたすべての連絡先を削除';
+
+ @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、SNR、および通信時間';
+
+ @override
+ String get scanner_linuxPairingShowPin => 'PINを表示';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'PINを非表示';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'Bluetooth ペアリング PIN';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return '$deviceNameのPINを入力してください(なしの場合は空欄のまま)。';
+ }
+}
diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart
new file mode 100644
index 0000000..5e5925f
--- /dev/null
+++ b/lib/l10n/app_localizations_ko.dart
@@ -0,0 +1,3417 @@
+// ignore: unused_import
+import 'package:intl/intl.dart' as intl;
+import 'app_localizations.dart';
+
+// ignore_for_file: type=lint
+
+/// The translations for Korean (`ko`).
+class AppLocalizationsKo extends AppLocalizations {
+ AppLocalizationsKo([String locale = 'ko']) : super(locale);
+
+ @override
+ String get appTitle => 'MeshCore Open';
+
+ @override
+ String get nav_contacts => '연락처';
+
+ @override
+ String get nav_channels => '채널';
+
+ @override
+ String get nav_map => '지도';
+
+ @override
+ String get common_cancel => '취소';
+
+ @override
+ String get common_ok => '알겠습니다';
+
+ @override
+ String get common_connect => '연결';
+
+ @override
+ String get common_unknownDevice => '알 수 없는 장치';
+
+ @override
+ String get common_save => '저장';
+
+ @override
+ String get common_delete => '삭제';
+
+ @override
+ String get common_deleteAll => '모두 삭제';
+
+ @override
+ String get common_close => '닫기';
+
+ @override
+ String get common_edit => '수정';
+
+ @override
+ String get common_add => '추가';
+
+ @override
+ String get common_settings => '설정';
+
+ @override
+ String get common_disconnect => '연결 해제';
+
+ @override
+ String get common_connected => '연결된';
+
+ @override
+ String get common_disconnected => '단절';
+
+ @override
+ String get common_create => '만들다';
+
+ @override
+ String get common_continue => '계속';
+
+ @override
+ String get common_share => '공유';
+
+ @override
+ String get common_copy => '복사';
+
+ @override
+ String get common_retry => '다시 시도';
+
+ @override
+ String get common_hide => '숨기다';
+
+ @override
+ String get common_remove => '제거';
+
+ @override
+ String get common_enable => '활성화';
+
+ @override
+ String get common_disable => '비활성화';
+
+ @override
+ String get common_reboot => '재부팅';
+
+ @override
+ String get common_loading => '로딩 중...';
+
+ @override
+ String get common_notAvailable => '—';
+
+ @override
+ String common_voltageValue(String volts) {
+ return '$volts V';
+ }
+
+ @override
+ String common_percentValue(int percent) {
+ return '$percent%';
+ }
+
+ @override
+ String get scanner_title => 'MeshCore 공개';
+
+ @override
+ String get connectionChoiceUsbLabel => 'USB';
+
+ @override
+ String get connectionChoiceBluetoothLabel => '블루투스';
+
+ @override
+ String get connectionChoiceTcpLabel => 'TCP';
+
+ @override
+ String get tcpScreenTitle => 'TCP를 통해 연결';
+
+ @override
+ String get tcpHostLabel => 'IP 주소';
+
+ @override
+ String get tcpHostHint => '192.168.40.10';
+
+ @override
+ String get tcpPortLabel => '항';
+
+ @override
+ String get tcpPortHint => '5000';
+
+ @override
+ String get tcpStatus_notConnected => '목적지 주소 입력 후 연결';
+
+ @override
+ String tcpStatus_connectingTo(String endpoint) {
+ return '$endpoint에 연결 중...';
+ }
+
+ @override
+ String get tcpErrorHostRequired => 'IP 주소가 필요합니다.';
+
+ @override
+ String get tcpErrorPortInvalid => '포트 번호는 1에서 65535 사이여야 합니다.';
+
+ @override
+ String get tcpErrorUnsupported => '이 플랫폼에서는 TCP 트랜스포트를 지원하지 않습니다.';
+
+ @override
+ String get tcpErrorTimedOut => 'TCP 연결이 시간 초과되었습니다.';
+
+ @override
+ String tcpConnectionFailed(String error) {
+ return 'TCP 연결 실패: $error';
+ }
+
+ @override
+ String get usbScreenTitle => 'USB를 통해 연결';
+
+ @override
+ String get usbScreenSubtitle => '감지된 시리얼 장치를 선택하고 MeshCore 노드에 직접 연결하십시오.';
+
+ @override
+ String get usbScreenStatus => 'USB 장치를 선택합니다.';
+
+ @override
+ String get usbScreenNote => 'USB 직렬 통신은 지원되는 안드로이드 장치 및 데스크톱 플랫폼에서 활성화됩니다.';
+
+ @override
+ String get usbScreenEmptyState =>
+ 'USB 장치가 탐지되지 않았습니다. USB 장치를 연결하고 다시 시도해 보세요.';
+
+ @override
+ String get usbErrorPermissionDenied => 'USB 접근 권한이 거부되었습니다.';
+
+ @override
+ String get usbErrorDeviceMissing => '선택한 USB 장치는 더 이상 사용 불가능합니다.';
+
+ @override
+ String get usbErrorInvalidPort => '유효한 USB 장치를 선택하세요.';
+
+ @override
+ String get usbErrorBusy => '또 다른 USB 연결 요청이 이미 진행 중입니다.';
+
+ @override
+ String get usbErrorNotConnected => 'USB 장치가 연결되지 않았습니다.';
+
+ @override
+ String get usbErrorOpenFailed => '선택한 USB 장치를 열 수 없습니다.';
+
+ @override
+ String get usbErrorConnectFailed => '선택한 USB 장치에 연결에 실패했습니다.';
+
+ @override
+ String get usbErrorUnsupported => '이 플랫폼에서는 USB 직렬 통신을 지원하지 않습니다.';
+
+ @override
+ String get usbErrorAlreadyActive => 'USB 연결이 이미 활성화되어 있습니다.';
+
+ @override
+ String get usbErrorNoDeviceSelected => 'USB 장치가 선택되지 않았습니다.';
+
+ @override
+ String get usbErrorPortClosed => 'USB 연결이 활성화되지 않았습니다.';
+
+ @override
+ String get usbErrorConnectTimedOut =>
+ '연결이 시간 초과되었습니다. 장치가 USB Companion 펌웨어를 가지고 있는지 확인해 주세요.';
+
+ @override
+ String get usbFallbackDeviceName => '웹 시리얼 장치';
+
+ @override
+ String get usbStatus_notConnected => 'USB 장치를 선택합니다.';
+
+ @override
+ String get usbStatus_connecting => 'USB 장치에 연결 중...';
+
+ @override
+ String get usbStatus_searching => 'USB 장치 검색 중...';
+
+ @override
+ String usbConnectionFailed(String error) {
+ return 'USB 연결 실패: $error';
+ }
+
+ @override
+ String get scanner_scanning => '장치 검색 중...';
+
+ @override
+ String get scanner_connecting => '연결 중...';
+
+ @override
+ String get scanner_disconnecting => '연결 해제 중...';
+
+ @override
+ String get scanner_notConnected => '연결되지 않음';
+
+ @override
+ String scanner_connectedTo(String deviceName) {
+ return '$deviceName에 연결됨';
+ }
+
+ @override
+ String get scanner_searchingDevices => 'MeshCore 장치를 검색 중...';
+
+ @override
+ String get scanner_tapToScan => 'MeshCore 장치를 찾기 위해 스캔 버튼을 누르세요.';
+
+ @override
+ String scanner_connectionFailed(String error) {
+ return '연결 실패: $error';
+ }
+
+ @override
+ String get scanner_stop => '멈춰';
+
+ @override
+ String get scanner_scan => '스캔';
+
+ @override
+ String get scanner_bluetoothOff => '블루투스는 꺼져 있습니다.';
+
+ @override
+ String get scanner_bluetoothOffMessage => '블루투스를 켜서 장치를 검색해주세요.';
+
+ @override
+ String get scanner_chromeRequired => '크롬 브라우저 필요';
+
+ @override
+ String get scanner_chromeRequiredMessage =>
+ '이 웹 애플리케이션은 블루투드 지원을 위해 Google Chrome 또는 Chromium 기반 브라우저가 필요합니다.';
+
+ @override
+ String get scanner_enableBluetooth => '블루투스 활성화';
+
+ @override
+ String get device_quickSwitch => '빠른 전환';
+
+ @override
+ String get device_meshcore => '메쉬코어';
+
+ @override
+ String get settings_title => '설정';
+
+ @override
+ String get settings_deviceInfo => '장치 정보';
+
+ @override
+ String get settings_appSettings => '앱 설정';
+
+ @override
+ String get settings_appSettingsSubtitle => '알림, 메시징, 지도 설정';
+
+ @override
+ String get settings_nodeSettings => '노드 설정';
+
+ @override
+ String get settings_nodeName => '노드 이름';
+
+ @override
+ String get settings_nodeNameNotSet => '설정되지 않음';
+
+ @override
+ String get settings_nodeNameHint => '노드 이름을 입력하세요';
+
+ @override
+ String get settings_nodeNameUpdated => '이름 변경';
+
+ @override
+ String get settings_radioSettings => '라디오 설정';
+
+ @override
+ String get settings_radioSettingsSubtitle => '주파수, 전력, 스펙트럼';
+
+ @override
+ String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.';
+
+ @override
+ String get settings_location => '위치';
+
+ @override
+ String get settings_locationSubtitle => 'GPS 좌표';
+
+ @override
+ String get settings_locationUpdated => '위치 및 GPS 설정이 업데이트되었습니다.';
+
+ @override
+ String get settings_locationBothRequired => '위도와 경도를 모두 입력하세요.';
+
+ @override
+ String get settings_locationInvalid => '유효하지 않은 위도 또는 경도.';
+
+ @override
+ String get settings_locationGPSEnable => 'GPS 활성화';
+
+ @override
+ String get settings_locationGPSEnableSubtitle =>
+ 'GPS를 사용하여 위치 정보를 자동으로 업데이트할 수 있도록 합니다.';
+
+ @override
+ String get settings_locationIntervalSec => 'GPS 간격 (초)';
+
+ @override
+ String get settings_locationIntervalInvalid =>
+ '간격은 최소 60초 이상, 86400초 미만이어야 합니다.';
+
+ @override
+ String get settings_latitude => '위도';
+
+ @override
+ String get settings_longitude => '경도';
+
+ @override
+ String get settings_contactSettings => '연락처 설정';
+
+ @override
+ String get settings_contactSettingsSubtitle => '연락처 추가 방식 설정';
+
+ @override
+ String get settings_privacyMode => '개인 정보 보호 모드';
+
+ @override
+ String get settings_privacyModeSubtitle => '광고에 이름/위치 정보 숨기기';
+
+ @override
+ String get settings_privacyModeToggle =>
+ '광고에 자신의 이름과 위치를 숨기기 위해 개인 정보 보호 모드를 켜거나 끄십시오.';
+
+ @override
+ String get settings_privacyModeEnabled => '개인 정보 보호 모드 활성화';
+
+ @override
+ String get settings_privacyModeDisabled => '개인 정보 보호 모드 비활성화';
+
+ @override
+ String get settings_privacy => '개인 정보 설정';
+
+ @override
+ String get settings_privacySubtitle => '어떤 정보를 공유할지 통제하세요.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ '어떤 정보를 기기가 다른 사람들과 공유할지 선택하세요.';
+
+ @override
+ String get settings_denyAll => '모든 것을 부정';
+
+ @override
+ String get settings_allowByContact => '연락처 표시 기능 활성화';
+
+ @override
+ String get settings_allowAll => '모든 것을 허용';
+
+ @override
+ String get settings_telemetryBaseMode => '원격 모니터링 기본 설정';
+
+ @override
+ String get settings_telemetryLocationMode => '텔레메트리 위치 모드';
+
+ @override
+ String get settings_telemetryEnvironmentMode => '텔레메트리 환경 모드';
+
+ @override
+ String get settings_advertLocation => '광고 위치';
+
+ @override
+ String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
+
+ @override
+ String settings_multiAck(String value) {
+ return '다중 ACK: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => '텔레메트리 모드 업데이트 완료';
+
+ @override
+ String get settings_actions => '행동';
+
+ @override
+ String get settings_sendAdvertisement => '광고 전송';
+
+ @override
+ String get settings_sendAdvertisementSubtitle => '방송 활동';
+
+ @override
+ String get settings_advertisementSent => '광고 전송';
+
+ @override
+ String get settings_syncTime => '동기화 시간';
+
+ @override
+ String get settings_syncTimeSubtitle => '장치 시계를 휴대폰 시간으로 설정';
+
+ @override
+ String get settings_timeSynchronized => '시간 동기화';
+
+ @override
+ String get settings_refreshContacts => '연락처 갱신';
+
+ @override
+ String get settings_refreshContactsSubtitle => '장치에서 연락처 목록을 다시 불러오기';
+
+ @override
+ String get settings_rebootDevice => '장치 재부팅';
+
+ @override
+ String get settings_rebootDeviceSubtitle => 'MeshCore 장치를 재부팅하세요.';
+
+ @override
+ String get settings_rebootDeviceConfirm =>
+ '정말로 장치를 재부팅하시겠습니까? 이 경우 연결이 끊어집니다.';
+
+ @override
+ String get settings_debug => '디버깅';
+
+ @override
+ String get settings_bleDebugLog => 'BLE 디버그 로그';
+
+ @override
+ String get settings_bleDebugLogSubtitle => 'BLE 명령어, 응답 및 원시 데이터';
+
+ @override
+ String get settings_appDebugLog => '앱 디버깅 로그';
+
+ @override
+ String get settings_appDebugLogSubtitle => '애플리케이션 디버깅 메시지';
+
+ @override
+ String get settings_about => '소개';
+
+ @override
+ String settings_aboutVersion(String version) {
+ return 'MeshCore Open $version 버전';
+ }
+
+ @override
+ String get settings_aboutLegalese => '2026년 MeshCore 오픈 소스 프로젝트';
+
+ @override
+ String get settings_aboutDescription =>
+ 'MeshCore LoRa 메시 네트워크 장치를 위한 오픈 소스 Flutter 클라이언트.';
+
+ @override
+ String get settings_aboutOpenMeteoAttribution =>
+ 'LOS 고도 데이터: Open-Meteo (CC BY 4.0)';
+
+ @override
+ String get settings_infoName => '이름';
+
+ @override
+ String get settings_infoId => 'ID';
+
+ @override
+ String get settings_infoStatus => '상태';
+
+ @override
+ String get settings_infoBattery => '배터리';
+
+ @override
+ String get settings_infoPublicKey => '공개 키';
+
+ @override
+ String get settings_infoContactsCount => '연락처 수';
+
+ @override
+ String get settings_infoChannelCount => '채널 수';
+
+ @override
+ String get settings_presets => '기본 설정';
+
+ @override
+ String get settings_frequency => '주파수 (MHz)';
+
+ @override
+ String get settings_frequencyHelper => '300.0 - 2500.0';
+
+ @override
+ String get settings_frequencyInvalid => '유효하지 않은 주파수 (300-2500 MHz)';
+
+ @override
+ String get settings_bandwidth => '대역폭';
+
+ @override
+ String get settings_spreadingFactor => '분산 계수';
+
+ @override
+ String get settings_codingRate => '코딩 속도';
+
+ @override
+ String get settings_txPower => 'TX 전력 (dBm)';
+
+ @override
+ String get settings_txPowerHelper => '0 - 22';
+
+ @override
+ String get settings_txPowerInvalid => '유효하지 않은 TX 전력 (0-22 dBm)';
+
+ @override
+ String get settings_clientRepeat => '오프그리드 반복';
+
+ @override
+ String get settings_clientRepeatSubtitle =>
+ '이 장치가 다른 사람들을 위해 메시 패킷을 반복하도록 허용합니다.';
+
+ @override
+ String get settings_clientRepeatFreqWarning =>
+ '오프그리드(무전력) 시스템 재연결에는 433MHz, 869MHz, 또는 918MHz 주파수가 필요합니다.';
+
+ @override
+ String settings_error(String message) {
+ return '오류: $message';
+ }
+
+ @override
+ String get appSettings_title => '앱 설정';
+
+ @override
+ String get appSettings_appearance => '외관';
+
+ @override
+ String get appSettings_theme => '주제';
+
+ @override
+ String get appSettings_themeSystem => '기본 설정';
+
+ @override
+ String get appSettings_themeLight => '빛';
+
+ @override
+ String get appSettings_themeDark => '어둡다';
+
+ @override
+ String get appSettings_language => '언어';
+
+ @override
+ String get appSettings_languageSystem => '기본 설정';
+
+ @override
+ String get appSettings_languageEn => '영어';
+
+ @override
+ String get appSettings_languageFr => '프랑스어';
+
+ @override
+ String get appSettings_languageEs => '스페인어';
+
+ @override
+ String get appSettings_languageDe => '독일어';
+
+ @override
+ String get appSettings_languagePl => '폴란드';
+
+ @override
+ String get appSettings_languageSl => '슬로베니아어';
+
+ @override
+ String get appSettings_languagePt => '포르투갈어';
+
+ @override
+ String get appSettings_languageIt => '이탈리아어';
+
+ @override
+ String get appSettings_languageZh => '중국어';
+
+ @override
+ String get appSettings_languageSv => '스웨덴어';
+
+ @override
+ String get appSettings_languageNl => '네덜란드어';
+
+ @override
+ String get appSettings_languageSk => '슬로베니아어';
+
+ @override
+ String get appSettings_languageBg => '불가리';
+
+ @override
+ String get appSettings_languageRu => '러시아어';
+
+ @override
+ String get appSettings_languageUk => '우크라이나';
+
+ @override
+ String get appSettings_enableMessageTracing => '메시지 추적 기능 활성화';
+
+ @override
+ String get appSettings_enableMessageTracingSubtitle =>
+ '메시지에 대한 상세한 경로 및 시간 정보를 표시';
+
+ @override
+ String get appSettings_notifications => '알림';
+
+ @override
+ String get appSettings_enableNotifications => '알림 활성화';
+
+ @override
+ String get appSettings_enableNotificationsSubtitle => '메시지와 광고에 대한 알림을 받으세요.';
+
+ @override
+ String get appSettings_notificationPermissionDenied => '알림 권한 거부';
+
+ @override
+ String get appSettings_notificationsEnabled => '알림 기능 활성화';
+
+ @override
+ String get appSettings_notificationsDisabled => '알림 기능 끄기';
+
+ @override
+ String get appSettings_messageNotifications => '메시지 알림';
+
+ @override
+ String get appSettings_messageNotificationsSubtitle => '새로운 메시지를 받을 때 알림 표시';
+
+ @override
+ String get appSettings_channelMessageNotifications => '채널 메시지 알림';
+
+ @override
+ String get appSettings_channelMessageNotificationsSubtitle =>
+ '채널 메시지를 수신할 때 알림 표시';
+
+ @override
+ String get appSettings_advertisementNotifications => '광고 알림';
+
+ @override
+ String get appSettings_advertisementNotificationsSubtitle =>
+ '새 노드가 발견되었을 때 알림 표시';
+
+ @override
+ String get appSettings_messaging => '메시징';
+
+ @override
+ String get appSettings_clearPathOnMaxRetry => 'Max 재시도 시 경로 명확하게 설정';
+
+ @override
+ String get appSettings_clearPathOnMaxRetrySubtitle =>
+ '5번의 전송 시도가 실패하면 연락 경로를 재설정';
+
+ @override
+ String get appSettings_pathsWillBeCleared => '5번의 시도 실패 후, 해당 경로가 확보될 것입니다.';
+
+ @override
+ String get appSettings_pathsWillNotBeCleared => '경로는 자동으로 정리되지 않습니다.';
+
+ @override
+ String get appSettings_autoRouteRotation => '자동 경로 순환';
+
+ @override
+ String get appSettings_autoRouteRotationSubtitle => '최적 경로와 방수 모드 사이를 전환';
+
+ @override
+ String get appSettings_autoRouteRotationEnabled => '자동 경로 순환 기능 활성화';
+
+ @override
+ String get appSettings_autoRouteRotationDisabled => '자동 경로 순환 기능 비활성화';
+
+ @override
+ String get appSettings_maxRouteWeight => '최대 경로 무게';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ '한 경로가 성공적인 배송을 통해 누적할 수 있는 최대 무게';
+
+ @override
+ String get appSettings_initialRouteWeight => '초기 경로 가중치';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle => '새롭게 발견된 경로의 초기 무게';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement => '성공 횟수 증가';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ '성공적으로 배송된 경로에 추가된 무게';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement => '오류 가중치 감소';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ '배송 실패 후 경로에서 제거된 무게';
+
+ @override
+ String get appSettings_maxMessageRetries => '최대 메시지 재시도 횟수';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle => '메시지를 실패로 처리하기 전 시도 횟수';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
+ @override
+ String get appSettings_battery => '배터리';
+
+ @override
+ String get appSettings_batteryChemistry => '배터리 화학';
+
+ @override
+ String appSettings_batteryChemistryPerDevice(String deviceName) {
+ return '$deviceName 당분간';
+ }
+
+ @override
+ String get appSettings_batteryChemistryConnectFirst => '장치를 선택하기 위해 연결';
+
+ @override
+ String get appSettings_batteryNmc => '18650 NMC (3.0-4.2V)';
+
+ @override
+ String get appSettings_batteryLifepo4 => 'LiFePO4 (2.6-3.65V)';
+
+ @override
+ String get appSettings_batteryLipo => '리튬 폴리머 (3.0-4.2V)';
+
+ @override
+ String get appSettings_mapDisplay => '지도 표시';
+
+ @override
+ String get appSettings_showRepeaters => '반복 기능 표시';
+
+ @override
+ String get appSettings_showRepeatersSubtitle => '지도에 반복자 노드를 표시';
+
+ @override
+ String get appSettings_showChatNodes => '채팅 노드 표시';
+
+ @override
+ String get appSettings_showChatNodesSubtitle => '지도에 채팅 노드를 표시';
+
+ @override
+ String get appSettings_showOtherNodes => '다른 노드 표시';
+
+ @override
+ String get appSettings_showOtherNodesSubtitle => '지도에서 다른 노드 유형을 표시';
+
+ @override
+ String get appSettings_timeFilter => '시간 필터';
+
+ @override
+ String get appSettings_timeFilterShowAll => '모든 노드 표시';
+
+ @override
+ String appSettings_timeFilterShowLast(int hours) {
+ return '지난 $hours 시간 동안의 노드 표시';
+ }
+
+ @override
+ String get appSettings_mapTimeFilter => '지도 필터';
+
+ @override
+ String get appSettings_showNodesDiscoveredWithin => '다음 내역에서 발견된 노드 표시:';
+
+ @override
+ String get appSettings_allTime => '모든 시간';
+
+ @override
+ String get appSettings_lastHour => '지난 시간';
+
+ @override
+ String get appSettings_last6Hours => '지난 6시간';
+
+ @override
+ String get appSettings_last24Hours => '지난 24시간';
+
+ @override
+ String get appSettings_lastWeek => '지난 주';
+
+ @override
+ String get appSettings_offlineMapCache => '오프라인 지도 캐시';
+
+ @override
+ String get appSettings_unitsTitle => '단위';
+
+ @override
+ String get appSettings_unitsMetric => '단위 (m / km)';
+
+ @override
+ String get appSettings_unitsImperial => '제국 (피트/마일)';
+
+ @override
+ String get appSettings_noAreaSelected => '선택된 영역 없음';
+
+ @override
+ String appSettings_areaSelectedZoom(int minZoom, int maxZoom) {
+ return '선택된 영역 (줌 레벨: $minZoom - $maxZoom)';
+ }
+
+ @override
+ String get appSettings_debugCard => '디버깅';
+
+ @override
+ String get appSettings_appDebugLogging => '앱 디버깅 로깅';
+
+ @override
+ String get appSettings_appDebugLoggingSubtitle => '로그 앱 디버깅 메시지 (문제 해결을 위한)';
+
+ @override
+ String get appSettings_appDebugLoggingEnabled => '앱 디버깅 로깅 활성화';
+
+ @override
+ String get appSettings_appDebugLoggingDisabled => '앱 디버깅 로깅 비활성화';
+
+ @override
+ String get contacts_title => '연락처';
+
+ @override
+ String get contacts_noContacts => '아직 연락처는 없습니다.';
+
+ @override
+ String get contacts_contactsWillAppear => '장치가 광고를 할 때, 연락처 정보가 표시됩니다.';
+
+ @override
+ String get contacts_unread => '읽지 않음';
+
+ @override
+ String get contacts_searchContactsNoNumber => '연락처 검색...';
+
+ @override
+ String contacts_searchContacts(int number, String str) {
+ return '$number $str 연락처 검색...';
+ }
+
+ @override
+ String contacts_searchFavorites(int number, String str) {
+ return '$number $str 검색 결과 보기...';
+ }
+
+ @override
+ String contacts_searchUsers(int number, String str) {
+ return '$number $str 사용자 검색...';
+ }
+
+ @override
+ String contacts_searchRepeaters(int number, String str) {
+ return '$number $str 검색 결과 반복기 검색';
+ }
+
+ @override
+ String contacts_searchRoomServers(int number, String str) {
+ return '$number $str 방 서버 검색';
+ }
+
+ @override
+ String get contacts_noUnreadContacts => '읽지 않은 연락처가 없습니다.';
+
+ @override
+ String get contacts_noContactsFound => '연락처 또는 그룹이 검색되지 않았습니다.';
+
+ @override
+ String get contacts_deleteContact => '연락처 삭제';
+
+ @override
+ String contacts_removeConfirm(String contactName) {
+ return '$contactName를 연락처 목록에서 제거하시겠습니까?';
+ }
+
+ @override
+ String get contacts_manageRepeater => '리피터 관리';
+
+ @override
+ String get contacts_manageRoom => '방 서버 관리';
+
+ @override
+ String get contacts_roomLogin => '방 서버 로그인';
+
+ @override
+ String get contacts_openChat => '자유로운 대화';
+
+ @override
+ String get contacts_editGroup => '편집 그룹';
+
+ @override
+ String get contacts_deleteGroup => '그룹 삭제';
+
+ @override
+ String contacts_deleteGroupConfirm(String groupName) {
+ return '$groupName 삭제?';
+ }
+
+ @override
+ String get contacts_newGroup => '새로운 그룹';
+
+ @override
+ String get contacts_groupName => '그룹 이름';
+
+ @override
+ String get contacts_groupNameRequired => '그룹 이름이 필요합니다';
+
+ @override
+ String get contacts_groupNameReserved => '이 그룹 이름은 이미 사용 중입니다.';
+
+ @override
+ String contacts_groupAlreadyExists(String name) {
+ return '그룹 \"$name\"은 이미 존재합니다.';
+ }
+
+ @override
+ String get contacts_filterContacts => '연락처 필터링...';
+
+ @override
+ String get contacts_noContactsMatchFilter => '입력하신 검색 조건과 일치하는 연락처가 없습니다.';
+
+ @override
+ String get contacts_noMembers => '회원 없음';
+
+ @override
+ String get contacts_lastSeenNow => '최근';
+
+ @override
+ String contacts_lastSeenMinsAgo(int minutes) {
+ return '~ $minutes min.';
+ }
+
+ @override
+ String get contacts_lastSeenHourAgo => '약 1시간';
+
+ @override
+ String contacts_lastSeenHoursAgo(int hours) {
+ return '~ $hours hours';
+ }
+
+ @override
+ String get contacts_lastSeenDayAgo => '~ 1일';
+
+ @override
+ String contacts_lastSeenDaysAgo(int days) {
+ return '~ $days일';
+ }
+
+ @override
+ String get contact_info => '연락처';
+
+ @override
+ String get contact_settings => '연락처 설정';
+
+ @override
+ String get contact_telemetry => '텔레메트리';
+
+ @override
+ String get contact_lastSeen => '마지막으로 목격';
+
+ @override
+ String get contact_clearChat => '명확한 대화';
+
+ @override
+ String get contact_teleBase => '텔레메트리 기반';
+
+ @override
+ String get contact_teleBaseSubtitle => '배터리 잔량 및 기본적인 통신 데이터를 공유할 수 있도록 허용';
+
+ @override
+ String get contact_teleLoc => '텔레메트리 위치';
+
+ @override
+ String get contact_teleLocSubtitle => '위치 정보 공유 허용';
+
+ @override
+ String get contact_teleEnv => '텔레메트리 환경';
+
+ @override
+ String get contact_teleEnvSubtitle => '환경 센서 데이터를 공유하도록 허용';
+
+ @override
+ String get channels_title => '채널';
+
+ @override
+ String get channels_noChannelsConfigured => '구성된 채널이 없습니다.';
+
+ @override
+ String get channels_addPublicChannel => '공개 채널 추가';
+
+ @override
+ String get channels_searchChannels => '검색 채널...';
+
+ @override
+ String get channels_noChannelsFound => '채널을 찾을 수 없습니다.';
+
+ @override
+ String channels_channelIndex(int index) {
+ return '채널 $index';
+ }
+
+ @override
+ String get channels_hashtagChannel => '해시태그 채널';
+
+ @override
+ String get channels_public => '대중의';
+
+ @override
+ String get channels_private => '사립';
+
+ @override
+ String get channels_publicChannel => '공개 채널';
+
+ @override
+ String get channels_privateChannel => '개인 채널';
+
+ @override
+ String get channels_editChannel => '채널 편집';
+
+ @override
+ String get channels_muteChannel => '음소거 채널';
+
+ @override
+ String get channels_unmuteChannel => '채널 음소거 해제';
+
+ @override
+ String get channels_deleteChannel => '채널 삭제';
+
+ @override
+ String channels_deleteChannelConfirm(String name) {
+ return '$name 삭제하시겠습니까? 이 작업은 취소할 수 없습니다.';
+ }
+
+ @override
+ String channels_channelDeleteFailed(String name) {
+ return '채널 \"$name\" 삭제에 실패했습니다.';
+ }
+
+ @override
+ String channels_channelDeleted(String name) {
+ return '채널 \"$name\" 삭제';
+ }
+
+ @override
+ String get channels_addChannel => '채널 추가';
+
+ @override
+ String get channels_channelIndexLabel => '채널 인덱스';
+
+ @override
+ String get channels_channelName => '채널 이름';
+
+ @override
+ String get channels_usePublicChannel => '공개 채널 사용';
+
+ @override
+ String get channels_standardPublicPsk => '표준 공공 PSK';
+
+ @override
+ String get channels_pskHex => 'PSK (헥스)';
+
+ @override
+ String get channels_generateRandomPsk => '임의의 PSK 생성';
+
+ @override
+ String get channels_enterChannelName => '채널 이름을 입력해 주세요.';
+
+ @override
+ String get channels_pskMustBe32Hex => 'PSK(개인식별키)는 32자리 16진수 문자여야 합니다.';
+
+ @override
+ String channels_channelAdded(String name) {
+ return '채널 \"$name\" 추가';
+ }
+
+ @override
+ String channels_editChannelTitle(int index) {
+ return '채널 $index 편집';
+ }
+
+ @override
+ String get channels_smazCompression => 'SMAZ 압축';
+
+ @override
+ String channels_channelUpdated(String name) {
+ return '채널 \"$name\"이 업데이트되었습니다.';
+ }
+
+ @override
+ String get channels_publicChannelAdded => '공개 채널 추가';
+
+ @override
+ String get channels_sortBy => '정렬 기준 선택';
+
+ @override
+ String get channels_sortManual => '사용 설명서';
+
+ @override
+ String get channels_sortAZ => 'A부터 Z까지';
+
+ @override
+ String get channels_sortLatestMessages => '최신 메시지';
+
+ @override
+ String get channels_sortUnread => '읽지 않음';
+
+ @override
+ String get channels_createPrivateChannel => '개인 채널 만들기';
+
+ @override
+ String get channels_createPrivateChannelDesc => '비밀 키로 암호화되어 있습니다.';
+
+ @override
+ String get channels_joinPrivateChannel => '개인 채널에 참여하기';
+
+ @override
+ String get channels_joinPrivateChannelDesc => '비밀 키를 수동으로 입력합니다.';
+
+ @override
+ String get channels_joinPublicChannel => '공개 채널에 참여하세요';
+
+ @override
+ String get channels_joinPublicChannelDesc => '누구나 이 채널에 참여할 수 있습니다.';
+
+ @override
+ String get channels_joinHashtagChannel => '해시태그 채널에 참여하세요';
+
+ @override
+ String get channels_joinHashtagChannelDesc => '누구나 해시태그 채널에 참여할 수 있습니다.';
+
+ @override
+ String get channels_scanQrCode => 'QR 코드를 스캔';
+
+ @override
+ String get channels_scanQrCodeComingSoon => '곧 출시';
+
+ @override
+ String get channels_enterHashtag => '해시태그 입력';
+
+ @override
+ String get channels_hashtagHint => '예: #팀';
+
+ @override
+ String get chat_noMessages => '아직 메시지가 없습니다.';
+
+ @override
+ String get chat_sendMessageToStart => '시작하려면 메시지를 보내세요.';
+
+ @override
+ String get chat_originalMessageNotFound => '원래 메시지를 찾을 수 없음';
+
+ @override
+ String chat_replyingTo(String name) {
+ return '$name에게 답변';
+ }
+
+ @override
+ String chat_replyTo(String name) {
+ return '$name님께 회신';
+ }
+
+ @override
+ String get chat_location => '위치';
+
+ @override
+ String chat_sendMessageTo(String contactName) {
+ return '$contactName에게 메시지를 보내';
+ }
+
+ @override
+ String get chat_typeMessage => '메시지를 입력하세요...';
+
+ @override
+ String chat_messageTooLong(int maxBytes) {
+ return '메시지가 너무 길어서 (최대 $maxBytes 바이트).';
+ }
+
+ @override
+ String get chat_messageCopied => '메시지가 복사되었습니다';
+
+ @override
+ String get chat_messageDeleted => '메시지가 삭제되었습니다.';
+
+ @override
+ String get chat_retryingMessage => '재시도 메시지';
+
+ @override
+ String chat_retryCount(int current, int max) {
+ return '$current/$max 시도';
+ }
+
+ @override
+ String get chat_sendGif => 'GIF 보내기';
+
+ @override
+ String get chat_reply => '답변';
+
+ @override
+ String get chat_addReaction => '댓글 추가';
+
+ @override
+ String get chat_me => '나';
+
+ @override
+ String get emojiCategorySmileys => '이모티콘';
+
+ @override
+ String get emojiCategoryGestures => '제스처';
+
+ @override
+ String get emojiCategoryHearts => '심장';
+
+ @override
+ String get emojiCategoryObjects => '대상';
+
+ @override
+ String get gifPicker_title => 'GIF 선택';
+
+ @override
+ String get gifPicker_searchHint => 'GIF 검색...';
+
+ @override
+ String get gifPicker_poweredBy => 'GIPHY에서 제공';
+
+ @override
+ String get gifPicker_noGifsFound => 'GIF 파일이 없습니다.';
+
+ @override
+ String get gifPicker_failedLoad => 'GIF 파일 로딩 실패';
+
+ @override
+ String get gifPicker_failedSearch => 'GIF 검색에 실패했습니다.';
+
+ @override
+ String get gifPicker_noInternet => '인터넷 연결 없음';
+
+ @override
+ String get debugLog_appTitle => '앱 디버깅 로그';
+
+ @override
+ String get debugLog_bleTitle => 'BLE 디버그 로그';
+
+ @override
+ String get debugLog_copyLog => '로그 기록';
+
+ @override
+ String get debugLog_clearLog => '명확한 로그';
+
+ @override
+ String get debugLog_copied => '디버깅 로그 복사';
+
+ @override
+ String get debugLog_bleCopied => 'BLE 로그 복사';
+
+ @override
+ String get debugLog_noEntries => '현재 디버깅 로그는 생성되지 않았습니다.';
+
+ @override
+ String get debugLog_enableInSettings => '설정에서 앱 디버깅 로깅을 활성화합니다.';
+
+ @override
+ String get debugLog_frames => '프레임';
+
+ @override
+ String get debugLog_rawLogRx => '원시 로그-RX';
+
+ @override
+ String get debugLog_noBleActivity => '현재 BLE 관련 활동은 없습니다.';
+
+ @override
+ String debugFrame_length(int count) {
+ return '프레임 길이: $count 바이트';
+ }
+
+ @override
+ String debugFrame_command(String value) {
+ return '명령: 0x$value';
+ }
+
+ @override
+ String get debugFrame_textMessageHeader => '텍스트 메시지 프레임:';
+
+ @override
+ String debugFrame_destinationPubKey(String pubKey) {
+ return '- 목적지 공개 키: $pubKey';
+ }
+
+ @override
+ String debugFrame_timestamp(int timestamp) {
+ return '- 시간: $timestamp';
+ }
+
+ @override
+ String debugFrame_flags(String value) {
+ return '- 플래그: 0x$value';
+ }
+
+ @override
+ String debugFrame_textType(int type, String label) {
+ return '- 텍스트 유형: $type ($label)';
+ }
+
+ @override
+ String get debugFrame_textTypeCli => '명령줄 인터페이스 (CLI)';
+
+ @override
+ String get debugFrame_textTypePlain => '단순한';
+
+ @override
+ String debugFrame_text(String text) {
+ return '- 텍스트: \"$text\"';
+ }
+
+ @override
+ String get debugFrame_hexDump => '헥스 덤프:';
+
+ @override
+ String get chat_pathManagement => '경로 관리';
+
+ @override
+ String get chat_ShowAllPaths => '모든 경로 표시';
+
+ @override
+ String get chat_routingMode => '라우팅 방식';
+
+ @override
+ String get chat_autoUseSavedPath => '자동 (저장된 경로 사용)';
+
+ @override
+ String get chat_forceFloodMode => '강수 모드 활성화';
+
+ @override
+ String get chat_recentAckPaths => '최근 사용한 ACK 경로 (사용하려면 탭):';
+
+ @override
+ String get chat_pathHistoryFull =>
+ '이력 기록은 이미 가득 차 있습니다. 항목을 삭제하여 새로운 항목을 추가할 수 있습니다.';
+
+ @override
+ String get chat_hopSingular => '점프';
+
+ @override
+ String get chat_hopPlural => '홉';
+
+ @override
+ String chat_hopsCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '홉',
+ one: '홉',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String get chat_successes => '성공 사례';
+
+ @override
+ String get chat_removePath => '경로 제거';
+
+ @override
+ String get chat_noPathHistoryYet => '아직 경로 기록이 없습니다.\n경로를 찾기 위해 메시지를 보내세요.';
+
+ @override
+ String get chat_pathActions => '경로 작업:';
+
+ @override
+ String get chat_setCustomPath => '사용자 지정 경로 설정';
+
+ @override
+ String get chat_setCustomPathSubtitle => '수동으로 경로를 지정';
+
+ @override
+ String get chat_clearPath => '명확한 길';
+
+ @override
+ String get chat_clearPathSubtitle => '다음 전송 시, 강제 재전송 설정';
+
+ @override
+ String get chat_pathCleared => '경로가 확보되었습니다. 다음 메시지는 경로를 다시 찾을 것입니다.';
+
+ @override
+ String get chat_floodModeSubtitle => '앱 바에서 라우팅 스위치를 사용';
+
+ @override
+ String get chat_floodModeEnabled =>
+ '홍수 모드 활성화됨. 앱 바의 경로 아이콘을 사용하여 다시 전환할 수 있습니다.';
+
+ @override
+ String get chat_fullPath => '전체 경로';
+
+ @override
+ String get chat_pathDetailsNotAvailable =>
+ '경로 정보는 아직 제공되지 않습니다. 메시지를 보내어 다시 시도해 보세요.';
+
+ @override
+ String chat_pathSetHops(int hopCount, String status) {
+ String _temp0 = intl.Intl.pluralLogic(
+ hopCount,
+ locale: localeName,
+ other: 'hops',
+ one: 'hop',
+ );
+ return 'Path set: $hopCount $_temp0 - $status';
+ }
+
+ @override
+ String get chat_pathSavedLocally => '로컬에 저장. 동기화 연결';
+
+ @override
+ String get chat_pathDeviceConfirmed => '장치 확인 완료.';
+
+ @override
+ String get chat_pathDeviceNotConfirmed => '기기가 아직 확인되지 않았습니다.';
+
+ @override
+ String get chat_type => '종류';
+
+ @override
+ String get chat_path => '경로';
+
+ @override
+ String get chat_publicKey => '공개 키';
+
+ @override
+ String get chat_compressOutgoingMessages => '전송되는 메시지 압축';
+
+ @override
+ String get chat_floodForced => '홍수 (강제)';
+
+ @override
+ String get chat_directForced => '직접적인 (강제적인)';
+
+ @override
+ String chat_hopsForced(int count) {
+ return '$count번 띄우기 (강제)';
+ }
+
+ @override
+ String get chat_floodAuto => '홍수 (자동)';
+
+ @override
+ String get chat_direct => '직접';
+
+ @override
+ String get chat_poiShared => '공유된 POI';
+
+ @override
+ String chat_unread(int count) {
+ return '읽지 않음: $count';
+ }
+
+ @override
+ String get chat_openLink => '링크를 열기?';
+
+ @override
+ String get chat_openLinkConfirmation => '이 링크를 브라우저에서 열고 싶으신가요?';
+
+ @override
+ String get chat_open => '열기';
+
+ @override
+ String chat_couldNotOpenLink(String url) {
+ return '링크를 열 수 없습니다: $url';
+ }
+
+ @override
+ String get chat_invalidLink => '유효하지 않은 링크 형식';
+
+ @override
+ String get map_title => '노드 매핑';
+
+ @override
+ String get map_lineOfSight => '시야';
+
+ @override
+ String get map_losScreenTitle => '시야';
+
+ @override
+ String get map_noNodesWithLocation => '위치 정보가 있는 노드가 없습니다.';
+
+ @override
+ String get map_nodesNeedGps => '노드는 지도에 표시되려면 GPS 좌표를 공유해야 합니다.';
+
+ @override
+ String map_nodesCount(int count) {
+ return '노드: $count';
+ }
+
+ @override
+ String map_pinsCount(int count) {
+ return '핀: $count';
+ }
+
+ @override
+ String get map_chat => '채팅';
+
+ @override
+ String get map_repeater => '반복기';
+
+ @override
+ String get map_room => '방';
+
+ @override
+ String get map_sensor => '센서';
+
+ @override
+ String get map_pinDm => '핀 (DM)';
+
+ @override
+ String get map_pinPrivate => '개인 계정';
+
+ @override
+ String get map_pinPublic => '공개 (일반 공개)';
+
+ @override
+ String get map_lastSeen => '마지막으로 목격';
+
+ @override
+ String get map_disconnectConfirm => '이 장치와의 연결을 해제하시겠습니까?';
+
+ @override
+ String get map_from => '~부터';
+
+ @override
+ String get map_source => '출처';
+
+ @override
+ String get map_flags => '깃발';
+
+ @override
+ String get map_shareMarkerHere => '여기에서 마커 공유';
+
+ @override
+ String get map_setAsMyLocation => '내 위치로 설정';
+
+ @override
+ String get map_pinLabel => '핀 라벨';
+
+ @override
+ String get map_label => '레이블';
+
+ @override
+ String get map_pointOfInterest => '관심 지점';
+
+ @override
+ String get map_sendToContact => '연락처로 보내기';
+
+ @override
+ String get map_sendToChannel => '채널로 전송';
+
+ @override
+ String get map_noChannelsAvailable => '사용 가능한 채널이 없습니다.';
+
+ @override
+ String get map_publicLocationShare => '공개 장소 공유';
+
+ @override
+ String map_publicLocationShareConfirm(String channelLabel) {
+ return '현재 $channelLabel 채널에서 위치 정보를 공유하려고 합니다. 이 채널은 공개되어 있으며, PSK를 가진 모든 사용자가 이 위치 정보를 볼 수 있습니다.';
+ }
+
+ @override
+ String get map_connectToShareMarkers => '장치를 연결하여 마커를 공유';
+
+ @override
+ String get map_filterNodes => '필터 노드';
+
+ @override
+ String get map_nodeTypes => '노드 유형';
+
+ @override
+ String get map_chatNodes => '채팅 노드';
+
+ @override
+ String get map_repeaters => '다시 보내는 장치';
+
+ @override
+ String get map_otherNodes => '다른 노드';
+
+ @override
+ String get map_showOverlaps => '반복 키 중복';
+
+ @override
+ String get map_keyPrefix => '핵심 접두사';
+
+ @override
+ String get map_filterByKeyPrefix => '주요 접두사 기준으로 필터링';
+
+ @override
+ String get map_publicKeyPrefix => '공개 키 접두사';
+
+ @override
+ String get map_markers => '마커';
+
+ @override
+ String get map_showSharedMarkers => '공통 마커 표시';
+
+ @override
+ String get map_showGuessedLocations => '추정된 노드 위치 표시';
+
+ @override
+ String get map_showDiscoveryContacts => '디스커버리 담당자 연락처 보기';
+
+ @override
+ String get map_guessedLocation => '추측된 위치';
+
+ @override
+ String get map_lastSeenTime => '마지막으로 확인된 시간';
+
+ @override
+ String get map_sharedPin => '공유 비밀번호';
+
+ @override
+ String get map_joinRoom => '방에 참여';
+
+ @override
+ String get map_manageRepeater => '리피터 관리';
+
+ @override
+ String get map_tapToAdd => '노드에 클릭하여 경로에 추가합니다.';
+
+ @override
+ String get map_runTrace => '경로 추적';
+
+ @override
+ String get map_runTraceWithReturnPath => '원래 경로로 돌아가세요.';
+
+ @override
+ String get map_removeLast => '마지막 항목 삭제';
+
+ @override
+ String get map_pathTraceCancelled => '경로 추적 기능이 취소되었습니다.';
+
+ @override
+ String get mapCache_title => '오프라인 지도 캐시';
+
+ @override
+ String get mapCache_selectAreaFirst => '캐시할 영역을 먼저 선택하세요';
+
+ @override
+ String get mapCache_noTilesToDownload => '이 지역에 다운로드할 타일이 없습니다.';
+
+ @override
+ String get mapCache_downloadTilesTitle => '타일 다운로드';
+
+ @override
+ String mapCache_downloadTilesPrompt(int count) {
+ return '$count개의 타일을 오프라인 사용을 위해 다운로드하시겠습니까?';
+ }
+
+ @override
+ String get mapCache_downloadAction => '다운로드';
+
+ @override
+ String mapCache_cachedTiles(int count) {
+ return '$count 개의 타일 캐시';
+ }
+
+ @override
+ String mapCache_cachedTilesWithFailed(int downloaded, int failed) {
+ return 'Cached $downloaded tiles ($failed failed)';
+ }
+
+ @override
+ String get mapCache_clearOfflineCacheTitle => '오프라인 캐시 삭제';
+
+ @override
+ String get mapCache_clearOfflineCachePrompt => '모든 캐시된 지도 템플릿을 삭제하시겠습니까?';
+
+ @override
+ String get mapCache_offlineCacheCleared => '오프라인 캐시 삭제';
+
+ @override
+ String get mapCache_noAreaSelected => '선택된 영역 없음';
+
+ @override
+ String get mapCache_cacheArea => '캐시 영역';
+
+ @override
+ String get mapCache_useCurrentView => '현재 보기 유지';
+
+ @override
+ String get mapCache_zoomRange => '줌 기능 범위';
+
+ @override
+ String mapCache_estimatedTiles(int count) {
+ return '예상되는 타일 개수: $count';
+ }
+
+ @override
+ String mapCache_downloadedTiles(int completed, int total) {
+ return 'Downloaded $completed / $total';
+ }
+
+ @override
+ String get mapCache_downloadTilesButton => '타일 다운로드';
+
+ @override
+ String get mapCache_clearCacheButton => '캐시 삭제';
+
+ @override
+ String mapCache_failedDownloads(int count) {
+ return '실패한 다운로드: $count';
+ }
+
+ @override
+ String mapCache_boundsLabel(
+ String north,
+ String south,
+ String east,
+ String west,
+ ) {
+ return 'N $north, S $south, E $east, W $west';
+ }
+
+ @override
+ String get time_justNow => '방금';
+
+ @override
+ String time_minutesAgo(int minutes) {
+ return '$minutes분 전';
+ }
+
+ @override
+ String time_hoursAgo(int hours) {
+ return '${hours}h ago';
+ }
+
+ @override
+ String time_daysAgo(int days) {
+ return '$days일 전';
+ }
+
+ @override
+ String get time_hour => '시간';
+
+ @override
+ String get time_hours => '시간';
+
+ @override
+ String get time_day => '하루';
+
+ @override
+ String get time_days => '일';
+
+ @override
+ String get time_week => '주';
+
+ @override
+ String get time_weeks => '몇 주';
+
+ @override
+ String get time_month => '달';
+
+ @override
+ String get time_months => '개월';
+
+ @override
+ String get time_minutes => '분';
+
+ @override
+ String get time_allTime => '모든 시간';
+
+ @override
+ String get dialog_disconnect => '연결 해제';
+
+ @override
+ String get dialog_disconnectConfirm => '이 장치와의 연결을 해제하시겠습니까?';
+
+ @override
+ String get login_repeaterLogin => '다시 로그인';
+
+ @override
+ String get login_roomLogin => '방 서버 로그인';
+
+ @override
+ String get login_password => '비밀번호';
+
+ @override
+ String get login_enterPassword => '비밀번호를 입력하세요';
+
+ @override
+ String get login_savePassword => '비밀번호 저장';
+
+ @override
+ String get login_savePasswordSubtitle => '비밀번호는 이 장치에 안전하게 저장됩니다.';
+
+ @override
+ String get login_repeaterDescription => '반복기 비밀번호를 입력하여 설정 및 상태를 확인하십시오.';
+
+ @override
+ String get login_roomDescription => '설정 및 상태에 액세스하려면 방 비밀번호를 입력하세요.';
+
+ @override
+ String get login_routing => '라우팅';
+
+ @override
+ String get login_routingMode => '라우팅 모드';
+
+ @override
+ String get login_autoUseSavedPath => '자동 (저장된 경로 사용)';
+
+ @override
+ String get login_forceFloodMode => '강수 모드 활성화';
+
+ @override
+ String get login_managePaths => '경로 관리';
+
+ @override
+ String get login_login => '로그인';
+
+ @override
+ String login_attempt(int current, int max) {
+ return '시도 $current/$max';
+ }
+
+ @override
+ String login_failed(String error) {
+ return '로그인 실패: $error';
+ }
+
+ @override
+ String get login_failedMessage =>
+ '로그인에 실패했습니다. 비밀번호가 잘못되었거나, 연결이 되지 않는 것 같습니다.';
+
+ @override
+ String get common_reload => '다시 로드';
+
+ @override
+ String get common_clear => '명확하게';
+
+ @override
+ String path_currentPath(String path) {
+ return '현재 경로: $path';
+ }
+
+ @override
+ String path_usingHopsPath(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: 'hops',
+ one: 'hop',
+ );
+ return 'Using $count $_temp0 path';
+ }
+
+ @override
+ String get path_enterCustomPath => '사용자 지정 경로 입력';
+
+ @override
+ String get path_currentPathLabel => '현재 경로';
+
+ @override
+ String get path_hexPrefixInstructions =>
+ '각 단계에 대한 2자리 헥사데진 접두사를 쉼표로 구분하여 입력하세요.';
+
+ @override
+ String get path_hexPrefixExample =>
+ '예시: A1, F2, 3C (각 노드는 자신의 공개 키의 첫 번째 바이트를 사용)';
+
+ @override
+ String get path_labelHexPrefixes => '경로 (헥스 접두사)';
+
+ @override
+ String get path_helperMaxHops =>
+ '최대 64개의 홉. 각 접두사는 2개의 16진수 문자(1바이트)로 구성됩니다.';
+
+ @override
+ String get path_selectFromContacts => '또 연락처 목록에서 선택:';
+
+ @override
+ String get path_noRepeatersFound => '반복 장치 또는 서버는 찾을 수 없습니다.';
+
+ @override
+ String get path_customPathsRequire =>
+ '사용자 정의 경로에는 메시지를 전달할 수 있는 중간 경로가 필요합니다.';
+
+ @override
+ String path_invalidHexPrefixes(String prefixes) {
+ return '유효하지 않은 16진수 접두사: $prefixes';
+ }
+
+ @override
+ String get path_tooLong => '경로가 너무 길어. 최대 64개의 연결만 허용됩니다.';
+
+ @override
+ String get path_setPath => '경로 설정';
+
+ @override
+ String get repeater_management => '리피터 관리';
+
+ @override
+ String get room_management => '방 서버 관리';
+
+ @override
+ String get repeater_managementTools => '관리 도구';
+
+ @override
+ String get repeater_status => '상태';
+
+ @override
+ String get repeater_statusSubtitle => '반복 장비의 상태, 통계, 및 이웃 장비 목록 보기';
+
+ @override
+ String get repeater_telemetry => '텔레메트리';
+
+ @override
+ String get repeater_telemetrySubtitle => '센서 및 시스템 상태에 대한 통신 데이터를 확인';
+
+ @override
+ String get repeater_cli => '명령줄 인터페이스 (CLI)';
+
+ @override
+ String get repeater_cliSubtitle => '리피터에 명령을 전송';
+
+ @override
+ String get repeater_neighbors => '이웃';
+
+ @override
+ String get repeater_neighborsSubtitle => '0홉 이웃 노드를 확인합니다.';
+
+ @override
+ String get repeater_settings => '설정';
+
+ @override
+ String get repeater_settingsSubtitle => '리피터 파라미터 설정';
+
+ @override
+ String get repeater_statusTitle => '반복 장치 상태';
+
+ @override
+ String get repeater_routingMode => '라우팅 방식';
+
+ @override
+ String get repeater_autoUseSavedPath => '자동 (저장된 경로 사용)';
+
+ @override
+ String get repeater_forceFloodMode => '강수 모드 활성화';
+
+ @override
+ String get repeater_pathManagement => '경로 관리';
+
+ @override
+ String get repeater_refresh => '새롭게';
+
+ @override
+ String get repeater_statusRequestTimeout => '상태 확인 요청이 시간 초과되었습니다.';
+
+ @override
+ String repeater_errorLoadingStatus(String error) {
+ return '상태 로딩 오류: $error';
+ }
+
+ @override
+ String get repeater_systemInformation => '시스템 정보';
+
+ @override
+ String get repeater_battery => '배터리';
+
+ @override
+ String get repeater_clockAtLogin => '로그인 시 시간 표시';
+
+ @override
+ String get repeater_uptime => '가동 시간';
+
+ @override
+ String get repeater_queueLength => '대기 줄의 길이';
+
+ @override
+ String get repeater_debugFlags => '디버깅 플래그';
+
+ @override
+ String get repeater_radioStatistics => '라디오 통계';
+
+ @override
+ String get repeater_lastRssi => '마지막 RSSI 값';
+
+ @override
+ String get repeater_lastSnr => '마지막 SNR';
+
+ @override
+ String get repeater_noiseFloor => '잡음 수준';
+
+ @override
+ String get repeater_txAirtime => 'TX 에어타임';
+
+ @override
+ String get repeater_rxAirtime => 'RX 에어타임';
+
+ @override
+ String get repeater_packetStatistics => '패킷 통계';
+
+ @override
+ String get repeater_sent => '발송';
+
+ @override
+ String get repeater_received => '수신';
+
+ @override
+ String get repeater_duplicates => '중복';
+
+ @override
+ String repeater_daysHoursMinsSecs(
+ int days,
+ int hours,
+ int minutes,
+ int seconds,
+ ) {
+ return '$days일 $hours시간 $minutes분 $seconds초';
+ }
+
+ @override
+ String repeater_packetTxTotal(int total, String flood, String direct) {
+ return '총: $total, 홍수: $flood, 직접: $direct';
+ }
+
+ @override
+ String repeater_packetRxTotal(int total, String flood, String direct) {
+ return '총: $total, 홍수: $flood, 직접: $direct';
+ }
+
+ @override
+ String repeater_duplicatesFloodDirect(String flood, String direct) {
+ return '홍수: $flood, 직접: $direct';
+ }
+
+ @override
+ String repeater_duplicatesTotal(int total) {
+ return '총: $total';
+ }
+
+ @override
+ String get repeater_settingsTitle => '리피터 설정';
+
+ @override
+ String get repeater_basicSettings => '기본 설정';
+
+ @override
+ String get repeater_repeaterName => '반복 장비 이름';
+
+ @override
+ String get repeater_repeaterNameHelper => '이 반복기용 표시 이름';
+
+ @override
+ String get repeater_adminPassword => '관리자 비밀번호';
+
+ @override
+ String get repeater_adminPasswordHelper => '전체 접근 권한 비밀번호';
+
+ @override
+ String get repeater_guestPassword => '게스트 비밀번호';
+
+ @override
+ String get repeater_guestPasswordHelper => '읽기 전용 접근 비밀번호';
+
+ @override
+ String get repeater_radioSettings => '라디오 설정';
+
+ @override
+ String get repeater_frequencyMhz => '주파수 (MHz)';
+
+ @override
+ String get repeater_frequencyHelper => '300-2500 MHz';
+
+ @override
+ String get repeater_txPower => 'TX 파워';
+
+ @override
+ String get repeater_txPowerHelper => '1~30 dBm';
+
+ @override
+ String get repeater_bandwidth => '대역폭';
+
+ @override
+ String get repeater_spreadingFactor => '분산 계수';
+
+ @override
+ String get repeater_codingRate => '코딩 속도';
+
+ @override
+ String get repeater_locationSettings => '위치 설정';
+
+ @override
+ String get repeater_latitude => '위도';
+
+ @override
+ String get repeater_latitudeHelper => '십진법 위도 (예: 37.7749)';
+
+ @override
+ String get repeater_longitude => '경도';
+
+ @override
+ String get repeater_longitudeHelper => '십진법 위도 (예: -122.4194)';
+
+ @override
+ String get repeater_features => '특징';
+
+ @override
+ String get repeater_packetForwarding => '패킷 전송';
+
+ @override
+ String get repeater_packetForwardingSubtitle => '리피터가 패킷을 전달하도록 설정';
+
+ @override
+ String get repeater_guestAccess => '게스트 접근';
+
+ @override
+ String get repeater_guestAccessSubtitle => '게스트의 읽기 전용 접근 권한 허용';
+
+ @override
+ String get repeater_privacyMode => '개인 정보 보호 모드';
+
+ @override
+ String get repeater_privacyModeSubtitle => '광고에 이름/위치 정보 숨기기';
+
+ @override
+ String get repeater_advertisementSettings => '광고 설정';
+
+ @override
+ String get repeater_localAdvertInterval => '지역 광고 시간 간격';
+
+ @override
+ String repeater_localAdvertIntervalMinutes(int minutes) {
+ return '$minutes 분';
+ }
+
+ @override
+ String get repeater_floodAdvertInterval => '홍수 광고 간격';
+
+ @override
+ String repeater_floodAdvertIntervalHours(int hours) {
+ return '$hours 시간';
+ }
+
+ @override
+ String get repeater_encryptedAdvertInterval => '암호화된 광고 간격';
+
+ @override
+ String get repeater_dangerZone => '위험 구역';
+
+ @override
+ String get repeater_rebootRepeater => '리부트 반복';
+
+ @override
+ String get repeater_rebootRepeaterSubtitle => '리피터 장치를 재시작하세요.';
+
+ @override
+ String get repeater_rebootRepeaterConfirm => '반복기를 재부팅하시려는 것이 맞으신가요?';
+
+ @override
+ String get repeater_regenerateIdentityKey => '아이디 키 재 생성';
+
+ @override
+ String get repeater_regenerateIdentityKeySubtitle => '새로운 공개/개인 키 쌍 생성';
+
+ @override
+ String get repeater_regenerateIdentityKeyConfirm =>
+ '이를 통해 리피터에 새로운 식별자를 할당합니다. 계속 진행하시겠습니까?';
+
+ @override
+ String get repeater_eraseFileSystem => '파일 시스템 삭제';
+
+ @override
+ String get repeater_eraseFileSystemSubtitle => '리피터 파일 시스템을 포맷합니다.';
+
+ @override
+ String get repeater_eraseFileSystemConfirm =>
+ '경고: 이 작업은 리피터에 있는 모든 데이터를 삭제합니다. 이 작업을 되돌릴 수 없습니다!';
+
+ @override
+ String get repeater_eraseSerialOnly =>
+ '\'Erase\' 기능은 시리얼 콘솔을 통해서만 사용할 수 있습니다.';
+
+ @override
+ String repeater_commandSent(String command) {
+ return '명령 전송: $command';
+ }
+
+ @override
+ String repeater_errorSendingCommand(String error) {
+ return '명령 전송 오류: $error';
+ }
+
+ @override
+ String get repeater_confirm => '확인';
+
+ @override
+ String get repeater_settingsSaved => '설정이 성공적으로 저장되었습니다.';
+
+ @override
+ String repeater_errorSavingSettings(String error) {
+ return '설정 저장 오류: $error';
+ }
+
+ @override
+ String get repeater_refreshBasicSettings => '기본 설정 초기화';
+
+ @override
+ String get repeater_refreshRadioSettings => '라디오 설정 초기화';
+
+ @override
+ String get repeater_refreshTxPower => 'TX 전원 재설정';
+
+ @override
+ String get repeater_refreshLocationSettings => '위치 설정 초기화';
+
+ @override
+ String get repeater_refreshPacketForwarding => '패킷 전송 재시작';
+
+ @override
+ String get repeater_refreshGuestAccess => '게스트 접근 권한 갱신';
+
+ @override
+ String get repeater_refreshPrivacyMode => '개인 정보 보호 모드 재설정';
+
+ @override
+ String get repeater_refreshAdvertisementSettings => '광고 설정 재설정';
+
+ @override
+ String repeater_refreshed(String label) {
+ return '$label가 갱신됨';
+ }
+
+ @override
+ String repeater_errorRefreshing(String label) {
+ return '$label를 새로 고침 중 오류 발생';
+ }
+
+ @override
+ String get repeater_cliTitle => '리피터 CLI';
+
+ @override
+ String get repeater_debugNextCommand => '다음 명령 디버깅';
+
+ @override
+ String get repeater_commandHelp => '명령 도움';
+
+ @override
+ String get repeater_clearHistory => '명확한 역사';
+
+ @override
+ String get repeater_noCommandsSent => '아직 명령이 전송되지 않았습니다.';
+
+ @override
+ String get repeater_typeCommandOrUseQuick => '아래에 명령어를 입력하거나, 빠른 명령어를 사용하세요.';
+
+ @override
+ String get repeater_enterCommandHint => '명령어를 입력하세요...';
+
+ @override
+ String get repeater_previousCommand => '이전 명령어';
+
+ @override
+ String get repeater_nextCommand => '다음 명령어';
+
+ @override
+ String get repeater_enterCommandFirst => '먼저 명령어를 입력하세요';
+
+ @override
+ String get repeater_cliCommandFrameTitle => 'CLI 명령어 프레임';
+
+ @override
+ String repeater_cliCommandError(String error) {
+ return '오류: $error';
+ }
+
+ @override
+ String get repeater_cliQuickGetName => '이름을 알려주세요';
+
+ @override
+ String get repeater_cliQuickGetRadio => '라디오 듣기';
+
+ @override
+ String get repeater_cliQuickGetTx => 'TX 획득';
+
+ @override
+ String get repeater_cliQuickNeighbors => '이웃';
+
+ @override
+ String get repeater_cliQuickVersion => '버전';
+
+ @override
+ String get repeater_cliQuickAdvertise => '광고';
+
+ @override
+ String get repeater_cliQuickClock => '시계';
+
+ @override
+ String get repeater_cliHelpAdvert => '광고 패킷을 발송';
+
+ @override
+ String get repeater_cliHelpReboot =>
+ '장치를 재부팅합니다. (참고: \'시간 초과\' 오류가 발생할 수 있으며, 이는 정상적인 현상입니다)';
+
+ @override
+ String get repeater_cliHelpClock => '각 기기의 시계에 표시되는 현재 시간';
+
+ @override
+ String get repeater_cliHelpPassword => '장치에 새로운 관리자 비밀번호를 설정합니다.';
+
+ @override
+ String get repeater_cliHelpVersion => '장치 버전 및 펌웨어 빌드 날짜를 표시합니다.';
+
+ @override
+ String get repeater_cliHelpClearStats => '다양한 통계 지표를 0으로 초기화합니다.';
+
+ @override
+ String get repeater_cliHelpSetAf => '에어 타임 요소를 설정합니다.';
+
+ @override
+ String get repeater_cliHelpSetTx =>
+ 'LoRa 전송 전력을 dBm 단위로 설정합니다. (설정을 적용하려면 재부팅 필요)';
+
+ @override
+ String get repeater_cliHelpSetRepeat => '이 노드에 대한 리피터 역할을 활성화하거나 비활성화합니다.';
+
+ @override
+ String get repeater_cliHelpSetAllowReadOnly =>
+ '(방 서버) \'켜짐\' 상태인 경우, 빈 비밀번호로 로그인할 수 있지만, 방에 게시할 수는 없습니다 (단, 읽기만 가능).';
+
+ @override
+ String get repeater_cliHelpSetFloodMax =>
+ '들어오는 플러드 패킷의 최대 홉 수를 설정합니다 (최대 홉 수보다 크거나 같으면 패킷은 전달되지 않습니다).';
+
+ @override
+ String get repeater_cliHelpSetIntThresh =>
+ '간섭 임계값을 설정합니다 (dB 단위). 기본값은 14입니다. 0으로 설정하면 채널 간섭 감지 기능을 비활성화합니다.';
+
+ @override
+ String get repeater_cliHelpSetAgcResetInterval =>
+ '자동 게인 제어기를 재설정하는 간격을 설정합니다. 0으로 설정하면 비활성화됩니다.';
+
+ @override
+ String get repeater_cliHelpSetMultiAcks =>
+ '\'더블 ACK\' 기능을 활성화하거나 비활성화할 수 있습니다.';
+
+ @override
+ String get repeater_cliHelpSetAdvertInterval =>
+ '로컬 (제로 홉) 광고 패킷을 전송하는 간격 (분 단위)을 설정합니다. 0으로 설정하면 비활성화됩니다.';
+
+ @override
+ String get repeater_cliHelpSetFloodAdvertInterval =>
+ '시간 단위로 광고 패킷을 전송하는 간격을 설정합니다. 0으로 설정하면 비활성화됩니다.';
+
+ @override
+ String get repeater_cliHelpSetGuestPassword =>
+ '게스트 비밀번호를 설정하거나 업데이트합니다. (반복 사용자, 게스트 로그인 시 \"통계 가져오기\" 요청을 보낼 수 있음)';
+
+ @override
+ String get repeater_cliHelpSetName => '광고 이름을 설정합니다.';
+
+ @override
+ String get repeater_cliHelpSetLat => '광고 지도의 위도를 설정합니다. (십진법 단위)';
+
+ @override
+ String get repeater_cliHelpSetLon => '광고 지도의 경도를 설정합니다. (십진도)';
+
+ @override
+ String get repeater_cliHelpSetRadio =>
+ '완전히 새로운 라디오 파라미터를 설정하고, 선호 사항에 저장합니다. 적용하려면 \"재부팅\" 명령이 필요합니다.';
+
+ @override
+ String get repeater_cliHelpSetRxDelay =>
+ '(실험용) 기본 설정 (최소 1이어야 함)으로, 수신된 패킷에 약간의 지연을 적용하며, 신호 강도/점수를 기준으로 설정합니다. 0으로 설정하면 비활성화됩니다.';
+
+ @override
+ String get repeater_cliHelpSetTxDelay =>
+ '공통 패킷의 전송 지연 시간을 설정하며, 시간-공기 시간과 무작위 슬롯 시스템을 곱하여 충돌 가능성을 줄입니다.';
+
+ @override
+ String get repeater_cliHelpSetDirectTxDelay =>
+ 'txdelay와 동일하게, 하지만 직접 모드 패킷 전송 시 무작위 지연을 적용하는 경우';
+
+ @override
+ String get repeater_cliHelpSetBridgeEnabled => '브리지 활성화/비활성화';
+
+ @override
+ String get repeater_cliHelpSetBridgeDelay => '패킷 재전송 전에 지연 시간을 설정합니다.';
+
+ @override
+ String get repeater_cliHelpSetBridgeSource =>
+ '브리지가 수신된 패킷을 다시 전송할지, 아니면 전송된 패킷을 다시 전송할지 선택하십시오.';
+
+ @override
+ String get repeater_cliHelpSetBridgeBaud =>
+ 'rs232 브리지에 대한 직렬 통신 속도(baud rate)를 설정합니다.';
+
+ @override
+ String get repeater_cliHelpSetBridgeSecret => 'ESPNow 브리지에 대한 비밀 설정';
+
+ @override
+ String get repeater_cliHelpSetAdcMultiplier =>
+ '특정 보드에서만 지원되는 방식으로, 보고되는 배터리 전압을 조정하기 위한 사용자 정의 요소를 설정할 수 있습니다.';
+
+ @override
+ String get repeater_cliHelpTempRadio =>
+ '주어진 시간(분) 동안 임시 라디오 파라미터를 설정하고, 이후 원래 라디오 파라미터로 되돌립니다. (설정을 저장하지 않습니다).';
+
+ @override
+ String get repeater_cliHelpSetPerm =>
+ 'ACL을 수정합니다. \"permissions\" 값이 0인 경우, 일치하는 항목(pubkey 접두사)을 제거합니다. pubkey-hex 길이가 완전하고 현재 ACL에 없는 경우 새로운 항목을 추가합니다. pubkey 접두사를 기준으로 항목을 업데이트합니다. 권한 비트는 펌웨어 역할에 따라 다르지만, 하위 2비트는 다음과 같습니다: 0 (게스트), 1 (읽기 전용), 2 (읽기/쓰기), 3 (관리자)';
+
+ @override
+ String get repeater_cliHelpGetBridgeType => '브리지형, RS232, ESPNOW 지원';
+
+ @override
+ String get repeater_cliHelpLogStart => '패킷 로깅을 파일 시스템으로 시작합니다.';
+
+ @override
+ String get repeater_cliHelpLogStop => '패킷 로깅을 파일 시스템으로 저장하는 것을 중단합니다.';
+
+ @override
+ String get repeater_cliHelpLogErase => '파일 시스템에서 패킷 로그를 삭제합니다.';
+
+ @override
+ String get repeater_cliHelpNeighbors =>
+ '제로 홉 광고를 통해 수신된 다른 리피터 노드 목록을 보여줍니다. 각 줄은 ID-프리픽스-16진수:타임스탬프:SNR-횟수-4 형식입니다.';
+
+ @override
+ String get repeater_cliHelpNeighborRemove =>
+ '이 함수는 지정된 pubkey 접두사(16진수)와 일치하는 첫 번째 항목을 이웃 목록에서 제거합니다.';
+
+ @override
+ String get repeater_cliHelpRegion =>
+ '(단일 시리즈) 정의된 모든 지역과 현재 홍수 허가 정보를 나열합니다.';
+
+ @override
+ String get repeater_cliHelpRegionLoad =>
+ '참고: 이는 여러 명령을 한 번에 실행하는 특별한 방식입니다. 각 후속 명령은 영역 이름이며 (부모 계층 구조를 나타내기 위해 공백으로 들여쓰기하며, 최소 1개의 공백을 사용) 공백으로 끝나는 줄 또는 명령을 보내어 종료합니다.';
+
+ @override
+ String get repeater_cliHelpRegionGet =>
+ '주어진 이름 접두사(또는 전역 검색을 위한 \"\\*\" 사용)를 사용하여 특정 지역을 검색합니다. 결과를 \"-> 지역 이름 (상위 지역 이름) \'F\'\" 형태로 반환합니다.';
+
+ @override
+ String get repeater_cliHelpRegionPut => '주어진 이름으로 지역 정의를 추가하거나 업데이트합니다.';
+
+ @override
+ String get repeater_cliHelpRegionRemove =>
+ '지정된 이름으로 특정 영역 정의를 제거합니다. (정확히 일치해야 하며, 하위 영역은 존재하지 않아야 합니다)';
+
+ @override
+ String get repeater_cliHelpRegionAllowf =>
+ '지정된 영역에 대한 \'물\' 접근 권한을 설정합니다. (\'*\'는 전역/기존 범위에 해당)';
+
+ @override
+ String get repeater_cliHelpRegionDenyf =>
+ '지정된 영역에 대해 \'Flood\' 권한을 제거합니다. (참고: 현재 단계에서는 전역/기존 범위에서 이 기능을 사용하지 않는 것이 좋습니다!!)';
+
+ @override
+ String get repeater_cliHelpRegionHome =>
+ '현재 \'홈\' 지역으로 응답합니다. (아직 적용되지 않았으며, 향후 사용을 위해 예약됨)';
+
+ @override
+ String get repeater_cliHelpRegionHomeSet => '\'홈\' 지역을 설정합니다.';
+
+ @override
+ String get repeater_cliHelpRegionSave => '지역 목록/지도를 저장에 유지합니다.';
+
+ @override
+ String get repeater_cliHelpGps =>
+ 'GPS 상태를 표시합니다. GPS가 꺼져 있으면 \"꺼짐\"이라고 표시하고, 켜져 있으면 \"켜짐\", 상태, 위치 정보, 위성 수 등을 표시합니다.';
+
+ @override
+ String get repeater_cliHelpGpsOnOff => 'GPS 전원 상태를 켜고 끄는 기능.';
+
+ @override
+ String get repeater_cliHelpGpsSync => '노드 시간을 GPS 시계와 동기화합니다.';
+
+ @override
+ String get repeater_cliHelpGpsSetLoc => '노드의 위치를 GPS 좌표로 설정하고, 설정을 저장합니다.';
+
+ @override
+ String get repeater_cliHelpGpsAdvert =>
+ '노드의 위치 광고 설정:\n- none: 광고에 위치 정보를 포함하지 않음\n- share: GPS 위치 정보를 공유 (SensorManager에서 가져옴)\n- prefs: 설정에 저장된 위치를 광고';
+
+ @override
+ String get repeater_cliHelpGpsAdvertSet => '위치 기반 광고 설정 구성';
+
+ @override
+ String get repeater_commandsListTitle => '명령 목록';
+
+ @override
+ String get repeater_commandsListNote =>
+ '참고: 다양한 \"set...\" 명령과 함께 \"get...\" 명령도 존재합니다.';
+
+ @override
+ String get repeater_general => '일반';
+
+ @override
+ String get repeater_settingsCategory => '설정';
+
+ @override
+ String get repeater_bridge => '다리';
+
+ @override
+ String get repeater_logging => '로깅';
+
+ @override
+ String get repeater_neighborsRepeaterOnly => '이웃 (단방향 통신만 지원)';
+
+ @override
+ String get repeater_regionManagementRepeaterOnly => '지역 관리 (단, 중계 기능만 사용)';
+
+ @override
+ String get repeater_regionNote =>
+ '지역별 관리 기능을 도입하여 지역 정의 및 권한 관리를 수행할 수 있습니다.';
+
+ @override
+ String get repeater_gpsManagement => 'GPS 관리';
+
+ @override
+ String get repeater_gpsNote => 'GPS 명령이 위치 관련 주제를 관리하기 위해 도입되었습니다.';
+
+ @override
+ String get telemetry_receivedData => '수신된 통신 데이터';
+
+ @override
+ String get telemetry_requestTimeout => '원격 모니터링 요청이 시간 초과되었습니다.';
+
+ @override
+ String telemetry_errorLoading(String error) {
+ return '$error 오류로 인해 통신 데이터를 로드하지 못했습니다.';
+ }
+
+ @override
+ String get telemetry_noData => '텔레메트리 데이터는 제공되지 않습니다.';
+
+ @override
+ String telemetry_channelTitle(int channel) {
+ return '채널 $channel';
+ }
+
+ @override
+ String get telemetry_batteryLabel => '배터리';
+
+ @override
+ String get telemetry_voltageLabel => '전압';
+
+ @override
+ String get telemetry_mcuTemperatureLabel => 'MCU의 온도';
+
+ @override
+ String get telemetry_temperatureLabel => '온도';
+
+ @override
+ String get telemetry_currentLabel => '현재';
+
+ @override
+ String telemetry_batteryValue(int percent, String volts) {
+ return '$percent% / ${volts}V';
+ }
+
+ @override
+ String telemetry_voltageValue(String volts) {
+ return '${volts}V';
+ }
+
+ @override
+ String telemetry_currentValue(String amps) {
+ return '${amps}A';
+ }
+
+ @override
+ String telemetry_temperatureValue(String celsius, String fahrenheit) {
+ return '$celsius°C / $fahrenheit°F';
+ }
+
+ @override
+ String get neighbors_receivedData => '이웃 정보 수집';
+
+ @override
+ String get neighbors_requestTimedOut => '이웃들이 시간 제한을 요청하고 있습니다.';
+
+ @override
+ String neighbors_errorLoading(String error) {
+ return '이웃 정보 로딩 중 오류: $error';
+ }
+
+ @override
+ String get neighbors_repeatersNeighbors => '반복기, 이웃';
+
+ @override
+ String get neighbors_noData => '이웃 정보는 없습니다.';
+
+ @override
+ String neighbors_unknownContact(String pubkey) {
+ return '알 수 없는 $pubkey';
+ }
+
+ @override
+ String neighbors_heardAgo(String time) {
+ return 'Heard: $time ago';
+ }
+
+ @override
+ String get channelPath_title => '패킷 경로';
+
+ @override
+ String get channelPath_viewMap => '지도 보기';
+
+ @override
+ String get channelPath_otherObservedPaths => '관찰된 다른 경로';
+
+ @override
+ String get channelPath_repeaterHops => '반복 홉';
+
+ @override
+ String get channelPath_noHopDetails => '이 패키지에 대한 자세한 정보는 제공되지 않습니다.';
+
+ @override
+ String get channelPath_messageDetails => '메시지 세부 정보';
+
+ @override
+ String get channelPath_senderLabel => '발신자';
+
+ @override
+ String get channelPath_timeLabel => '시간';
+
+ @override
+ String get channelPath_repeatsLabel => '반복';
+
+ @override
+ String channelPath_pathLabel(int index) {
+ return '경로 $index';
+ }
+
+ @override
+ String get channelPath_observedLabel => '관찰';
+
+ @override
+ String channelPath_observedPathTitle(int index, String hops) {
+ return '관찰된 경로 $index • $hops';
+ }
+
+ @override
+ String get channelPath_noLocationData => '위치 정보 없음';
+
+ @override
+ String channelPath_timeWithDate(int day, int month, String time) {
+ return '$day/$month $time';
+ }
+
+ @override
+ String channelPath_timeOnly(String time) {
+ return '$time';
+ }
+
+ @override
+ String get channelPath_unknownPath => '알 수 없음';
+
+ @override
+ String get channelPath_floodPath => '홍수';
+
+ @override
+ String get channelPath_directPath => '직접';
+
+ @override
+ String channelPath_observedZeroOf(int total) {
+ return '$total 중 0개';
+ }
+
+ @override
+ String channelPath_observedSomeOf(int observed, int total) {
+ return '$observed of $total hops';
+ }
+
+ @override
+ String get channelPath_mapTitle => '경로 지도';
+
+ @override
+ String get channelPath_noRepeaterLocations => '이 경로에 대한 중계기 설치 위치는 없습니다.';
+
+ @override
+ String channelPath_primaryPath(int index) {
+ return '경로 $index (주 경로)';
+ }
+
+ @override
+ String get channelPath_pathLabelTitle => '경로';
+
+ @override
+ String get channelPath_observedPathHeader => '관찰된 경로';
+
+ @override
+ String channelPath_selectedPathLabel(String label, String prefixes) {
+ return '$label • $prefixes';
+ }
+
+ @override
+ String get channelPath_noHopDetailsAvailable => '이 패킷에 대한 이동 정보는 제공되지 않습니다.';
+
+ @override
+ String get channelPath_unknownRepeater => '알 수 없는 중계기';
+
+ @override
+ String get community_title => '지역 사회';
+
+ @override
+ String get community_create => '커뮤니티 만들기';
+
+ @override
+ String get community_createDesc => '새로운 커뮤니티를 만들고 QR 코드를 통해 공유하세요.';
+
+ @override
+ String get community_join => '참여하기';
+
+ @override
+ String get community_joinTitle => '커뮤니티에 참여하기';
+
+ @override
+ String community_joinConfirmation(String name) {
+ return '$name님, 커뮤니티에 참여하고 싶으신가요?';
+ }
+
+ @override
+ String get community_scanQr => '커뮤니티 QR 스캔';
+
+ @override
+ String get community_scanInstructions => '카메라를 커뮤니티 QR 코드 방향으로 향하게 하세요.';
+
+ @override
+ String get community_showQr => 'QR 코드 표시';
+
+ @override
+ String get community_publicChannel => '지역 사회 대상';
+
+ @override
+ String get community_hashtagChannel => '커뮤니티 해시태그';
+
+ @override
+ String get community_name => '지역 이름';
+
+ @override
+ String get community_enterName => '커뮤니티 이름을 입력하세요';
+
+ @override
+ String community_created(String name) {
+ return '커뮤니티 \"$name\"이 생성되었습니다.';
+ }
+
+ @override
+ String community_joined(String name) {
+ return '\"$name\" 커뮤니티에 가입';
+ }
+
+ @override
+ String get community_qrTitle => '커뮤니티 공유';
+
+ @override
+ String community_qrInstructions(String name) {
+ return '이 QR 코드를 스캔하여 \"$name\"에 가입하세요.';
+ }
+
+ @override
+ String get community_hashtagPrivacyHint =>
+ '커뮤니티 해시태그 채널은 커뮤니티 구성원만 가입할 수 있습니다.';
+
+ @override
+ String get community_invalidQrCode => '유효하지 않은 커뮤니티 QR 코드';
+
+ @override
+ String get community_alreadyMember => '이미 회원인 경우';
+
+ @override
+ String community_alreadyMemberMessage(String name) {
+ return '이미 $name의 회원입니다.';
+ }
+
+ @override
+ String get community_addPublicChannel => '커뮤니티 공개 채널 추가';
+
+ @override
+ String get community_addPublicChannelHint => '이 커뮤니티에 공개 채널을 자동으로 추가합니다.';
+
+ @override
+ String get community_noCommunities => '아직 어느 커뮤니티도 가입하지 않았습니다.';
+
+ @override
+ String get community_scanOrCreate => 'QR 코드를 스캔하거나 커뮤니티를 만들어 시작하세요.';
+
+ @override
+ String get community_manageCommunities => '커뮤니티 관리';
+
+ @override
+ String get community_delete => '커뮤니티 떠나기';
+
+ @override
+ String community_deleteConfirm(String name) {
+ return '$name을 묻어두나요?';
+ }
+
+ @override
+ String community_deleteChannelsWarning(int count) {
+ return '또한, 이 기능은 $count개의 채널과 그에 해당하는 메시지를 삭제합니다.';
+ }
+
+ @override
+ String community_deleted(String name) {
+ return '지역 커뮤니티 \"$name\"';
+ }
+
+ @override
+ String get community_regenerateSecret => '비밀 복원';
+
+ @override
+ String community_regenerateSecretConfirm(String name) {
+ return '$name의 비밀 키를 재생성하시겠습니까? 모든 회원은 계속 통신을 위해 새로운 QR 코드를 스캔해야 합니다.';
+ }
+
+ @override
+ String get community_regenerate => '재생';
+
+ @override
+ String community_secretRegenerated(String name) {
+ return '$name을 위한 비밀 정보가 복원되었습니다.';
+ }
+
+ @override
+ String get community_updateSecret => '비밀 업데이트';
+
+ @override
+ String community_secretUpdated(String name) {
+ return '$name을 위한 비밀 정보 업데이트';
+ }
+
+ @override
+ String community_scanToUpdateSecret(String name) {
+ return '새로운 QR 코드를 스캔하여 $name의 비밀번호를 업데이트하세요.';
+ }
+
+ @override
+ String get community_addHashtagChannel => '커뮤니티 해시태그 추가';
+
+ @override
+ String get community_addHashtagChannelDesc => '이 커뮤니티를 위한 해시태그 채널을 추가하세요.';
+
+ @override
+ String get community_selectCommunity => '커뮤니티 선택';
+
+ @override
+ String get community_regularHashtag => '일반 해시태그';
+
+ @override
+ String get community_regularHashtagDesc => '공개 해시태그 (누구나 참여 가능)';
+
+ @override
+ String get community_communityHashtag => '커뮤니티 해시태그';
+
+ @override
+ String get community_communityHashtagDesc => '지역 주민을 위한';
+
+ @override
+ String community_forCommunity(String name) {
+ return '$name 님께';
+ }
+
+ @override
+ String get listFilter_tooltip => '필터링 및 정렬';
+
+ @override
+ String get listFilter_sortBy => '정렬 기준 선택';
+
+ @override
+ String get listFilter_latestMessages => '최신 메시지';
+
+ @override
+ String get listFilter_heardRecently => '최근에 들었습니다';
+
+ @override
+ String get listFilter_az => 'A부터 Z까지';
+
+ @override
+ String get listFilter_filters => '필터';
+
+ @override
+ String get listFilter_all => '모든';
+
+ @override
+ String get listFilter_favorites => '관심 목록';
+
+ @override
+ String get listFilter_addToFavorites => '즐겨찾으로 추가';
+
+ @override
+ String get listFilter_removeFromFavorites => '즐겨찾에서 제거';
+
+ @override
+ String get listFilter_users => '사용자';
+
+ @override
+ String get listFilter_repeaters => '다시 보내는 장치';
+
+ @override
+ String get listFilter_roomServers => '방 내 서버';
+
+ @override
+ String get listFilter_unreadOnly => '읽지 않은 항목만';
+
+ @override
+ String get listFilter_newGroup => '새로운 그룹';
+
+ @override
+ String get pathTrace_you => '당신';
+
+ @override
+ String get pathTrace_failed => '경로 추적 실패.';
+
+ @override
+ String get pathTrace_notAvailable => '경로 추적 기능은 제공되지 않습니다.';
+
+ @override
+ String get pathTrace_refreshTooltip => '경로 추적 재시작';
+
+ @override
+ String get pathTrace_someHopsNoLocation => '홉 중 하나 또는 여러 개에 위치 정보가 누락되었습니다!';
+
+ @override
+ String get pathTrace_clearTooltip => '명확한 경로.';
+
+ @override
+ String get losSelectStartEnd => 'LOS(최소 거리 경로)의 시작 및 종료 노드를 선택합니다.';
+
+ @override
+ String losRunFailed(String error) {
+ return '시야 확인 실패: $error';
+ }
+
+ @override
+ String get losClearAllPoints => '모든 사항을 명확히 합니다.';
+
+ @override
+ String get losRunToViewElevationProfile =>
+ 'LOS(Line of Sight)를 사용하여 고도 프로필을 확인합니다.';
+
+ @override
+ String get losMenuTitle => 'LOS 메뉴';
+
+ @override
+ String get losMenuSubtitle => '사용자 지정 지점을 추가하려면, 노드를 탭하거나 맵을 길게 눌러 주세요.';
+
+ @override
+ String get losShowDisplayNodes => '노드 표시';
+
+ @override
+ String get losCustomPoints => '사용자 지정 포인트';
+
+ @override
+ String losCustomPointLabel(int index) {
+ return '맞춤형 $index';
+ }
+
+ @override
+ String get losPointA => 'A 지점';
+
+ @override
+ String get losPointB => '점 B';
+
+ @override
+ String losAntennaA(String value, String unit) {
+ return '안테나 A: $value $unit';
+ }
+
+ @override
+ String losAntennaB(String value, String unit) {
+ return 'Antenna B: $value $unit';
+ }
+
+ @override
+ String get losRun => 'LOS (Loss of Signal) 상태로 전환';
+
+ @override
+ String get losNoElevationData => '고도 정보 없음';
+
+ @override
+ String losProfileClear(
+ String distance,
+ String distanceUnit,
+ String clearance,
+ String heightUnit,
+ ) {
+ return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
+ }
+
+ @override
+ String losProfileBlocked(
+ String distance,
+ String distanceUnit,
+ String obstruction,
+ String heightUnit,
+ ) {
+ return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
+ }
+
+ @override
+ String get losStatusChecking => 'LOS: 확인 중...';
+
+ @override
+ String get losStatusNoData => 'LOS: 데이터 없음';
+
+ @override
+ String losStatusSummary(int clear, int total, int blocked, int unknown) {
+ return 'LOS: $clear/$total 개, $blocked 개, $unknown 개';
+ }
+
+ @override
+ String get losErrorElevationUnavailable => '샘플 중 하나 이상에 대한 고도 데이터가 없습니다.';
+
+ @override
+ String get losErrorInvalidInput => 'LOS 계산에 사용되는 부정확한 지점/고도 데이터.';
+
+ @override
+ String get losRenameCustomPoint => '사용자 지정된 지점의 이름을 변경';
+
+ @override
+ String get losPointName => '항목 이름';
+
+ @override
+ String get losShowPanelTooltip => 'LOS 패널 표시';
+
+ @override
+ String get losHidePanelTooltip => 'LOS 패널 숨기기';
+
+ @override
+ String get losElevationAttribution => '고도 데이터: Open-Meteo (CC BY 4.0)';
+
+ @override
+ String get losLegendRadioHorizon => '라디오 호라이즌';
+
+ @override
+ String get losLegendLosBeam => 'LOS 빔';
+
+ @override
+ String get losLegendTerrain => '지형';
+
+ @override
+ String get losFrequencyLabel => '빈도';
+
+ @override
+ String get losFrequencyInfoTooltip => '계산 내역 보기';
+
+ @override
+ String get losFrequencyDialogTitle => '라디오 수신 가능 범위 계산';
+
+ @override
+ String losFrequencyDialogDescription(
+ double baselineK,
+ double baselineFreq,
+ double frequencyMHz,
+ double kFactor,
+ ) {
+ return '$baselineK에서 시작하여 $baselineFreq MHz의 주파수에서 계산을 시작하면, 현재 $frequencyMHz MHz 대역에 대한 k-값을 조정하여, 이는 곡선형 라디오 지평선 상한선을 정의합니다.';
+ }
+
+ @override
+ String get contacts_pathTrace => '경로 추적';
+
+ @override
+ String get contacts_ping => '핑';
+
+ @override
+ String get contacts_repeaterPathTrace => '리피터로 가는 경로';
+
+ @override
+ String get contacts_repeaterPing => '핑 반복';
+
+ @override
+ String get contacts_roomPathTrace => '방 서버로의 경로 추적';
+
+ @override
+ String get contacts_roomPing => '피нг 룸 서버';
+
+ @override
+ String get contacts_chatTraceRoute => '경로 추적 경로';
+
+ @override
+ String contacts_pathTraceTo(String name) {
+ return '$name까지의 경로 추적';
+ }
+
+ @override
+ String get contacts_clipboardEmpty => '클립보드가 비어 있습니다.';
+
+ @override
+ String get contacts_invalidAdvertFormat => '유효하지 않은 연락 정보';
+
+ @override
+ String get contacts_contactImported => '연락이 수신되었습니다.';
+
+ @override
+ String get contacts_contactImportFailed => '연락처를 가져오지 못했습니다.';
+
+ @override
+ String get contacts_zeroHopAdvert => '제로 홉 광고';
+
+ @override
+ String get contacts_floodAdvert => '홍수 광고';
+
+ @override
+ String get contacts_copyAdvertToClipboard => '광고 텍스트를 클립보드에 복사';
+
+ @override
+ String get contacts_addContactFromClipboard => '복사본에서 연락처 추가';
+
+ @override
+ String get contacts_ShareContact => '연락처를 복사';
+
+ @override
+ String get contacts_ShareContactZeroHop => '광고를 통해 연락처 공유';
+
+ @override
+ String get contacts_zeroHopContactAdvertSent => '광고를 통해 연락처를 받았습니다.';
+
+ @override
+ String get contacts_zeroHopContactAdvertFailed => '연락처 전송에 실패했습니다.';
+
+ @override
+ String get contacts_contactAdvertCopied => '광고 내용이 복사되었습니다.';
+
+ @override
+ String get contacts_contactAdvertCopyFailed => '광고를 클립보드에 복사하는 데 실패했습니다.';
+
+ @override
+ String get notification_activityTitle => '메쉬코어 활동';
+
+ @override
+ String notification_messagesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '메시지들',
+ one: '메시지',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_channelMessagesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '채널 메시지',
+ one: '채널 메시지',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_newNodesCount(int count) {
+ String _temp0 = intl.Intl.pluralLogic(
+ count,
+ locale: localeName,
+ other: '새 노드들',
+ one: '새 노드',
+ );
+ return '$count $_temp0';
+ }
+
+ @override
+ String notification_newTypeDiscovered(String contactType) {
+ return '새로운 $contactType 발견';
+ }
+
+ @override
+ String get notification_receivedNewMessage => '새로운 메시지를 받았습니다';
+
+ @override
+ String get settings_gpxExportRepeaters => 'GPX로 전송/방 관리 서버';
+
+ @override
+ String get settings_gpxExportRepeatersSubtitle =>
+ 'GPX 파일에 위치 정보를 포함하여 반복자/룸 서버를 내보냅니다.';
+
+ @override
+ String get settings_gpxExportContacts => 'GPX 형식으로 내보내기';
+
+ @override
+ String get settings_gpxExportContactsSubtitle =>
+ 'GPX 파일에 위치 정보를 포함하여 동행하는 기능을 내보냅니다.';
+
+ @override
+ String get settings_gpxExportAll => '모든 연락처를 GPX 형식으로 내보내기';
+
+ @override
+ String get settings_gpxExportAllSubtitle =>
+ '위치 정보가 있는 모든 연락처를 GPX 파일로 내보냅니다.';
+
+ @override
+ String get settings_gpxExportSuccess => 'GPX 파일이 성공적으로 내보내졌습니다.';
+
+ @override
+ String get settings_gpxExportNoContacts => '수출할 연락처가 없습니다.';
+
+ @override
+ String get settings_gpxExportNotAvailable => '귀하의 장치/운영체제에서는 지원되지 않습니다.';
+
+ @override
+ String get settings_gpxExportError => '데이터 내보내기 과정에서 오류가 발생했습니다.';
+
+ @override
+ String get settings_gpxExportRepeatersRoom => '중계 장치 및 서버 위치';
+
+ @override
+ String get settings_gpxExportChat => '함께 방문할 장소';
+
+ @override
+ String get settings_gpxExportAllContacts => '모든 연락처 위치';
+
+ @override
+ String get settings_gpxExportShareText => 'meshcore-open에서 추출한 지도 데이터';
+
+ @override
+ String get settings_gpxExportShareSubject => 'meshcore-open GPX 지도 데이터 내보내기';
+
+ @override
+ String get snrIndicator_nearByRepeaters => '주변의 중계기';
+
+ @override
+ String get snrIndicator_lastSeen => '마지막으로 목격';
+
+ @override
+ String get contactsSettings_title => '연락처 설정';
+
+ @override
+ String get contactsSettings_autoAddTitle => '자동 검색';
+
+ @override
+ String get contactsSettings_otherTitle => '다른 연락 관련 설정';
+
+ @override
+ String get contactsSettings_autoAddUsersTitle => '자동으로 사용자 추가';
+
+ @override
+ String get contactsSettings_autoAddUsersSubtitle =>
+ '동반자가 자동으로 발견한 사용자를 추가할 수 있도록 합니다.';
+
+ @override
+ String get contactsSettings_autoAddRepeatersTitle => '자동으로 중계기 추가';
+
+ @override
+ String get contactsSettings_autoAddRepeatersSubtitle =>
+ '애완동물이 발견한 무선 라디오를 자동으로 추가할 수 있도록 설정합니다.';
+
+ @override
+ String get contactsSettings_autoAddRoomServersTitle => '자동으로 방 서버 추가';
+
+ @override
+ String get contactsSettings_autoAddRoomServersSubtitle =>
+ '애완동물이 발견한 방 서버를 자동으로 추가할 수 있도록 설정합니다.';
+
+ @override
+ String get contactsSettings_autoAddSensorsTitle => '자동으로 센서 추가';
+
+ @override
+ String get contactsSettings_autoAddSensorsSubtitle =>
+ '애완동물이 발견한 센서를 자동으로 추가할 수 있도록 설정합니다.';
+
+ @override
+ String get contactsSettings_overwriteOldestTitle => '가장 오래된 것을 덮어쓰기';
+
+ @override
+ String get contactsSettings_overwriteOldestSubtitle =>
+ '연락처 목록이 가득 차면, 가장 오래된 (선호하지 않은) 연락처가 대체됩니다.';
+
+ @override
+ String get discoveredContacts_Title => '연락처 찾기';
+
+ @override
+ String get discoveredContacts_noMatching => '일치하는 연락처가 없습니다.';
+
+ @override
+ String get discoveredContacts_searchHint => '발견된 연락처 검색';
+
+ @override
+ String get discoveredContacts_contactAdded => '연락처 추가';
+
+ @override
+ String get discoveredContacts_addContact => '연락처 추가';
+
+ @override
+ String get discoveredContacts_copyContact => '복사';
+
+ @override
+ String get discoveredContacts_deleteContact => '발견된 연락처 삭제';
+
+ @override
+ String get discoveredContacts_deleteContactAll => '발견된 모든 연락처 삭제';
+
+ @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 scanner_linuxPairingShowPin => 'PIN 표시';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'PIN 숨기기';
+
+ @override
+ String get scanner_linuxPairingPinTitle => '블루투스 페어링 PIN';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return '$deviceName에 대한 PIN을 입력하세요 (없으면 비워두세요).';
+ }
+}
diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart
index 4031ddf..bcb2d5d 100644
--- a/lib/l10n/app_localizations_nl.dart
+++ b/lib/l10n/app_localizations_nl.dart
@@ -395,6 +395,50 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privacy modus is uitgeschakeld';
+ @override
+ String get settings_privacy => 'Privacyinstellingen';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Beheer welke informatie wordt gedeeld';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Kies welke informatie uw apparaat deelt met anderen';
+
+ @override
+ String get settings_denyAll => 'Weiger alles';
+
+ @override
+ String get settings_allowByContact => 'Toestaan op basis van contactvlaggen';
+
+ @override
+ String get settings_allowAll => 'Alles toestaan';
+
+ @override
+ String get settings_telemetryBaseMode => 'Telemetrie-basismodus';
+
+ @override
+ String get settings_telemetryLocationMode => 'Telemetrie-locatiemodus';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'Telemetrie-omgevingsmodus';
+
+ @override
+ String get settings_advertLocation => 'Advertentielocatie';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Locatie opnemen in advertentie';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Multi-ACKs: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Telemetrie-modus bijgewerkt';
+
@override
String get settings_actions => 'Acties';
@@ -689,6 +733,49 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatische route rotatie is uitgeschakeld';
+ @override
+ String get appSettings_maxRouteWeight => 'Maximale gewicht voor de route';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'เริ่มต้น gewicht van de route';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Startgewicht voor nieuwe, ontdekte routes';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Toename in het gewicht van het succes';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Gewicht wordt toegevoegd aan een route na een succesvolle levering.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Vermindering van het gewicht van fouten';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Gewicht verwijderd van een pad na een mislukte levering';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Aantal pogingen om berichten te versturen';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Batterij';
@@ -937,6 +1024,40 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Laast gezien $days dagen geleden';
}
+ @override
+ String get contact_info => 'Contactinformatie';
+
+ @override
+ String get contact_settings => 'Contactinstellingen';
+
+ @override
+ String get contact_telemetry => 'Telemetrie';
+
+ @override
+ String get contact_lastSeen => 'Laatst gezien';
+
+ @override
+ String get contact_clearChat => 'Chat leegmaken';
+
+ @override
+ String get contact_teleBase => 'Telemetrie_basis';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Sta delen van batterij niveau en basis telemetrie toe';
+
+ @override
+ String get contact_teleLoc => 'Telemetrielocatie';
+
+ @override
+ String get contact_teleLocSubtitle => 'Locatiegegevens delen toestaan';
+
+ @override
+ String get contact_teleEnv => 'Telemetrieomgeving';
+
+ @override
+ String get contact_teleEnvSubtitle => 'Delen van omgevingsensordata toestaan';
+
@override
String get channels_title => 'Kanaal';
@@ -1553,6 +1674,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_otherNodes => 'Andere Nodes';
+ @override
+ String get map_showOverlaps => 'Herhalingssleutel overlapt';
+
@override
String get map_keyPrefix => 'Prefix sleutel';
@@ -1597,6 +1721,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get map_runTrace => 'Padeshulp traceren';
+ @override
+ String get map_runTraceWithReturnPath => 'Terugkeren op hetzelfde pad.';
+
@override
String get map_removeLast => 'Verwijder Laatste';
@@ -3342,4 +3469,100 @@ 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 scanner_linuxPairingShowPin => 'Toon PIN';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'PIN verbergen';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'Bluetooth‑koppelings‑PIN';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Voer PIN in voor $deviceName (laat leeg als er geen is).';
+ }
}
diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart
index 6378e74..5c66761 100644
--- a/lib/l10n/app_localizations_pl.dart
+++ b/lib/l10n/app_localizations_pl.dart
@@ -42,7 +42,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get common_deleteAll => 'Usuń wszystko';
@override
- String get common_close => 'Zamknąć';
+ String get common_close => 'Zamknij';
@override
String get common_edit => 'Edytuj';
@@ -75,7 +75,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get common_copy => 'Kopiuj';
@override
- String get common_retry => 'Spróbować';
+ String get common_retry => 'Ponów';
@override
String get common_hide => 'Ukryj';
@@ -87,10 +87,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get common_enable => 'Włącz';
@override
- String get common_disable => 'Wyłączyć';
+ String get common_disable => 'Wyłącz';
@override
- String get common_reboot => 'Zrestartować';
+ String get common_reboot => 'Uruchom ponownie';
@override
String get common_loading => 'Ładowanie...';
@@ -152,7 +152,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get tcpErrorUnsupported =>
- 'Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.';
+ 'Transport TCP nie jest obsługiwany na tej platformie.';
@override
String get tcpErrorTimedOut =>
@@ -168,7 +168,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get usbScreenSubtitle =>
- 'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.';
+ 'Wybierz wykryte urządzenie szeregowe i połącz się bezpośrednio ze swoim węzłem MeshCore.';
@override
String get usbScreenStatus => 'Wybierz urządzenie USB';
@@ -253,7 +253,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get scanner_disconnecting => 'Odłączanie...';
@override
- String get scanner_notConnected => 'Niepołączony';
+ String get scanner_notConnected => 'Nie połączono';
@override
String scanner_connectedTo(String deviceName) {
@@ -327,7 +327,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_nodeNameHint => 'Wprowadź nazwę węzła';
@override
- String get settings_nodeNameUpdated => 'Imię zaktualizowane';
+ String get settings_nodeNameUpdated => 'Nazwa zaktualizowana';
@override
String get settings_radioSettings => 'Ustawienia radia';
@@ -378,22 +378,22 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_longitude => 'Długość';
@override
- String get settings_contactSettings => 'Ustawienia kontaktowe';
+ String get settings_contactSettings => 'Ustawienia kontaktów';
@override
String get settings_contactSettingsSubtitle =>
'Ustawienia dotyczące sposobu dodawania kontaktów';
@override
- String get settings_privacyMode => 'Tryb Prywatny';
+ String get settings_privacyMode => 'Tryb prywatności';
@override
String get settings_privacyModeSubtitle =>
- 'Ukryj imię/lokalizację w reklamach';
+ 'Ukryj imię/lokalizację w rozgłoszeniach';
@override
String get settings_privacyModeToggle =>
- 'Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w reklamach.';
+ 'Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.';
@override
String get settings_privacyModeEnabled => 'Tryb prywatności włączony';
@@ -401,28 +401,73 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Tryb prywatności wyłączony';
+ @override
+ String get settings_privacy => 'Ustawienia prywatności';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Kontroluj jakie informacje są udostępniane.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Wybierz jakie informacje urządzenie udostępni innym.';
+
+ @override
+ String get settings_denyAll => 'Odmów wszystkim';
+
+ @override
+ String get settings_allowByContact => 'Zezwalaj według flag kontaktowych';
+
+ @override
+ String get settings_allowAll => 'Zezwalaj na wszystko';
+
+ @override
+ String get settings_telemetryBaseMode => 'Tryb podstawowy telemetrii';
+
+ @override
+ String get settings_telemetryLocationMode => 'Tryb położenia telemetrycznego';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Tryb środowiska telemetrycznego';
+
+ @override
+ String get settings_advertLocation => 'Lokalizacja reklamowa';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Uwzględnij lokalizację w ogłoszeniu';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Wiele potwierdzeń: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated =>
+ 'Tryb telemetryczny zaktualizowany';
+
@override
String get settings_actions => 'Działania';
@override
- String get settings_sendAdvertisement => 'Wyślij Reklamę';
+ String get settings_sendAdvertisement => 'Wyślij rozgłoszenie';
@override
- String get settings_sendAdvertisementSubtitle =>
- 'Obecność transmisji jest teraz';
+ String get settings_sendAdvertisementSubtitle => 'Nadaj obecność teraz';
@override
- String get settings_advertisementSent => 'Reklama wysłana';
+ String get settings_advertisementSent => 'Rozgłoszenie wysłane';
@override
- String get settings_syncTime => 'Czas synchronizacji';
+ String get settings_syncTime => 'Synchronizacja czasu';
@override
String get settings_syncTimeSubtitle =>
'Ustaw zegar urządzenia na czas telefonu.';
@override
- String get settings_timeSynchronized => 'Synchronizacja czasu';
+ String get settings_timeSynchronized => 'Czas zsynchronizowany';
@override
String get settings_refreshContacts => 'Odśwież Kontakty';
@@ -445,20 +490,20 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_debug => 'Debug';
@override
- String get settings_bleDebugLog => 'Log błędów BLE';
+ String get settings_bleDebugLog => 'Dziennik debugowania BLE';
@override
String get settings_bleDebugLogSubtitle =>
'Polecenia BLE, odpowiedzi i surowe dane';
@override
- String get settings_appDebugLog => 'Log Wykonywania Aplikacji';
+ String get settings_appDebugLog => 'Dziennik debugowania aplikacji';
@override
String get settings_appDebugLogSubtitle => 'Komunikaty debugowania aplikacji';
@override
- String get settings_about => 'O mnie';
+ String get settings_about => 'O aplikacji';
@override
String settings_aboutVersion(String version) {
@@ -470,14 +515,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_aboutDescription =>
- 'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
+ 'Otwartoźródłowy klient Flutter dla urządzeń MeshCore LoRa do sieci mesh.';
@override
String get settings_aboutOpenMeteoAttribution =>
'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)';
@override
- String get settings_infoName => 'Imię';
+ String get settings_infoName => 'Nazwa';
@override
String get settings_infoId => 'ID';
@@ -498,7 +543,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_infoChannelCount => 'Liczba kanałów';
@override
- String get settings_presets => 'Preset';
+ String get settings_presets => 'Presety';
@override
String get settings_frequency => 'Częstotliwość (MHz)';
@@ -514,13 +559,13 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_bandwidth => 'Przepustowość';
@override
- String get settings_spreadingFactor => 'Rozkład Czynnika';
+ String get settings_spreadingFactor => 'Współczynnik rozpraszania';
@override
- String get settings_codingRate => 'Stawka Kodowania';
+ String get settings_codingRate => 'Współczynnik kodowania';
@override
- String get settings_txPower => 'TX Moc (dBm)';
+ String get settings_txPower => 'Moc TX (dBm)';
@override
String get settings_txPowerHelper => '0 - 22';
@@ -566,7 +611,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_language => 'Język';
@override
- String get appSettings_languageSystem => 'Domyślny systemowy';
+ String get appSettings_languageSystem => 'Domyślny systemu';
@override
String get appSettings_languageEn => 'English';
@@ -628,7 +673,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_enableNotificationsSubtitle =>
- 'Otrzymuj powiadomienia o wiadomościach i reklamach.';
+ 'Otrzymuj powiadomienia o wiadomościach i rozgłoszeniach.';
@override
String get appSettings_notificationPermissionDenied =>
@@ -658,18 +703,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_advertisementNotifications =>
- 'Powiadomienia Reklamowe';
+ 'Powiadomienia o rozgłoszeniach';
@override
String get appSettings_advertisementNotificationsSubtitle =>
- 'Wyświetl powiadomienie, gdy zostaną odkryte nowe węzły.';
+ 'Wyświetl powiadomienie, gdy zostaną wykryte nowe węzły.';
@override
String get appSettings_messaging => 'Wiadomości';
@override
String get appSettings_clearPathOnMaxRetry =>
- 'Wyczyść Ścieżkę na Maksymalnej Próbie';
+ 'Wyczyść ścieżkę po maks. liczbie prób';
@override
String get appSettings_clearPathOnMaxRetrySubtitle =>
@@ -677,14 +722,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_pathsWillBeCleared =>
- 'Droga będzie wyczyszczona po 5 nieudanych próbach.';
+ 'Ścieżka zostanie wyczyszczona po 5 nieudanych próbach.';
@override
String get appSettings_pathsWillNotBeCleared =>
- 'Droga nie zostanie automatycznie wyczyszczona.';
+ 'Ścieżka nie zostanie automatycznie wyczyszczona.';
@override
- String get appSettings_autoRouteRotation => 'Automatyczne Rotowanie Trasy';
+ String get appSettings_autoRouteRotation => 'Automatyczna rotacja trasy';
@override
String get appSettings_autoRouteRotationSubtitle =>
@@ -698,6 +743,49 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatyczne obracanie tras wyłączone';
+ @override
+ String get appSettings_maxRouteWeight =>
+ 'Maksymalny dopuszczalny ciężar pojazdu';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Początkowa waga trasy';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Początkowa waga dla nowych, odkrytych ścieżek';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement => 'Wzrost wagi sukcesu';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Waga dodana do ścieżki po pomyślnym dostarczeniu';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Zmniejszenie wagi kary';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Waga usunięta z trasy po nieudanej dostawie';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Maksymalna liczba prób wysłania wiadomości';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Liczba prób ponownego wysłania wiadomości przed oznaczaniem jej jako nieudanej';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Bateria';
@@ -706,7 +794,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String appSettings_batteryChemistryPerDevice(String deviceName) {
- return 'Ustawione na urządzenie ($deviceName)';
+ return 'Ustaw dla urządzenia ($deviceName)';
}
@override
@@ -726,11 +814,11 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_mapDisplay => 'Wyświetlanie mapy';
@override
- String get appSettings_showRepeaters => 'Pokaż Powtórniki';
+ String get appSettings_showRepeaters => 'Pokaż przekaźniki';
@override
String get appSettings_showRepeatersSubtitle =>
- 'Wyświetl węzły powtarzające się na mapie';
+ 'Wyświetl węzły przekaźników na mapie';
@override
String get appSettings_showChatNodes => 'Pokaż Węzły Rozmowy';
@@ -758,13 +846,13 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get appSettings_mapTimeFilter => 'Filtrowanie Czasu Mapy';
+ String get appSettings_mapTimeFilter => 'Filtr czasu mapy';
@override
String get appSettings_showNodesDiscoveredWithin => 'Pokaż węzły odkryte w:';
@override
- String get appSettings_allTime => 'Wszystko czasowo';
+ String get appSettings_allTime => 'Cały czas';
@override
String get appSettings_lastHour => 'Ostatnia godzina';
@@ -776,10 +864,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_last24Hours => 'Ostatnie 24 godziny';
@override
- String get appSettings_lastWeek => 'Tydzień temu';
+ String get appSettings_lastWeek => 'Ostatni tydzień';
@override
- String get appSettings_offlineMapCache => 'Bufor Map Offline';
+ String get appSettings_offlineMapCache => 'Pamięć podręczna map offline';
@override
String get appSettings_unitsTitle => 'Jednostki';
@@ -791,7 +879,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get appSettings_unitsImperial => 'Imperialne (ft / mi)';
@override
- String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
+ String get appSettings_noAreaSelected => 'Nie wybrano żadnego obszaru.';
@override
String appSettings_areaSelectedZoom(int minZoom, int maxZoom) {
@@ -806,15 +894,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get appSettings_appDebugLoggingSubtitle =>
- 'Loguj wiadomości debugowania aplikacji w celu rozwiązywania problemów.';
+ 'Rejestruj komunikaty debugowania aplikacji w celu diagnozowania problemów.';
@override
String get appSettings_appDebugLoggingEnabled =>
- 'Zdebugowanie aplikacji włączone';
+ 'Logowanie debugowania aplikacji włączone';
@override
String get appSettings_appDebugLoggingDisabled =>
- 'Zasubskrybowane logi debugowania aplikacji wyłączone.';
+ 'Logowanie debugowania aplikacji wyłączone.';
@override
String get contacts_title => 'Kontakty';
@@ -824,7 +912,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_contactsWillAppear =>
- 'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
+ 'Kontakty będą wyświetlane, gdy urządzenia nadają rozgłoszenia.';
@override
String get contacts_unread => 'Nieprzeczytane';
@@ -834,7 +922,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String contacts_searchContacts(int number, String str) {
- return 'Wyszukaj kontakty...';
+ String _temp0 = intl.Intl.pluralLogic(
+ number,
+ locale: localeName,
+ other: 'kontaktu',
+ many: 'kontaktów',
+ few: 'kontakty',
+ one: 'kontakt',
+ );
+ return 'Wyszukaj $number$str $_temp0...';
}
@override
@@ -849,7 +945,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String contacts_searchRepeaters(int number, String str) {
- return 'Wyszukaj $number$str powtórników...';
+ return 'Wyszukaj $number$str przekaźników...';
}
@override
@@ -869,11 +965,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String contacts_removeConfirm(String contactName) {
- return 'Usuń $contactName z kontaktów?';
+ return 'Usunąć $contactName z kontaktów?';
}
@override
- String get contacts_manageRepeater => 'Zarządzaj Powtórzami';
+ String get contacts_manageRepeater => 'Zarządzaj przekaźnikiem';
@override
String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju';
@@ -923,29 +1019,65 @@ class AppLocalizationsPl extends AppLocalizations {
String get contacts_noMembers => 'Brak członków';
@override
- String get contacts_lastSeenNow => 'Ostatnie połączenie';
+ String get contacts_lastSeenNow => 'niedawno';
@override
String contacts_lastSeenMinsAgo(int minutes) {
- return 'Ostatnie połączenie $minutes min temu';
+ return '~ $minutes min';
}
@override
- String get contacts_lastSeenHourAgo => 'Ostatni raz widziany 1 godzinę temu';
+ String get contacts_lastSeenHourAgo => '~ 1 godz.';
@override
String contacts_lastSeenHoursAgo(int hours) {
- return 'Ostatnie połączenie $hours godzin temu';
+ return '~ $hours godz.';
}
@override
- String get contacts_lastSeenDayAgo => 'Ostatni raz widziany 1 dzień temu';
+ String get contacts_lastSeenDayAgo => '~ 1 dzień';
@override
String contacts_lastSeenDaysAgo(int days) {
- return 'Ostatnie połączenie $days dni temu';
+ return '~ $days dni';
}
+ @override
+ String get contact_info => 'Informacje kontaktowe';
+
+ @override
+ String get contact_settings => 'Ustawienia kontaktowe';
+
+ @override
+ String get contact_telemetry => 'Telemetryka';
+
+ @override
+ String get contact_lastSeen => 'Ostatnio widziany';
+
+ @override
+ String get contact_clearChat => 'Wyczyść czat';
+
+ @override
+ String get contact_teleBase => 'Baza telemetryczna';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych';
+
+ @override
+ String get contact_teleLoc => 'Lokalizacja telemetryczna';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Zezwalaj na udostępnianie danych lokalizacji';
+
+ @override
+ String get contact_teleEnv => 'Środowisko telemetryczne';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Zezwalaj na udostępnianie danych czujników środowiskowych';
+
@override
String get channels_title => 'Kanały';
@@ -967,13 +1099,13 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get channels_hashtagChannel => 'Kanał z hashtagami';
+ String get channels_hashtagChannel => 'Kanał hashtagów';
@override
String get channels_public => 'Publiczny';
@override
- String get channels_private => 'Prywatne';
+ String get channels_private => 'Prywatny';
@override
String get channels_publicChannel => 'Kanał publiczny';
@@ -1021,7 +1153,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get channels_usePublicChannel => 'Użyj kanału publicznego';
@override
- String get channels_standardPublicPsk => 'Standard public PSK';
+ String get channels_standardPublicPsk => 'Standardowy publiczny PSK';
@override
String get channels_pskHex => 'PSK (Hex)';
@@ -1033,7 +1165,8 @@ class AppLocalizationsPl extends AppLocalizations {
String get channels_enterChannelName => 'Proszę podać nazwę kanału.';
@override
- String get channels_pskMustBe32Hex => 'PSK musi mieć 32 znaki szesnastkowe.';
+ String get channels_pskMustBe32Hex =>
+ 'PSK musi składać się z 32 znaków szesnastkowych.';
@override
String channels_channelAdded(String name) {
@@ -1069,7 +1202,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get channels_sortLatestMessages => 'Najnowsze wiadomości';
@override
- String get channels_sortUnread => 'Niezgłoszone';
+ String get channels_sortUnread => 'Nieprzeczytane';
@override
String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał';
@@ -1128,7 +1261,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String chat_replyTo(String name) {
- return 'Odpowiedz $name';
+ return 'Odpowiedz do $name';
}
@override
@@ -1154,11 +1287,11 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_messageDeleted => 'Wiadomość usunięta';
@override
- String get chat_retryingMessage => 'Próba ponowienia';
+ String get chat_retryingMessage => 'Ponawianie wiadomości';
@override
String chat_retryCount(int current, int max) {
- return 'Spróbuj $current/$max';
+ return 'Próba $current/$max';
}
@override
@@ -1177,10 +1310,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get emojiCategorySmileys => 'Emoji';
@override
- String get emojiCategoryGestures => 'Gestikulacje';
+ String get emojiCategoryGestures => 'Gesty';
@override
- String get emojiCategoryHearts => 'Serce';
+ String get emojiCategoryHearts => 'Serca';
@override
String get emojiCategoryObjects => 'Obiekty';
@@ -1232,13 +1365,13 @@ class AppLocalizationsPl extends AppLocalizations {
'Włącz logowanie debugowania aplikacji w ustawieniach';
@override
- String get debugLog_frames => 'Ramy';
+ String get debugLog_frames => 'Ramki';
@override
- String get debugLog_rawLogRx => 'Surowe Log-RX';
+ String get debugLog_rawLogRx => 'Surowy log RX';
@override
- String get debugLog_noBleActivity => 'Brak aktywności BLE jeszcze.';
+ String get debugLog_noBleActivity => 'Brak aktywności BLE.';
@override
String debugFrame_length(int count) {
@@ -1255,12 +1388,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String debugFrame_destinationPubKey(String pubKey) {
- return '- Oznaczenie PubKey: $pubKey';
+ return '- Docelowy klucz publiczny: $pubKey';
}
@override
String debugFrame_timestamp(int timestamp) {
- return '- Timestamp: $timestamp';
+ return '- Znacznik czasu: $timestamp';
}
@override
@@ -1277,7 +1410,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get debugFrame_textTypeCli => 'CLI';
@override
- String get debugFrame_textTypePlain => 'Proste';
+ String get debugFrame_textTypePlain => 'Zwykły';
@override
String debugFrame_text(String text) {
@@ -1285,7 +1418,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get debugFrame_hexDump => 'Wyjście SzESZCZNULNE:';
+ String get debugFrame_hexDump => 'Zrzut hex:';
@override
String get chat_pathManagement => 'Zarządzanie ścieżkami';
@@ -1300,7 +1433,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_autoUseSavedPath => 'Automatyczne (użyj zapisanej ścieżki)';
@override
- String get chat_forceFloodMode => 'Wymusz Tryb Powodowany';
+ String get chat_forceFloodMode => 'Wymuś tryb zalewowy';
@override
String get chat_recentAckPaths =>
@@ -1311,18 +1444,20 @@ class AppLocalizationsPl extends AppLocalizations {
'Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.';
@override
- String get chat_hopSingular => 'Skacz';
+ String get chat_hopSingular => 'skok';
@override
- String get chat_hopPlural => 'skoczkowie';
+ String get chat_hopPlural => 'skoki';
@override
String chat_hopsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'hops',
- one: 'hop',
+ other: 'skoków',
+ many: 'skoków',
+ few: 'skoki',
+ one: 'skok',
);
return '$count $_temp0';
}
@@ -1335,13 +1470,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_noPathHistoryYet =>
- 'Brak jeszcze historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.';
+ 'Brak historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.';
@override
String get chat_pathActions => 'Działania ścieżki:';
@override
- String get chat_setCustomPath => 'Ustaw Ścieżkę Dostosowaną';
+ String get chat_setCustomPath => 'Ustaw ścieżkę niestandardową';
@override
String get chat_setCustomPathSubtitle => 'Ręcznie określ trasę.';
@@ -1351,11 +1486,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_clearPathSubtitle =>
- 'Zmusz do ponownej identyfikacji przy następnym wysłaniu';
+ 'Wymuś ponowne wyznaczenie trasy przy następnym wysłaniu';
@override
String get chat_pathCleared =>
- 'Ścieżka oczyszczona. Kolejne powiadomienie odnajdzie trasę.';
+ 'Ścieżka wyczyszczona. Następna wiadomość odnajdzie trasę.';
@override
String get chat_floodModeSubtitle =>
@@ -1363,7 +1498,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get chat_floodModeEnabled =>
- 'Tryb powodziowy włączony. Włącz ponownie za pomocą ikony routingu w pasku narzędzi.';
+ 'Tryb zalewowy włączony. Przełącz z powrotem ikoną routingu w pasku aplikacji.';
@override
String get chat_fullPath => 'Pełna ścieżka';
@@ -1395,7 +1530,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Urządzenie nie zostało jeszcze potwierdzone.';
@override
- String get chat_type => 'Wprowadź';
+ String get chat_type => 'Typ';
@override
String get chat_path => 'Ścieżka';
@@ -1407,7 +1542,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get chat_compressOutgoingMessages => 'Kompresuj wychodzące wiadomości';
@override
- String get chat_floodForced => 'Powodowana Powódź';
+ String get chat_floodForced => 'Zalew (wymuszony)';
@override
String get chat_directForced => 'Bezpośrednio (wymuszono)';
@@ -1418,7 +1553,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get chat_floodAuto => 'Powodzie (automatyczne)';
+ String get chat_floodAuto => 'Zalew (automatyczny)';
@override
String get chat_direct => 'Bezpośrednio';
@@ -1428,7 +1563,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String chat_unread(int count) {
- return 'Niezgłoszone: $count';
+ return 'Nieprzeczytane: $count';
}
@override
@@ -1472,14 +1607,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String map_pinsCount(int count) {
- return 'Pinki: $count';
+ return 'Pinezki: $count';
}
@override
String get map_chat => 'Rozmowa';
@override
- String get map_repeater => 'Powtórzacz';
+ String get map_repeater => 'Przekaźnik';
@override
String get map_room => 'Pokój';
@@ -1488,13 +1623,13 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_sensor => 'Czujnik';
@override
- String get map_pinDm => 'Zablokuj (DM)';
+ String get map_pinDm => 'Pinezka (DM)';
@override
- String get map_pinPrivate => 'Zablokuj (Prywatnie)';
+ String get map_pinPrivate => 'Pinezka (prywatna)';
@override
- String get map_pinPublic => 'Oznacz jako publiczne';
+ String get map_pinPublic => 'Pinezka (publiczna)';
@override
String get map_lastSeen => 'Ostatni raz widziany';
@@ -1516,10 +1651,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_shareMarkerHere => 'Udostępnij znacznik tutaj';
@override
- String get map_setAsMyLocation => 'Ustaw jako moje lokalizację';
+ String get map_setAsMyLocation => 'Ustaw jako moją lokalizację';
@override
- String get map_pinLabel => 'Oznacz etykietę';
+ String get map_pinLabel => 'Etykieta pinezki';
@override
String get map_label => 'Etykieta';
@@ -1541,12 +1676,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String map_publicLocationShareConfirm(String channelLabel) {
- return 'Wkrótce udostępnisz lokalizację w $channelLabel. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.';
+ return 'Zamierzasz udostępnić lokalizację w $channelLabel. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.';
}
@override
String get map_connectToShareMarkers =>
- 'Połącz się z urządzeniem, aby udostępniać znacznik.';
+ 'Połącz się z urządzeniem, aby udostępniać znaczniki.';
@override
String get map_filterNodes => 'Filtruj Węzły';
@@ -1558,11 +1693,14 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_chatNodes => 'Węzły czatu';
@override
- String get map_repeaters => 'Powtarzacze';
+ String get map_repeaters => 'Przekaźniki';
@override
String get map_otherNodes => 'Inne węzły';
+ @override
+ String get map_showOverlaps => 'Nakładające się klucze powtarzalne';
+
@override
String get map_keyPrefix => 'Prefiks klucza';
@@ -1570,35 +1708,35 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_filterByKeyPrefix => 'Filtruj po prefiksie klucza';
@override
- String get map_publicKeyPrefix => 'Przewód klucza publicznego';
+ String get map_publicKeyPrefix => 'Prefiks klucza publicznego';
@override
- String get map_markers => 'Oznaczarki';
+ String get map_markers => 'Znaczniki';
@override
- String get map_showSharedMarkers => 'Pokaż współdzielone znaki.';
+ String get map_showSharedMarkers => 'Pokaż udostępnione znaczniki.';
@override
String get map_showGuessedLocations =>
- 'Wyświetl lokalizacje zgadanych węzłów';
+ 'Pokaż przypuszczalne lokalizacje węzłów';
@override
- String get map_showDiscoveryContacts => 'Pokaż kontakty odkrywania';
+ String get map_showDiscoveryContacts => 'Pokaż odkryte kontakty';
@override
- String get map_guessedLocation => 'Wydana lokalizacja';
+ String get map_guessedLocation => 'Przypuszczalna lokalizacja';
@override
- String get map_lastSeenTime => 'Ostatni raz widiany';
+ String get map_lastSeenTime => 'Ostatni raz widziany';
@override
- String get map_sharedPin => 'Podzielony PIN';
+ String get map_sharedPin => 'Udostępniona pinezka';
@override
String get map_joinRoom => 'Dołącz do pokoju';
@override
- String get map_manageRepeater => 'Zarządzaj Powtórzami';
+ String get map_manageRepeater => 'Zarządzaj przekaźnikiem';
@override
String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.';
@@ -1606,6 +1744,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get map_runTrace => 'Uruchom ślad ścieżki';
+ @override
+ String get map_runTraceWithReturnPath => 'Wróć z powrotem tą samą ścieżką';
+
@override
String get map_removeLast => 'Usuń ostatni';
@@ -1613,22 +1754,22 @@ class AppLocalizationsPl extends AppLocalizations {
String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.';
@override
- String get mapCache_title => 'Bufor Map Offline';
+ String get mapCache_title => 'Pamięć podręczna map offline';
@override
String get mapCache_selectAreaFirst =>
- 'Wybierz obszar do wstępnego pobrania.';
+ 'Najpierw wybierz obszar do zapisania w pamięci podręcznej.';
@override
String get mapCache_noTilesToDownload =>
- 'Brak dostępnych płytek do pobrania dla tego obszaru.';
+ 'Brak kafelków do pobrania dla tego obszaru.';
@override
- String get mapCache_downloadTilesTitle => 'Pobierz płytki';
+ String get mapCache_downloadTilesTitle => 'Pobierz kafelki';
@override
String mapCache_downloadTilesPrompt(int count) {
- return 'Pobierz $count płytek do użytku offline?';
+ return 'Pobrać $count kafelków do użytku offline?';
}
@override
@@ -1636,12 +1777,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String mapCache_cachedTiles(int count) {
- return 'Pamiętanych $count płytek';
+ return 'Zapisano w pamięci podręcznej $count kafelków';
}
@override
String mapCache_cachedTilesWithFailed(int downloaded, int failed) {
- return 'Pamiętane $downloaded płytki ($failed nieudane)';
+ return 'Zapisano w pamięci podręcznej $downloaded kafelków ($failed nieudanych)';
}
@override
@@ -1650,14 +1791,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get mapCache_clearOfflineCachePrompt =>
- 'Usuń wszystkie tymczasowe kafelki mapy?';
+ 'Usunąć wszystkie zapisane kafelki mapy?';
@override
String get mapCache_offlineCacheCleared =>
- 'Pamięć podręczna offline została wyczyszczona';
+ 'Wyczyszczono pamięć podręczną offline';
@override
- String get mapCache_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
+ String get mapCache_noAreaSelected => 'Nie wybrano żadnego obszaru.';
@override
String get mapCache_cacheArea => 'Obszar pamięci podręcznej';
@@ -1666,11 +1807,11 @@ class AppLocalizationsPl extends AppLocalizations {
String get mapCache_useCurrentView => 'Użyj aktualnego widoku';
@override
- String get mapCache_zoomRange => 'Zakres powiększenia';
+ String get mapCache_zoomRange => 'Zakres przybliżenia';
@override
String mapCache_estimatedTiles(int count) {
- return 'Szacunkowa liczba płytek: $count';
+ return 'Szacowana liczba kafelków: $count';
}
@override
@@ -1679,7 +1820,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get mapCache_downloadTilesButton => 'Pobierz Paski';
+ String get mapCache_downloadTilesButton => 'Pobierz kafelki';
@override
String get mapCache_clearCacheButton => 'Wyczyść pamięć podręczną';
@@ -1739,13 +1880,13 @@ class AppLocalizationsPl extends AppLocalizations {
String get time_month => 'miesiąc';
@override
- String get time_months => 'miesiace';
+ String get time_months => 'miesiące';
@override
String get time_minutes => 'minuty';
@override
- String get time_allTime => 'Wszystko czasowo';
+ String get time_allTime => 'Cały czas';
@override
String get dialog_disconnect => 'Odłącz';
@@ -1755,7 +1896,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Czy na pewno chcesz się odłączyć od tego urządzenia?';
@override
- String get login_repeaterLogin => 'Powtórz Logowanie';
+ String get login_repeaterLogin => 'Logowanie do przekaźnika';
@override
String get login_roomLogin => 'Logowanie do pokoju';
@@ -1775,14 +1916,14 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get login_repeaterDescription =>
- 'Wprowadź hasło do powtarzacza, aby uzyskać dostęp do ustawień i statusu.';
+ 'Wprowadź hasło do przekaźnika, aby uzyskać dostęp do ustawień i stanu.';
@override
String get login_roomDescription =>
'Wprowadź hasło do pokoju, aby uzyskać dostęp do ustawień i statusu.';
@override
- String get login_routing => 'Przekierowanie';
+ String get login_routing => 'Trasowanie';
@override
String get login_routingMode => 'Tryb routingu';
@@ -1791,7 +1932,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get login_autoUseSavedPath => 'Automatycznie (użyj zapisanej ścieżki)';
@override
- String get login_forceFloodMode => 'Wymusz Tryb Powodowany';
+ String get login_forceFloodMode => 'Wymuś tryb zalewowy';
@override
String get login_managePaths => 'Zarządzaj Ścieżkami';
@@ -1811,17 +1952,17 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get login_failedMessage =>
- 'Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.';
+ 'Logowanie nie powiodło się. Hasło jest nieprawidłowe albo przekaźnik jest nieosiągalny.';
@override
- String get common_reload => 'Ponownie załadować';
+ String get common_reload => 'Odśwież';
@override
String get common_clear => 'Wyczyść';
@override
String path_currentPath(String path) {
- return 'Aktualny ścieżka: $path';
+ return 'Aktualna ścieżka: $path';
}
@override
@@ -1829,8 +1970,10 @@ class AppLocalizationsPl extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
- other: 'hops',
- one: 'hop',
+ other: 'skoków',
+ many: 'skoków',
+ few: 'skoki',
+ one: 'skok',
);
return 'Użyj ścieżki $count $_temp0.';
}
@@ -1839,7 +1982,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get path_enterCustomPath => 'Wprowadź własną ścieżkę';
@override
- String get path_currentPathLabel => 'Aktualny ścieżka';
+ String get path_currentPathLabel => 'Aktualna ścieżka';
@override
String get path_hexPrefixInstructions =>
@@ -1847,21 +1990,21 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get path_hexPrefixExample =>
- 'A1,F2,3C (każedy węzeł używa pierwszego bajtu swojego klucza publicznego)';
+ 'A1,F2,3C (każdy węzeł używa pierwszego bajtu swojego klucza publicznego)';
@override
- String get path_labelHexPrefixes => 'Ścieżka (przesunięcia bitowe)';
+ String get path_labelHexPrefixes => 'Ścieżka (prefiksy hex)';
@override
String get path_helperMaxHops =>
- 'Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).';
+ 'Maksymalnie 64 skoki. Każdy prefiks ma 2 znaki szesnastkowe (1 bajt).';
@override
String get path_selectFromContacts => 'Albo wybierz z kontaktów:';
@override
String get path_noRepeatersFound =>
- 'Nie znaleziono repeaterów ani serwerów pokoi.';
+ 'Nie znaleziono przekaźników ani serwerów pokoi.';
@override
String get path_customPathsRequire =>
@@ -1880,7 +2023,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get path_setPath => 'Ustaw Ścieżkę';
@override
- String get repeater_management => 'Zarządzanie Powtórzami';
+ String get repeater_management => 'Zarządzanie przekaźnikami';
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@@ -1893,10 +2036,10 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_statusSubtitle =>
- 'Wyświetl status powtarzacza, statystyki i sąsiadów.';
+ 'Wyświetl status przekaźnika, statystyki i sąsiadów.';
@override
- String get repeater_telemetry => 'Telemetry';
+ String get repeater_telemetry => 'Telemetria';
@override
String get repeater_telemetrySubtitle =>
@@ -1906,7 +2049,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_cli => 'CLI';
@override
- String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
+ String get repeater_cliSubtitle => 'Wyślij polecenia do przekaźnika';
@override
String get repeater_neighbors => 'Sąsiedzi';
@@ -1919,10 +2062,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_settings => 'Ustawienia';
@override
- String get repeater_settingsSubtitle => 'Skonfiguruj parametry powtarzacza';
+ String get repeater_settingsSubtitle => 'Skonfiguruj parametry przekaźnika';
@override
- String get repeater_statusTitle => 'Status powtarzacza';
+ String get repeater_statusTitle => 'Status przekaźnika';
@override
String get repeater_routingMode => 'Tryb routingu';
@@ -1932,7 +2075,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Automatycznie (użyj zapisanej ścieżki)';
@override
- String get repeater_forceFloodMode => 'Wymusz Tryb Powodowany';
+ String get repeater_forceFloodMode => 'Wymuś tryb zalewowy';
@override
String get repeater_pathManagement => 'Zarządzanie ścieżkami';
@@ -1941,7 +2084,8 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_refresh => 'Odśwież';
@override
- String get repeater_statusRequestTimeout => 'Życzenie statusu timed out.';
+ String get repeater_statusRequestTimeout =>
+ 'Przekroczono czas oczekiwania na status.';
@override
String repeater_errorLoadingStatus(String error) {
@@ -1958,7 +2102,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_clockAtLogin => 'Godzina (przy logowaniu)';
@override
- String get repeater_uptime => 'Dostępność';
+ String get repeater_uptime => 'Czas pracy';
@override
String get repeater_queueLength => 'Długość kolejki';
@@ -1979,10 +2123,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_noiseFloor => 'Poziom Szumów';
@override
- String get repeater_txAirtime => 'TX Airtime';
+ String get repeater_txAirtime => 'Czas nadawania TX';
@override
- String get repeater_rxAirtime => 'RX Airtime';
+ String get repeater_rxAirtime => 'Czas odbioru RX';
@override
String get repeater_packetStatistics => 'Statystyki pakietów';
@@ -2008,17 +2152,17 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String repeater_packetTxTotal(int total, String flood, String direct) {
- return 'Razem: $total, Powodzenie: $flood, Bezpośrednio: $direct';
+ return 'Razem: $total, Zalew: $flood, Bezpośrednio: $direct';
}
@override
String repeater_packetRxTotal(int total, String flood, String direct) {
- return 'Razem: $total, Powodzenie: $flood, Bezpośrednio: $direct';
+ return 'Razem: $total, Zalew: $flood, Bezpośrednio: $direct';
}
@override
String repeater_duplicatesFloodDirect(String flood, String direct) {
- return 'Powodzie: $flood, Bezpośrednie: $direct';
+ return 'Zalew: $flood, Bezpośrednie: $direct';
}
@override
@@ -2027,28 +2171,28 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get repeater_settingsTitle => 'Ustawienia Powtórki';
+ String get repeater_settingsTitle => 'Ustawienia przekaźnika';
@override
String get repeater_basicSettings => 'Podstawowe Ustawienia';
@override
- String get repeater_repeaterName => 'Nazwa Powtórnika';
+ String get repeater_repeaterName => 'Nazwa przekaźnika';
@override
- String get repeater_repeaterNameHelper => 'Wyświetl nazwę tego powtarzacza';
+ String get repeater_repeaterNameHelper => 'Wyświetl nazwę tego przekaźnika';
@override
String get repeater_adminPassword => 'Hasło Administracyjne';
@override
- String get repeater_adminPasswordHelper => 'Pełny dostęp hasło';
+ String get repeater_adminPasswordHelper => 'Hasło z pełnym dostępem';
@override
String get repeater_guestPassword => 'Hasło gościa';
@override
- String get repeater_guestPasswordHelper => 'Dostęp tylko do odczytu hasło';
+ String get repeater_guestPasswordHelper => 'Hasło tylko do odczytu';
@override
String get repeater_radioSettings => 'Ustawienia radia';
@@ -2060,7 +2204,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_frequencyHelper => '300-2500 MHz';
@override
- String get repeater_txPower => 'TX Power';
+ String get repeater_txPower => 'Moc TX';
@override
String get repeater_txPowerHelper => '1-30 dBm';
@@ -2069,10 +2213,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_bandwidth => 'Przepustowość';
@override
- String get repeater_spreadingFactor => 'Rozkład Czynnika';
+ String get repeater_spreadingFactor => 'Współczynnik rozpraszania';
@override
- String get repeater_codingRate => 'Stawka kodowania';
+ String get repeater_codingRate => 'Współczynnik kodowania';
@override
String get repeater_locationSettings => 'Ustawienia Lokalizacji';
@@ -2097,7 +2241,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_packetForwardingSubtitle =>
- 'Włącz repeater, aby przekazywać pakiety.';
+ 'Włącz przekaźnik, aby przekazywać pakiety.';
@override
String get repeater_guestAccess => 'Dostęp dla gości';
@@ -2107,17 +2251,17 @@ class AppLocalizationsPl extends AppLocalizations {
'Umożliw dostęp tylko do odczytu dla gości.';
@override
- String get repeater_privacyMode => 'Tryb Prywatności';
+ String get repeater_privacyMode => 'Tryb prywatności';
@override
String get repeater_privacyModeSubtitle =>
- 'Ukryj imię/lokalizację w reklamach';
+ 'Ukryj imię/lokalizację w rozgłoszeniach';
@override
- String get repeater_advertisementSettings => 'Ustawienia Reklam';
+ String get repeater_advertisementSettings => 'Ustawienia rozgłoszeń';
@override
- String get repeater_localAdvertInterval => 'Interwał Reklamy Lokalnej';
+ String get repeater_localAdvertInterval => 'Interwał rozgłoszenia lokalnego';
@override
String repeater_localAdvertIntervalMinutes(int minutes) {
@@ -2125,7 +2269,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get repeater_floodAdvertInterval => 'Interwał Reklamy Powodziowej';
+ String get repeater_floodAdvertInterval => 'Interwał rozgłoszenia zalewowego';
@override
String repeater_floodAdvertIntervalHours(int hours) {
@@ -2134,21 +2278,20 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_encryptedAdvertInterval =>
- 'Zaszyfrowany Interwał Reklamowy';
+ 'Interwał Zaszyfrowanego Rozgłoszenia';
@override
String get repeater_dangerZone => 'Strefa Zagrożeń';
@override
- String get repeater_rebootRepeater => 'Zrestartuj Powtarzacz';
+ String get repeater_rebootRepeater => 'Zrestartuj Przekaźnik';
@override
- String get repeater_rebootRepeaterSubtitle =>
- 'Zrestartuj urządzenie powtarzające.';
+ String get repeater_rebootRepeaterSubtitle => 'Zrestartuj przekaźnik.';
@override
String get repeater_rebootRepeaterConfirm =>
- 'Czy na pewno chcesz zrestartować ten repeater?';
+ 'Czy na pewno chcesz zrestartować ten przekaźnik?';
@override
String get repeater_regenerateIdentityKey => 'Wygeneruj klucz tożsamości';
@@ -2159,18 +2302,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_regenerateIdentityKeyConfirm =>
- 'To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?';
+ 'Zostanie wygenerowana nowa tożsamość dla przekaźnika. Kontynuować?';
@override
String get repeater_eraseFileSystem => 'Wyczyść System Plików';
@override
String get repeater_eraseFileSystemSubtitle =>
- 'Sformatuj system plików powielacza';
+ 'Sformatuj system plików przekaźnika';
@override
String get repeater_eraseFileSystemConfirm =>
- 'OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z powtarzacza. Nie da się tego cofnąć!';
+ 'OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z przekaźnika. Nie da się tego cofnąć!';
@override
String get repeater_eraseSerialOnly =>
@@ -2201,10 +2344,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_refreshBasicSettings => 'Odśwież Podstawowe Ustawienia';
@override
- String get repeater_refreshRadioSettings => 'Odśwież Ustawienia Radio';
+ String get repeater_refreshRadioSettings => 'Odśwież ustawienia radia';
@override
- String get repeater_refreshTxPower => 'Odśwież TX power';
+ String get repeater_refreshTxPower => 'Odśwież moc TX';
@override
String get repeater_refreshLocationSettings =>
@@ -2221,7 +2364,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_refreshAdvertisementSettings =>
- 'Odśwież Ustawienia Reklamy';
+ 'Odśwież ustawienia rozgłoszeń';
@override
String repeater_refreshed(String label) {
@@ -2234,7 +2377,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get repeater_cliTitle => 'Powtarzacz CLI';
+ String get repeater_cliTitle => 'Przekaźnik CLI';
@override
String get repeater_debugNextCommand => 'Debug Następną Komendę';
@@ -2265,7 +2408,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_enterCommandFirst => 'Wprowadź najpierw polecenie';
@override
- String get repeater_cliCommandFrameTitle => 'Określony Wyraz Polecenia CLI';
+ String get repeater_cliCommandFrameTitle => 'Ramka polecenia CLI';
@override
String repeater_cliCommandError(String error) {
@@ -2273,10 +2416,10 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get repeater_cliQuickGetName => 'Pobierz imię';
+ String get repeater_cliQuickGetName => 'Pobierz nazwę';
@override
- String get repeater_cliQuickGetRadio => 'Uzyskaj Radio';
+ String get repeater_cliQuickGetRadio => 'Pobierz radio';
@override
String get repeater_cliQuickGetTx => 'Pobierz TX';
@@ -2288,13 +2431,13 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_cliQuickVersion => 'Wersja';
@override
- String get repeater_cliQuickAdvertise => 'Reklama';
+ String get repeater_cliQuickAdvertise => 'Rozgłoś';
@override
String get repeater_cliQuickClock => 'Godzina';
@override
- String get repeater_cliHelpAdvert => 'Wysyła pakiet reklamowy';
+ String get repeater_cliHelpAdvert => 'Wysyła pakiet rozgłoszeniowy';
@override
String get repeater_cliHelpReboot =>
@@ -2325,7 +2468,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpSetRepeat =>
- 'Włącza lub wyłącza rolę powtarzacza dla tego węzła.';
+ 'Włącza lub wyłącza rolę przekaźnika dla tego węzła.';
@override
String get repeater_cliHelpSetAllowReadOnly =>
@@ -2333,7 +2476,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpSetFloodMax =>
- 'Ustawia maksymalną liczbę skoków pakietu powrotnego (jeśli >= max, pakiet nie jest przekierowywany)';
+ 'Ustawia maksymalną liczbę skoków pakietu zalewowego (jeśli >= max, pakiet nie jest przekierowywany)';
@override
String get repeater_cliHelpSetIntThresh =>
@@ -2341,7 +2484,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpSetAgcResetInterval =>
- 'Ustawia interwał do zresetowania Automatycznego Sterownika Głośności. Ustaw na 0, aby wyłączyć.';
+ 'Ustawia interwał do zresetowania automatycznego wzmocnienia (AGC). Ustaw na 0, aby wyłączyć.';
@override
String get repeater_cliHelpSetMultiAcks =>
@@ -2349,26 +2492,26 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpSetAdvertInterval =>
- 'Ustawia interwał timera w minutach do wysyłania pakietu reklamy lokalnej (bezpośredniej). Ustaw na 0, aby wyłączyć.';
+ 'Ustawia interwał timera w minutach do wysyłania pakietu rozgłoszenia lokalnego (bezpośredniego). Ustaw na 0, aby wyłączyć.';
@override
String get repeater_cliHelpSetFloodAdvertInterval =>
- 'Ustawia interwał timera w godzinach do wysłania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.';
+ 'Ustawia interwał timera w godzinach do wysłania pakietu rozgłoszeniowego typu \"flood\". Ustaw na 0, aby wyłączyć.';
@override
String get repeater_cliHelpSetGuestPassword =>
- 'Ustawia/aktualizuje hasło gościa. (dla repeaterów, loginy gości mogą wysyłać żądanie \"Get Stats\")';
+ 'Ustawia/aktualizuje hasło gościa. (dla przekaźników loginy gości mogą wysyłać żądanie \"Get Stats\")';
@override
- String get repeater_cliHelpSetName => 'Ustawia nazwę reklamy.';
+ String get repeater_cliHelpSetName => 'Ustawia nazwę rozgłoszenia.';
@override
String get repeater_cliHelpSetLat =>
- 'Ustawia współrzędną geograficzne (w stopniach dziesiętnych) mapy reklam.';
+ 'Ustawia współrzędną geograficzną (w stopniach dziesiętnych) mapy rozgłoszeń.';
@override
String get repeater_cliHelpSetLon =>
- 'Ustawia współrzędną długościową mapy reklamy. (stopnie dziesiętne)';
+ 'Ustawia współrzędną długościową mapy rozgłoszeń. (stopnie dziesiętne)';
@override
String get repeater_cliHelpSetRadio =>
@@ -2419,7 +2562,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpGetBridgeType =>
- 'Uzyskano typ mostu: brak, rs232, espnow';
+ 'Pobiera typ mostka: brak, rs232, espnow';
@override
String get repeater_cliHelpLogStart =>
@@ -2435,7 +2578,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpNeighbors =>
- 'Wyświetla listę innych węzłów powtarzających się, które usłyszano dzięki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4';
+ 'Wyświetla listę innych węzłów przekaźnikowych usłyszanych przez rozgłoszenia zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4';
@override
String get repeater_cliHelpNeighborRemove =>
@@ -2443,11 +2586,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpRegion =>
- '(tylko seria) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.';
+ '(tylko port szeregowy) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do zalewu.';
@override
String get repeater_cliHelpRegionLoad =>
- 'ZAPOMNIJ: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.';
+ 'UWAGA: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.';
@override
String get repeater_cliHelpRegionGet =>
@@ -2463,11 +2606,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpRegionAllowf =>
- 'Ustawia uprawnienia \'P\'łytkowe dla podanego regionu. (\'\' dla zakresu globalnego/starszego)';
+ 'Ustawia uprawnienia \'F\' (zalewowe) dla podanego regionu. (\'\' dla zakresu globalnego/starszego)';
@override
String get repeater_cliHelpRegionDenyf =>
- 'Usuwa uprawnienie \'Pływające\' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).';
+ 'Usuwa uprawnienie \'F\' (zalewowe) dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).';
@override
String get repeater_cliHelpRegionHome =>
@@ -2497,18 +2640,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliHelpGpsAdvert =>
- 'Udostępnia konfigurację reklamy lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w reklamach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: reklamuj lokalizację przechowywaną w ustawieniach';
+ 'Udostępnia konfigurację rozgłoszeń lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w rozgłoszeniach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: rozgłaszaj lokalizację przechowywaną w ustawieniach';
@override
String get repeater_cliHelpGpsAdvertSet =>
- 'Ustawia konfigurację reklamy w lokalizacji.';
+ 'Ustawia konfigurację rozgłoszeń lokalizacji.';
@override
String get repeater_commandsListTitle => 'Lista poleceń';
@override
String get repeater_commandsListNote =>
- 'ZAPAMIĘTAJ: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".';
+ 'UWAGA: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".';
@override
String get repeater_general => 'Ogólne';
@@ -2523,11 +2666,11 @@ class AppLocalizationsPl extends AppLocalizations {
String get repeater_logging => 'Rejestrowanie';
@override
- String get repeater_neighborsRepeaterOnly => 'Sąsiedzi (tylko powtarzacz)';
+ String get repeater_neighborsRepeaterOnly => 'Sąsiedzi (tylko przekaźnik)';
@override
String get repeater_regionManagementRepeaterOnly =>
- 'Zarządzanie Regionem (tylko Powtarzacz)';
+ 'Zarządzanie Regionem (tylko Przekaźnik)';
@override
String get repeater_regionNote =>
@@ -2541,15 +2684,15 @@ class AppLocalizationsPl extends AppLocalizations {
'Polecenie GPS zostało wprowadzone w celu zarządzania tematami związanymi z lokalizacją.';
@override
- String get telemetry_receivedData => 'Otrzymano Dane Telemetrii';
+ String get telemetry_receivedData => 'Odebrane dane telemetrii';
@override
String get telemetry_requestTimeout =>
- 'Życzenie o danych telemetrycznych nie udało się.';
+ 'Przekroczono czas oczekiwania na telemetrię.';
@override
String telemetry_errorLoading(String error) {
- return 'Błąd podczas ładowania telemetry: $error';
+ return 'Błąd podczas ładowania telemetrii: $error';
}
@override
@@ -2573,7 +2716,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get telemetry_temperatureLabel => 'Temperatura';
@override
- String get telemetry_currentLabel => 'Obecny';
+ String get telemetry_currentLabel => 'Prąd';
@override
String telemetry_batteryValue(int percent, String volts) {
@@ -2582,7 +2725,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String telemetry_voltageValue(String volts) {
- return '${volts}W';
+ return '${volts}V';
}
@override
@@ -2608,7 +2751,7 @@ class AppLocalizationsPl extends AppLocalizations {
}
@override
- String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
+ String get neighbors_repeatersNeighbors => 'Sąsiedzi przekaźników';
@override
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
@@ -2630,10 +2773,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get channelPath_viewMap => 'Wyświetl mapę';
@override
- String get channelPath_otherObservedPaths => 'Inne Zauważone Ścieżki';
+ String get channelPath_otherObservedPaths => 'Inne zaobserwowane ścieżki';
@override
- String get channelPath_repeaterHops => 'Skoki Powtórki';
+ String get channelPath_repeaterHops => 'Skoki przekaźników';
@override
String get channelPath_noHopDetails =>
@@ -2661,7 +2804,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String channelPath_observedPathTitle(int index, String hops) {
- return 'Obserwowany ścieżka $index • $hops';
+ return 'Obserwowana ścieżka $index • $hops';
}
@override
@@ -2681,7 +2824,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get channelPath_unknownPath => 'Nieznane';
@override
- String get channelPath_floodPath => 'Powodzenie';
+ String get channelPath_floodPath => 'Zalew';
@override
String get channelPath_directPath => 'Bezpośrednio';
@@ -2701,7 +2844,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channelPath_noRepeaterLocations =>
- 'Brak dostępnych lokalizacji powtarzaczy dla tego ścieżki.';
+ 'Brak dostępnych lokalizacji przekaźników dla tej ścieżki.';
@override
String channelPath_primaryPath(int index) {
@@ -2724,7 +2867,7 @@ class AppLocalizationsPl extends AppLocalizations {
'Brak dostępnych szczegółów hopa dla tego pakietu.';
@override
- String get channelPath_unknownRepeater => 'Nieznany Powtarzacz';
+ String get channelPath_unknownRepeater => 'Nieznany Przekaźnik';
@override
String get community_title => 'Społeczność';
@@ -2930,7 +3073,7 @@ class AppLocalizationsPl extends AppLocalizations {
String get listFilter_users => 'Użytkownicy';
@override
- String get listFilter_repeaters => 'Powtarzacze';
+ String get listFilter_repeaters => 'Przekaźniki';
@override
String get listFilter_roomServers => 'Serwery pokoju';
@@ -3105,10 +3248,10 @@ class AppLocalizationsPl extends AppLocalizations {
String get contacts_ping => 'Pingować';
@override
- String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera';
+ String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do przekaźnika';
@override
- String get contacts_repeaterPing => 'Repeater pingowy';
+ String get contacts_repeaterPing => 'Ping przekaźnika';
@override
String get contacts_roomPathTrace =>
@@ -3139,13 +3282,13 @@ class AppLocalizationsPl extends AppLocalizations {
'Kontakt nie został zaimportowany.';
@override
- String get contacts_zeroHopAdvert => 'Reklama Zero Hop';
+ String get contacts_zeroHopAdvert => 'Rozgłoszenie zero-hop';
@override
- String get contacts_floodAdvert => 'Reklama powodziowa';
+ String get contacts_floodAdvert => 'Rozgłoszenie zalewowe';
@override
- String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka';
+ String get contacts_copyAdvertToClipboard => 'Kopiuj rozgłoszenie do schowka';
@override
String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka';
@@ -3155,22 +3298,23 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_ShareContactZeroHop =>
- 'Udostępnij kontakt przez ogłoszenie';
+ 'Udostępnij kontakt przez rozgłoszenie';
@override
String get contacts_zeroHopContactAdvertSent =>
- 'Wysłano kontakt przez ogłoszenie.';
+ 'Wysłano kontakt przez rozgłoszenie.';
@override
String get contacts_zeroHopContactAdvertFailed =>
'Nie udało się wysłać kontaktu.';
@override
- String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.';
+ String get contacts_contactAdvertCopied =>
+ 'Rozgłoszenie skopiowano do schowka.';
@override
String get contacts_contactAdvertCopyFailed =>
- 'Kopiowanie ogłoszenia do schowka nie powiodło się.';
+ 'Kopiowanie rozgłoszenia do schowka nie powiodło się.';
@override
String get notification_activityTitle => 'Aktywność MeshCore';
@@ -3224,11 +3368,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportRepeaters =>
- 'Eksportuj powtórki / serwer pokojowy do GPX';
+ 'Eksportuj przekaźniki / serwer pokojowy do GPX';
@override
String get settings_gpxExportRepeatersSubtitle =>
- 'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.';
+ 'Eksportuje przekaźniki / roomserver z lokalizacją do pliku GPX.';
@override
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
@@ -3260,7 +3404,7 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get settings_gpxExportRepeatersRoom =>
- 'Lokalizacje serwerów powtarzających i pomieszczeń';
+ 'Lokalizacje przekaźników i serwerów pokojowych';
@override
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
@@ -3302,11 +3446,11 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contactsSettings_autoAddRepeatersTitle =>
- 'Automatyczne dodawanie powtarzalników';
+ 'Automatyczne dodawanie przekaźników';
@override
String get contactsSettings_autoAddRepeatersSubtitle =>
- 'Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.';
+ 'Zezwól towarzyszowi na automatyczne dodawanie odkrytych przekaźników.';
@override
String get contactsSettings_autoAddRoomServersTitle =>
@@ -3359,4 +3503,100 @@ 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 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).';
+ }
}
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 908ad96..98c72f5 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -398,6 +398,51 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Modo de privacidade desativado';
+ @override
+ String get settings_privacy => 'Configurações de Privacidade';
+
+ @override
+ String get settings_privacySubtitle => 'Controle o que é compartilhado.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Escolha quais informações o seu dispositivo compartilha com os outros.';
+
+ @override
+ String get settings_denyAll => 'Negar todos';
+
+ @override
+ String get settings_allowByContact => 'Permitir por bandeiras de contato';
+
+ @override
+ String get settings_allowAll => 'Permitir todos';
+
+ @override
+ String get settings_telemetryBaseMode => 'Modo Base de Telemetria';
+
+ @override
+ String get settings_telemetryLocationMode =>
+ 'Modo de Localização de Telemetria';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Modo de Ambiente de Telemetria';
+
+ @override
+ String get settings_advertLocation => 'Localização do Anúncio';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Incluir localização no anúncio';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Multi-ACKs: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Modo de telemetria atualizado';
+
@override
String get settings_actions => 'Ações';
@@ -696,6 +741,49 @@ class AppLocalizationsPt extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Rotação de roteamento automático desativada';
+ @override
+ String get appSettings_maxRouteWeight => 'Peso Máximo da Rota';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Peso Inicial da Rota';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Peso inicial para novos caminhos descobertos';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Aumento do peso para indicar sucesso';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Peso adicionado a um caminho após a entrega bem-sucedida.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Redução do peso da falha';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Peso removido de um caminho após uma tentativa de entrega malsucedida.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Número máximo de tentativas de envio de mensagens';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Número de tentativas de reenvio antes de classificar uma mensagem como falha.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Bateria';
@@ -945,6 +1033,42 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Última vez visto $days dias atrás';
}
+ @override
+ String get contact_info => 'Informações de Contato';
+
+ @override
+ String get contact_settings => 'Configurações de Contato';
+
+ @override
+ String get contact_telemetry => 'Telemetria';
+
+ @override
+ String get contact_lastSeen => 'Visto pela última vez';
+
+ @override
+ String get contact_clearChat => 'Limpar Chat';
+
+ @override
+ String get contact_teleBase => 'Base de Telemetria';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Permitir compartilhamento do nível da bateria e telemetria básica';
+
+ @override
+ String get contact_teleLoc => 'Localização de Telemetria';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Permitir compartilhamento de dados de localização';
+
+ @override
+ String get contact_teleEnv => 'Ambiente de Telemetria';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Permitir compartilhamento de dados do sensor de ambiente';
+
@override
String get channels_title => 'Canais';
@@ -1562,6 +1686,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_otherNodes => 'Outros Nós';
+ @override
+ String get map_showOverlaps => 'Sobreposições da Chave Repeater';
+
@override
String get map_keyPrefix => 'Prefixo Chave';
@@ -1605,6 +1732,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get map_runTrace => 'Executar Traçado de Caminho';
+ @override
+ String get map_runTraceWithReturnPath => 'Retornar ao mesmo caminho.';
+
@override
String get map_removeLast => 'Remover Último';
@@ -3354,4 +3484,100 @@ 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 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).';
+ }
}
diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart
index 67011fb..4184641 100644
--- a/lib/l10n/app_localizations_ru.dart
+++ b/lib/l10n/app_localizations_ru.dart
@@ -398,6 +398,51 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_privacyModeDisabled =>
'Режим конфиденциальности выключен';
+ @override
+ String get settings_privacy => 'Настройки конфиденциальности';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Контролируйте, какую информацию делиться.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Выберите, какую информацию ваше устройство будет делиться с другими.';
+
+ @override
+ String get settings_denyAll => 'Отклонить все';
+
+ @override
+ String get settings_allowByContact => 'Разрешить по флагам контактов';
+
+ @override
+ String get settings_allowAll => 'Разрешить все';
+
+ @override
+ String get settings_telemetryBaseMode => 'Базовый режим телеметрии';
+
+ @override
+ String get settings_telemetryLocationMode =>
+ 'Режим местоположения телеметрии';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'Режим среды телеметрии';
+
+ @override
+ String get settings_advertLocation => 'Местоположение рекламы';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Включить местоположение в объявление';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Мульти-ACK: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Режим телеметрии обновлен';
+
@override
String get settings_actions => 'Действия';
@@ -696,6 +741,50 @@ class AppLocalizationsRu extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Автоматическое переключение маршрутов отключено';
+ @override
+ String get appSettings_maxRouteWeight =>
+ 'Максимальный допустимый вес маршрута';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Начальный вес маршрута';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Начальный вес для новых, только что открытых маршрутов';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Увеличение веса успеха';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Вес, добавленный к маршруту после успешной доставки.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Уменьшение веса неудачи';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Вес, который был удален с пути после неудачной доставки.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Максимальное количество повторных попыток отправки сообщения';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Батарея';
@@ -944,6 +1033,42 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Видели $days дн. назад';
}
+ @override
+ String get contact_info => 'Контактная информация';
+
+ @override
+ String get contact_settings => 'Настройки контактов';
+
+ @override
+ String get contact_telemetry => 'Телеметрия';
+
+ @override
+ String get contact_lastSeen => 'Последний раз видели';
+
+ @override
+ String get contact_clearChat => 'Очистить чат';
+
+ @override
+ String get contact_teleBase => 'База телеметрии';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Разрешить обмен уровнем заряда батареи и базовой телеметрией';
+
+ @override
+ String get contact_teleLoc => 'Местоположение телеметрии';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Разрешить обмен данными о местоположении';
+
+ @override
+ String get contact_teleEnv => 'Среда телеметрии';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Разрешить обмен данными датчиков окружающей среды';
+
@override
String get channels_title => 'Каналы';
@@ -1564,6 +1689,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_otherNodes => 'Другие ноды';
+ @override
+ String get map_showOverlaps => 'Перекрытия ключа повтора';
+
@override
String get map_keyPrefix => 'Префикс ключа';
@@ -1607,6 +1735,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get map_runTrace => 'Запустить трассировку пути';
+ @override
+ String get map_runTraceWithReturnPath => 'Вернуться обратно по тому же пути';
+
@override
String get map_removeLast => 'Удалить последний';
@@ -3367,4 +3498,100 @@ 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 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 (оставьте пустым, если нет).';
+ }
}
diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart
index 4f033f9..59f46bd 100644
--- a/lib/l10n/app_localizations_sk.dart
+++ b/lib/l10n/app_localizations_sk.dart
@@ -395,6 +395,49 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Ochranný režim je vypnutý';
+ @override
+ String get settings_privacy => 'Nastavenia súkromia';
+
+ @override
+ String get settings_privacySubtitle => 'Ovládni, aké informácie sa zdieľajú.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.';
+
+ @override
+ String get settings_denyAll => 'Zamietnuť všetko';
+
+ @override
+ String get settings_allowByContact => 'Povoliť podľa kontaktových vlajok';
+
+ @override
+ String get settings_allowAll => 'Povoliť všetko';
+
+ @override
+ String get settings_telemetryBaseMode => 'Základný režim telemetrie';
+
+ @override
+ String get settings_telemetryLocationMode => 'Režim umiestnenia telemetrie';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'Režim prostredia telemetrie';
+
+ @override
+ String get settings_advertLocation => 'Umiestnenie inzerátu';
+
+ @override
+ String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Viaceré ACK: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated =>
+ 'Režim telemetrie bol aktualizovaný';
+
@override
String get settings_actions => 'Možné akcie';
@@ -687,6 +730,48 @@ class AppLocalizationsSk extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatické prekladanie trás pozastavené';
+ @override
+ String get appSettings_maxRouteWeight => 'Maximálna hmotnosť trasy';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Počiatočná váha trasy';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Počiatočná váha pre nové, objavené cesty';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement => 'Zvyšenie váhy úspechu';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Hmotnosť pridaná k trase po úspešnej doručení';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Sníženie váhy, ktorá sa používa na odhad rizika.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Hmotnosť odstránená z cesty po neúspešnej doručenie';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Maximalný počet pokusov o doručenie správ';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Počet pokusov o odošleť pred označením správy ako neúspešnej';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Batéria';
@@ -938,6 +1023,41 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Posledné zobrazenie $days dní dozadu';
}
+ @override
+ String get contact_info => 'Kontaktné informácie';
+
+ @override
+ String get contact_settings => 'Nastavenia kontaktov';
+
+ @override
+ String get contact_telemetry => 'Telemetria';
+
+ @override
+ String get contact_lastSeen => 'Naposledy videný';
+
+ @override
+ String get contact_clearChat => 'Vymazať chat';
+
+ @override
+ String get contact_teleBase => 'Báza telemetrie';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Povoliť zdieľanie úrovne batérie a základnej telemetrie';
+
+ @override
+ String get contact_teleLoc => 'Lokácia telemetrie';
+
+ @override
+ String get contact_teleLocSubtitle => 'Povoliť zdieľanie údajov o lokalite';
+
+ @override
+ String get contact_teleEnv => 'Prostredie telemetrie';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Povoliť zdieľanie údajov senzorov prostredia';
+
@override
String get channels_title => 'Kanály';
@@ -1555,6 +1675,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_otherNodes => 'Ostatné uzly';
+ @override
+ String get map_showOverlaps => 'Prekrývanie opakovača kľúča';
+
@override
String get map_keyPrefix => 'Päťciferné predpona';
@@ -1598,6 +1721,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get map_runTrace => 'Spustiť trasovaním cesty';
+ @override
+ String get map_runTraceWithReturnPath => 'Vráťte sa späť po tej istej ceste.';
+
@override
String get map_removeLast => 'Odstrániť posledný';
@@ -3338,4 +3464,98 @@ 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 scanner_linuxPairingShowPin => 'Zobraziť PIN';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'Skryť PIN';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'Bluetooth párovací PIN';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Zadajte PIN pre $deviceName (ak nie je, nechajte prázdne).';
+ }
}
diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart
index e7c48f6..171353c 100644
--- a/lib/l10n/app_localizations_sl.dart
+++ b/lib/l10n/app_localizations_sl.dart
@@ -393,6 +393,50 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privatni način je onemogočen.';
+ @override
+ String get settings_privacy => 'Nastavitve zasebnosti';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Kontrolirajte, katere informacije so deljene.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Izberite, katere informacije vaš naprava deli z drugimi.';
+
+ @override
+ String get settings_denyAll => 'Zavrniti vse';
+
+ @override
+ String get settings_allowByContact => 'Dovoli po kontaktnih zastavah';
+
+ @override
+ String get settings_allowAll => 'Dovoli vse';
+
+ @override
+ String get settings_telemetryBaseMode => 'Osnovni način telemetrije';
+
+ @override
+ String get settings_telemetryLocationMode => 'Način delovanja telemetrije';
+
+ @override
+ String get settings_telemetryEnvironmentMode =>
+ 'Način delovanja okolja telemetrije';
+
+ @override
+ String get settings_advertLocation => 'Lokacija oglasa';
+
+ @override
+ String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Večkratni potrditvi: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Način telemetrije posodobljen';
+
@override
String get settings_actions => 'Akcije';
@@ -687,6 +731,49 @@ class AppLocalizationsSl extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Samodejno krmilno rotiranje je onemogočeno';
+ @override
+ String get appSettings_maxRouteWeight => 'Največja dovoljena teža poti';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Izvirna teža poti';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Izguba teže za nove, odkriti poti';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Učinkovitost: povečanje';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Težava, dodana poti po uspešni dostavi';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Zmanjšanje teže, ki je povezana s pomanjkanjem';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Težo, ki ni bila uspešno dostavljena, odstranili s poti.';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Najve število poskusov pošiljanja sporočil';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Baterija';
@@ -934,6 +1021,41 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Zadnjič viden pred $days dnem';
}
+ @override
+ String get contact_info => 'Kontaktni podatki';
+
+ @override
+ String get contact_settings => 'Nastavitve stika';
+
+ @override
+ String get contact_telemetry => 'Telemetrija';
+
+ @override
+ String get contact_lastSeen => 'Zadnjič videno';
+
+ @override
+ String get contact_clearChat => 'Počisti klepet';
+
+ @override
+ String get contact_teleBase => 'Baza telemetrije';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Dovoli deljenje stanja baterije in osnovne telemetrije';
+
+ @override
+ String get contact_teleLoc => 'Lokacija telemetrije';
+
+ @override
+ String get contact_teleLocSubtitle => 'Dovoli deljenje podatkov o lokaciji';
+
+ @override
+ String get contact_teleEnv => 'Okolje telemetrije';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Dovoli deljenje podatkov okoljskih senzorjev';
+
@override
String get channels_title => 'Kanali';
@@ -1549,6 +1671,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_otherNodes => 'Druge vozlišča';
+ @override
+ String get map_showOverlaps => 'Prekrivanje ključa ponovnega predvajanja';
+
@override
String get map_keyPrefix => 'Predpona ključa';
@@ -1591,6 +1716,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_runTrace => 'Zaženi sledenje poti';
+ @override
+ String get map_runTraceWithReturnPath => 'Vrni se nazaj po isti poti.';
+
@override
String get map_removeLast => 'Odstrani Zadnji';
@@ -3339,4 +3467,100 @@ 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 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).';
+ }
}
diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart
index 6ccea2f..6a776d7 100644
--- a/lib/l10n/app_localizations_sv.dart
+++ b/lib/l10n/app_localizations_sv.dart
@@ -392,6 +392,49 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Privatläge är avstängt';
+ @override
+ String get settings_privacy => 'Inställningar för sekretess';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Kontrollera vilken information som delas.';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Välj vilken information din enhet delar med andra.';
+
+ @override
+ String get settings_denyAll => 'Neka alla';
+
+ @override
+ String get settings_allowByContact => 'Tillåt via kontaktflaggor';
+
+ @override
+ String get settings_allowAll => 'Tillåt alla';
+
+ @override
+ String get settings_telemetryBaseMode => 'Telemetribasläge';
+
+ @override
+ String get settings_telemetryLocationMode => 'Telemetritillstånd för plats';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'Telemetri miljöläge';
+
+ @override
+ String get settings_advertLocation => 'Annonsplacering';
+
+ @override
+ String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Multi-ACKs: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Telemetri-läge uppdaterat';
+
@override
String get settings_actions => 'Åtgärder';
@@ -682,6 +725,48 @@ class AppLocalizationsSv extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Automatisk ruttrotation är avstängd';
+ @override
+ String get appSettings_maxRouteWeight => 'Maximalt tillåtet vikt för rutten';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Initial vikt för rutt';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Initial vikt för nyligen upptäckta vägar';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Ökning av vikt för framgång';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Vikt läggs till en väg efter en lyckad leverans.';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Minskning av vikten för misslyckande';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Vikt som tagits bort från en väg efter ett misslyckat leveransförsök';
+
+ @override
+ String get appSettings_maxMessageRetries => 'Maximalt antal försök';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Antal försök att skicka om ett meddelande innan det markeras som misslyckat.';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Batteri';
@@ -930,6 +1015,40 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Senast synlig $days dagar sedan';
}
+ @override
+ String get contact_info => 'Kontaktinformation';
+
+ @override
+ String get contact_settings => 'Kontaktinställningar';
+
+ @override
+ String get contact_telemetry => 'Telemetri';
+
+ @override
+ String get contact_lastSeen => 'Senast sedd';
+
+ @override
+ String get contact_clearChat => 'Rensa Chatt';
+
+ @override
+ String get contact_teleBase => 'Telemetribas';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Tillåt delning av batterinivå och grundläggande telemetri';
+
+ @override
+ String get contact_teleLoc => 'Telemetridata plats';
+
+ @override
+ String get contact_teleLocSubtitle => 'Tillåt delning av platsdata';
+
+ @override
+ String get contact_teleEnv => 'Telemetri Miljö';
+
+ @override
+ String get contact_teleEnvSubtitle => 'Tillåt delning av miljösensordata';
+
@override
String get channels_title => 'Kanaler';
@@ -1545,6 +1664,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_otherNodes => 'Andra noder';
+ @override
+ String get map_showOverlaps => 'Repeater-nyckelöverlappningar';
+
@override
String get map_keyPrefix => 'Nyckelprefix';
@@ -1588,6 +1710,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get map_runTrace => 'Kör spårsökning';
+ @override
+ String get map_runTraceWithReturnPath => 'Gå tillbaka på samma väg';
+
@override
String get map_removeLast => 'Ta bort sista';
@@ -3319,4 +3444,100 @@ 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 scanner_linuxPairingShowPin => 'Visa PIN';
+
+ @override
+ String get scanner_linuxPairingHidePin => 'Dölj PIN';
+
+ @override
+ String get scanner_linuxPairingPinTitle => 'Bluetooth‑parnings‑PIN';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return 'Ange PIN för $deviceName (lämna tomt om ingen).';
+ }
}
diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart
index 788c9d1..9ebead2 100644
--- a/lib/l10n/app_localizations_uk.dart
+++ b/lib/l10n/app_localizations_uk.dart
@@ -395,6 +395,50 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get settings_privacyModeDisabled => 'Режим приватності вимкнено';
+ @override
+ String get settings_privacy => 'Налаштування приватності';
+
+ @override
+ String get settings_privacySubtitle =>
+ 'Керуйте інформацією, яку буде спільно використовуватися';
+
+ @override
+ String get settings_privacySettingsDescription =>
+ 'Виберіть, яку інформацію ваш пристрій буде передавати іншим.';
+
+ @override
+ String get settings_denyAll => 'Відхилити все';
+
+ @override
+ String get settings_allowByContact => 'Дозволити за контактними прапорцями';
+
+ @override
+ String get settings_allowAll => 'Дозволити все';
+
+ @override
+ String get settings_telemetryBaseMode => 'Режим базової телеметрії';
+
+ @override
+ String get settings_telemetryLocationMode => 'Режим місця телеметрії';
+
+ @override
+ String get settings_telemetryEnvironmentMode => 'Режим середовища телеметрії';
+
+ @override
+ String get settings_advertLocation => 'Розміщення реклами';
+
+ @override
+ String get settings_advertLocationSubtitle =>
+ 'Включити місце розташування в оголошення';
+
+ @override
+ String settings_multiAck(String value) {
+ return 'Багатократне підтвердження: $value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => 'Режим телеметрії оновлено';
+
@override
String get settings_actions => 'Дії';
@@ -692,6 +736,49 @@ class AppLocalizationsUk extends AppLocalizations {
String get appSettings_autoRouteRotationDisabled =>
'Авторотація маршрутизації вимкнена';
+ @override
+ String get appSettings_maxRouteWeight => 'Максимальна вага маршруту';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle =>
+ 'Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.';
+
+ @override
+ String get appSettings_initialRouteWeight => 'Початкова вартість маршруту';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle =>
+ 'Початкова вага для нових відкритих шляхів';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement =>
+ 'Збільшення ваги успіху';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ 'Вага, додана до маршруту після успішної доставки';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement =>
+ 'Зменшення ваги помилки';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ 'Вага, яка була знята з маршруту після невдалої доставки';
+
+ @override
+ String get appSettings_maxMessageRetries =>
+ 'Максимальна кількість повторних спроб надсилання повідомлення';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle =>
+ 'Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => 'Батарея';
@@ -940,6 +1027,42 @@ class AppLocalizationsUk extends AppLocalizations {
return 'В мережі $days дн. тому';
}
+ @override
+ String get contact_info => 'Контактна інформація';
+
+ @override
+ String get contact_settings => 'Налаштування контактів';
+
+ @override
+ String get contact_telemetry => 'Телеметрія';
+
+ @override
+ String get contact_lastSeen => 'Останній раз бачили';
+
+ @override
+ String get contact_clearChat => 'Очистити чат';
+
+ @override
+ String get contact_teleBase => 'Базовий телебачення';
+
+ @override
+ String get contact_teleBaseSubtitle =>
+ 'Дозволити спільний доступ до рівня заряду батареї та базової телеметрії';
+
+ @override
+ String get contact_teleLoc => 'Розташування телеметрії';
+
+ @override
+ String get contact_teleLocSubtitle =>
+ 'Дозволити спільне використання даних про місцеположення';
+
+ @override
+ String get contact_teleEnv => 'Середовище телеметрії';
+
+ @override
+ String get contact_teleEnvSubtitle =>
+ 'Дозволити спільний доступ до даних датчиків середовища';
+
@override
String get channels_title => 'Канали';
@@ -1561,6 +1684,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_otherNodes => 'Інші вузли';
+ @override
+ String get map_showOverlaps => 'Перекриття ключа повторювача';
+
@override
String get map_keyPrefix => 'Префікс ключа';
@@ -1604,6 +1730,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get map_runTrace => 'Виконати трасування шляху';
+ @override
+ String get map_runTraceWithReturnPath => 'Повернутися назад тим же шляхом';
+
@override
String get map_removeLast => 'Видалити останній';
@@ -3372,4 +3501,100 @@ 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 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 (залиште порожнім, якщо його немає).';
+ }
}
diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart
index be7eeb0..6d3a856 100644
--- a/lib/l10n/app_localizations_zh.dart
+++ b/lib/l10n/app_localizations_zh.dart
@@ -374,6 +374,47 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get settings_privacyModeDisabled => '隐私模式已关闭';
+ @override
+ String get settings_privacy => '隐私设置';
+
+ @override
+ String get settings_privacySubtitle => '控制要共享的信息。';
+
+ @override
+ String get settings_privacySettingsDescription => '选择您的设备与他人共享的信息。';
+
+ @override
+ String get settings_denyAll => '拒绝所有';
+
+ @override
+ String get settings_allowByContact => '按联系人标志允许';
+
+ @override
+ String get settings_allowAll => '允许全部';
+
+ @override
+ String get settings_telemetryBaseMode => '遥测基础模式';
+
+ @override
+ String get settings_telemetryLocationMode => '遥测位置模式';
+
+ @override
+ String get settings_telemetryEnvironmentMode => '遥测环境模式';
+
+ @override
+ String get settings_advertLocation => '广告位置';
+
+ @override
+ String get settings_advertLocationSubtitle => '在广告中包含位置';
+
+ @override
+ String settings_multiAck(String value) {
+ return '多重ACK:$value';
+ }
+
+ @override
+ String get settings_telemetryModeUpdated => '遥测模式已更新';
+
@override
String get settings_actions => '操作';
@@ -648,6 +689,43 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get appSettings_autoRouteRotationDisabled => '自动路径轮换已禁用';
+ @override
+ String get appSettings_maxRouteWeight => '最大路径重量';
+
+ @override
+ String get appSettings_maxRouteWeightSubtitle => '一条路径可以累积的最大重量,取决于成功交付的数量。';
+
+ @override
+ String get appSettings_initialRouteWeight => '初始路线权重';
+
+ @override
+ String get appSettings_initialRouteWeightSubtitle => '新发现路径的初始重量';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrement => '成功权重增加';
+
+ @override
+ String get appSettings_routeWeightSuccessIncrementSubtitle =>
+ '在成功交付后,将重量添加到路径中';
+
+ @override
+ String get appSettings_routeWeightFailureDecrement => '失败权重降低';
+
+ @override
+ String get appSettings_routeWeightFailureDecrementSubtitle =>
+ '从一条路径上移除的货物,由于无法成功交付而移除。';
+
+ @override
+ String get appSettings_maxMessageRetries => '最大消息重试次数';
+
+ @override
+ String get appSettings_maxMessageRetriesSubtitle => '在将消息标记为失败之前,允许尝试的次数';
+
+ @override
+ String path_routeWeight(String weight, String max) {
+ return '$weight/$max';
+ }
+
@override
String get appSettings_battery => '电池';
@@ -886,6 +964,39 @@ class AppLocalizationsZh extends AppLocalizations {
return '最后在线 $days 天前';
}
+ @override
+ String get contact_info => '联系信息';
+
+ @override
+ String get contact_settings => '联系人设置';
+
+ @override
+ String get contact_telemetry => '遥测数据';
+
+ @override
+ String get contact_lastSeen => '最近出现';
+
+ @override
+ String get contact_clearChat => '清除聊天记录';
+
+ @override
+ String get contact_teleBase => '遥测基站';
+
+ @override
+ String get contact_teleBaseSubtitle => '允许共享电池电量和基本遥测数据';
+
+ @override
+ String get contact_teleLoc => '遥测位置';
+
+ @override
+ String get contact_teleLocSubtitle => '允许共享位置数据';
+
+ @override
+ String get contact_teleEnv => '遥测环境';
+
+ @override
+ String get contact_teleEnvSubtitle => '允许共享环境传感器数据';
+
@override
String get channels_title => '频道';
@@ -1471,6 +1582,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_otherNodes => '其他节点';
+ @override
+ String get map_showOverlaps => '重复键重叠';
+
@override
String get map_keyPrefix => '关键字前缀';
@@ -1513,6 +1627,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get map_runTrace => '运行路径追踪';
+ @override
+ String get map_runTraceWithReturnPath => '沿着相同的路径返回';
+
@override
String get map_removeLast => '移除最后一个';
@@ -3105,4 +3222,94 @@ 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 scanner_linuxPairingShowPin => '显示 PIN码';
+
+ @override
+ String get scanner_linuxPairingHidePin => '隐藏 PIN';
+
+ @override
+ String get scanner_linuxPairingPinTitle => '蓝牙配对 PIN';
+
+ @override
+ String scanner_linuxPairingPinPrompt(String deviceName) {
+ return '输入 $deviceName 的 PIN(如果没有,请留空)。';
+ }
}
diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb
index 648d711..9f164fd 100644
--- a/lib/l10n/app_nl.arb
+++ b/lib/l10n/app_nl.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "De TCP-verbinding is verlopen.",
"tcpConnectionFailed": "Verbinding met TCP mislukt: {error}",
"map_showDiscoveryContacts": "Ontdek contacten weergeven",
- "map_setAsMyLocation": "Stel dit in als mijn locatie"
+ "map_setAsMyLocation": "Stel dit in als mijn locatie",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "Privacyinstellingen",
+ "settings_privacySubtitle": "Beheer welke informatie wordt gedeeld",
+ "settings_telemetryLocationMode": "Telemetrie-locatiemodus",
+ "settings_telemetryEnvironmentMode": "Telemetrie-omgevingsmodus",
+ "settings_advertLocation": "Advertentielocatie",
+ "settings_advertLocationSubtitle": "Locatie opnemen in advertentie",
+ "settings_privacySettingsDescription": "Kies welke informatie uw apparaat deelt met anderen",
+ "settings_allowByContact": "Toestaan op basis van contactvlaggen",
+ "settings_allowAll": "Alles toestaan",
+ "settings_denyAll": "Weiger alles",
+ "contact_info": "Contactinformatie",
+ "settings_telemetryBaseMode": "Telemetrie-basismodus",
+ "contact_teleBase": "Telemetrie_basis",
+ "contact_teleLoc": "Telemetrielocatie",
+ "contact_teleLocSubtitle": "Locatiegegevens delen toestaan",
+ "contact_teleEnv": "Telemetrieomgeving",
+ "contact_teleEnvSubtitle": "Delen van omgevingsensordata toestaan",
+ "contact_settings": "Contactinstellingen",
+ "contact_telemetry": "Telemetrie",
+ "contact_lastSeen": "Laatst gezien",
+ "contact_clearChat": "Chat leegmaken",
+ "contact_teleBaseSubtitle": "Sta delen van batterij niveau en basis telemetrie toe",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_maxRouteWeightSubtitle": "Het maximale gewicht dat een route kan bereiken door succesvolle leveringen.",
+ "appSettings_initialRouteWeight": "เริ่มต้น gewicht van de route",
+ "appSettings_maxRouteWeight": "Maximale gewicht voor de route",
+ "appSettings_initialRouteWeightSubtitle": "Startgewicht voor nieuwe, ontdekte routes",
+ "appSettings_routeWeightSuccessIncrement": "Toename in het gewicht van het succes",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Gewicht wordt toegevoegd aan een route na een succesvolle levering.",
+ "appSettings_routeWeightFailureDecrement": "Vermindering van het gewicht van fouten",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Gewicht verwijderd van een pad na een mislukte levering",
+ "appSettings_maxMessageRetries": "Aantal pogingen om berichten te versturen",
+ "appSettings_maxMessageRetriesSubtitle": "Aantal pogingen om een bericht opnieuw te versturen voordat het als mislukt wordt gemarkeerd",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_telemetryModeUpdated": "Telemetrie-modus bijgewerkt",
+ "settings_multiAck": "Multi-ACKs: {value}",
+ "map_showOverlaps": "Herhalingssleutel overlapt",
+ "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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "Toon PIN",
+ "scanner_linuxPairingHidePin": "PIN verbergen",
+ "scanner_linuxPairingPinPrompt": "Voer PIN in voor {deviceName} (laat leeg als er geen is).",
+ "scanner_linuxPairingPinTitle": "Bluetooth‑koppelings‑PIN"
}
diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb
index f4f3ac7..87b4754 100644
--- a/lib/l10n/app_pl.arb
+++ b/lib/l10n/app_pl.arb
@@ -17,7 +17,7 @@
"common_unknownDevice": "Nieznane urządzenie",
"common_save": "Zapisz",
"common_delete": "Usuń",
- "common_close": "Zamknąć",
+ "common_close": "Zamknij",
"common_edit": "Edytuj",
"common_add": "Dodaj",
"common_settings": "Ustawienia",
@@ -28,12 +28,12 @@
"common_continue": "Kontynuuj",
"common_share": "Udostępnij",
"common_copy": "Kopiuj",
- "common_retry": "Spróbować",
+ "common_retry": "Ponów",
"common_hide": "Ukryj",
"common_remove": "Usuń",
"common_enable": "Włącz",
- "common_disable": "Wyłączyć",
- "common_reboot": "Zrestartować",
+ "common_disable": "Wyłącz",
+ "common_reboot": "Uruchom ponownie",
"common_loading": "Ładowanie...",
"common_notAvailable": "—",
"common_voltageValue": "{volts} V",
@@ -56,7 +56,7 @@
"scanner_scanning": "Skanowanie urządzeń...",
"scanner_connecting": "Łączenie...",
"scanner_disconnecting": "Odłączanie...",
- "scanner_notConnected": "Niepołączony",
+ "scanner_notConnected": "Nie połączono",
"scanner_connectedTo": "Połączono z {deviceName}",
"@scanner_connectedTo": {
"placeholders": {
@@ -87,7 +87,7 @@
"settings_nodeName": "Nazwa węzła",
"settings_nodeNameNotSet": "Nie ustawione",
"settings_nodeNameHint": "Wprowadź nazwę węzła",
- "settings_nodeNameUpdated": "Imię zaktualizowane",
+ "settings_nodeNameUpdated": "Nazwa zaktualizowana",
"settings_radioSettings": "Ustawienia radia",
"settings_radioSettingsSubtitle": "Częstotliwość, moc, współczynnik rozpraszania",
"settings_radioSettingsUpdated": "Ustawienia radia zostały zaktualizowane",
@@ -98,29 +98,29 @@
"settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.",
"settings_latitude": "Szerokość",
"settings_longitude": "Długość",
- "settings_privacyMode": "Tryb Prywatny",
- "settings_privacyModeSubtitle": "Ukryj imię/lokalizację w reklamach",
- "settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w reklamach.",
+ "settings_privacyMode": "Tryb prywatności",
+ "settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach",
+ "settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.",
"settings_privacyModeEnabled": "Tryb prywatności włączony",
"settings_privacyModeDisabled": "Tryb prywatności wyłączony",
"settings_actions": "Działania",
- "settings_sendAdvertisement": "Wyślij Reklamę",
- "settings_sendAdvertisementSubtitle": "Obecność transmisji jest teraz",
- "settings_advertisementSent": "Reklama wysłana",
- "settings_syncTime": "Czas synchronizacji",
+ "settings_sendAdvertisement": "Wyślij rozgłoszenie",
+ "settings_sendAdvertisementSubtitle": "Nadaj obecność teraz",
+ "settings_advertisementSent": "Rozgłoszenie wysłane",
+ "settings_syncTime": "Synchronizacja czasu",
"settings_syncTimeSubtitle": "Ustaw zegar urządzenia na czas telefonu.",
- "settings_timeSynchronized": "Synchronizacja czasu",
+ "settings_timeSynchronized": "Czas zsynchronizowany",
"settings_refreshContacts": "Odśwież Kontakty",
"settings_refreshContactsSubtitle": "Odśwież listę kontaktów z urządzenia",
"settings_rebootDevice": "Zrestartuj Urządzenie",
"settings_rebootDeviceSubtitle": "Zrestartuj urządzenie MeshCore",
"settings_rebootDeviceConfirm": "Czy na pewno chcesz zrestartować urządzenie? Będziesz odłączony.",
"settings_debug": "Debug",
- "settings_bleDebugLog": "Log błędów BLE",
+ "settings_bleDebugLog": "Dziennik debugowania BLE",
"settings_bleDebugLogSubtitle": "Polecenia BLE, odpowiedzi i surowe dane",
- "settings_appDebugLog": "Log Wykonywania Aplikacji",
+ "settings_appDebugLog": "Dziennik debugowania aplikacji",
"settings_appDebugLogSubtitle": "Komunikaty debugowania aplikacji",
- "settings_about": "O mnie",
+ "settings_about": "O aplikacji",
"settings_aboutVersion": "MeshCore Open v{version}",
"@settings_aboutVersion": {
"placeholders": {
@@ -130,22 +130,22 @@
}
},
"settings_aboutLegalese": "Projekt MeshCore Open Source 2026",
- "settings_aboutDescription": "Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.",
- "settings_infoName": "Imię",
+ "settings_aboutDescription": "Otwartoźródłowy klient Flutter dla urządzeń MeshCore LoRa do sieci mesh.",
+ "settings_infoName": "Nazwa",
"settings_infoId": "ID",
"settings_infoStatus": "Status",
"settings_infoBattery": "Bateria",
"settings_infoPublicKey": "Klucz Publiczny",
"settings_infoContactsCount": "Liczba kontaktów",
"settings_infoChannelCount": "Liczba kanałów",
- "settings_presets": "Preset",
+ "settings_presets": "Presety",
"settings_frequency": "Częstotliwość (MHz)",
"settings_frequencyHelper": "300,0 - 2500,0",
"settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)",
"settings_bandwidth": "Przepustowość",
- "settings_spreadingFactor": "Rozkład Czynnika",
- "settings_codingRate": "Stawka Kodowania",
- "settings_txPower": "TX Moc (dBm)",
+ "settings_spreadingFactor": "Współczynnik rozpraszania",
+ "settings_codingRate": "Współczynnik kodowania",
+ "settings_txPower": "Moc TX (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)",
"settings_error": "Błąd: {message}",
@@ -163,7 +163,7 @@
"appSettings_themeLight": "Jasne",
"appSettings_themeDark": "Ciemny",
"appSettings_language": "Język",
- "appSettings_languageSystem": "Domyślny systemowy",
+ "appSettings_languageSystem": "Domyślny systemu",
"appSettings_languageEn": "English",
"appSettings_languageFr": "Français",
"appSettings_languageEs": "Español",
@@ -179,7 +179,7 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Powiadomienia",
"appSettings_enableNotifications": "Włącz Powiadomienia",
- "appSettings_enableNotificationsSubtitle": "Otrzymuj powiadomienia o wiadomościach i reklamach.",
+ "appSettings_enableNotificationsSubtitle": "Otrzymuj powiadomienia o wiadomościach i rozgłoszeniach.",
"appSettings_notificationPermissionDenied": "Odmowa zezwolenia na powiadomienia",
"appSettings_notificationsEnabled": "Powiadomienia włączone",
"appSettings_notificationsDisabled": "Powiadomienia wyłączone",
@@ -187,20 +187,20 @@
"appSettings_messageNotificationsSubtitle": "Pokaż powiadomienie przy otrzymywaniu nowych wiadomości",
"appSettings_channelMessageNotifications": "Powiadomienia o Wiadomościach na Kanałach",
"appSettings_channelMessageNotificationsSubtitle": "Pokaż powiadomienie przy odbieraniu wiadomości z kanału",
- "appSettings_advertisementNotifications": "Powiadomienia Reklamowe",
- "appSettings_advertisementNotificationsSubtitle": "Wyświetl powiadomienie, gdy zostaną odkryte nowe węzły.",
+ "appSettings_advertisementNotifications": "Powiadomienia o rozgłoszeniach",
+ "appSettings_advertisementNotificationsSubtitle": "Wyświetl powiadomienie, gdy zostaną wykryte nowe węzły.",
"appSettings_messaging": "Wiadomości",
- "appSettings_clearPathOnMaxRetry": "Wyczyść Ścieżkę na Maksymalnej Próbie",
+ "appSettings_clearPathOnMaxRetry": "Wyczyść ścieżkę po maks. liczbie prób",
"appSettings_clearPathOnMaxRetrySubtitle": "Resetuj ścieżkę kontaktu po 5 nieudanych próbach wysłania",
- "appSettings_pathsWillBeCleared": "Droga będzie wyczyszczona po 5 nieudanych próbach.",
- "appSettings_pathsWillNotBeCleared": "Droga nie zostanie automatycznie wyczyszczona.",
- "appSettings_autoRouteRotation": "Automatyczne Rotowanie Trasy",
+ "appSettings_pathsWillBeCleared": "Ścieżka zostanie wyczyszczona po 5 nieudanych próbach.",
+ "appSettings_pathsWillNotBeCleared": "Ścieżka nie zostanie automatycznie wyczyszczona.",
+ "appSettings_autoRouteRotation": "Automatyczna rotacja trasy",
"appSettings_autoRouteRotationSubtitle": "Przełączaj się między najlepszymi ścieżkami a trybem zalewowym.",
"appSettings_autoRouteRotationEnabled": "Automatyczne obracanie tras włączone",
"appSettings_autoRouteRotationDisabled": "Automatyczne obracanie tras wyłączone",
"appSettings_battery": "Bateria",
"appSettings_batteryChemistry": "Chemia Baterii",
- "appSettings_batteryChemistryPerDevice": "Ustawione na urządzenie ({deviceName})",
+ "appSettings_batteryChemistryPerDevice": "Ustaw dla urządzenia ({deviceName})",
"@appSettings_batteryChemistryPerDevice": {
"placeholders": {
"deviceName": {
@@ -213,8 +213,8 @@
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65 V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Wyświetlanie mapy",
- "appSettings_showRepeaters": "Pokaż Powtórniki",
- "appSettings_showRepeatersSubtitle": "Wyświetl węzły powtarzające się na mapie",
+ "appSettings_showRepeaters": "Pokaż przekaźniki",
+ "appSettings_showRepeatersSubtitle": "Wyświetl węzły przekaźników na mapie",
"appSettings_showChatNodes": "Pokaż Węzły Rozmowy",
"appSettings_showChatNodesSubtitle": "Wyświetl węzły czatu na mapie",
"appSettings_showOtherNodes": "Pokaż inne węzły",
@@ -229,15 +229,15 @@
}
}
},
- "appSettings_mapTimeFilter": "Filtrowanie Czasu Mapy",
+ "appSettings_mapTimeFilter": "Filtr czasu mapy",
"appSettings_showNodesDiscoveredWithin": "Pokaż węzły odkryte w:",
- "appSettings_allTime": "Wszystko czasowo",
+ "appSettings_allTime": "Cały czas",
"appSettings_lastHour": "Ostatnia godzina",
"appSettings_last6Hours": "Ostatnie 6 godzin",
"appSettings_last24Hours": "Ostatnie 24 godziny",
- "appSettings_lastWeek": "Tydzień temu",
- "appSettings_offlineMapCache": "Bufor Map Offline",
- "appSettings_noAreaSelected": "Nie zaznaczono żadnej powierzchni.",
+ "appSettings_lastWeek": "Ostatni tydzień",
+ "appSettings_offlineMapCache": "Pamięć podręczna map offline",
+ "appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.",
"appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})",
"@appSettings_areaSelectedZoom": {
"placeholders": {
@@ -251,17 +251,27 @@
},
"appSettings_debugCard": "Debug",
"appSettings_appDebugLogging": "Logowanie Debugowania Aplikacji",
- "appSettings_appDebugLoggingSubtitle": "Loguj wiadomości debugowania aplikacji w celu rozwiązywania problemów.",
- "appSettings_appDebugLoggingEnabled": "Zdebugowanie aplikacji włączone",
- "appSettings_appDebugLoggingDisabled": "Zasubskrybowane logi debugowania aplikacji wyłączone.",
+ "appSettings_appDebugLoggingSubtitle": "Rejestruj komunikaty debugowania aplikacji w celu diagnozowania problemów.",
+ "appSettings_appDebugLoggingEnabled": "Logowanie debugowania aplikacji włączone",
+ "appSettings_appDebugLoggingDisabled": "Logowanie debugowania aplikacji wyłączone.",
"contacts_title": "Kontakty",
"contacts_noContacts": "Brak jeszcze kontaktów.",
- "contacts_contactsWillAppear": "Kontakty będą wyświetlane, gdy urządzenia reklamują się.",
- "contacts_searchContacts": "Wyszukaj kontakty...",
+ "contacts_contactsWillAppear": "Kontakty będą wyświetlane, gdy urządzenia nadają rozgłoszenia.",
+ "contacts_searchContacts": "Wyszukaj {number}{str} {number, plural, one{kontakt} few{kontakty} many{kontaktów} other{kontaktu}}...",
+ "@contacts_searchContacts": {
+ "placeholders": {
+ "number": {
+ "type": "int"
+ },
+ "str": {
+ "type": "String"
+ }
+ }
+ },
"contacts_noUnreadContacts": "Brak nieprzeczytanych kontaktów",
"contacts_noContactsFound": "Brak znalezionych kontaktów ani grup.",
"contacts_deleteContact": "Usuń Kontakt",
- "contacts_removeConfirm": "Usuń {contactName} z kontaktów?",
+ "contacts_removeConfirm": "Usunąć {contactName} z kontaktów?",
"@contacts_removeConfirm": {
"placeholders": {
"contactName": {
@@ -269,7 +279,7 @@
}
}
},
- "contacts_manageRepeater": "Zarządzaj Powtórzami",
+ "contacts_manageRepeater": "Zarządzaj przekaźnikiem",
"contacts_roomLogin": "Logowanie do pokoju",
"contacts_openChat": "Otwórz czat",
"contacts_editGroup": "Edytuj Grupę",
@@ -297,8 +307,8 @@
"contacts_filterContacts": "Filtruj kontakty...",
"contacts_noContactsMatchFilter": "Brak pasujących kontaktów do Twojego filtra",
"contacts_noMembers": "Brak członków",
- "contacts_lastSeenNow": "Ostatnie połączenie",
- "contacts_lastSeenMinsAgo": "Ostatnie połączenie {minutes} min temu",
+ "contacts_lastSeenNow": "niedawno",
+ "contacts_lastSeenMinsAgo": "~ {minutes} min",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -306,8 +316,8 @@
}
}
},
- "contacts_lastSeenHourAgo": "Ostatni raz widziany 1 godzinę temu",
- "contacts_lastSeenHoursAgo": "Ostatnie połączenie {hours} godzin temu",
+ "contacts_lastSeenHourAgo": "~ 1 godz.",
+ "contacts_lastSeenHoursAgo": "~ {hours} godz.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -315,8 +325,8 @@
}
}
},
- "contacts_lastSeenDayAgo": "Ostatni raz widziany 1 dzień temu",
- "contacts_lastSeenDaysAgo": "Ostatnie połączenie {days} dni temu",
+ "contacts_lastSeenDayAgo": "~ 1 dzień",
+ "contacts_lastSeenDaysAgo": "~ {days} dni",
"@contacts_lastSeenDaysAgo": {
"placeholders": {
"days": {
@@ -337,9 +347,9 @@
}
}
},
- "channels_hashtagChannel": "Kanał z hashtagami",
+ "channels_hashtagChannel": "Kanał hashtagów",
"channels_public": "Publiczny",
- "channels_private": "Prywatne",
+ "channels_private": "Prywatny",
"channels_publicChannel": "Kanał publiczny",
"channels_privateChannel": "Prywatny kanał",
"channels_editChannel": "Edytuj kanał",
@@ -366,11 +376,11 @@
"channels_channelIndexLabel": "Indeks kanału",
"channels_channelName": "Nazwa kanału",
"channels_usePublicChannel": "Użyj kanału publicznego",
- "channels_standardPublicPsk": "Standard public PSK",
+ "channels_standardPublicPsk": "Standardowy publiczny PSK",
"channels_pskHex": "PSK (Hex)",
"channels_generateRandomPsk": "Wygeneruj losowy klucz PSK",
"channels_enterChannelName": "Proszę podać nazwę kanału.",
- "channels_pskMustBe32Hex": "PSK musi mieć 32 znaki szesnastkowe.",
+ "channels_pskMustBe32Hex": "PSK musi składać się z 32 znaków szesnastkowych.",
"channels_channelAdded": "Kanał \"{name}\" dodany",
"@channels_channelAdded": {
"placeholders": {
@@ -401,7 +411,7 @@
"channels_sortManual": "Ręczna",
"channels_sortAZ": "A-Z",
"channels_sortLatestMessages": "Najnowsze wiadomości",
- "channels_sortUnread": "Niezgłoszone",
+ "channels_sortUnread": "Nieprzeczytane",
"chat_noMessages": "Brak jeszcze wiadomości",
"chat_sendMessageToStart": "Wyślij wiadomość, aby rozpocząć.",
"chat_originalMessageNotFound": "Błąd: Nie znaleziono oryginalnego komunikatu",
@@ -413,7 +423,7 @@
}
}
},
- "chat_replyTo": "Odpowiedz {name}",
+ "chat_replyTo": "Odpowiedz do {name}",
"@chat_replyTo": {
"placeholders": {
"name": {
@@ -441,8 +451,8 @@
},
"chat_messageCopied": "Wiadomość skopiowana",
"chat_messageDeleted": "Wiadomość usunięta",
- "chat_retryingMessage": "Próba ponowienia",
- "chat_retryCount": "Spróbuj {current}/{max}",
+ "chat_retryingMessage": "Ponawianie wiadomości",
+ "chat_retryCount": "Próba {current}/{max}",
"@chat_retryCount": {
"placeholders": {
"current": {
@@ -458,8 +468,8 @@
"chat_addReaction": "Dodaj Reakcję",
"chat_me": "Ja",
"emojiCategorySmileys": "Emoji",
- "emojiCategoryGestures": "Gestikulacje",
- "emojiCategoryHearts": "Serce",
+ "emojiCategoryGestures": "Gesty",
+ "emojiCategoryHearts": "Serca",
"emojiCategoryObjects": "Obiekty",
"gifPicker_title": "Wybierz GIF",
"gifPicker_searchHint": "Wyszukaj GIF-y...",
@@ -476,9 +486,9 @@
"debugLog_bleCopied": "Skopiowany log BLE",
"debugLog_noEntries": "Nie ma jeszcze żadnych logów debugowania.",
"debugLog_enableInSettings": "Włącz logowanie debugowania aplikacji w ustawieniach",
- "debugLog_frames": "Ramy",
- "debugLog_rawLogRx": "Surowe Log-RX",
- "debugLog_noBleActivity": "Brak aktywności BLE jeszcze.",
+ "debugLog_frames": "Ramki",
+ "debugLog_rawLogRx": "Surowy log RX",
+ "debugLog_noBleActivity": "Brak aktywności BLE.",
"debugFrame_length": "Długość ramy: {count} bajtów",
"@debugFrame_length": {
"placeholders": {
@@ -496,7 +506,7 @@
}
},
"debugFrame_textMessageHeader": "Wiadomość tekstowa:",
- "debugFrame_destinationPubKey": "- Oznaczenie PubKey: {pubKey}",
+ "debugFrame_destinationPubKey": "- Docelowy klucz publiczny: {pubKey}",
"@debugFrame_destinationPubKey": {
"placeholders": {
"pubKey": {
@@ -504,7 +514,7 @@
}
}
},
- "debugFrame_timestamp": "- Timestamp: {timestamp}",
+ "debugFrame_timestamp": "- Znacznik czasu: {timestamp}",
"@debugFrame_timestamp": {
"placeholders": {
"timestamp": {
@@ -532,7 +542,7 @@
}
},
"debugFrame_textTypeCli": "CLI",
- "debugFrame_textTypePlain": "Proste",
+ "debugFrame_textTypePlain": "Zwykły",
"debugFrame_text": "- Tekst: \"{text}\"",
"@debugFrame_text": {
"placeholders": {
@@ -541,16 +551,16 @@
}
}
},
- "debugFrame_hexDump": "Wyjście SzESZCZNULNE:",
+ "debugFrame_hexDump": "Zrzut hex:",
"chat_pathManagement": "Zarządzanie ścieżkami",
"chat_routingMode": "Tryb routingu",
"chat_autoUseSavedPath": "Automatyczne (użyj zapisanej ścieżki)",
- "chat_forceFloodMode": "Wymusz Tryb Powodowany",
+ "chat_forceFloodMode": "Wymuś tryb zalewowy",
"chat_recentAckPaths": "Ostatnie ścieżki ACK (naciśnij, aby użyć):",
"chat_pathHistoryFull": "Historia ścieżek jest pełna. Usuń wpisy, aby dodać nowe.",
- "chat_hopSingular": "Skacz",
- "chat_hopPlural": "skoczkowie",
- "chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
+ "chat_hopSingular": "skok",
+ "chat_hopPlural": "skoki",
+ "chat_hopsCount": "{count} {count, plural, one{skok} few{skoki} many{skoków} other{skoków}}",
"@chat_hopsCount": {
"placeholders": {
"count": {
@@ -560,15 +570,15 @@
},
"chat_successes": "Sukcesy",
"chat_removePath": "Usuń ścieżkę",
- "chat_noPathHistoryYet": "Brak jeszcze historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.",
+ "chat_noPathHistoryYet": "Brak historii ścieżek.\nWyślij wiadomość, aby odkryć ścieżki.",
"chat_pathActions": "Działania ścieżki:",
- "chat_setCustomPath": "Ustaw Ścieżkę Dostosowaną",
+ "chat_setCustomPath": "Ustaw ścieżkę niestandardową",
"chat_setCustomPathSubtitle": "Ręcznie określ trasę.",
"chat_clearPath": "Wyczyść Ścieżkę",
- "chat_clearPathSubtitle": "Zmusz do ponownej identyfikacji przy następnym wysłaniu",
- "chat_pathCleared": "Ścieżka oczyszczona. Kolejne powiadomienie odnajdzie trasę.",
+ "chat_clearPathSubtitle": "Wymuś ponowne wyznaczenie trasy przy następnym wysłaniu",
+ "chat_pathCleared": "Ścieżka wyczyszczona. Następna wiadomość odnajdzie trasę.",
"chat_floodModeSubtitle": "Użyj przełącznika routingu w pasku narzędzi.",
- "chat_floodModeEnabled": "Tryb powodziowy włączony. Włącz ponownie za pomocą ikony routingu w pasku narzędzi.",
+ "chat_floodModeEnabled": "Tryb zalewowy włączony. Przełącz z powrotem ikoną routingu w pasku aplikacji.",
"chat_fullPath": "Pełna ścieżka",
"chat_pathDetailsNotAvailable": "Szczegóły ścieżki jeszcze niedostępne. Spróbuj wysłać wiadomość, aby odświeżyć.",
"chat_pathSetHops": "Ścieżka ustawiona: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -585,11 +595,11 @@
"chat_pathSavedLocally": "Zapisano lokalnie. Połącz się, aby zsynchronizować.",
"chat_pathDeviceConfirmed": "Urządzenie potwierdzone.",
"chat_pathDeviceNotConfirmed": "Urządzenie nie zostało jeszcze potwierdzone.",
- "chat_type": "Wprowadź",
+ "chat_type": "Typ",
"chat_path": "Ścieżka",
"chat_publicKey": "Klucz Publiczny",
"chat_compressOutgoingMessages": "Kompresuj wychodzące wiadomości",
- "chat_floodForced": "Powodowana Powódź",
+ "chat_floodForced": "Zalew (wymuszony)",
"chat_directForced": "Bezpośrednio (wymuszono)",
"chat_hopsForced": "{count} skoków (wymuszonych)",
"@chat_hopsForced": {
@@ -599,10 +609,10 @@
}
}
},
- "chat_floodAuto": "Powodzie (automatyczne)",
+ "chat_floodAuto": "Zalew (automatyczny)",
"chat_direct": "Bezpośrednio",
"chat_poiShared": "Wspólny POI",
- "chat_unread": "Niezgłoszone: {count}",
+ "chat_unread": "Nieprzeczytane: {count}",
"@chat_unread": {
"placeholders": {
"count": {
@@ -633,7 +643,7 @@
}
}
},
- "map_pinsCount": "Pinki: {count}",
+ "map_pinsCount": "Pinezki: {count}",
"@map_pinsCount": {
"placeholders": {
"count": {
@@ -642,26 +652,26 @@
}
},
"map_chat": "Rozmowa",
- "map_repeater": "Powtórzacz",
+ "map_repeater": "Przekaźnik",
"map_room": "Pokój",
"map_sensor": "Czujnik",
- "map_pinDm": "Zablokuj (DM)",
- "map_pinPrivate": "Zablokuj (Prywatnie)",
- "map_pinPublic": "Oznacz jako publiczne",
+ "map_pinDm": "Pinezka (DM)",
+ "map_pinPrivate": "Pinezka (prywatna)",
+ "map_pinPublic": "Pinezka (publiczna)",
"map_lastSeen": "Ostatni raz widziany",
"map_disconnectConfirm": "Czy na pewno chcesz się odłączyć od tego urządzenia?",
"map_from": "Od",
"map_source": "Źródło",
"map_flags": "Flagi",
"map_shareMarkerHere": "Udostępnij znacznik tutaj",
- "map_pinLabel": "Oznacz etykietę",
+ "map_pinLabel": "Etykieta pinezki",
"map_label": "Etykieta",
"map_pointOfInterest": "Punkt zainteresowań",
"map_sendToContact": "Wyślij do kontaktu",
"map_sendToChannel": "Wyślij do kanału",
"map_noChannelsAvailable": "Brak dostępnych kanałów",
"map_publicLocationShare": "Udostępnij lokalizację publicznie",
- "map_publicLocationShareConfirm": "Wkrótce udostępnisz lokalizację w {channelLabel}. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.",
+ "map_publicLocationShareConfirm": "Zamierzasz udostępnić lokalizację w {channelLabel}. Ten kanał jest publiczny i każdy z PSK może go zobaczyć.",
"@map_publicLocationShareConfirm": {
"placeholders": {
"channelLabel": {
@@ -669,26 +679,26 @@
}
}
},
- "map_connectToShareMarkers": "Połącz się z urządzeniem, aby udostępniać znacznik.",
+ "map_connectToShareMarkers": "Połącz się z urządzeniem, aby udostępniać znaczniki.",
"map_filterNodes": "Filtruj Węzły",
"map_nodeTypes": "Typy węzłów",
"map_chatNodes": "Węzły czatu",
- "map_repeaters": "Powtarzacze",
+ "map_repeaters": "Przekaźniki",
"map_otherNodes": "Inne węzły",
"map_keyPrefix": "Prefiks klucza",
"map_filterByKeyPrefix": "Filtruj po prefiksie klucza",
- "map_publicKeyPrefix": "Przewód klucza publicznego",
- "map_markers": "Oznaczarki",
- "map_showSharedMarkers": "Pokaż współdzielone znaki.",
- "map_lastSeenTime": "Ostatni raz widiany",
- "map_sharedPin": "Podzielony PIN",
+ "map_publicKeyPrefix": "Prefiks klucza publicznego",
+ "map_markers": "Znaczniki",
+ "map_showSharedMarkers": "Pokaż udostępnione znaczniki.",
+ "map_lastSeenTime": "Ostatni raz widziany",
+ "map_sharedPin": "Udostępniona pinezka",
"map_joinRoom": "Dołącz do pokoju",
- "map_manageRepeater": "Zarządzaj Powtórzami",
- "mapCache_title": "Bufor Map Offline",
- "mapCache_selectAreaFirst": "Wybierz obszar do wstępnego pobrania.",
- "mapCache_noTilesToDownload": "Brak dostępnych płytek do pobrania dla tego obszaru.",
- "mapCache_downloadTilesTitle": "Pobierz płytki",
- "mapCache_downloadTilesPrompt": "Pobierz {count} płytek do użytku offline?",
+ "map_manageRepeater": "Zarządzaj przekaźnikiem",
+ "mapCache_title": "Pamięć podręczna map offline",
+ "mapCache_selectAreaFirst": "Najpierw wybierz obszar do zapisania w pamięci podręcznej.",
+ "mapCache_noTilesToDownload": "Brak kafelków do pobrania dla tego obszaru.",
+ "mapCache_downloadTilesTitle": "Pobierz kafelki",
+ "mapCache_downloadTilesPrompt": "Pobrać {count} kafelków do użytku offline?",
"@mapCache_downloadTilesPrompt": {
"placeholders": {
"count": {
@@ -697,7 +707,7 @@
}
},
"mapCache_downloadAction": "Pobierz",
- "mapCache_cachedTiles": "Pamiętanych {count} płytek",
+ "mapCache_cachedTiles": "Zapisano w pamięci podręcznej {count} kafelków",
"@mapCache_cachedTiles": {
"placeholders": {
"count": {
@@ -705,7 +715,7 @@
}
}
},
- "mapCache_cachedTilesWithFailed": "Pamiętane {downloaded} płytki ({failed} nieudane)",
+ "mapCache_cachedTilesWithFailed": "Zapisano w pamięci podręcznej {downloaded} kafelków ({failed} nieudanych)",
"@mapCache_cachedTilesWithFailed": {
"placeholders": {
"downloaded": {
@@ -717,13 +727,13 @@
}
},
"mapCache_clearOfflineCacheTitle": "Wyczyść pamięć podręczną offline",
- "mapCache_clearOfflineCachePrompt": "Usuń wszystkie tymczasowe kafelki mapy?",
- "mapCache_offlineCacheCleared": "Pamięć podręczna offline została wyczyszczona",
- "mapCache_noAreaSelected": "Nie zaznaczono żadnej powierzchni.",
+ "mapCache_clearOfflineCachePrompt": "Usunąć wszystkie zapisane kafelki mapy?",
+ "mapCache_offlineCacheCleared": "Wyczyszczono pamięć podręczną offline",
+ "mapCache_noAreaSelected": "Nie wybrano żadnego obszaru.",
"mapCache_cacheArea": "Obszar pamięci podręcznej",
"mapCache_useCurrentView": "Użyj aktualnego widoku",
- "mapCache_zoomRange": "Zakres powiększenia",
- "mapCache_estimatedTiles": "Szacunkowa liczba płytek: {count}",
+ "mapCache_zoomRange": "Zakres przybliżenia",
+ "mapCache_estimatedTiles": "Szacowana liczba kafelków: {count}",
"@mapCache_estimatedTiles": {
"placeholders": {
"count": {
@@ -742,7 +752,7 @@
}
}
},
- "mapCache_downloadTilesButton": "Pobierz Paski",
+ "mapCache_downloadTilesButton": "Pobierz kafelki",
"mapCache_clearCacheButton": "Wyczyść pamięć podręczną",
"mapCache_failedDownloads": "Nieudane pobrania: {count}",
"@mapCache_failedDownloads": {
@@ -801,23 +811,23 @@
"time_week": "tydzień",
"time_weeks": "tygodnie",
"time_month": "miesiąc",
- "time_months": "miesiace",
+ "time_months": "miesiące",
"time_minutes": "minuty",
- "time_allTime": "Wszystko czasowo",
+ "time_allTime": "Cały czas",
"dialog_disconnect": "Odłącz",
"dialog_disconnectConfirm": "Czy na pewno chcesz się odłączyć od tego urządzenia?",
- "login_repeaterLogin": "Powtórz Logowanie",
+ "login_repeaterLogin": "Logowanie do przekaźnika",
"login_roomLogin": "Logowanie do pokoju",
"login_password": "Hasło",
"login_enterPassword": "Wprowadź hasło",
"login_savePassword": "Zapisz hasło",
"login_savePasswordSubtitle": "Hasło będzie bezpiecznie przechowywane na tym urządzeniu.",
- "login_repeaterDescription": "Wprowadź hasło do powtarzacza, aby uzyskać dostęp do ustawień i statusu.",
+ "login_repeaterDescription": "Wprowadź hasło do przekaźnika, aby uzyskać dostęp do ustawień i stanu.",
"login_roomDescription": "Wprowadź hasło do pokoju, aby uzyskać dostęp do ustawień i statusu.",
- "login_routing": "Przekierowanie",
+ "login_routing": "Trasowanie",
"login_routingMode": "Tryb routingu",
"login_autoUseSavedPath": "Automatycznie (użyj zapisanej ścieżki)",
- "login_forceFloodMode": "Wymusz Tryb Powodowany",
+ "login_forceFloodMode": "Wymuś tryb zalewowy",
"login_managePaths": "Zarządzaj Ścieżkami",
"login_login": "Zaloguj się",
"login_attempt": "Próba {current}/{max}",
@@ -839,10 +849,10 @@
}
}
},
- "login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.",
- "common_reload": "Ponownie załadować",
+ "login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo przekaźnik jest nieosiągalny.",
+ "common_reload": "Odśwież",
"common_clear": "Wyczyść",
- "path_currentPath": "Aktualny ścieżka: {path}",
+ "path_currentPath": "Aktualna ścieżka: {path}",
"@path_currentPath": {
"placeholders": {
"path": {
@@ -850,7 +860,7 @@
}
}
},
- "path_usingHopsPath": "Użyj ścieżki {count} {count, plural, =1{hop} other{hops}}.",
+ "path_usingHopsPath": "Użyj ścieżki {count} {count, plural, one{skok} few{skoki} many{skoków} other{skoków}}.",
"@path_usingHopsPath": {
"placeholders": {
"count": {
@@ -859,13 +869,13 @@
}
},
"path_enterCustomPath": "Wprowadź własną ścieżkę",
- "path_currentPathLabel": "Aktualny ścieżka",
+ "path_currentPathLabel": "Aktualna ścieżka",
"path_hexPrefixInstructions": "Wprowadź 2-znakowe prefiksy szesnastkowe dla każdego skoku, oddzielone przecinkami.",
- "path_hexPrefixExample": "A1,F2,3C (każedy węzeł używa pierwszego bajtu swojego klucza publicznego)",
- "path_labelHexPrefixes": "Ścieżka (przesunięcia bitowe)",
- "path_helperMaxHops": "Maksymalnie 64 skoki. Każda prefiks ma 2 znaki szesnastkowe (1 bajt).",
+ "path_hexPrefixExample": "A1,F2,3C (każdy węzeł używa pierwszego bajtu swojego klucza publicznego)",
+ "path_labelHexPrefixes": "Ścieżka (prefiksy hex)",
+ "path_helperMaxHops": "Maksymalnie 64 skoki. Każdy prefiks ma 2 znaki szesnastkowe (1 bajt).",
"path_selectFromContacts": "Albo wybierz z kontaktów:",
- "path_noRepeatersFound": "Nie znaleziono repeaterów ani serwerów pokoi.",
+ "path_noRepeatersFound": "Nie znaleziono przekaźników ani serwerów pokoi.",
"path_customPathsRequire": "Dostosowane ścieżki wymagają pośrednich skoków, które mogą przekazywać wiadomości.",
"path_invalidHexPrefixes": "Nieprawidłowe prefiksy szesnastkowe: {prefixes}",
"@path_invalidHexPrefixes": {
@@ -877,23 +887,23 @@
},
"path_tooLong": "Ścieżka jest zbyt długa. Dozwolonych skoków wynosi 64.",
"path_setPath": "Ustaw Ścieżkę",
- "repeater_management": "Zarządzanie Powtórzami",
+ "repeater_management": "Zarządzanie przekaźnikami",
"repeater_managementTools": "Narzędzia Zarządzania",
"repeater_status": "Status",
- "repeater_statusSubtitle": "Wyświetl status powtarzacza, statystyki i sąsiadów.",
- "repeater_telemetry": "Telemetry",
+ "repeater_statusSubtitle": "Wyświetl status przekaźnika, statystyki i sąsiadów.",
+ "repeater_telemetry": "Telemetria",
"repeater_telemetrySubtitle": "Wyświetl dane telemetryczne z czujników i statystyki systemu",
"repeater_cli": "CLI",
- "repeater_cliSubtitle": "Wyślij polecenia do powielacza",
+ "repeater_cliSubtitle": "Wyślij polecenia do przekaźnika",
"repeater_settings": "Ustawienia",
- "repeater_settingsSubtitle": "Skonfiguruj parametry powtarzacza",
- "repeater_statusTitle": "Status powtarzacza",
+ "repeater_settingsSubtitle": "Skonfiguruj parametry przekaźnika",
+ "repeater_statusTitle": "Status przekaźnika",
"repeater_routingMode": "Tryb routingu",
"repeater_autoUseSavedPath": "Automatycznie (użyj zapisanej ścieżki)",
- "repeater_forceFloodMode": "Wymusz Tryb Powodowany",
+ "repeater_forceFloodMode": "Wymuś tryb zalewowy",
"repeater_pathManagement": "Zarządzanie ścieżkami",
"repeater_refresh": "Odśwież",
- "repeater_statusRequestTimeout": "Życzenie statusu timed out.",
+ "repeater_statusRequestTimeout": "Przekroczono czas oczekiwania na status.",
"repeater_errorLoadingStatus": "Błąd podczas ładowania statusu: {error}",
"@repeater_errorLoadingStatus": {
"placeholders": {
@@ -905,15 +915,15 @@
"repeater_systemInformation": "Informacje o systemie",
"repeater_battery": "Bateria",
"repeater_clockAtLogin": "Godzina (przy logowaniu)",
- "repeater_uptime": "Dostępność",
+ "repeater_uptime": "Czas pracy",
"repeater_queueLength": "Długość kolejki",
"repeater_debugFlags": "Opcje debugowania",
"repeater_radioStatistics": "Statystyki Radia",
"repeater_lastRssi": "Ostatni RSSI",
"repeater_lastSnr": "Ostatnie SNR",
"repeater_noiseFloor": "Poziom Szumów",
- "repeater_txAirtime": "TX Airtime",
- "repeater_rxAirtime": "RX Airtime",
+ "repeater_txAirtime": "Czas nadawania TX",
+ "repeater_rxAirtime": "Czas odbioru RX",
"repeater_packetStatistics": "Statystyki pakietów",
"repeater_sent": "Wysłane",
"repeater_received": "Otrzymano",
@@ -935,7 +945,7 @@
}
}
},
- "repeater_packetTxTotal": "Razem: {total}, Powodzenie: {flood}, Bezpośrednio: {direct}",
+ "repeater_packetTxTotal": "Razem: {total}, Zalew: {flood}, Bezpośrednio: {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -949,7 +959,7 @@
}
}
},
- "repeater_packetRxTotal": "Razem: {total}, Powodzenie: {flood}, Bezpośrednio: {direct}",
+ "repeater_packetRxTotal": "Razem: {total}, Zalew: {flood}, Bezpośrednio: {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -963,7 +973,7 @@
}
}
},
- "repeater_duplicatesFloodDirect": "Powodzie: {flood}, Bezpośrednie: {direct}",
+ "repeater_duplicatesFloodDirect": "Zalew: {flood}, Bezpośrednie: {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -982,22 +992,22 @@
}
}
},
- "repeater_settingsTitle": "Ustawienia Powtórki",
+ "repeater_settingsTitle": "Ustawienia przekaźnika",
"repeater_basicSettings": "Podstawowe Ustawienia",
- "repeater_repeaterName": "Nazwa Powtórnika",
- "repeater_repeaterNameHelper": "Wyświetl nazwę tego powtarzacza",
+ "repeater_repeaterName": "Nazwa przekaźnika",
+ "repeater_repeaterNameHelper": "Wyświetl nazwę tego przekaźnika",
"repeater_adminPassword": "Hasło Administracyjne",
- "repeater_adminPasswordHelper": "Pełny dostęp hasło",
+ "repeater_adminPasswordHelper": "Hasło z pełnym dostępem",
"repeater_guestPassword": "Hasło gościa",
- "repeater_guestPasswordHelper": "Dostęp tylko do odczytu hasło",
+ "repeater_guestPasswordHelper": "Hasło tylko do odczytu",
"repeater_radioSettings": "Ustawienia radia",
"repeater_frequencyMhz": "Częstotliwość (MHz)",
"repeater_frequencyHelper": "300-2500 MHz",
- "repeater_txPower": "TX Power",
+ "repeater_txPower": "Moc TX",
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Przepustowość",
- "repeater_spreadingFactor": "Rozkład Czynnika",
- "repeater_codingRate": "Stawka kodowania",
+ "repeater_spreadingFactor": "Współczynnik rozpraszania",
+ "repeater_codingRate": "Współczynnik kodowania",
"repeater_locationSettings": "Ustawienia Lokalizacji",
"repeater_latitude": "Szerokość",
"repeater_latitudeHelper": "Stopnie dziesiętne (np. 37.7749)",
@@ -1005,13 +1015,13 @@
"repeater_longitudeHelper": "Stopnie dziesiętne (np. -122,4194)",
"repeater_features": "Funkcje",
"repeater_packetForwarding": "Przekierowanie pakietów",
- "repeater_packetForwardingSubtitle": "Włącz repeater, aby przekazywać pakiety.",
+ "repeater_packetForwardingSubtitle": "Włącz przekaźnik, aby przekazywać pakiety.",
"repeater_guestAccess": "Dostęp dla gości",
"repeater_guestAccessSubtitle": "Umożliw dostęp tylko do odczytu dla gości.",
- "repeater_privacyMode": "Tryb Prywatności",
- "repeater_privacyModeSubtitle": "Ukryj imię/lokalizację w reklamach",
- "repeater_advertisementSettings": "Ustawienia Reklam",
- "repeater_localAdvertInterval": "Interwał Reklamy Lokalnej",
+ "repeater_privacyMode": "Tryb prywatności",
+ "repeater_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach",
+ "repeater_advertisementSettings": "Ustawienia rozgłoszeń",
+ "repeater_localAdvertInterval": "Interwał rozgłoszenia lokalnego",
"repeater_localAdvertIntervalMinutes": "{minutes} minut",
"@repeater_localAdvertIntervalMinutes": {
"placeholders": {
@@ -1020,7 +1030,7 @@
}
}
},
- "repeater_floodAdvertInterval": "Interwał Reklamy Powodziowej",
+ "repeater_floodAdvertInterval": "Interwał rozgłoszenia zalewowego",
"repeater_floodAdvertIntervalHours": "{hours} godzin",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1029,17 +1039,17 @@
}
}
},
- "repeater_encryptedAdvertInterval": "Zaszyfrowany Interwał Reklamowy",
+ "repeater_encryptedAdvertInterval": "Interwał Zaszyfrowanego Rozgłoszenia",
"repeater_dangerZone": "Strefa Zagrożeń",
- "repeater_rebootRepeater": "Zrestartuj Powtarzacz",
- "repeater_rebootRepeaterSubtitle": "Zrestartuj urządzenie powtarzające.",
- "repeater_rebootRepeaterConfirm": "Czy na pewno chcesz zrestartować ten repeater?",
+ "repeater_rebootRepeater": "Zrestartuj Przekaźnik",
+ "repeater_rebootRepeaterSubtitle": "Zrestartuj przekaźnik.",
+ "repeater_rebootRepeaterConfirm": "Czy na pewno chcesz zrestartować ten przekaźnik?",
"repeater_regenerateIdentityKey": "Wygeneruj klucz tożsamości",
"repeater_regenerateIdentityKeySubtitle": "Wygeneruj nową parę kluczy publicznych/prywatnych",
- "repeater_regenerateIdentityKeyConfirm": "To zostanie wygenerowane nowe tożsamość dla powtarzacza. Kontynuować?",
+ "repeater_regenerateIdentityKeyConfirm": "Zostanie wygenerowana nowa tożsamość dla przekaźnika. Kontynuować?",
"repeater_eraseFileSystem": "Wyczyść System Plików",
- "repeater_eraseFileSystemSubtitle": "Sformatuj system plików powielacza",
- "repeater_eraseFileSystemConfirm": "OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z powtarzacza. Nie da się tego cofnąć!",
+ "repeater_eraseFileSystemSubtitle": "Sformatuj system plików przekaźnika",
+ "repeater_eraseFileSystemConfirm": "OSTRZEŻENIE: To spowoduje usunięcie wszystkich danych z przekaźnika. Nie da się tego cofnąć!",
"repeater_eraseSerialOnly": "Usunięcie jest dostępne tylko przez konsolę szeregową.",
"repeater_commandSent": "Polecenie wysłane: {command}",
"@repeater_commandSent": {
@@ -1068,13 +1078,13 @@
}
},
"repeater_refreshBasicSettings": "Odśwież Podstawowe Ustawienia",
- "repeater_refreshRadioSettings": "Odśwież Ustawienia Radio",
- "repeater_refreshTxPower": "Odśwież TX power",
+ "repeater_refreshRadioSettings": "Odśwież ustawienia radia",
+ "repeater_refreshTxPower": "Odśwież moc TX",
"repeater_refreshLocationSettings": "Odśwież Ustawienia Lokalizacji",
"repeater_refreshPacketForwarding": "Odśwież trasowanie pakietów",
"repeater_refreshGuestAccess": "Odśwież dostęp gościa",
"repeater_refreshPrivacyMode": "Odśwież Tryb Prywatności",
- "repeater_refreshAdvertisementSettings": "Odśwież Ustawienia Reklamy",
+ "repeater_refreshAdvertisementSettings": "Odśwież ustawienia rozgłoszeń",
"repeater_refreshed": "{label} odświeżone",
"@repeater_refreshed": {
"placeholders": {
@@ -1091,7 +1101,7 @@
}
}
},
- "repeater_cliTitle": "Powtarzacz CLI",
+ "repeater_cliTitle": "Przekaźnik CLI",
"repeater_debugNextCommand": "Debug Następną Komendę",
"repeater_commandHelp": "Pomoc",
"repeater_clearHistory": "Wyczyść historię",
@@ -1101,7 +1111,7 @@
"repeater_previousCommand": "Poprzednia komenda",
"repeater_nextCommand": "Następna komenda",
"repeater_enterCommandFirst": "Wprowadź najpierw polecenie",
- "repeater_cliCommandFrameTitle": "Określony Wyraz Polecenia CLI",
+ "repeater_cliCommandFrameTitle": "Ramka polecenia CLI",
"repeater_cliCommandError": "Błąd: {error}",
"@repeater_cliCommandError": {
"placeholders": {
@@ -1110,14 +1120,14 @@
}
}
},
- "repeater_cliQuickGetName": "Pobierz imię",
- "repeater_cliQuickGetRadio": "Uzyskaj Radio",
+ "repeater_cliQuickGetName": "Pobierz nazwę",
+ "repeater_cliQuickGetRadio": "Pobierz radio",
"repeater_cliQuickGetTx": "Pobierz TX",
"repeater_cliQuickNeighbors": "Sąsiedzi",
"repeater_cliQuickVersion": "Wersja",
- "repeater_cliQuickAdvertise": "Reklama",
+ "repeater_cliQuickAdvertise": "Rozgłoś",
"repeater_cliQuickClock": "Godzina",
- "repeater_cliHelpAdvert": "Wysyła pakiet reklamowy",
+ "repeater_cliHelpAdvert": "Wysyła pakiet rozgłoszeniowy",
"repeater_cliHelpReboot": "Zresetuj urządzenie. (Uwaga, może pojawić się 'Timeout', co jest normalne)",
"repeater_cliHelpClock": "Wyświetla aktualny czas zgodnie z zegarem urządzenia.",
"repeater_cliHelpPassword": "Ustawia nowe hasło administratora dla urządzenia.",
@@ -1125,18 +1135,18 @@
"repeater_cliHelpClearStats": "Resetuje różne wskaźniki statystyk do zera.",
"repeater_cliHelpSetAf": "Ustawia czynnik czasu powietrznego.",
"repeater_cliHelpSetTx": "Ustawia moc transmisji LoRa w dBm. (zrestartuj, aby zastosować)",
- "repeater_cliHelpSetRepeat": "Włącza lub wyłącza rolę powtarzacza dla tego węzła.",
+ "repeater_cliHelpSetRepeat": "Włącza lub wyłącza rolę przekaźnika dla tego węzła.",
"repeater_cliHelpSetAllowReadOnly": "(Serwer pokoju) Jeśli 'włączone', to logowanie z pustym hasłem będzie dozwolone, ale nie można publikować w pokoju (tylko czytać).",
- "repeater_cliHelpSetFloodMax": "Ustawia maksymalną liczbę skoków pakietu powrotnego (jeśli >= max, pakiet nie jest przekierowywany)",
+ "repeater_cliHelpSetFloodMax": "Ustawia maksymalną liczbę skoków pakietu zalewowego (jeśli >= max, pakiet nie jest przekierowywany)",
"repeater_cliHelpSetIntThresh": "Ustawia Próg Interferencji (w dB). Domyślnie wynosi 14. Ustaw na 0, aby wyłączyć wykrywanie zakłóceń kanału.",
- "repeater_cliHelpSetAgcResetInterval": "Ustawia interwał do zresetowania Automatycznego Sterownika Głośności. Ustaw na 0, aby wyłączyć.",
+ "repeater_cliHelpSetAgcResetInterval": "Ustawia interwał do zresetowania automatycznego wzmocnienia (AGC). Ustaw na 0, aby wyłączyć.",
"repeater_cliHelpSetMultiAcks": "Włącza lub wyłącza funkcję 'podwójnych potwierdzeń'.",
- "repeater_cliHelpSetAdvertInterval": "Ustawia interwał timera w minutach do wysyłania pakietu reklamy lokalnej (bezpośredniej). Ustaw na 0, aby wyłączyć.",
- "repeater_cliHelpSetFloodAdvertInterval": "Ustawia interwał timera w godzinach do wysłania pakietu reklamowego typu \"powiew\". Ustaw na 0, aby wyłączyć.",
- "repeater_cliHelpSetGuestPassword": "Ustawia/aktualizuje hasło gościa. (dla repeaterów, loginy gości mogą wysyłać żądanie \"Get Stats\")",
- "repeater_cliHelpSetName": "Ustawia nazwę reklamy.",
- "repeater_cliHelpSetLat": "Ustawia współrzędną geograficzne (w stopniach dziesiętnych) mapy reklam.",
- "repeater_cliHelpSetLon": "Ustawia współrzędną długościową mapy reklamy. (stopnie dziesiętne)",
+ "repeater_cliHelpSetAdvertInterval": "Ustawia interwał timera w minutach do wysyłania pakietu rozgłoszenia lokalnego (bezpośredniego). Ustaw na 0, aby wyłączyć.",
+ "repeater_cliHelpSetFloodAdvertInterval": "Ustawia interwał timera w godzinach do wysłania pakietu rozgłoszeniowego typu \"flood\". Ustaw na 0, aby wyłączyć.",
+ "repeater_cliHelpSetGuestPassword": "Ustawia/aktualizuje hasło gościa. (dla przekaźników loginy gości mogą wysyłać żądanie \"Get Stats\")",
+ "repeater_cliHelpSetName": "Ustawia nazwę rozgłoszenia.",
+ "repeater_cliHelpSetLat": "Ustawia współrzędną geograficzną (w stopniach dziesiętnych) mapy rozgłoszeń.",
+ "repeater_cliHelpSetLon": "Ustawia współrzędną długościową mapy rozgłoszeń. (stopnie dziesiętne)",
"repeater_cliHelpSetRadio": "Ustawia nowe parametry radia i zapisuje je w preferencjach. Wymaga polecenia \"reboot\" do zastosowania.",
"repeater_cliHelpSetRxDelay": "Ustawienia (eksperymentalne) bazowe (muszą być > 1, aby działać) do stosowania lekkiego opóźnienia dla odebranych pakietów, w oparciu o siłę sygnału/wynik. Ustaw na 0, aby wyłączyć.",
"repeater_cliHelpSetTxDelay": "Ustawia czynnik mnożony przez czas utrzymania w trybie zalewowym dla pakietu oraz z wykorzystaniem losowego systemu slotów, aby opóźnić jego przesyłanie (zmniejszając prawdopodobieństwo kolizji).",
@@ -1149,19 +1159,19 @@
"repeater_cliHelpSetAdcMultiplier": "Ustawia niestandardowy współczynnik do korekty zgłaszanego napięcia baterii (obsługa tylko na wybranych płytach).",
"repeater_cliHelpTempRadio": "Ustawia tymczasowe parametry radia na podany czas trwania w minutach, a następnie powraca do oryginalnych parametrów radia. (nie zapisuje zmian w preferencjach).",
"repeater_cliHelpSetPerm": "Modyfikuje ACL. Usuwa dopasowaną wpis (z prefiksem pubkey), jeśli \"permissions\" wynosi zero. Dodaje nowy wpis, jeśli pubkey-hex ma pełną długość i nie znajduje się obecnie w ACL. Aktualizuje wpis, dopasowując prefiks pubkey. Bit uprawnień zależy od roli firmware, ale dolne 2 bity to: 0 (Gość), 1 (tylko odczyt), 2 (odczyt i zapis), 3 (administrator).",
- "repeater_cliHelpGetBridgeType": "Uzyskano typ mostu: brak, rs232, espnow",
+ "repeater_cliHelpGetBridgeType": "Pobiera typ mostka: brak, rs232, espnow",
"repeater_cliHelpLogStart": "Rozpoczyna się logowanie pakietów do systemu plików.",
"repeater_cliHelpLogStop": "Zatrzymuje logowanie pakietów do systemu plików.",
"repeater_cliHelpLogErase": "Usuwa logi pakietów z systemu plików.",
- "repeater_cliHelpNeighbors": "Wyświetla listę innych węzłów powtarzających się, które usłyszano dzięki reklamom zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4",
+ "repeater_cliHelpNeighbors": "Wyświetla listę innych węzłów przekaźnikowych usłyszanych przez rozgłoszenia zero-hop. Każda linia to: id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Usuwa pierwszy pasujący wpis (z prefiksem pubkey (hex)) z listy sąsiadów.",
- "repeater_cliHelpRegion": "(tylko seria) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do powodzi.",
- "repeater_cliHelpRegionLoad": "ZAPOMNIJ: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.",
+ "repeater_cliHelpRegion": "(tylko port szeregowy) Wyświetla wszystkie zdefiniowane regiony i aktualne uprawnienia do zalewu.",
+ "repeater_cliHelpRegionLoad": "UWAGA: to jest specjalne wywołanie wielokomendowe. Każda następna komenda jest nazwą regionu (wcięta spacjami, aby wskazywać hierarchię nadrzędną, z minimum jedną spacją). Zakończona wysłaniem pustej linii/komendy.",
"repeater_cliHelpRegionGet": "Wyszukuje region o podanej nazwie prefiksu (lub \"\" dla zakresu globalnego). Odpowiada \"-> region-name (parent-name) 'F'\"",
"repeater_cliHelpRegionPut": "Dodaje lub aktualizuje definicję regionu z podaną nazwą.",
"repeater_cliHelpRegionRemove": "Usuwa definicję regionu o podanej nazwie. (musi się dokładnie zgadzać i nie może mieć podregionów).",
- "repeater_cliHelpRegionAllowf": "Ustawia uprawnienia 'P'łytkowe dla podanego regionu. ('' dla zakresu globalnego/starszego)",
- "repeater_cliHelpRegionDenyf": "Usuwa uprawnienie 'Pływające' dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).",
+ "repeater_cliHelpRegionAllowf": "Ustawia uprawnienia 'F' (zalewowe) dla podanego regionu. ('' dla zakresu globalnego/starszego)",
+ "repeater_cliHelpRegionDenyf": "Usuwa uprawnienie 'F' (zalewowe) dla podanej strefy. (ZALECANE: na tym etapie NIE zaleca się używania tego na globalnym/starszym zakresie!!).",
"repeater_cliHelpRegionHome": "Odpowiada z aktualnej 'home' region. (Uwaga: nie zostało jeszcze zastosowane, zarezerwowane na przyszłość).",
"repeater_cliHelpRegionHomeSet": "Ustawia region 'domowe'.",
"repeater_cliHelpRegionSave": "Zapisuje listę/mapę regionów do pamięci.",
@@ -1169,22 +1179,22 @@
"repeater_cliHelpGpsOnOff": "Włącza/wyłącza nawigację GPS.",
"repeater_cliHelpGpsSync": "Synchronizuje czas węzła z zegarem GPS.",
"repeater_cliHelpGpsSetLoc": "Ustawia pozycję węzła na współrzędne GPS i zapisuje preferencje.",
- "repeater_cliHelpGpsAdvert": "Udostępnia konfigurację reklamy lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w reklamach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: reklamuj lokalizację przechowywaną w ustawieniach",
- "repeater_cliHelpGpsAdvertSet": "Ustawia konfigurację reklamy w lokalizacji.",
+ "repeater_cliHelpGpsAdvert": "Udostępnia konfigurację rozgłoszeń lokalizacji węzła:\n- brak: nie uwzględniaj lokalizacji w rozgłoszeniach\n- udostępnia: udostępnia lokalizację GPS (z SensorManager)\n- ustawienia: rozgłaszaj lokalizację przechowywaną w ustawieniach",
+ "repeater_cliHelpGpsAdvertSet": "Ustawia konfigurację rozgłoszeń lokalizacji.",
"repeater_commandsListTitle": "Lista poleceń",
- "repeater_commandsListNote": "ZAPAMIĘTAJ: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".",
+ "repeater_commandsListNote": "UWAGA: dla różnych poleceń \"set ...\" istnieje również polecenie \"get ...\".",
"repeater_general": "Ogólne",
"repeater_settingsCategory": "Ustawienia",
"repeater_bridge": "Most",
"repeater_logging": "Rejestrowanie",
- "repeater_neighborsRepeaterOnly": "Sąsiedzi (tylko powtarzacz)",
- "repeater_regionManagementRepeaterOnly": "Zarządzanie Regionem (tylko Powtarzacz)",
+ "repeater_neighborsRepeaterOnly": "Sąsiedzi (tylko przekaźnik)",
+ "repeater_regionManagementRepeaterOnly": "Zarządzanie Regionem (tylko Przekaźnik)",
"repeater_regionNote": "Wprowadzono komendy regionalne w celu zarządzania definicjami i uprawnieniami regionów.",
"repeater_gpsManagement": "Zarządzanie GPS",
"repeater_gpsNote": "Polecenie GPS zostało wprowadzone w celu zarządzania tematami związanymi z lokalizacją.",
- "telemetry_receivedData": "Otrzymano Dane Telemetrii",
- "telemetry_requestTimeout": "Życzenie o danych telemetrycznych nie udało się.",
- "telemetry_errorLoading": "Błąd podczas ładowania telemetry: {error}",
+ "telemetry_receivedData": "Odebrane dane telemetrii",
+ "telemetry_requestTimeout": "Przekroczono czas oczekiwania na telemetrię.",
+ "telemetry_errorLoading": "Błąd podczas ładowania telemetrii: {error}",
"@telemetry_errorLoading": {
"placeholders": {
"error": {
@@ -1205,7 +1215,7 @@
"telemetry_voltageLabel": "Napięcie",
"telemetry_mcuTemperatureLabel": "Temperatura MCU",
"telemetry_temperatureLabel": "Temperatura",
- "telemetry_currentLabel": "Obecny",
+ "telemetry_currentLabel": "Prąd",
"telemetry_batteryValue": "{percent}% / {volts}V",
"@telemetry_batteryValue": {
"placeholders": {
@@ -1217,7 +1227,7 @@
}
}
},
- "telemetry_voltageValue": "{volts}W",
+ "telemetry_voltageValue": "{volts}V",
"@telemetry_voltageValue": {
"placeholders": {
"volts": {
@@ -1246,8 +1256,8 @@
},
"channelPath_title": "Ścieżka pakietu",
"channelPath_viewMap": "Wyświetl mapę",
- "channelPath_otherObservedPaths": "Inne Zauważone Ścieżki",
- "channelPath_repeaterHops": "Skoki Powtórki",
+ "channelPath_otherObservedPaths": "Inne zaobserwowane ścieżki",
+ "channelPath_repeaterHops": "Skoki przekaźników",
"channelPath_noHopDetails": "Szczegóły dotyczące tego pakietu nie zostały podane.",
"channelPath_messageDetails": "Szczegóły wiadomości",
"channelPath_senderLabel": "Nadawca",
@@ -1255,7 +1265,7 @@
"channelPath_repeatsLabel": "Powtórzenia",
"channelPath_pathLabel": "Ścieżka {index}",
"channelPath_observedLabel": "Obserwowane",
- "channelPath_observedPathTitle": "Obserwowany ścieżka {index} • {hops}",
+ "channelPath_observedPathTitle": "Obserwowana ścieżka {index} • {hops}",
"@channelPath_observedPathTitle": {
"placeholders": {
"index": {
@@ -1290,7 +1300,7 @@
}
},
"channelPath_unknownPath": "Nieznane",
- "channelPath_floodPath": "Powodzenie",
+ "channelPath_floodPath": "Zalew",
"channelPath_directPath": "Bezpośrednio",
"channelPath_observedZeroOf": "0 z {total} skoków",
"@channelPath_observedZeroOf": {
@@ -1312,7 +1322,7 @@
}
},
"channelPath_mapTitle": "Mapa ścieżek",
- "channelPath_noRepeaterLocations": "Brak dostępnych lokalizacji powtarzaczy dla tego ścieżki.",
+ "channelPath_noRepeaterLocations": "Brak dostępnych lokalizacji przekaźników dla tej ścieżki.",
"channelPath_primaryPath": "Ścieżka {index} (Główna)",
"@channelPath_primaryPath": {
"placeholders": {
@@ -1342,7 +1352,7 @@
}
},
"channelPath_noHopDetailsAvailable": "Brak dostępnych szczegółów hopa dla tego pakietu.",
- "channelPath_unknownRepeater": "Nieznany Powtarzacz",
+ "channelPath_unknownRepeater": "Nieznany Przekaźnik",
"listFilter_tooltip": "Filtruj i sortuj",
"listFilter_sortBy": "Sortuj po",
"listFilter_latestMessages": "Najnowsze wiadomości",
@@ -1351,7 +1361,7 @@
"listFilter_filters": "Filtry",
"listFilter_all": "Wszystko",
"listFilter_users": "Użytkownicy",
- "listFilter_repeaters": "Powtarzacze",
+ "listFilter_repeaters": "Przekaźniki",
"listFilter_roomServers": "Serwery pokoju",
"listFilter_unreadOnly": "Tylko nieprzeczytane",
"listFilter_newGroup": "Nowa grupa",
@@ -1367,7 +1377,7 @@
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
- "neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
+ "neighbors_repeatersNeighbors": "Sąsiedzi przekaźników",
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
@@ -1552,11 +1562,11 @@
"pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.",
"contacts_pathTrace": "Śledzenie Ścieżek",
"contacts_ping": "Pingować",
- "contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera",
+ "contacts_repeaterPathTrace": "Śledzenie ścieżki do przekaźnika",
"contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego",
"contacts_roomPing": "Pinguj serwer pokoju",
"pathTrace_refreshTooltip": "Odśwież ścieżkę.",
- "contacts_repeaterPing": "Repeater pingowy",
+ "contacts_repeaterPing": "Ping przekaźnika",
"contacts_pathTraceTo": "Śledź trasę do {name}",
"contacts_chatTraceRoute": "Śledź trasę promienia",
"appSettings_languageRu": "Rosyjski",
@@ -1564,32 +1574,60 @@
"appSettings_enableMessageTracing": "Włącz śledzenie wiadomości",
"appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości",
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
- "contacts_zeroHopAdvert": "Reklama Zero Hop",
- "contacts_floodAdvert": "Reklama powodziowa",
- "contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka",
+ "contacts_zeroHopAdvert": "Rozgłoszenie zero-hop",
+ "contacts_floodAdvert": "Rozgłoszenie zalewowe",
+ "contacts_copyAdvertToClipboard": "Kopiuj rozgłoszenie do schowka",
"contacts_clipboardEmpty": "Schowek jest pusty.",
"contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe",
"contacts_addContactFromClipboard": "Dodaj kontakt z schowka",
"contacts_contactImported": "Kontakt został zaimportowany.",
- "contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.",
- "contacts_contactAdvertCopied": "Reklama skopiowana do schowka.",
- "contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.",
- "contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie",
+ "contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez rozgłoszenie.",
+ "contacts_contactAdvertCopied": "Rozgłoszenie skopiowano do schowka.",
+ "contacts_contactAdvertCopyFailed": "Kopiowanie rozgłoszenia do schowka nie powiodło się.",
+ "contacts_ShareContactZeroHop": "Udostępnij kontakt przez rozgłoszenie",
"contacts_ShareContact": "Kopiuj kontakt do schowka",
"contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.",
"notification_activityTitle": "Aktywność MeshCore",
"notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}",
+ "@notification_messagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}",
+ "@notification_channelMessagesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}",
+ "@notification_newNodesCount": {
+ "placeholders": {
+ "count": {
+ "type": "int"
+ }
+ }
+ },
"notification_newTypeDiscovered": "Nowy {contactType} wykryty",
+ "@notification_newTypeDiscovered": {
+ "placeholders": {
+ "contactType": {
+ "type": "String"
+ }
+ }
+ },
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
- "settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX",
- "settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.",
+ "settings_gpxExportRepeaters": "Eksportuj przekaźniki / serwer pokojowy do GPX",
+ "settings_gpxExportRepeatersSubtitle": "Eksportuje przekaźniki / roomserver 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 serwerów powtarzających i pomieszczeń",
+ "settings_gpxExportRepeatersRoom": "Lokalizacje przekaźników i serwerów pokojowych",
"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.",
@@ -1802,15 +1840,15 @@
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
- "contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...",
+ "contacts_searchRepeaters": "Wyszukaj {number}{str} przekaźników...",
"contactsSettings_title": "Ustawienia kontaktów",
"settings_contactSettingsSubtitle": "Ustawienia dotyczące sposobu dodawania kontaktów",
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
- "contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie powtarzalników",
- "contactsSettings_autoAddRepeatersSubtitle": "Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.",
+ "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_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
- "settings_contactSettings": "Ustawienia kontaktowe",
+ "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.",
@@ -1828,9 +1866,9 @@
"common_deleteAll": "Usuń wszystko",
"discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?",
"discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty",
- "map_guessedLocation": "Wydana lokalizacja",
- "map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów",
- "usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.",
+ "map_guessedLocation": "Przypuszczalna lokalizacja",
+ "map_showGuessedLocations": "Pokaż przypuszczalne lokalizacje węzłów",
+ "usbScreenSubtitle": "Wybierz wykryte urządzenie szeregowe i połącz się bezpośrednio ze swoim węzłem MeshCore.",
"usbScreenTitle": "Połącz przez USB",
"connectionChoiceUsbLabel": "USB",
"connectionChoiceBluetoothLabel": "Bluetooth",
@@ -1885,9 +1923,137 @@
"tcpStatus_connectingTo": "Połączenie z {endpoint}...",
"tcpErrorHostRequired": "Wymagana jest adresa IP.",
"tcpErrorPortInvalid": "Numer portu musi mieścić się w zakresie od 1 do 65535.",
- "tcpErrorUnsupported": "Transport protokoł TCP nie jest obsługiwany na tym urządzeniu.",
+ "tcpErrorUnsupported": "Transport TCP nie jest obsługiwany na tej platformie.",
"tcpErrorTimedOut": "Połączenie TCP zakończyło się bez powodzenia.",
"tcpConnectionFailed": "Błąd połączenia TCP: {error}",
- "map_showDiscoveryContacts": "Pokaż kontakty odkrywania",
- "map_setAsMyLocation": "Ustaw jako moje lokalizację"
+ "map_showDiscoveryContacts": "Pokaż odkryte kontakty",
+ "map_setAsMyLocation": "Ustaw jako moją lokalizację",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_allowByContact": "Zezwalaj według flag kontaktowych",
+ "settings_allowAll": "Zezwalaj na wszystko",
+ "settings_telemetryLocationMode": "Tryb położenia telemetrycznego",
+ "settings_telemetryEnvironmentMode": "Tryb środowiska telemetrycznego",
+ "settings_advertLocation": "Lokalizacja reklamowa",
+ "settings_advertLocationSubtitle": "Uwzględnij lokalizację w ogłoszeniu",
+ "settings_denyAll": "Odmów wszystkim",
+ "settings_privacySubtitle": "Kontroluj jakie informacje są udostępniane.",
+ "settings_privacy": "Ustawienia prywatności",
+ "settings_privacySettingsDescription": "Wybierz jakie informacje urządzenie udostępni innym.",
+ "contact_info": "Informacje kontaktowe",
+ "settings_telemetryBaseMode": "Tryb podstawowy telemetrii",
+ "contact_teleBase": "Baza telemetryczna",
+ "contact_teleLoc": "Lokalizacja telemetryczna",
+ "contact_teleLocSubtitle": "Zezwalaj na udostępnianie danych lokalizacji",
+ "contact_teleEnv": "Środowisko telemetryczne",
+ "contact_teleEnvSubtitle": "Zezwalaj na udostępnianie danych czujników środowiskowych",
+ "contact_telemetry": "Telemetryka",
+ "contact_clearChat": "Wyczyść czat",
+ "contact_settings": "Ustawienia kontaktowe",
+ "contact_lastSeen": "Ostatnio widziany",
+ "contact_teleBaseSubtitle": "Pozwól na udostępnianie poziomu naładowania baterii i podstawowych danych telemetrycznych",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeight": "Początkowa waga trasy",
+ "appSettings_maxRouteWeight": "Maksymalny dopuszczalny ciężar pojazdu",
+ "appSettings_initialRouteWeightSubtitle": "Początkowa waga dla nowych, odkrytych ścieżek",
+ "appSettings_maxRouteWeightSubtitle": "Maksymalna waga, jaką ścieżka może zgromadzić dzięki udanym dostawom.",
+ "appSettings_routeWeightSuccessIncrement": "Wzrost wagi sukcesu",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Waga dodana do ścieżki po pomyślnym dostarczeniu",
+ "appSettings_routeWeightFailureDecrement": "Zmniejszenie wagi kary",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Waga usunięta z trasy po nieudanej dostawie",
+ "appSettings_maxMessageRetries": "Maksymalna liczba prób wysłania wiadomości",
+ "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ą",
+ "@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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "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"
}
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index dd1698c..eb87a15 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "A conexão TCP expirou.",
"tcpConnectionFailed": "Falha na conexão TCP: {error}",
"map_showDiscoveryContacts": "Mostrar Contatos de Descoberta",
- "map_setAsMyLocation": "Defina minha localização"
+ "map_setAsMyLocation": "Defina minha localização",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacySettingsDescription": "Escolha quais informações o seu dispositivo compartilha com os outros.",
+ "settings_allowByContact": "Permitir por bandeiras de contato",
+ "settings_telemetryLocationMode": "Modo de Localização de Telemetria",
+ "settings_telemetryEnvironmentMode": "Modo de Ambiente de Telemetria",
+ "settings_advertLocation": "Localização do Anúncio",
+ "settings_advertLocationSubtitle": "Incluir localização no anúncio",
+ "settings_privacySubtitle": "Controle o que é compartilhado.",
+ "settings_denyAll": "Negar todos",
+ "settings_allowAll": "Permitir todos",
+ "settings_privacy": "Configurações de Privacidade",
+ "contact_info": "Informações de Contato",
+ "settings_telemetryBaseMode": "Modo Base de Telemetria",
+ "contact_teleBase": "Base de Telemetria",
+ "contact_teleLoc": "Localização de Telemetria",
+ "contact_teleLocSubtitle": "Permitir compartilhamento de dados de localização",
+ "contact_teleEnv": "Ambiente de Telemetria",
+ "contact_teleEnvSubtitle": "Permitir compartilhamento de dados do sensor de ambiente",
+ "contact_lastSeen": "Visto pela última vez",
+ "contact_clearChat": "Limpar Chat",
+ "contact_telemetry": "Telemetria",
+ "contact_settings": "Configurações de Contato",
+ "contact_teleBaseSubtitle": "Permitir compartilhamento do nível da bateria e telemetria básica",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeight": "Peso Inicial da Rota",
+ "appSettings_maxRouteWeight": "Peso Máximo da Rota",
+ "appSettings_maxRouteWeightSubtitle": "Peso máximo que um determinado percurso pode acumular com entregas bem-sucedidas.",
+ "appSettings_initialRouteWeightSubtitle": "Peso inicial para novos caminhos descobertos",
+ "appSettings_routeWeightSuccessIncrement": "Aumento do peso para indicar sucesso",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Peso adicionado a um caminho após a entrega bem-sucedida.",
+ "appSettings_routeWeightFailureDecrement": "Redução do peso da falha",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Peso removido de um caminho após uma tentativa de entrega malsucedida.",
+ "appSettings_maxMessageRetries": "Número máximo de tentativas de envio de mensagens",
+ "appSettings_maxMessageRetriesSubtitle": "Número de tentativas de reenvio antes de classificar uma mensagem como falha.",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_telemetryModeUpdated": "Modo de telemetria atualizado",
+ "settings_multiAck": "Multi-ACKs: {value}",
+ "map_showOverlaps": "Sobreposições da Chave Repeater",
+ "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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "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"
}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index ea75aca..c9493a0 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -1129,5 +1129,133 @@
"tcpErrorTimedOut": "Соединение TCP не удалось установить.",
"tcpConnectionFailed": "Не удалось установить соединение TCP: {error}",
"map_showDiscoveryContacts": "Показать контакты Discovery",
- "map_setAsMyLocation": "Установить мое местоположение"
+ "map_setAsMyLocation": "Установить мое местоположение",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "Настройки конфиденциальности",
+ "settings_privacySubtitle": "Контролируйте, какую информацию делиться.",
+ "settings_telemetryLocationMode": "Режим местоположения телеметрии",
+ "settings_telemetryEnvironmentMode": "Режим среды телеметрии",
+ "settings_advertLocation": "Местоположение рекламы",
+ "settings_advertLocationSubtitle": "Включить местоположение в объявление",
+ "settings_allowAll": "Разрешить все",
+ "settings_privacySettingsDescription": "Выберите, какую информацию ваше устройство будет делиться с другими.",
+ "settings_denyAll": "Отклонить все",
+ "settings_allowByContact": "Разрешить по флагам контактов",
+ "contact_info": "Контактная информация",
+ "settings_telemetryBaseMode": "Базовый режим телеметрии",
+ "contact_teleBase": "База телеметрии",
+ "contact_teleLoc": "Местоположение телеметрии",
+ "contact_teleLocSubtitle": "Разрешить обмен данными о местоположении",
+ "contact_teleEnv": "Среда телеметрии",
+ "contact_teleEnvSubtitle": "Разрешить обмен данными датчиков окружающей среды",
+ "contact_settings": "Настройки контактов",
+ "contact_telemetry": "Телеметрия",
+ "contact_clearChat": "Очистить чат",
+ "contact_lastSeen": "Последний раз видели",
+ "contact_teleBaseSubtitle": "Разрешить обмен уровнем заряда батареи и базовой телеметрией",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_maxRouteWeight": "Максимальный допустимый вес маршрута",
+ "appSettings_maxRouteWeightSubtitle": "Максимальный вес, который может быть перевезён по определённому маршруту при успешных доставках.",
+ "appSettings_initialRouteWeightSubtitle": "Начальный вес для новых, только что открытых маршрутов",
+ "appSettings_initialRouteWeight": "Начальный вес маршрута",
+ "appSettings_routeWeightSuccessIncrement": "Увеличение веса успеха",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Вес, добавленный к маршруту после успешной доставки.",
+ "appSettings_routeWeightFailureDecrement": "Уменьшение веса неудачи",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Вес, который был удален с пути после неудачной доставки.",
+ "appSettings_maxMessageRetries": "Максимальное количество повторных попыток отправки сообщения",
+ "appSettings_maxMessageRetriesSubtitle": "Количество попыток повторной отправки сообщения перед тем, как пометить его как неудачное.",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_telemetryModeUpdated": "Режим телеметрии обновлен",
+ "settings_multiAck": "Мульти-ACK: {value}",
+ "map_showOverlaps": "Перекрытия ключа повтора",
+ "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 и время передачи",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "Показать PIN",
+ "scanner_linuxPairingPinPrompt": "Введите PIN‑код для {deviceName} (оставьте пустым, если нет).",
+ "scanner_linuxPairingHidePin": "Скрыть PIN",
+ "scanner_linuxPairingPinTitle": "PIN‑код сопряжения Bluetooth"
}
diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb
index 636556e..5a7aa6d 100644
--- a/lib/l10n/app_sk.arb
+++ b/lib/l10n/app_sk.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "Pripojenie TCP vypršalo.",
"tcpConnectionFailed": "Neúspešné vytvorenie TCP spojenia: {error}",
"map_showDiscoveryContacts": "Zobraziť kontakty objavov",
- "map_setAsMyLocation": "Nastavte ako moju polohu"
+ "map_setAsMyLocation": "Nastavte ako moju polohu",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "Nastavenia súkromia",
+ "settings_privacySubtitle": "Ovládni, aké informácie sa zdieľajú.",
+ "settings_telemetryLocationMode": "Režim umiestnenia telemetrie",
+ "settings_telemetryBaseMode": "Základný režim telemetrie",
+ "settings_advertLocation": "Umiestnenie inzerátu",
+ "settings_telemetryEnvironmentMode": "Režim prostredia telemetrie",
+ "settings_advertLocationSubtitle": "Zahrnúť polohu do inzerátu",
+ "settings_allowAll": "Povoliť všetko",
+ "settings_privacySettingsDescription": "Vyberte, ktoré informácie váš zariadenie zdieľa s ostatnými.",
+ "settings_denyAll": "Zamietnuť všetko",
+ "settings_allowByContact": "Povoliť podľa kontaktových vlajok",
+ "contact_info": "Kontaktné informácie",
+ "contact_settings": "Nastavenia kontaktov",
+ "contact_teleBaseSubtitle": "Povoliť zdieľanie úrovne batérie a základnej telemetrie",
+ "contact_teleLoc": "Lokácia telemetrie",
+ "contact_teleLocSubtitle": "Povoliť zdieľanie údajov o lokalite",
+ "contact_teleEnv": "Prostredie telemetrie",
+ "contact_telemetry": "Telemetria",
+ "contact_clearChat": "Vymazať chat",
+ "contact_lastSeen": "Naposledy videný",
+ "contact_teleBase": "Báza telemetrie",
+ "contact_teleEnvSubtitle": "Povoliť zdieľanie údajov senzorov prostredia",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_maxRouteWeightSubtitle": "Maximálna hmotnosť, ktorú môže trás prenášať vďaka úspešným zásielkam.",
+ "appSettings_initialRouteWeightSubtitle": "Počiatočná váha pre nové, objavené cesty",
+ "appSettings_initialRouteWeight": "Počiatočná váha trasy",
+ "appSettings_maxRouteWeight": "Maximálna hmotnosť trasy",
+ "appSettings_routeWeightSuccessIncrement": "Zvyšenie váhy úspechu",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Hmotnosť pridaná k trase po úspešnej doručení",
+ "appSettings_routeWeightFailureDecrement": "Sníženie váhy, ktorá sa používa na odhad rizika.",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Hmotnosť odstránená z cesty po neúspešnej doručenie",
+ "appSettings_maxMessageRetries": "Maximalný počet pokusov o doručenie správ",
+ "appSettings_maxMessageRetriesSubtitle": "Počet pokusov o odošleť pred označením správy ako neúspešnej",
+ "path_routeWeight": "{weight}/{max}",
+ "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.",
+ "@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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingPinPrompt": "Zadajte PIN pre {deviceName} (ak nie je, nechajte prázdne).",
+ "scanner_linuxPairingShowPin": "Zobraziť PIN",
+ "scanner_linuxPairingHidePin": "Skryť PIN",
+ "scanner_linuxPairingPinTitle": "Bluetooth párovací PIN"
}
diff --git a/lib/l10n/app_sl.arb b/lib/l10n/app_sl.arb
index dfc5a69..9adb387 100644
--- a/lib/l10n/app_sl.arb
+++ b/lib/l10n/app_sl.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "Povezava TCP je presegla časovno obdobje.",
"tcpConnectionFailed": "Napaka pri povezavi TCP: {error}",
"map_showDiscoveryContacts": "Prikaži odkritja kontaktov",
- "map_setAsMyLocation": "Nastavite to kot mojo lokacijo"
+ "map_setAsMyLocation": "Nastavite to kot mojo lokacijo",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "Nastavitve zasebnosti",
+ "settings_privacySettingsDescription": "Izberite, katere informacije vaš naprava deli z drugimi.",
+ "settings_telemetryBaseMode": "Osnovni način telemetrije",
+ "settings_telemetryLocationMode": "Način delovanja telemetrije",
+ "settings_telemetryEnvironmentMode": "Način delovanja okolja telemetrije",
+ "settings_advertLocation": "Lokacija oglasa",
+ "settings_allowByContact": "Dovoli po kontaktnih zastavah",
+ "settings_denyAll": "Zavrniti vse",
+ "settings_allowAll": "Dovoli vse",
+ "settings_privacySubtitle": "Kontrolirajte, katere informacije so deljene.",
+ "contact_info": "Kontaktni podatki",
+ "contact_teleBase": "Baza telemetrije",
+ "contact_teleBaseSubtitle": "Dovoli deljenje stanja baterije in osnovne telemetrije",
+ "contact_teleLoc": "Lokacija telemetrije",
+ "contact_lastSeen": "Zadnjič videno",
+ "contact_settings": "Nastavitve stika",
+ "settings_advertLocationSubtitle": "Vključi lokacijo v oglas.",
+ "contact_telemetry": "Telemetrija",
+ "contact_clearChat": "Počisti klepet",
+ "contact_teleEnv": "Okolje telemetrije",
+ "contact_teleEnvSubtitle": "Dovoli deljenje podatkov okoljskih senzorjev",
+ "contact_teleLocSubtitle": "Dovoli deljenje podatkov o lokaciji",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_maxRouteWeightSubtitle": "Največja teža, ki jo lahko pot doseže s uspešnimi dostavnami.",
+ "appSettings_initialRouteWeight": "Izvirna teža poti",
+ "appSettings_initialRouteWeightSubtitle": "Izguba teže za nove, odkriti poti",
+ "appSettings_maxRouteWeight": "Največja dovoljena teža poti",
+ "appSettings_routeWeightSuccessIncrement": "Učinkovitost: povečanje",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Težava, dodana poti po uspešni dostavi",
+ "appSettings_routeWeightFailureDecrement": "Zmanjšanje teže, ki je povezana s pomanjkanjem",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Težo, ki ni bila uspešno dostavljena, odstranili s poti.",
+ "appSettings_maxMessageRetries": "Najve število poskusov pošiljanja sporočil",
+ "appSettings_maxMessageRetriesSubtitle": "Število poskusov ponovnega poslanja, preden se sporočilo označuje kot neuspešno",
+ "path_routeWeight": "{weight}/{max}",
+ "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.",
+ "@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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "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"
}
diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb
index 6a8d801..e4ace3e 100644
--- a/lib/l10n/app_sv.arb
+++ b/lib/l10n/app_sv.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "TCP-anslutningen har tidsut gått.",
"tcpConnectionFailed": "Fel vid TCP-anslutning: {error}",
"map_showDiscoveryContacts": "Visa Discovery-kontakter",
- "map_setAsMyLocation": "Ange som min plats"
+ "map_setAsMyLocation": "Ange som min plats",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacy": "Inställningar för sekretess",
+ "settings_allowAll": "Tillåt alla",
+ "settings_privacySubtitle": "Kontrollera vilken information som delas.",
+ "settings_telemetryEnvironmentMode": "Telemetri miljöläge",
+ "settings_telemetryBaseMode": "Telemetribasläge",
+ "settings_telemetryLocationMode": "Telemetritillstånd för plats",
+ "settings_advertLocation": "Annonsplacering",
+ "contact_info": "Kontaktinformation",
+ "contact_settings": "Kontaktinställningar",
+ "contact_telemetry": "Telemetri",
+ "settings_denyAll": "Neka alla",
+ "settings_allowByContact": "Tillåt via kontaktflaggor",
+ "settings_privacySettingsDescription": "Välj vilken information din enhet delar med andra.",
+ "contact_lastSeen": "Senast sedd",
+ "contact_clearChat": "Rensa Chatt",
+ "contact_teleEnv": "Telemetri Miljö",
+ "settings_advertLocationSubtitle": "Inkludera plats i annonsen",
+ "contact_teleEnvSubtitle": "Tillåt delning av miljösensordata",
+ "contact_teleBase": "Telemetribas",
+ "contact_teleBaseSubtitle": "Tillåt delning av batterinivå och grundläggande telemetri",
+ "contact_teleLoc": "Telemetridata plats",
+ "contact_teleLocSubtitle": "Tillåt delning av platsdata",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeightSubtitle": "Initial vikt för nyligen upptäckta vägar",
+ "appSettings_maxRouteWeight": "Maximalt tillåtet vikt för rutten",
+ "appSettings_maxRouteWeightSubtitle": "Maximal vikt som en leveransväg kan ackumulera från framgångsrika leveranser.",
+ "appSettings_initialRouteWeight": "Initial vikt för rutt",
+ "appSettings_routeWeightSuccessIncrement": "Ökning av vikt för framgång",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Vikt läggs till en väg efter en lyckad leverans.",
+ "appSettings_routeWeightFailureDecrement": "Minskning av vikten för misslyckande",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Vikt som tagits bort från en väg efter ett misslyckat leveransförsök",
+ "appSettings_maxMessageRetries": "Maximalt antal försök",
+ "appSettings_maxMessageRetriesSubtitle": "Antal försök att skicka om ett meddelande innan det markeras som misslyckat.",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_telemetryModeUpdated": "Telemetri-läge uppdaterat",
+ "settings_multiAck": "Multi-ACKs: {value}",
+ "map_showOverlaps": "Repeater-nyckelöverlappningar",
+ "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",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "Visa PIN",
+ "scanner_linuxPairingPinTitle": "Bluetooth‑parnings‑PIN",
+ "scanner_linuxPairingPinPrompt": "Ange PIN för {deviceName} (lämna tomt om ingen).",
+ "scanner_linuxPairingHidePin": "Dölj PIN"
}
diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb
index a50bd78..8e27da1 100644
--- a/lib/l10n/app_uk.arb
+++ b/lib/l10n/app_uk.arb
@@ -1889,5 +1889,133 @@
"tcpErrorTimedOut": "З'єднання TCP завершилося через закінчення часу очікування.",
"tcpConnectionFailed": "Не вдалося встановити з'єднання TCP: {error}",
"map_showDiscoveryContacts": "Показати контакти Відкриття",
- "map_setAsMyLocation": "Встановити моє місцезнаходження"
+ "map_setAsMyLocation": "Встановити моє місцезнаходження",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacySubtitle": "Керуйте інформацією, яку буде спільно використовуватися",
+ "settings_privacy": "Налаштування приватності",
+ "settings_telemetryBaseMode": "Режим базової телеметрії",
+ "settings_telemetryLocationMode": "Режим місця телеметрії",
+ "settings_advertLocation": "Розміщення реклами",
+ "settings_advertLocationSubtitle": "Включити місце розташування в оголошення",
+ "settings_privacySettingsDescription": "Виберіть, яку інформацію ваш пристрій буде передавати іншим.",
+ "settings_allowAll": "Дозволити все",
+ "settings_denyAll": "Відхилити все",
+ "settings_allowByContact": "Дозволити за контактними прапорцями",
+ "settings_telemetryEnvironmentMode": "Режим середовища телеметрії",
+ "contact_info": "Контактна інформація",
+ "contact_teleBaseSubtitle": "Дозволити спільний доступ до рівня заряду батареї та базової телеметрії",
+ "contact_teleLoc": "Розташування телеметрії",
+ "contact_teleBase": "Базовий телебачення",
+ "contact_teleLocSubtitle": "Дозволити спільне використання даних про місцеположення",
+ "contact_settings": "Налаштування контактів",
+ "contact_telemetry": "Телеметрія",
+ "contact_clearChat": "Очистити чат",
+ "contact_lastSeen": "Останній раз бачили",
+ "contact_teleEnv": "Середовище телеметрії",
+ "contact_teleEnvSubtitle": "Дозволити спільний доступ до даних датчиків середовища",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_initialRouteWeight": "Початкова вартість маршруту",
+ "appSettings_initialRouteWeightSubtitle": "Початкова вага для нових відкритих шляхів",
+ "appSettings_maxRouteWeight": "Максимальна вага маршруту",
+ "appSettings_maxRouteWeightSubtitle": "Максимальна вага, яку може накопичити маршрут завдяки успішним доставкам.",
+ "appSettings_routeWeightSuccessIncrement": "Збільшення ваги успіху",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "Вага, додана до маршруту після успішної доставки",
+ "appSettings_routeWeightFailureDecrement": "Зменшення ваги помилки",
+ "appSettings_routeWeightFailureDecrementSubtitle": "Вага, яка була знята з маршруту після невдалої доставки",
+ "appSettings_maxMessageRetries": "Максимальна кількість повторних спроб надсилання повідомлення",
+ "appSettings_maxMessageRetriesSubtitle": "Кількість спроб повторного відправлення повідомлення перед тим, як позначити його як невдале",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_telemetryModeUpdated": "Режим телеметрії оновлено",
+ "settings_multiAck": "Багатократне підтвердження: {value}",
+ "map_showOverlaps": "Перекриття ключа повторювача",
+ "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 та час, протягом якого пристрій використовує радіоканал.",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingPinTitle": "PIN‑код спарювання Bluetooth",
+ "scanner_linuxPairingShowPin": "Показати PIN",
+ "scanner_linuxPairingPinPrompt": "Введіть PIN для {deviceName} (залиште порожнім, якщо його немає).",
+ "scanner_linuxPairingHidePin": "Приховати PIN"
}
diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb
index 54d1e3c..cd7b44d 100644
--- a/lib/l10n/app_zh.arb
+++ b/lib/l10n/app_zh.arb
@@ -1894,5 +1894,133 @@
"tcpErrorTimedOut": "TCP 连接超时。",
"tcpConnectionFailed": "TCP 连接失败:{error}",
"map_showDiscoveryContacts": "显示发现联系人",
- "map_setAsMyLocation": "设置为我的位置"
+ "map_setAsMyLocation": "设置为我的位置",
+ "@path_routeWeight": {
+ "placeholders": {
+ "weight": {
+ "type": "String"
+ },
+ "max": {
+ "type": "String"
+ }
+ }
+ },
+ "settings_privacySubtitle": "控制要共享的信息。",
+ "settings_privacySettingsDescription": "选择您的设备与他人共享的信息。",
+ "settings_telemetryBaseMode": "遥测基础模式",
+ "settings_telemetryLocationMode": "遥测位置模式",
+ "settings_advertLocation": "广告位置",
+ "settings_advertLocationSubtitle": "在广告中包含位置",
+ "settings_allowByContact": "按联系人标志允许",
+ "settings_denyAll": "拒绝所有",
+ "settings_privacy": "隐私设置",
+ "settings_allowAll": "允许全部",
+ "contact_info": "联系信息",
+ "contact_teleBase": "遥测基站",
+ "contact_teleBaseSubtitle": "允许共享电池电量和基本遥测数据",
+ "settings_telemetryEnvironmentMode": "遥测环境模式",
+ "contact_teleLoc": "遥测位置",
+ "contact_teleEnv": "遥测环境",
+ "contact_teleEnvSubtitle": "允许共享环境传感器数据",
+ "contact_clearChat": "清除聊天记录",
+ "contact_lastSeen": "最近出现",
+ "contact_settings": "联系人设置",
+ "contact_teleLocSubtitle": "允许共享位置数据",
+ "contact_telemetry": "遥测数据",
+ "@settings_multiAck": {
+ "placeholders": {
+ "value": {
+ "type": "String"
+ }
+ }
+ },
+ "appSettings_maxRouteWeight": "最大路径重量",
+ "appSettings_initialRouteWeightSubtitle": "新发现路径的初始重量",
+ "appSettings_initialRouteWeight": "初始路线权重",
+ "appSettings_maxRouteWeightSubtitle": "一条路径可以累积的最大重量,取决于成功交付的数量。",
+ "appSettings_routeWeightSuccessIncrement": "成功权重增加",
+ "appSettings_routeWeightSuccessIncrementSubtitle": "在成功交付后,将重量添加到路径中",
+ "appSettings_routeWeightFailureDecrement": "失败权重降低",
+ "appSettings_routeWeightFailureDecrementSubtitle": "从一条路径上移除的货物,由于无法成功交付而移除。",
+ "appSettings_maxMessageRetries": "最大消息重试次数",
+ "appSettings_maxMessageRetriesSubtitle": "在将消息标记为失败之前,允许尝试的次数",
+ "path_routeWeight": "{weight}/{max}",
+ "settings_multiAck": "多重ACK:{value}",
+ "settings_telemetryModeUpdated": "遥测模式已更新",
+ "map_showOverlaps": "重复键重叠",
+ "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、信噪比和空中时间",
+ "@scanner_linuxPairingPinPrompt": {
+ "placeholders": {
+ "deviceName": {
+ "type": "String"
+ }
+ }
+ },
+ "scanner_linuxPairingShowPin": "显示 PIN码",
+ "scanner_linuxPairingPinPrompt": "输入 {deviceName} 的 PIN(如果没有,请留空)。",
+ "scanner_linuxPairingPinTitle": "蓝牙配对 PIN",
+ "scanner_linuxPairingHidePin": "隐藏 PIN"
}
diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart
index fc84851..36d36a6 100644
--- a/lib/models/app_settings.dart
+++ b/lib/models/app_settings.dart
@@ -18,6 +18,7 @@ class AppSettings {
final bool mapShowRepeaters;
final bool mapShowChatNodes;
final bool mapShowOtherNodes;
+ final bool mapShowOverlaps;
final double mapTimeFilterHours; // 0 = all time
final bool mapKeyPrefixEnabled;
final String mapKeyPrefix;
@@ -32,6 +33,11 @@ class AppSettings {
final bool notifyOnNewChannelMessage;
final bool notifyOnNewAdvert;
final bool autoRouteRotationEnabled;
+ final double maxRouteWeight;
+ final double initialRouteWeight;
+ final double routeWeightSuccessIncrement;
+ final double routeWeightFailureDecrement;
+ final int maxMessageRetries;
final String themeMode;
final String? languageOverride; // null = system default
final bool appDebugLogEnabled;
@@ -42,12 +48,14 @@ class AppSettings {
final bool mapShowDiscoveryContacts;
final String tcpServerAddress;
final int tcpServerPort;
+ final bool jumpToOldestUnread;
AppSettings({
this.clearPathOnMaxRetry = false,
this.mapShowRepeaters = true,
this.mapShowChatNodes = true,
this.mapShowOtherNodes = true,
+ this.mapShowOverlaps = false,
this.mapTimeFilterHours = 0, // Default to all time
this.mapKeyPrefixEnabled = false,
this.mapKeyPrefix = '',
@@ -62,6 +70,11 @@ class AppSettings {
this.notifyOnNewChannelMessage = true,
this.notifyOnNewAdvert = true,
this.autoRouteRotationEnabled = false,
+ this.maxRouteWeight = 5.0,
+ this.initialRouteWeight = 3.0,
+ this.routeWeightSuccessIncrement = 0.5,
+ this.routeWeightFailureDecrement = 0.2,
+ this.maxMessageRetries = 5,
this.themeMode = 'system',
this.languageOverride,
this.appDebugLogEnabled = false,
@@ -72,6 +85,7 @@ class AppSettings {
this.mapShowDiscoveryContacts = true,
this.tcpServerAddress = '',
this.tcpServerPort = 0,
+ this.jumpToOldestUnread = false,
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {};
@@ -82,6 +96,7 @@ class AppSettings {
'map_show_repeaters': mapShowRepeaters,
'map_show_chat_nodes': mapShowChatNodes,
'map_show_other_nodes': mapShowOtherNodes,
+ 'map_show_overlaps': mapShowOverlaps,
'map_time_filter_hours': mapTimeFilterHours,
'map_key_prefix_enabled': mapKeyPrefixEnabled,
'map_key_prefix': mapKeyPrefix,
@@ -96,6 +111,11 @@ class AppSettings {
'notify_on_new_channel_message': notifyOnNewChannelMessage,
'notify_on_new_advert': notifyOnNewAdvert,
'auto_route_rotation_enabled': autoRouteRotationEnabled,
+ 'max_route_weight': maxRouteWeight,
+ 'initial_route_weight': initialRouteWeight,
+ 'route_weight_success_increment': routeWeightSuccessIncrement,
+ 'route_weight_failure_decrement': routeWeightFailureDecrement,
+ 'max_message_retries': maxMessageRetries,
'theme_mode': themeMode,
'language_override': languageOverride,
'app_debug_log_enabled': appDebugLogEnabled,
@@ -106,6 +126,7 @@ class AppSettings {
'map_show_discovery_contacts': mapShowDiscoveryContacts,
'tcp_server_address': tcpServerAddress,
'tcp_server_port': tcpServerPort,
+ 'jump_to_oldest_unread': jumpToOldestUnread,
};
}
@@ -122,6 +143,7 @@ class AppSettings {
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
+ mapShowOverlaps: json['map_show_overlaps'] as bool? ?? false,
mapTimeFilterHours:
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
@@ -142,6 +164,14 @@ class AppSettings {
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
autoRouteRotationEnabled:
json['auto_route_rotation_enabled'] as bool? ?? false,
+ maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
+ initialRouteWeight:
+ (json['initial_route_weight'] as num?)?.toDouble() ?? 3.0,
+ routeWeightSuccessIncrement:
+ (json['route_weight_success_increment'] as num?)?.toDouble() ?? 0.5,
+ routeWeightFailureDecrement:
+ (json['route_weight_failure_decrement'] as num?)?.toDouble() ?? 0.2,
+ maxMessageRetries: json['max_message_retries'] as int? ?? 5,
themeMode: json['theme_mode'] as String? ?? 'system',
languageOverride: json['language_override'] as String?,
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
@@ -165,6 +195,7 @@ 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,
);
}
@@ -173,6 +204,7 @@ class AppSettings {
bool? mapShowRepeaters,
bool? mapShowChatNodes,
bool? mapShowOtherNodes,
+ bool? mapShowOverlaps,
double? mapTimeFilterHours,
bool? mapKeyPrefixEnabled,
String? mapKeyPrefix,
@@ -187,6 +219,11 @@ class AppSettings {
bool? notifyOnNewChannelMessage,
bool? notifyOnNewAdvert,
bool? autoRouteRotationEnabled,
+ double? maxRouteWeight,
+ double? initialRouteWeight,
+ double? routeWeightSuccessIncrement,
+ double? routeWeightFailureDecrement,
+ int? maxMessageRetries,
String? themeMode,
Object? languageOverride = _unset,
bool? appDebugLogEnabled,
@@ -197,12 +234,14 @@ class AppSettings {
bool? mapShowDiscoveryContacts,
String? tcpServerAddress,
int? tcpServerPort,
+ bool? jumpToOldestUnread,
}) {
return AppSettings(
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
mapShowRepeaters: mapShowRepeaters ?? this.mapShowRepeaters,
mapShowChatNodes: mapShowChatNodes ?? this.mapShowChatNodes,
mapShowOtherNodes: mapShowOtherNodes ?? this.mapShowOtherNodes,
+ mapShowOverlaps: mapShowOverlaps ?? this.mapShowOverlaps,
mapTimeFilterHours: mapTimeFilterHours ?? this.mapTimeFilterHours,
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
@@ -222,6 +261,13 @@ class AppSettings {
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoRouteRotationEnabled:
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
+ maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
+ initialRouteWeight: initialRouteWeight ?? this.initialRouteWeight,
+ routeWeightSuccessIncrement:
+ routeWeightSuccessIncrement ?? this.routeWeightSuccessIncrement,
+ routeWeightFailureDecrement:
+ routeWeightFailureDecrement ?? this.routeWeightFailureDecrement,
+ maxMessageRetries: maxMessageRetries ?? this.maxMessageRetries,
themeMode: themeMode ?? this.themeMode,
languageOverride: languageOverride == _unset
? this.languageOverride
@@ -237,6 +283,7 @@ class AppSettings {
mapShowDiscoveryContacts ?? this.mapShowDiscoveryContacts,
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
+ jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
);
}
}
diff --git a/lib/models/channel.dart b/lib/models/channel.dart
index 1a2ecdc..4fdd627 100644
--- a/lib/models/channel.dart
+++ b/lib/models/channel.dart
@@ -24,20 +24,23 @@ class Channel {
bool get isPublicChannel => pskHex == publicChannelPsk;
- static Channel? fromFrame(Uint8List data) {
+ static Channel? fromFrame(Uint8List frame) {
// CHANNEL_INFO format:
// [0] = RESP_CODE_CHANNEL_INFO (18)
// [1] = channel_idx
// [2-33] = name (32 bytes, null-terminated)
// [34-49] = psk (16 bytes)
- if (data.length < 50) return null;
- if (data[0] != respCodeChannelInfo) return null;
-
- final index = data[1];
- final name = readCString(data, 2, 32);
- final psk = Uint8List.fromList(data.sublist(34, 50));
-
- return Channel(index: index, name: name, psk: psk);
+ if (frame.length < 50) return null;
+ final reader = BufferReader(frame);
+ try {
+ if (reader.readByte() != respCodeChannelInfo) return null;
+ final index = reader.readByte();
+ final name = reader.readCStringGreedy(32);
+ final psk = reader.readBytes(16);
+ return Channel(index: index, name: name, psk: psk);
+ } catch (e) {
+ return null;
+ }
}
static Channel empty(int index) {
diff --git a/lib/models/channel_message.dart b/lib/models/channel_message.dart
index 2418871..7c09089 100644
--- a/lib/models/channel_message.dart
+++ b/lib/models/channel_message.dart
@@ -2,6 +2,7 @@ import 'dart:typed_data';
import '../connector/meshcore_protocol.dart';
import '../helpers/reaction_helper.dart';
import '../helpers/smaz.dart';
+import '../utils/app_logger.dart';
enum ChannelMessageStatus { pending, sent, failed }
@@ -36,6 +37,7 @@ class ChannelMessage {
final List pathVariants;
final int? channelIndex;
final String messageId;
+ final String? packetHash;
final String? replyToMessageId;
final String? replyToSenderName;
final String? replyToText;
@@ -55,6 +57,7 @@ class ChannelMessage {
List? pathVariants,
this.channelIndex,
String? messageId,
+ this.packetHash,
this.replyToMessageId,
this.replyToSenderName,
this.replyToText,
@@ -79,6 +82,7 @@ class ChannelMessage {
int? pathLength,
Uint8List? pathBytes,
List? pathVariants,
+ String? packetHash,
String? replyToMessageId,
String? replyToSenderName,
String? replyToText,
@@ -98,6 +102,7 @@ class ChannelMessage {
pathVariants: pathVariants ?? this.pathVariants,
channelIndex: channelIndex,
messageId: messageId,
+ packetHash: packetHash ?? this.packetHash,
replyToMessageId: replyToMessageId ?? this.replyToMessageId,
replyToSenderName: replyToSenderName ?? this.replyToSenderName,
replyToText: replyToText ?? this.replyToText,
@@ -105,89 +110,82 @@ class ChannelMessage {
);
}
- static ChannelMessage? fromFrame(Uint8List data) {
+ static ChannelMessage? fromFrame(Uint8List frame) {
// CHANNEL_MSG_RECV format varies by version:
// V3: [0]=code [1]=SNR [2]=rsv1 [3]=rsv2 [4]=channel_idx [5]=path_len [path... optional] [txt_type] [timestamp x4] [text...]
// Non-V3: [0]=code [1]=channel_idx [2]=path_len [3]=txt_type [4-7]=timestamp [8+]=text
- if (data.length < 8) return null;
+ if (frame.length < 8) return null;
+ try {
+ final reader = BufferReader(frame);
+ final code = reader.readByte();
+ if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
+ return null;
+ }
- final code = data[0];
- if (code != respCodeChannelMsgRecv && code != respCodeChannelMsgRecvV3) {
+ int pathLen;
+ int txtType;
+ Uint8List pathBytes = Uint8List(0);
+ int channelIdx;
+ if (code == respCodeChannelMsgRecvV3) {
+ reader.skipBytes(1); // Skip SNR
+ final flags = reader.readByte();
+ final hasPath = (flags & 0x01) != 0;
+ reader.skipBytes(1); // Skip reserved byte
+ channelIdx = reader.readByte();
+ pathLen = reader.readInt8();
+ txtType = reader.readByte();
+ if (hasPath && pathLen > 0) {
+ reader.rewind(); // Rewind to read path length again for pathBytes
+ pathBytes = reader.readBytes(pathLen);
+ }
+ } else {
+ channelIdx = reader.readByte();
+ pathLen = reader.readInt8();
+ txtType = reader.readByte();
+ }
+ final timestampRaw = reader.readUInt32LE();
+
+ if (txtType != txtTypePlain) {
+ return null;
+ }
+
+ final text = reader.readCString();
+
+ // Extract sender name and actual message from "name: msg" format
+ String senderName = 'Unknown';
+ String actualText = text;
+
+ final colonIndex = text.indexOf(':');
+ if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
+ final potentialSender = text.substring(0, colonIndex);
+ if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
+ senderName = potentialSender;
+ final offset =
+ (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
+ ? colonIndex + 2
+ : colonIndex + 1;
+ actualText = text.substring(offset);
+ }
+ }
+
+ final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
+
+ return ChannelMessage(
+ senderKey: null,
+ senderName: senderName,
+ text: decodedText,
+ timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
+ isOutgoing: false,
+ status: ChannelMessageStatus.sent,
+ pathLength: pathLen,
+ pathBytes: pathBytes,
+ channelIndex: channelIdx,
+ );
+ } catch (e) {
+ appLogger.error('Error parsing channel message frame: $e');
+ // If parsing fails, return null to avoid crashes
return null;
}
-
- int timestampOffset, textOffset, pathLenOffset, txtTypeOffset;
- Uint8List pathBytes = Uint8List(0);
- int channelIdx;
-
- if (code == respCodeChannelMsgRecvV3) {
- channelIdx = data[4];
- pathLenOffset = 5;
- final pathLen = data[pathLenOffset].toSigned(8);
- var cursor = 6;
- final hasPathBytesFlag = (data[2] & 0x01) != 0;
- final canFitPath = pathLen > 0 && data.length >= cursor + pathLen + 5;
- final hasValidTxtType =
- cursor < data.length &&
- (data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
- if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
- canFitPath) {
- pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
- cursor += pathLen;
- }
- txtTypeOffset = cursor;
- cursor += 1; // txt_type
- timestampOffset = cursor;
- textOffset = cursor + 4;
- } else {
- channelIdx = data[1];
- pathLenOffset = 2;
- txtTypeOffset = 3;
- timestampOffset = 4;
- textOffset = 8;
- }
-
- if (data.length < textOffset + 1) return null;
-
- final txtType = data[txtTypeOffset];
- if (txtType != txtTypePlain) {
- return null;
- }
-
- final pathLen = data[pathLenOffset].toSigned(8);
- final timestampRaw = readUint32LE(data, timestampOffset);
- final text = readCString(data, textOffset, data.length - textOffset);
-
- // Extract sender name and actual message from "name: msg" format
- String senderName = 'Unknown';
- String actualText = text;
-
- final colonIndex = text.indexOf(':');
- if (colonIndex > 0 && colonIndex < text.length - 1 && colonIndex < 50) {
- final potentialSender = text.substring(0, colonIndex);
- if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
- senderName = potentialSender;
- final offset =
- (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
- ? colonIndex + 2
- : colonIndex + 1;
- actualText = text.substring(offset);
- }
- }
-
- final decodedText = Smaz.tryDecodePrefixed(actualText) ?? actualText;
-
- return ChannelMessage(
- senderKey: null,
- senderName: senderName,
- text: decodedText,
- timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
- isOutgoing: false,
- status: ChannelMessageStatus.sent,
- pathLength: pathLen,
- pathBytes: pathBytes,
- channelIndex: channelIdx,
- );
}
static ChannelMessage outgoing(
diff --git a/lib/models/companion_radio_stats.dart b/lib/models/companion_radio_stats.dart
new file mode 100644
index 0000000..1e3d3c1
--- /dev/null
+++ b/lib/models/companion_radio_stats.dart
@@ -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;
+ }
+ }
+}
diff --git a/lib/models/contact.dart b/lib/models/contact.dart
index c047622..2699f93 100644
--- a/lib/models/contact.dart
+++ b/lib/models/contact.dart
@@ -18,6 +18,7 @@ class Contact {
final DateTime lastSeen;
final DateTime lastMessageAt;
final bool isActive;
+ final bool wasPulled;
final Uint8List? rawPacket;
Contact({
@@ -34,6 +35,7 @@ class Contact {
required this.lastSeen,
DateTime? lastMessageAt,
this.isActive = true,
+ this.wasPulled = false,
this.rawPacket,
}) : lastMessageAt = lastMessageAt ?? lastSeen;
@@ -117,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 = [];
- 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
@@ -136,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)}>";
}
@@ -157,6 +161,12 @@ class Contact {
return null;
}
final pubKey = reader.readBytes(pubKeySize);
+
+ // Guard: reject contacts with zeroed or mostly-zeroed public keys
+ // (indicates corrupt flash storage on the firmware side)
+ final zeroCount = pubKey.where((b) => b == 0).length;
+ if (zeroCount > pubKeySize ~/ 2) return null;
+
final type = reader.readByte();
final flags = reader.readByte();
final pathLen = reader.readByte();
@@ -166,15 +176,22 @@ class Contact {
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize);
+ // Guard: reject contacts with non-printable names (corrupt flash data)
+ if (name.isNotEmpty &&
+ name.codeUnits.every((c) => c < 0x20 || c == 0xFFFD)) {
+ return null;
+ }
+
final lastMod = reader.readUInt32LE();
double? lat, lon;
- final latRaw = reader.readInt32LE();
- final lonRaw = reader.readInt32LE();
-
- if (latRaw != 0 || lonRaw != 0) {
- lat = latRaw / 1e6;
- lon = lonRaw / 1e6;
+ if (reader.remaining >= 8) {
+ final latRaw = reader.readInt32LE();
+ final lonRaw = reader.readInt32LE();
+ if (latRaw != 0 || lonRaw != 0) {
+ lat = latRaw / 1e6;
+ lon = lonRaw / 1e6;
+ }
}
return Contact(
@@ -182,7 +199,7 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name,
type: type,
flags: flags,
- pathLength: pathLen > 0 ? (pathLen > maxPathSize ? -1 : pathLen) : -1,
+ pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
path: pathBytes,
latitude: lat,
longitude: lon,
@@ -202,4 +219,7 @@ class Contact {
@override
int get hashCode => publicKeyHex.hashCode;
+ bool get teleBaseEnabled => (flags & contactFlagTeleBase) != 0;
+ bool get teleLocEnabled => (flags & contactFlagTeleLoc) != 0;
+ bool get teleEnvEnabled => (flags & contactFlagTeleEnv) != 0;
}
diff --git a/lib/models/message.dart b/lib/models/message.dart
index 4f42d96..6b930c0 100644
--- a/lib/models/message.dart
+++ b/lib/models/message.dart
@@ -16,13 +16,14 @@ class Message {
final String? messageId;
final int retryCount;
final int? estimatedTimeoutMs;
- final Uint8List? expectedAckHash;
+ final int? expectedAckHash;
final DateTime? sentAt;
final DateTime? deliveredAt;
final int? tripTimeMs;
final int? pathLength;
final Uint8List pathBytes;
final Map reactions;
+ final Map reactionStatuses;
final Uint8List fourByteRoomContactKey;
Message({
@@ -43,9 +44,11 @@ class Message {
Uint8List? pathBytes,
Uint8List? fourByteRoomContactKey,
Map? reactions,
+ Map? reactionStatuses,
}) : pathBytes = pathBytes ?? Uint8List(0),
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
- reactions = reactions ?? {};
+ reactions = reactions ?? {},
+ reactionStatuses = reactionStatuses ?? {};
String get senderKeyHex => pubKeyToHex(senderKey);
@@ -53,7 +56,7 @@ class Message {
MessageStatus? status,
int? retryCount,
int? estimatedTimeoutMs,
- Uint8List? expectedAckHash,
+ int? expectedAckHash,
DateTime? sentAt,
DateTime? deliveredAt,
int? tripTimeMs,
@@ -61,6 +64,7 @@ class Message {
Uint8List? pathBytes,
bool? isCli,
Map? reactions,
+ Map? reactionStatuses,
Uint8List? fourByteRoomContactKey,
}) {
return Message(
@@ -80,38 +84,41 @@ class Message {
pathLength: pathLength ?? this.pathLength,
pathBytes: pathBytes ?? this.pathBytes,
reactions: reactions ?? this.reactions,
+ reactionStatuses: reactionStatuses ?? this.reactionStatuses,
fourByteRoomContactKey:
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
);
}
- static Message? fromFrame(Uint8List data, Uint8List selfPubKey) {
- if (data.length < msgTextOffset + 1) return null;
+ static Message? fromFrame(Uint8List frame, Uint8List selfPubKey) {
+ if (frame.length < msgTextOffset + 1) return null;
+ final reader = BufferReader(frame);
+ try {
+ final code = reader.readByte();
+ if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
+ return null;
+ }
- final code = data[0];
- if (code != respCodeContactMsgRecv && code != respCodeContactMsgRecvV3) {
+ final senderKey = reader.readBytes(pubKeySize);
+ final timestampRaw = reader.readInt32LE();
+ final flags = reader.readByte();
+ if ((flags >> 2) != txtTypePlain) {
+ return null;
+ }
+ final text = reader.readCString();
+
+ return Message(
+ senderKey: senderKey,
+ text: text,
+ timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
+ isOutgoing: false,
+ isCli: false,
+ status: MessageStatus.delivered,
+ pathBytes: Uint8List(0),
+ );
+ } catch (e) {
return null;
}
-
- final senderKey = Uint8List.fromList(
- data.sublist(msgPubKeyOffset, msgPubKeyOffset + pubKeySize),
- );
- final timestampRaw = readUint32LE(data, msgTimestampOffset);
- final flags = data[msgFlagsOffset];
- if ((flags >> 2) != txtTypePlain) {
- return null;
- }
- final text = readCString(data, msgTextOffset, data.length - msgTextOffset);
-
- return Message(
- senderKey: senderKey,
- text: text,
- timestamp: DateTime.fromMillisecondsSinceEpoch(timestampRaw * 1000),
- isOutgoing: false,
- isCli: false,
- status: MessageStatus.delivered,
- pathBytes: Uint8List(0),
- );
}
static Message outgoing(
diff --git a/lib/models/path_history.dart b/lib/models/path_history.dart
index 5e3ea1f..ff2d226 100644
--- a/lib/models/path_history.dart
+++ b/lib/models/path_history.dart
@@ -1,11 +1,12 @@
class PathRecord {
final int hopCount;
final int tripTimeMs;
- final DateTime timestamp;
+ final DateTime? timestamp;
final bool wasFloodDiscovery;
final List pathBytes;
final int successCount;
final int failureCount;
+ final double routeWeight;
PathRecord({
required this.hopCount,
@@ -15,6 +16,7 @@ class PathRecord {
required this.pathBytes,
required this.successCount,
required this.failureCount,
+ this.routeWeight = 1.0,
});
String get displayText =>
@@ -24,11 +26,12 @@ class PathRecord {
return {
'hop_count': hopCount,
'trip_time_ms': tripTimeMs,
- 'timestamp': timestamp.toIso8601String(),
+ 'timestamp': timestamp?.toIso8601String(),
'was_flood': wasFloodDiscovery,
'path_bytes': pathBytes,
'success_count': successCount,
'failure_count': failureCount,
+ 'route_weight': routeWeight,
};
}
@@ -36,12 +39,15 @@ class PathRecord {
return PathRecord(
hopCount: json['hop_count'] as int,
tripTimeMs: json['trip_time_ms'] as int,
- timestamp: DateTime.parse(json['timestamp'] as String),
+ timestamp: json['timestamp'] != null
+ ? DateTime.parse(json['timestamp'] as String)
+ : null,
wasFloodDiscovery: json['was_flood'] as bool,
pathBytes:
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
successCount: json['success_count'] as int? ?? 0,
failureCount: json['failure_count'] as int? ?? 0,
+ routeWeight: (json['route_weight'] as num?)?.toDouble() ?? 1.0,
);
}
}
diff --git a/lib/models/path_selection.dart b/lib/models/path_selection.dart
index 65f2f27..cdb3d72 100644
--- a/lib/models/path_selection.dart
+++ b/lib/models/path_selection.dart
@@ -1,3 +1,9 @@
+import 'dart:typed_data';
+
+import 'contact.dart';
+
+const int recentAttemptDiversityWindow = 2;
+
class PathSelection {
final List pathBytes;
final int hopCount;
@@ -9,3 +15,38 @@ class PathSelection {
required this.useFlood,
});
}
+
+PathSelection resolvePathSelection(
+ Contact contact, {
+ PathSelection? selection,
+ bool forceFlood = false,
+}) {
+ if (contact.pathOverride != null) {
+ if (contact.pathOverride! < 0) {
+ return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
+ }
+ return PathSelection(
+ pathBytes: contact.pathOverrideBytes ?? Uint8List(0),
+ hopCount: contact.pathOverride!,
+ useFlood: false,
+ );
+ }
+
+ if (forceFlood || contact.pathLength < 0 || selection?.useFlood == true) {
+ return const PathSelection(pathBytes: [], hopCount: -1, useFlood: true);
+ }
+
+ if (selection != null && selection.pathBytes.isNotEmpty) {
+ return PathSelection(
+ pathBytes: selection.pathBytes,
+ hopCount: selection.hopCount,
+ useFlood: false,
+ );
+ }
+
+ return PathSelection(
+ pathBytes: contact.path,
+ hopCount: contact.pathLength,
+ useFlood: false,
+ );
+}
diff --git a/lib/screens/app_settings_screen.dart b/lib/screens/app_settings_screen.dart
index a2c920e..82b0f1f 100644
--- a/lib/screens/app_settings_screen.dart
+++ b/lib/screens/app_settings_screen.dart
@@ -291,6 +291,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),
@@ -310,6 +318,118 @@ class AppSettingsScreen extends StatelessWidget {
);
},
),
+ if (settingsService.settings.autoRouteRotationEnabled) ...[
+ const Divider(height: 1),
+ ListTile(
+ title: Text(context.l10n.appSettings_maxRouteWeight),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(context.l10n.appSettings_maxRouteWeightSubtitle),
+ Slider(
+ value: settingsService.settings.maxRouteWeight,
+ min: 1,
+ max: 10,
+ divisions: 9,
+ label: settingsService.settings.maxRouteWeight
+ .round()
+ .toString(),
+ onChanged: (value) =>
+ settingsService.setMaxRouteWeight(value),
+ ),
+ ],
+ ),
+ ),
+ const Divider(height: 1),
+ ListTile(
+ title: Text(context.l10n.appSettings_initialRouteWeight),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(context.l10n.appSettings_initialRouteWeightSubtitle),
+ Slider(
+ value: settingsService.settings.initialRouteWeight,
+ min: 0.5,
+ max: 5.0,
+ divisions: 9,
+ label: settingsService.settings.initialRouteWeight
+ .toStringAsFixed(1),
+ onChanged: (value) =>
+ settingsService.setInitialRouteWeight(value),
+ ),
+ ],
+ ),
+ ),
+ const Divider(height: 1),
+ ListTile(
+ title: Text(context.l10n.appSettings_routeWeightSuccessIncrement),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context
+ .l10n
+ .appSettings_routeWeightSuccessIncrementSubtitle,
+ ),
+ Slider(
+ value: settingsService.settings.routeWeightSuccessIncrement,
+ min: 0.1,
+ max: 2.0,
+ divisions: 19,
+ label: settingsService.settings.routeWeightSuccessIncrement
+ .toStringAsFixed(1),
+ onChanged: (value) =>
+ settingsService.setRouteWeightSuccessIncrement(value),
+ ),
+ ],
+ ),
+ ),
+ const Divider(height: 1),
+ ListTile(
+ title: Text(context.l10n.appSettings_routeWeightFailureDecrement),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context
+ .l10n
+ .appSettings_routeWeightFailureDecrementSubtitle,
+ ),
+ Slider(
+ value: settingsService.settings.routeWeightFailureDecrement,
+ min: 0.1,
+ max: 2.0,
+ divisions: 19,
+ label: settingsService.settings.routeWeightFailureDecrement
+ .toStringAsFixed(1),
+ onChanged: (value) =>
+ settingsService.setRouteWeightFailureDecrement(value),
+ ),
+ ],
+ ),
+ ),
+ const Divider(height: 1),
+ ListTile(
+ title: Text(context.l10n.appSettings_maxMessageRetries),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(context.l10n.appSettings_maxMessageRetriesSubtitle),
+ Slider(
+ value: settingsService.settings.maxMessageRetries
+ .toDouble(),
+ min: 2,
+ max: 10,
+ divisions: 8,
+ label: settingsService.settings.maxMessageRetries
+ .toString(),
+ onChanged: (value) =>
+ settingsService.setMaxMessageRetries(value.toInt()),
+ ),
+ ],
+ ),
+ ),
+ ],
],
),
);
@@ -577,6 +697,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;
}
@@ -664,6 +790,18 @@ class AppSettingsScreen extends StatelessWidget {
title: Text(context.l10n.appSettings_languageUk),
value: 'uk',
),
+ RadioListTile(
+ title: Text(context.l10n.appSettings_languageHu),
+ value: 'hu',
+ ),
+ RadioListTile(
+ title: Text(context.l10n.appSettings_languageJa),
+ value: 'ja',
+ ),
+ RadioListTile(
+ title: Text(context.l10n.appSettings_languageKo),
+ value: 'ko',
+ ),
],
),
),
diff --git a/lib/screens/ble_debug_log_screen.dart b/lib/screens/ble_debug_log_screen.dart
index a90f9f0..1009bc4 100644
--- a/lib/screens/ble_debug_log_screen.dart
+++ b/lib/screens/ble_debug_log_screen.dart
@@ -283,66 +283,66 @@ class _BleDebugLogScreenState extends State {
if (payload.length < 101) {
return 'ADVERT (short)';
}
- var offset = 0;
- final pubKey = _bytesToHex(
- payload.sublist(offset, offset + 32),
- spaced: false,
- );
- offset += 32;
- final timestamp = readUint32LE(payload, offset);
- offset += 4;
- offset += 64; // signature
- final flags = payload[offset++];
- final role = _deviceRoleLabel(flags & 0x0F);
- final hasLocation = (flags & 0x10) != 0;
- final hasFeature1 = (flags & 0x20) != 0;
- final hasFeature2 = (flags & 0x40) != 0;
- final hasName = (flags & 0x80) != 0;
- String? name;
- double? lat;
- double? lon;
- if (hasLocation && payload.length >= offset + 8) {
- lat = readInt32LE(payload, offset) / 1000000.0;
- lon = readInt32LE(payload, offset + 4) / 1000000.0;
- offset += 8;
+ final reader = BufferReader(payload);
+ try {
+ final pubKey = _bytesToHex(reader.readBytes(pubKeySize), spaced: false);
+
+ final timestamp = reader.readUInt32LE();
+ reader.skipBytes(signatureSize);
+ final flags = reader.readByte();
+ final role = _deviceRoleLabel(flags & 0x0F);
+ final hasLocation = (flags & 0x10) != 0;
+ final hasFeature1 = (flags & 0x20) != 0;
+ final hasFeature2 = (flags & 0x40) != 0;
+ final hasName = (flags & 0x80) != 0;
+ String? name;
+ double? lat;
+ double? lon;
+ if (hasLocation) {
+ lat = reader.readInt32LE() / 1000000.0;
+ lon = reader.readInt32LE() / 1000000.0;
+ }
+ if (hasFeature1) reader.skipBytes(2);
+ if (hasFeature2) reader.skipBytes(2);
+ if (hasName) {
+ name = reader.readCStringGreedy(maxNameSize);
+ }
+ final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
+ final locPart = (lat != null && lon != null)
+ ? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
+ : '';
+ return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
+ } catch (e) {
+ return 'ADVERT (invalid)';
}
- if (hasFeature1) offset += 2;
- if (hasFeature2) offset += 2;
- if (hasName && payload.length > offset) {
- final rawName = String.fromCharCodes(payload.sublist(offset));
- final nul = rawName.indexOf('\u0000');
- name = nul >= 0 ? rawName.substring(0, nul) : rawName;
- name = name.trim();
- }
- final namePart = (name != null && name.isNotEmpty) ? ' name="$name"' : '';
- final locPart = (lat != null && lon != null)
- ? ' loc=${lat.toStringAsFixed(6)},${lon.toStringAsFixed(6)}'
- : '';
- return 'ADVERT role=$role ts=$timestamp$namePart$locPart key=${pubKey.substring(0, 12)}…';
}
String _decodeControlSummary(Uint8List payload) {
- if (payload.isEmpty) return 'CONTROL (empty)';
- final flags = payload[0];
- final subType = flags & 0xF0;
- if (subType == 0x80) {
- if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
- final typeFilter = payload[1];
- final tag = readUint32LE(payload, 2);
- final since = payload.length >= 10 ? readUint32LE(payload, 6) : 0;
- return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
+ final reader = BufferReader(payload);
+ try {
+ final flags = reader.readByte();
+ final subType = flags & 0xF0;
+ if (subType == 0x80) {
+ if (payload.length < 6) return 'CONTROL DISCOVER_REQ (short)';
+ final typeFilter = reader.readByte();
+ final tag = reader.readInt32LE();
+ final since = payload.length >= 10 ? reader.readInt32LE() : 0;
+ return 'CONTROL DISCOVER_REQ filter=0x${typeFilter.toRadixString(16).padLeft(2, '0')} tag=$tag since=$since';
+ }
+ if (subType == 0x90) {
+ if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
+ final nodeType = flags & 0x0F;
+ final snrRaw = payload[1];
+ final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
+ final snr = snrSigned / 4.0;
+ final tag = reader.readInt32LE();
+ final keyLen = payload.length - 6;
+ return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
+ }
+ return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
+ } catch (e) {
+ return 'CONTROL (invalid)';
}
- if (subType == 0x90) {
- if (payload.length < 14) return 'CONTROL DISCOVER_RESP (short)';
- final nodeType = flags & 0x0F;
- final snrRaw = payload[1];
- final snrSigned = snrRaw > 127 ? snrRaw - 256 : snrRaw;
- final snr = snrSigned / 4.0;
- final tag = readUint32LE(payload, 2);
- final keyLen = payload.length - 6;
- return 'CONTROL DISCOVER_RESP node=${_deviceRoleLabel(nodeType)} snr=${snr.toStringAsFixed(2)} tag=$tag key=$keyLen';
- }
- return 'CONTROL subtype=0x${subType.toRadixString(16).padLeft(2, '0')}';
}
String _payloadTypeLabel(int payloadType) {
diff --git a/lib/screens/channel_chat_screen.dart b/lib/screens/channel_chat_screen.dart
index 4e3743d..12e98d6 100644
--- a/lib/screens/channel_chat_screen.dart
+++ b/lib/screens/channel_chat_screen.dart
@@ -4,11 +4,11 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
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';
@@ -26,6 +26,7 @@ import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/message_status_icon.dart';
+import '../widgets/radio_stats_entry.dart';
import 'channel_message_path_screen.dart';
import 'map_screen.dart';
@@ -47,6 +48,8 @@ class _ChannelChatScreenState extends State {
bool _isLoadingOlder = false;
MeshCoreConnector? _connector;
+ DateTime? _lastChannelSendAt;
+ bool _channelSkipNextBottomSnap = false;
@override
void initState() {
@@ -55,11 +58,45 @@ class _ChannelChatScreenState extends State {
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
- _connector = context.read();
- _connector?.setActiveChannel(widget.channel.index);
+ final connector = context.read();
+ final settings = context.read().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 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();
@@ -166,6 +203,34 @@ class _ChannelChatScreenState extends State {
],
),
centerTitle: false,
+ actions: [
+ const RadioStatsIconButton(),
+ PopupMenuButton(
+ icon: const Icon(Icons.more_vert),
+ onSelected: (value) {
+ if (value == 'clearChat') {
+ context.read().clearMessagesForChannel(
+ widget.channel.index,
+ );
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ value: 'clearChat',
+ child: Row(
+ children: [
+ const Icon(Icons.delete, size: 20, color: Colors.red),
+ const SizedBox(width: 12),
+ Text(
+ context.l10n.contact_clearChat,
+ style: const TextStyle(color: Colors.red),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ],
),
body: SafeArea(
top: false,
@@ -216,6 +281,10 @@ class _ChannelChatScreenState extends State {
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (_channelSkipNextBottomSnap) {
+ _channelSkipNextBottomSnap = false;
+ return;
+ }
_scrollController.scrollToBottomIfAtBottom();
});
@@ -311,8 +380,13 @@ class _ChannelChatScreenState extends State {
],
Flexible(
child: GestureDetector(
- onTap: () => _showMessagePathInfo(message),
+ onTap: PlatformInfo.isDesktop
+ ? null
+ : () => _showMessagePathInfo(message),
onLongPress: () => _showMessageActions(message),
+ onSecondaryTapUp: PlatformInfo.isDesktop
+ ? (_) => _showMessageActions(message)
+ : null,
child: Container(
padding: gifId != null
? const EdgeInsets.all(4)
@@ -430,25 +504,12 @@ class _ChannelChatScreenState extends State {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
- child: Linkify(
+ child: LinkHandler.buildLinkifyText(
+ context: context,
text: message.text,
style: TextStyle(
fontSize: bodyFontSize * textScale,
),
- linkStyle: TextStyle(
- fontSize: bodyFontSize * textScale,
- color: Colors.green,
- decoration: TextDecoration.underline,
- ),
- options: const LinkifyOptions(
- humanize: false,
- defaultToHttps: false,
- ),
- linkifiers: const [UrlLinkifier()],
- onOpen: (link) => LinkHandler.handleLinkTap(
- context,
- link.url,
- ),
),
),
if (!enableTracing && isOutgoing) ...[
@@ -557,7 +618,7 @@ class _ChannelChatScreenState extends State {
],
);
- if (!isOutgoing) {
+ if (!isOutgoing && !PlatformInfo.isDesktop) {
return _SwipeReplyBubble(
maxSwipeOffset: maxSwipeOffset,
replySwipeThreshold: replySwipeThreshold,
@@ -1055,6 +1116,16 @@ class _ChannelChatScreenState extends State {
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();
String messageText = text;
@@ -1112,6 +1183,15 @@ class _ChannelChatScreenState extends State {
_setReplyingTo(message);
},
),
+ if (PlatformInfo.isDesktop)
+ ListTile(
+ leading: const Icon(Icons.route),
+ title: Text(context.l10n.chat_path),
+ onTap: () {
+ Navigator.pop(sheetContext);
+ _showMessagePathInfo(message);
+ },
+ ),
// Can't react to your own messages
if (!message.isOutgoing)
ListTile(
diff --git a/lib/screens/channel_message_path_screen.dart b/lib/screens/channel_message_path_screen.dart
index 747c2bf..94b8eee 100644
--- a/lib/screens/channel_message_path_screen.dart
+++ b/lib/screens/channel_message_path_screen.dart
@@ -40,8 +40,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
final primaryPath = !channelMessage && !message.isOutgoing
? Uint8List.fromList(primaryPathTmp.reversed.toList())
: primaryPathTmp;
- final contacts = connector.allContacts;
- final hops = _buildPathHops(primaryPath, contacts, l10n);
+ final hops = _buildPathHops(primaryPath, connector, l10n);
final hasHopDetails = primaryPath.isNotEmpty;
final observedLabel = _formatObservedHops(
primaryPath.length,
@@ -65,6 +64,9 @@ class ChannelMessagePathScreen extends StatelessWidget {
flipPathAround: true,
reversePathAround:
!(!channelMessage && !message.isOutgoing),
+ pathHashByteWidth: context
+ .read()
+ .pathHashByteWidth,
),
),
),
@@ -303,10 +305,12 @@ class _ChannelMessagePathMapScreenState
extends State {
static const double _labelZoomThreshold = 8.5;
+ final MapController _mapController = MapController();
Uint8List? _selectedPath;
double _pathDistance = 0.0;
bool _showNodeLabels = true;
bool _didReceivePositionUpdate = false;
+ int? _focusedHopIndex;
@override
void initState() {
@@ -337,6 +341,22 @@ class _ChannelMessagePathMapScreenState
return totalDistance;
}
+ void _focusHop(_PathHop hop) {
+ if (!hop.hasLocation) return;
+ final targetZoom = _didReceivePositionUpdate
+ ? max(_mapController.camera.zoom, 10.0)
+ : 12.0;
+ _mapController.move(hop.position!, targetZoom);
+ }
+
+ void _onHopTapped(_PathHop hop) {
+ _focusHop(hop);
+ if (!mounted) return;
+ setState(() {
+ _focusedHopIndex = hop.index;
+ });
+ }
+
@override
Widget build(BuildContext context) {
return Consumer(
@@ -365,8 +385,7 @@ class _ChannelMessagePathMapScreenState
: selectedPathTmp;
final selectedIndex = _indexForPath(selectedPath, observedPaths);
- final contacts = connector.allContacts;
- final hops = _buildPathHops(selectedPath, contacts, context.l10n);
+ final hops = _buildPathHops(selectedPath, connector, context.l10n);
final points = [];
@@ -421,6 +440,7 @@ class _ChannelMessagePathMapScreenState
children: [
FlutterMap(
key: mapKey,
+ mapController: _mapController,
options: MapOptions(
initialCenter: initialCenter,
initialZoom: initialZoom,
@@ -472,6 +492,7 @@ class _ChannelMessagePathMapScreenState
) {
setState(() {
_selectedPath = observedPaths[index].pathBytes;
+ _focusedHopIndex = null;
});
}),
if (points.isEmpty)
@@ -727,8 +748,17 @@ class _ChannelMessagePathMapScreenState
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final hop = hops[index];
+ final isFocused = _focusedHopIndex == hop.index;
return ListTile(
dense: true,
+ enabled: hop.hasLocation,
+ selected: isFocused,
+ selectedTileColor: Theme.of(
+ context,
+ ).colorScheme.primary.withValues(alpha: 0.12),
+ onTap: hop.hasLocation
+ ? () => _onHopTapped(hop)
+ : null,
leading: CircleAvatar(
radius: 14,
child: Text(
@@ -787,19 +817,71 @@ class _ObservedPath {
List<_PathHop> _buildPathHops(
Uint8List pathBytes,
- List contacts,
+ MeshCoreConnector connector,
AppLocalizations l10n,
) {
+ if (pathBytes.isEmpty) return const [];
+ final candidatesByPrefix = >{};
+ for (final contact in connector.allContacts) {
+ if (contact.publicKey.isEmpty) continue;
+ if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
+ continue;
+ }
+ final prefix = contact.publicKey.first;
+ candidatesByPrefix.putIfAbsent(prefix, () => []).add(contact);
+ }
+ for (final candidates in candidatesByPrefix.values) {
+ candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
+ }
+ final startPoint =
+ (connector.selfLatitude != null && connector.selfLongitude != null)
+ ? LatLng(connector.selfLatitude!, connector.selfLongitude!)
+ : null;
+ var previousPosition = startPoint;
+ final distance = Distance();
+
final hops = <_PathHop>[];
for (var i = 0; i < pathBytes.length; i++) {
- final prefix = pathBytes[i];
- final contact = _matchContactForPrefix(contacts, prefix);
+ final searchPoint = i == 0 ? startPoint : previousPosition;
+ final candidates = candidatesByPrefix[pathBytes[i]];
+ Contact? contact;
+ if (candidates != null && candidates.isNotEmpty) {
+ var bestIndex = 0;
+ if (searchPoint != null) {
+ var bestDistance = double.infinity;
+ for (var j = 0; j < candidates.length; j++) {
+ final candidate = candidates[j];
+ if (!candidate.hasLocation ||
+ candidate.latitude == null ||
+ candidate.longitude == null) {
+ continue;
+ }
+ final currentDistance = distance(
+ searchPoint,
+ LatLng(candidate.latitude!, candidate.longitude!),
+ );
+ if (currentDistance < bestDistance) {
+ bestDistance = currentDistance;
+ bestIndex = j;
+ }
+ }
+ }
+ contact = candidates.removeAt(bestIndex);
+ if (candidates.isEmpty) {
+ candidatesByPrefix.remove(pathBytes[i]);
+ }
+ }
+
+ final resolvedPosition = _resolvePosition(contact);
+ if (resolvedPosition != null) {
+ previousPosition = resolvedPosition;
+ }
hops.add(
_PathHop(
index: i + 1,
- prefix: prefix,
+ prefix: pathBytes[i],
contact: contact,
- position: _resolvePosition(contact),
+ position: resolvedPosition,
l10n: l10n,
),
);
@@ -807,42 +889,13 @@ List<_PathHop> _buildPathHops(
return hops;
}
-Contact? _matchContactForPrefix(List contacts, int prefix) {
- final matches = contacts
- .where(
- (contact) =>
- (contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
- contact.publicKey.isNotEmpty &&
- contact.publicKey[0] == prefix,
- )
- .toList();
- if (matches.isEmpty) return null;
-
- Contact? pickWhere(bool Function(Contact) predicate) {
- for (final contact in matches) {
- if (predicate(contact)) return contact;
- }
- return null;
- }
-
- return pickWhere((c) => c.type == advTypeRepeater && _hasValidLocation(c)) ??
- pickWhere((c) => c.type == advTypeRepeater) ??
- pickWhere(_hasValidLocation) ??
- matches.first;
-}
-
LatLng? _resolvePosition(Contact? contact) {
if (contact == null) return null;
- if (!_hasValidLocation(contact)) return null;
- return LatLng(contact.latitude!, contact.longitude!);
-}
-
-bool _hasValidLocation(Contact contact) {
- final lat = contact.latitude;
- final lon = contact.longitude;
- if (lat == null || lon == null) return false;
- if (lat == 0 && lon == 0) return false;
- return true;
+ if (!contact.hasLocation) return null;
+ final latitude = contact.latitude;
+ final longitude = contact.longitude;
+ if (latitude == null || longitude == null) return null;
+ return LatLng(latitude, longitude);
}
String _formatPrefix(int prefix) {
diff --git a/lib/screens/channels_screen.dart b/lib/screens/channels_screen.dart
index 98694be..d67d03d 100644
--- a/lib/screens/channels_screen.dart
+++ b/lib/screens/channels_screen.dart
@@ -4,6 +4,7 @@ import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:meshcore_open/storage/channel_message_store.dart';
+import 'package:meshcore_open/utils/platform_info.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
@@ -126,7 +127,7 @@ class _ChannelsScreenState extends State
canPop: allowBack,
child: Scaffold(
appBar: AppBar(
- title: AppBarTitle(context.l10n.channels_title),
+ title: AppBarTitle(context.l10n.channels_title, indicators: false),
centerTitle: true,
automaticallyImplyLeading: false,
actions: [
@@ -417,78 +418,96 @@ class _ChannelsScreenState extends State
return Card(
key: ValueKey('channel_${channel.index}'),
margin: const EdgeInsets.only(bottom: 12),
- child: ListTile(
- dense: true,
- minVerticalPadding: 0,
- contentPadding: const EdgeInsets.symmetric(horizontal: 12),
- visualDensity: const VisualDensity(vertical: -2),
- leading: Stack(
- children: [
- CircleAvatar(
- backgroundColor: bgColor,
- child: Icon(icon, color: iconColor),
- ),
- if (isCommunityChannel)
- Positioned(
- right: 0,
- bottom: 0,
- child: Container(
- width: 14,
- height: 14,
- decoration: BoxDecoration(
- color: Colors.purple,
- shape: BoxShape.circle,
- border: Border.all(
- color: Theme.of(context).cardColor,
- width: 2,
+ child: GestureDetector(
+ onSecondaryTapUp: PlatformInfo.isDesktop
+ ? (_) => _showChannelActions(
+ context,
+ connector,
+ channelMessageStore,
+ channel,
+ )
+ : null,
+ child: ListTile(
+ dense: true,
+ minVerticalPadding: 0,
+ contentPadding: const EdgeInsets.symmetric(horizontal: 12),
+ visualDensity: const VisualDensity(vertical: -2),
+ leading: Stack(
+ children: [
+ CircleAvatar(
+ backgroundColor: bgColor,
+ child: Icon(icon, color: iconColor),
+ ),
+ if (isCommunityChannel)
+ Positioned(
+ right: 0,
+ bottom: 0,
+ child: Container(
+ width: 14,
+ height: 14,
+ decoration: BoxDecoration(
+ color: Colors.purple,
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: Theme.of(context).cardColor,
+ width: 2,
+ ),
+ ),
+ child: const Icon(
+ Icons.people,
+ size: 8,
+ color: Colors.white,
),
),
- child: const Icon(Icons.people, size: 8, color: Colors.white),
),
- ),
- ],
- ),
- title: Text(
- channel.name.isEmpty
- ? context.l10n.channels_channelIndex(channel.index)
- : channel.name,
- style: const TextStyle(fontWeight: FontWeight.w500),
- ),
- subtitle: Text(subtitle, maxLines: 1, overflow: TextOverflow.ellipsis),
- trailing: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (unreadCount > 0) ...[
- UnreadBadge(count: unreadCount),
- const SizedBox(width: 4),
],
- if (showDragHandle && dragIndex != null)
- ReorderableDelayedDragStartListener(
- index: dragIndex,
- child: Icon(
- Icons.drag_handle,
- color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ title: Text(
+ channel.name.isEmpty
+ ? context.l10n.channels_channelIndex(channel.index)
+ : channel.name,
+ style: const TextStyle(fontWeight: FontWeight.w500),
+ ),
+ subtitle: Text(
+ subtitle,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ trailing: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (unreadCount > 0) ...[
+ UnreadBadge(count: unreadCount),
+ const SizedBox(width: 4),
+ ],
+ if (showDragHandle && dragIndex != null)
+ ReorderableDelayedDragStartListener(
+ index: dragIndex,
+ child: Icon(
+ Icons.drag_handle,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
),
- ),
- ],
- ),
- onTap: () async {
- connector.markChannelRead(channel.index);
- await Future.delayed(const Duration(milliseconds: 50));
- if (context.mounted) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => ChannelChatScreen(channel: channel),
- ),
- );
- }
- },
- onLongPress: () => _showChannelActions(
- context,
- connector,
- channelMessageStore,
- channel,
+ ],
+ ),
+ onTap: () async {
+ connector.markChannelRead(channel.index);
+ await Future.delayed(const Duration(milliseconds: 50));
+ if (context.mounted) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => ChannelChatScreen(channel: channel),
+ ),
+ );
+ }
+ },
+ onLongPress: () => _showChannelActions(
+ context,
+ connector,
+ channelMessageStore,
+ channel,
+ ),
),
),
);
diff --git a/lib/screens/chat_screen.dart b/lib/screens/chat_screen.dart
index 5209b41..aecdc81 100644
--- a/lib/screens/chat_screen.dart
+++ b/lib/screens/chat_screen.dart
@@ -5,9 +5,10 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:provider/provider.dart';
+
+import '../utils/platform_info.dart';
import 'package:latlong2/latlong.dart';
import '../connector/meshcore_connector.dart';
@@ -16,6 +17,7 @@ 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';
@@ -34,8 +36,10 @@ import '../widgets/gif_message.dart';
import '../widgets/jump_to_bottom_button.dart';
import '../widgets/gif_picker.dart';
import '../widgets/path_selection_dialog.dart';
+import '../widgets/radio_stats_entry.dart';
import '../utils/app_logger.dart';
import '../l10n/l10n.dart';
+import 'telemetry_screen.dart';
class ChatScreen extends StatefulWidget {
final Contact contact;
@@ -50,8 +54,11 @@ class _ChatScreenState extends State {
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() {
@@ -60,11 +67,50 @@ class _ChatScreenState extends State {
_scrollController.onScrollNearTop = _loadOlderMessages;
SchedulerBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
- _connector = context.read();
- _connector?.setActiveContact(widget.contact.publicKeyHex);
+ final connector = context.read();
+ final settings = context.read().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 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();
@@ -244,10 +290,79 @@ class _ChatScreenState extends State {
tooltip: context.l10n.chat_pathManagement,
onPressed: () => _showPathHistory(context),
),
- IconButton(
- icon: const Icon(Icons.info_outline),
- onPressed: () => _showContactInfo(context),
+ Consumer(
+ builder: (context, connector, _) {
+ return PopupMenuButton(
+ icon: const Icon(Icons.more_vert),
+ onSelected: (value) {
+ if (value == 'info') {
+ _showContactInfo(context);
+ }
+ if (value == 'settings') {
+ _showContactSettings(context);
+ }
+ if (value == 'telemetry') {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) =>
+ TelemetryScreen(contact: widget.contact),
+ ),
+ );
+ }
+ if (value == 'clearChat') {
+ connector.clearMessagesForContact(widget.contact);
+ }
+ },
+ itemBuilder: (context) => [
+ PopupMenuItem(
+ value: 'info',
+ child: Row(
+ children: [
+ const Icon(Icons.info_outline, size: 20),
+ const SizedBox(width: 12),
+ Text(context.l10n.contact_info),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'telemetry',
+ child: Row(
+ children: [
+ const Icon(Icons.bar_chart, size: 20),
+ const SizedBox(width: 12),
+ Text(context.l10n.contact_telemetry),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'settings',
+ child: Row(
+ children: [
+ const Icon(Icons.settings, size: 20),
+ const SizedBox(width: 12),
+ Text(context.l10n.contact_settings),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ value: 'clearChat',
+ child: Row(
+ children: [
+ const Icon(Icons.delete, size: 20, color: Colors.red),
+ const SizedBox(width: 12),
+ Text(
+ context.l10n.contact_clearChat,
+ style: const TextStyle(color: Colors.red),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ },
),
+ const RadioStatsIconButton(),
],
),
body: Consumer(
@@ -307,6 +422,7 @@ class _ChatScreenState extends State {
// Auto-scroll to bottom if user is already at bottom
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
+ if (_pendingUnreadScrollTarget != null) return;
_scrollController.scrollToBottomIfAtBottom();
});
@@ -353,7 +469,7 @@ class _ChatScreenState extends State {
(service) => service.scale,
);
final resolvedContact = _resolveContact(connector);
- return _MessageBubble(
+ final bubble = _MessageBubble(
message: message,
senderName: resolvedContact.type == advTypeRoom
? "${contact.name} [$fourByteHex]"
@@ -362,7 +478,13 @@ class _ChatScreenState extends State {
textScale: textScale,
onTap: () => _openMessagePath(message, contact),
onLongPress: () => _showMessageActions(message, contact),
+ onRetryReaction: (msg, emoji) =>
+ _sendReaction(msg, contact, emoji),
);
+ if (identical(message, _pendingUnreadScrollTarget)) {
+ return KeyedSubtree(key: _unreadScrollKey, child: bubble);
+ }
+ return bubble;
},
);
},
@@ -488,6 +610,16 @@ class _ChatScreenState extends State {
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 maxBytes = maxContactMessageBytes();
if (utf8.encode(text).length > maxBytes) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -820,7 +952,8 @@ class _ChatScreenState extends State {
);
}
- String _formatRelativeTime(DateTime time) {
+ String _formatRelativeTime(DateTime? time) {
+ if (time == null) return '—';
final diff = DateTime.now().difference(time);
if (diff.inSeconds < 60) return context.l10n.time_justNow;
if (diff.inMinutes < 60) {
@@ -841,15 +974,31 @@ class _ChatScreenState extends State {
return;
}
- final formattedPath = pathBytes
- .map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
- .join(',');
+ final connector = context.read();
+ final allContacts = connector.allContacts;
+
+ final formattedPath = PathHelper.formatPathHex(pathBytes);
+ final resolvedNames = PathHelper.resolvePathNames(pathBytes, allContacts);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(context.l10n.chat_fullPath),
- content: SelectableText(formattedPath),
+ content: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ SelectableText(formattedPath),
+ const SizedBox(height: 8),
+ SelectableText(
+ resolvedNames,
+ style: TextStyle(
+ fontSize: 13,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
+ ),
+ ],
+ ),
actions: [
TextButton(
onPressed: () => Navigator.push(
@@ -860,6 +1009,7 @@ class _ChatScreenState extends State {
path: Uint8List.fromList(pathBytes),
flipPathAround: true,
targetContact: widget.contact,
+ pathHashByteWidth: connector.pathHashByteWidth,
),
),
),
@@ -874,11 +1024,22 @@ class _ChatScreenState extends State {
);
}
+ int _resolveContactIndex = -1;
+
Contact _resolveContact(MeshCoreConnector connector) {
- return connector.contacts.firstWhere(
+ if (_resolveContactIndex >= 0 &&
+ _resolveContactIndex < connector.contacts.length &&
+ connector.contacts[_resolveContactIndex].publicKeyHex ==
+ widget.contact.publicKeyHex) {
+ return connector.contacts[_resolveContactIndex];
+ }
+ _resolveContactIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.contact.publicKeyHex,
- orElse: () => widget.contact,
);
+ if (_resolveContactIndex == -1) {
+ return widget.contact;
+ }
+ return connector.contacts[_resolveContactIndex];
}
Contact _resolveContactFrom4Bytes(
@@ -931,59 +1092,127 @@ class _ChatScreenState extends State {
void _showContactInfo(BuildContext context) {
final connector = Provider.of(context, listen: false);
- connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
-
+ final contact = _resolveContact(connector);
showDialog(
context: context,
- builder: (context) => Consumer(
- builder: (context, connector, _) {
- final contact = _resolveContact(connector);
- final smazEnabled = connector.isContactSmazEnabled(
- contact.publicKeyHex,
- );
-
- return AlertDialog(
- title: Text(contact.name),
- content: SingleChildScrollView(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- _buildInfoRow(context.l10n.chat_type, contact.typeLabel),
- _buildInfoRow(context.l10n.chat_path, contact.pathLabel),
- if (contact.hasLocation)
- _buildInfoRow(
- context.l10n.chat_location,
- '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
- ),
- _buildInfoRow(
- context.l10n.chat_publicKey,
- '${contact.publicKeyHex.substring(0, 16)}...',
- ),
- const Divider(),
- SwitchListTile(
- contentPadding: EdgeInsets.zero,
- title: Text(context.l10n.channels_smazCompression),
- subtitle: Text(context.l10n.chat_compressOutgoingMessages),
- value: smazEnabled,
- onChanged: (value) {
- connector.setContactSmazEnabled(
- contact.publicKeyHex,
- value,
- );
- },
- ),
- ],
- ),
- ),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(context),
- child: Text(context.l10n.common_close),
+ builder: (context) => AlertDialog(
+ title: SelectableText(contact.name),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildInfoRow(context.l10n.chat_type, contact.typeLabel),
+ _buildInfoRow(context.l10n.chat_path, contact.pathLabel),
+ _buildInfoRow(
+ context.l10n.contact_lastSeen,
+ _formatContactLastMessage(contact.lastMessageAt),
),
+ if (contact.hasLocation)
+ _buildInfoRow(
+ context.l10n.chat_location,
+ '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
+ ),
+ _buildInfoRow(context.l10n.chat_publicKey, contact.publicKeyHex),
],
- );
- },
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: Text(context.l10n.common_close),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showContactSettings(BuildContext context) {
+ final connector = Provider.of(context, listen: false);
+ connector.ensureContactSmazSettingLoaded(widget.contact.publicKeyHex);
+ final contact = widget.contact;
+ bool smazEnabled = connector.isContactSmazEnabled(contact.publicKeyHex);
+ bool teleBaseEnabled = contact.teleBaseEnabled;
+ bool teleLocEnabled = contact.teleLocEnabled;
+ bool teleEnvEnabled = contact.teleEnvEnabled;
+ showDialog(
+ context: context,
+ builder: (context) => StatefulBuilder(
+ builder: (context, setDialogState) => AlertDialog(
+ title: Text(context.l10n.contact_settings),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (contact.hasLocation) ...[
+ _buildInfoRow(
+ context.l10n.chat_location,
+ '${contact.latitude?.toStringAsFixed(4)}, ${contact.longitude?.toStringAsFixed(4)}',
+ ),
+ const Divider(height: 8),
+ ],
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(context.l10n.channels_smazCompression),
+ subtitle: Text(context.l10n.chat_compressOutgoingMessages),
+ value: smazEnabled,
+ onChanged: (value) {
+ connector.setContactSmazEnabled(
+ contact.publicKeyHex,
+ value,
+ );
+ setDialogState(() => smazEnabled = value);
+ },
+ ),
+ const Divider(height: 8),
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(context.l10n.contact_teleBase),
+ subtitle: Text(context.l10n.contact_teleBaseSubtitle),
+ value: teleBaseEnabled,
+ onChanged: (value) {
+ setDialogState(() => teleBaseEnabled = value);
+ },
+ ),
+ const Divider(height: 8),
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(context.l10n.contact_teleLoc),
+ subtitle: Text(context.l10n.contact_teleLocSubtitle),
+ value: teleLocEnabled,
+ onChanged: (value) {
+ setDialogState(() => teleLocEnabled = value);
+ },
+ ),
+ const Divider(height: 8),
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ title: Text(context.l10n.contact_teleEnv),
+ subtitle: Text(context.l10n.contact_teleEnvSubtitle),
+ value: teleEnvEnabled,
+ onChanged: (value) {
+ setDialogState(() => teleEnvEnabled = value);
+ },
+ ),
+ ],
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () {
+ connector.setContactFlags(
+ contact,
+ teleBase: teleBaseEnabled,
+ teleLoc: teleLocEnabled,
+ teleEnv: teleEnvEnabled,
+ );
+ Navigator.pop(context);
+ },
+ child: Text(context.l10n.common_close),
+ ),
+ ],
+ ),
),
);
}
@@ -998,12 +1227,32 @@ class _ChatScreenState extends State {
width: 80,
child: Text(label, style: TextStyle(color: Colors.grey[600])),
),
- Expanded(child: Text(value)),
+ Expanded(child: SelectableText(value)),
],
),
);
}
+ String _formatContactLastMessage(DateTime timestamp) {
+ final diff = DateTime.now().difference(timestamp);
+ if (diff.isNegative || diff.inMinutes < 5) {
+ return context.l10n.contacts_lastSeenNow;
+ }
+ if (diff.inMinutes < 60) {
+ return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
+ }
+ if (diff.inHours < 24) {
+ final hours = diff.inHours;
+ return hours == 1
+ ? context.l10n.contacts_lastSeenHourAgo
+ : context.l10n.contacts_lastSeenHoursAgo(hours);
+ }
+ final days = diff.inDays;
+ return days == 1
+ ? context.l10n.contacts_lastSeenDayAgo
+ : context.l10n.contacts_lastSeenDaysAgo(days);
+ }
+
void _openChat(BuildContext context, Contact contact) {
// Check if this is a repeater
context.read().markContactRead(contact.publicKeyHex);
@@ -1023,7 +1272,9 @@ class _ChatScreenState extends State {
connector.getContacts();
}
- final pathForInput = currentContact.pathIdList;
+ final pathForInput = currentContact.pathFormattedIdList(
+ connector.pathHashByteWidth,
+ );
final currentPathLabel = _currentPathLabel(currentContact);
// Filter out the current contact from available contacts
@@ -1127,6 +1378,15 @@ class _ChatScreenState extends State {
_showEmojiPicker(message, contact);
},
),
+ if (PlatformInfo.isDesktop)
+ ListTile(
+ leading: const Icon(Icons.route),
+ title: Text(context.l10n.chat_path),
+ onTap: () {
+ Navigator.pop(sheetContext);
+ _openMessagePath(message, contact);
+ },
+ ),
ListTile(
leading: const Icon(Icons.copy),
title: Text(context.l10n.common_copy),
@@ -1237,6 +1497,7 @@ class _MessageBubble extends StatelessWidget {
final bool isRoomServer;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
+ final void Function(Message message, String emoji)? onRetryReaction;
final double textScale;
const _MessageBubble({
@@ -1246,6 +1507,7 @@ class _MessageBubble extends StatelessWidget {
required this.textScale,
this.onTap,
this.onLongPress,
+ this.onRetryReaction,
});
@override
@@ -1279,8 +1541,11 @@ class _MessageBubble extends StatelessWidget {
: CrossAxisAlignment.start,
children: [
GestureDetector(
- onTap: onTap,
+ onTap: PlatformInfo.isDesktop ? null : onTap,
onLongPress: onLongPress,
+ onSecondaryTapUp: PlatformInfo.isDesktop
+ ? (_) => onLongPress?.call()
+ : null,
child: Row(
mainAxisAlignment: isOutgoing
? MainAxisAlignment.end
@@ -1397,26 +1662,13 @@ class _MessageBubble extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(
- child: Linkify(
+ child: LinkHandler.buildLinkifyText(
+ context: context,
text: messageText,
style: TextStyle(
color: textColor,
fontSize: bodyFontSize * textScale,
),
- linkStyle: TextStyle(
- color: Colors.green,
- decoration: TextDecoration.underline,
- fontSize: bodyFontSize * textScale,
- ),
- options: const LinkifyOptions(
- humanize: false,
- defaultToHttps: false,
- ),
- linkifiers: const [UrlLinkifier()],
- onOpen: (link) => LinkHandler.handleLinkTap(
- context,
- link.url,
- ),
),
),
if (!enableTracing && isOutgoing) ...[
@@ -1445,7 +1697,10 @@ class _MessageBubble extends StatelessWidget {
child: Text(
context.l10n.chat_retryCount(
message.retryCount,
- 4,
+ context
+ .read()
+ .settings
+ .maxMessageRetries,
),
style: TextStyle(
fontSize: 10,
@@ -1606,33 +1861,64 @@ class _MessageBubble extends StatelessWidget {
children: message.reactions.entries.map((entry) {
final emoji = entry.key;
final count = entry.value;
+ final status = message.reactionStatuses[emoji];
+ final isPending =
+ status == MessageStatus.pending || status == MessageStatus.sent;
+ final isFailed = status == MessageStatus.failed;
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- decoration: BoxDecoration(
- color: colorScheme.secondaryContainer,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: colorScheme.outline.withValues(alpha: 0.3),
- width: 1,
- ),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Text(emoji, style: const TextStyle(fontSize: 16)),
- if (count > 1) ...[
- const SizedBox(width: 4),
- Text(
- '$count',
- style: TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.bold,
- color: colorScheme.onSecondaryContainer,
- ),
+ return GestureDetector(
+ onTap: isFailed && onRetryReaction != null
+ ? () => onRetryReaction!(message, emoji)
+ : null,
+ child: Opacity(
+ opacity: isPending ? 0.5 : 1.0,
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ decoration: BoxDecoration(
+ color: isFailed
+ ? colorScheme.errorContainer
+ : colorScheme.secondaryContainer,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(
+ color: isFailed
+ ? colorScheme.error
+ : colorScheme.outline.withValues(alpha: 0.3),
+ width: 1,
),
- ],
- ],
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(emoji, style: const TextStyle(fontSize: 16)),
+ if (count > 1) ...[
+ const SizedBox(width: 4),
+ Text(
+ '$count',
+ style: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.bold,
+ color: colorScheme.onSecondaryContainer,
+ ),
+ ),
+ ],
+ if (isPending) ...[
+ const SizedBox(width: 2),
+ SizedBox(
+ width: 8,
+ height: 8,
+ child: CircularProgressIndicator(
+ strokeWidth: 1.5,
+ color: colorScheme.onSecondaryContainer,
+ ),
+ ),
+ ],
+ if (isFailed) ...[
+ const SizedBox(width: 2),
+ Icon(Icons.replay, size: 10, color: colorScheme.error),
+ ],
+ ],
+ ),
+ ),
),
);
}).toList(),
diff --git a/lib/screens/companion_radio_stats_screen.dart b/lib/screens/companion_radio_stats_screen.dart
new file mode 100644
index 0000000..01fb64d
--- /dev/null
+++ b/lib/screens/companion_radio_stats_screen.dart
@@ -0,0 +1,250 @@
+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 createState() =>
+ _CompanionRadioStatsScreenState();
+}
+
+class _CompanionRadioStatsScreenState extends State {
+ final List _noiseHistory = [];
+ static const int _maxSamples = 120;
+ MeshCoreConnector? _connector;
+ DateTime? _lastChartSampleAt;
+
+ @override
+ void initState() {
+ super.initState();
+ final c = context.read();
+ _connector = c;
+ c.acquireRadioStatsPolling();
+ 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();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final l10n = context.l10n;
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(l10n.radioStats_screenTitle),
+ centerTitle: true,
+ ),
+ body: Selector(
+ 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();
+ final scheme = Theme.of(context).colorScheme;
+ final tt = Theme.of(context).textTheme;
+
+ return ValueListenableBuilder(
+ 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.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 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;
+ }
+}
diff --git a/lib/screens/contacts_screen.dart b/lib/screens/contacts_screen.dart
index 23844fb..d5b01f2 100644
--- a/lib/screens/contacts_screen.dart
+++ b/lib/screens/contacts_screen.dart
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/services/notification_service.dart';
import 'package:meshcore_open/utils/app_logger.dart';
+import 'package:meshcore_open/utils/platform_info.dart';
import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart';
@@ -1243,6 +1244,9 @@ class _ContactsScreenState extends State
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
onTap: () {
+ final hw = context
+ .read()
+ .pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
@@ -1253,6 +1257,7 @@ class _ContactsScreenState extends State
path: contact.pathBytesForDisplay,
flipPathAround: true,
targetContact: contact,
+ pathHashByteWidth: hw,
),
),
);
@@ -1273,6 +1278,9 @@ class _ContactsScreenState extends State
? Text(context.l10n.contacts_pathTrace)
: Text(context.l10n.contacts_ping),
onTap: () {
+ final hw = context
+ .read()
+ .pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
@@ -1283,6 +1291,7 @@ class _ContactsScreenState extends State
path: contact.pathBytesForDisplay,
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
targetContact: contact,
+ pathHashByteWidth: hw,
),
),
);
@@ -1317,6 +1326,9 @@ class _ContactsScreenState extends State
leading: const Icon(Icons.radar, color: Colors.green),
title: Text(context.l10n.contacts_chatTraceRoute),
onTap: () {
+ final hw = context
+ .read()
+ .pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
@@ -1327,6 +1339,7 @@ class _ContactsScreenState extends State
path: contact.pathBytesForDisplay,
flipPathAround: true,
targetContact: contact,
+ pathHashByteWidth: hw,
),
),
);
@@ -1353,7 +1366,10 @@ class _ContactsScreenState extends State
),
onTap: () async {
Navigator.pop(sheetContext);
- await connector.setContactFavorite(contact, !isFavorite);
+ await connector.setContactFlags(
+ contact,
+ isFavorite: !isFavorite,
+ );
},
),
ListTile(
@@ -1439,66 +1455,77 @@ class _ContactTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return ListTile(
- leading: CircleAvatar(
- backgroundColor: _getTypeColor(contact.type),
- child: _buildContactAvatar(contact),
- ),
- title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
- subtitle: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis),
- Text(
- contact.shortPubKeyHex,
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(fontSize: 12),
- ),
- ],
- ),
- // 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),
- ),
+ return GestureDetector(
+ onSecondaryTapUp: PlatformInfo.isDesktop ? (_) => onLongPress() : null,
+ child: ListTile(
+ leading: CircleAvatar(
+ backgroundColor: _getTypeColor(contact.type),
+ child: _buildContactAvatar(contact),
),
- child: SizedBox(
- width: 120,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- if (unreadCount > 0) ...[
- UnreadBadge(count: unreadCount),
- const SizedBox(height: 4),
- ],
- Text(
- _formatLastSeen(context, lastSeen),
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- textAlign: TextAlign.right,
- style: TextStyle(fontSize: 12, color: Colors.grey[600]),
- ),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (isFavorite)
- Icon(Icons.star, size: 14, color: Colors.amber[700]),
- if (isFavorite && contact.hasLocation)
- const SizedBox(width: 2),
- if (contact.hasLocation)
- Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
+ title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ contact.pathLabel,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ Text(
+ contact.shortPubKeyHex,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(fontSize: 12),
+ ),
+ ],
+ ),
+ // 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: [
+ if (unreadCount > 0) ...[
+ UnreadBadge(count: unreadCount),
+ const SizedBox(height: 4),
],
- ),
- ],
+ Text(
+ _formatLastSeen(context, lastSeen),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ textAlign: TextAlign.right,
+ style: TextStyle(fontSize: 12, color: Colors.grey[600]),
+ ),
+ Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (isFavorite)
+ Icon(Icons.star, size: 14, color: Colors.amber[700]),
+ if (isFavorite && contact.hasLocation)
+ const SizedBox(width: 2),
+ if (contact.hasLocation)
+ Icon(
+ Icons.location_on,
+ size: 14,
+ color: Colors.grey[400],
+ ),
+ ],
+ ),
+ ],
+ ),
),
),
+ onTap: onTap,
+ onLongPress: onLongPress,
),
- onTap: onTap,
- onLongPress: onLongPress,
);
}
diff --git a/lib/screens/device_screen.dart b/lib/screens/device_screen.dart
deleted file mode 100644
index c5967cf..0000000
--- a/lib/screens/device_screen.dart
+++ /dev/null
@@ -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 createState() => _DeviceScreenState();
-}
-
-class _DeviceScreenState extends State
- with DisconnectNavigationMixin {
- bool _showBatteryVoltage = false;
- int _quickIndex = 0;
-
- @override
- Widget build(BuildContext context) {
- return Consumer(
- 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 _disconnect(
- BuildContext context,
- MeshCoreConnector connector,
- ) async {
- await showDisconnectDialog(context, connector);
- }
-}
diff --git a/lib/screens/discovery_screen.dart b/lib/screens/discovery_screen.dart
index 7f065aa..4e7c6e8 100644
--- a/lib/screens/discovery_screen.dart
+++ b/lib/screens/discovery_screen.dart
@@ -9,6 +9,7 @@ import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../utils/contact_search.dart';
+import '../utils/platform_info.dart';
import '../widgets/app_bar.dart';
import '../widgets/list_filter_widget.dart';
@@ -88,7 +89,7 @@ class _DiscoveryScreenState extends State {
itemCount: filteredAndSorted.length,
itemBuilder: (context, index) {
final contact = filteredAndSorted[index];
- return ListTile(
+ final tile = ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
child: Icon(
@@ -120,6 +121,14 @@ class _DiscoveryScreenState extends State {
onLongPress: () =>
_showContactContextMenu(contact, connector),
);
+ if (PlatformInfo.isDesktop) {
+ return GestureDetector(
+ onSecondaryTapUp: (_) =>
+ _showContactContextMenu(contact, connector),
+ child: tile,
+ );
+ }
+ return tile;
},
),
),
diff --git a/lib/screens/map_screen.dart b/lib/screens/map_screen.dart
index df16a59..9616d47 100644
--- a/lib/screens/map_screen.dart
+++ b/lib/screens/map_screen.dart
@@ -1,3 +1,4 @@
+import 'dart:collection';
import 'dart:math';
import 'dart:typed_data';
@@ -52,7 +53,7 @@ class MapScreen extends StatefulWidget {
class _MapScreenState extends State {
// Zoom level at which node labels start to appear
- static const double _labelZoomThreshold = 12.0;
+ static const double _labelZoomThreshold = 14.0;
final MapController _mapController = MapController();
final MapMarkerService _markerService = MapMarkerService();
@@ -329,7 +330,9 @@ class _MapScreenState extends State {
if (!_isBuildingPathTrace)
IconButton(
icon: const Icon(Icons.radar),
- onPressed: () => _startPath(),
+ onPressed: () => _startPath(
+ LatLng(connector.selfLatitude!, connector.selfLongitude!),
+ ),
tooltip: context.l10n.contacts_pathTrace,
),
if (!_isBuildingPathTrace)
@@ -477,10 +480,12 @@ class _MapScreenState extends State {
point: highlightPosition,
width: 40,
height: 40,
- child: Icon(
- Icons.location_on_outlined,
- color: Colors.red[600],
- size: 34,
+ child: IgnorePointer(
+ child: Icon(
+ Icons.location_on_outlined,
+ color: Colors.red[600],
+ size: 34,
+ ),
),
),
if (!_isBuildingPathTrace)
@@ -503,28 +508,33 @@ class _MapScreenState extends State {
),
width: 40,
height: 40,
- child: Container(
- padding: const EdgeInsets.all(4),
- decoration: BoxDecoration(
- color: Colors.teal,
- shape: BoxShape.circle,
- border: Border.all(
- color: Colors.white,
- width: 2,
- ),
- boxShadow: [
- BoxShadow(
- color: Colors.black.withValues(alpha: 0.3),
- blurRadius: 4,
- offset: const Offset(0, 2),
+ child: IgnorePointer(
+ ignoring: true,
+ child: Container(
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: Colors.teal,
+ shape: BoxShape.circle,
+ border: Border.all(
+ color: Colors.white,
+ width: 2,
),
- ],
- ),
- alignment: Alignment.center,
- child: const Icon(
- Icons.person_pin_circle,
- color: Colors.white,
- size: 20,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withValues(
+ alpha: 0.3,
+ ),
+ blurRadius: 4,
+ offset: const Offset(0, 2),
+ ),
+ ],
+ ),
+ alignment: Alignment.center,
+ child: const Icon(
+ Icons.person_pin_circle,
+ color: Colors.white,
+ size: 20,
+ ),
),
),
),
@@ -544,6 +554,7 @@ class _MapScreenState extends State {
),
if (!_isBuildingPathTrace)
_buildLegend(
+ contacts,
contactsWithLocation,
settings,
sharedMarkers.length,
@@ -580,6 +591,7 @@ class _MapScreenState extends State {
// Index known-location repeaters by their 1-byte hash.
// null value = two repeaters share the same hash byte (ambiguous collision).
final repeaterByHash = {};
+
for (final c in withLocation) {
if (c.type == advTypeRepeater) {
if (repeaterByHash.containsKey(c.publicKey[0])) {
@@ -595,6 +607,11 @@ class _MapScreenState extends State {
for (final contact in allContacts) {
if (contact.hasLocation) continue;
+ if (contact.lastSeen.isBefore(
+ DateTime.now().subtract(const Duration(hours: 24)),
+ )) {
+ continue; // skip stale contacts
+ }
final anchorSet = {};
@@ -617,19 +634,6 @@ class _MapScreenState extends State {
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
}
- // Fallback: for any last-hop byte with no GPS repeater, average the
- // positions of contacts with known GPS that share the same last hop.
- // Those contacts are all adjacent to the same unknown repeater, so their
- // centroid is a reasonable proxy for its location.
- for (final byte in lastHopBytes) {
- if (repeaterByHash.containsKey(byte)) continue;
- for (final c in withLocation) {
- if (c.path.isNotEmpty && c.path.last == byte) {
- anchorSet.add(LatLng(c.latitude!, c.longitude!));
- }
- }
- }
-
// Filter anchors that are geometrically inconsistent with radio range.
// Two anchors more than 2 * maxRange apart cannot both be in direct radio
// range of the same node, so isolated outliers are removed.
@@ -641,15 +645,12 @@ class _MapScreenState extends State {
final LatLng position;
if (anchors.length == 1) {
- // Offset single-anchor guesses so they don't overlap the repeater marker.
- // Use the contact's public key byte as a deterministic angle seed.
- const offsetDeg = 0.003; // ~330 m at the equator
- final angle = (contact.publicKey[1] / 255.0) * 2 * pi;
- position = LatLng(
- anchors[0].latitude + offsetDeg * cos(angle),
- anchors[0].longitude + offsetDeg * sin(angle),
+ // Spread single-anchor guesses around the anchor so they remain visible.
+ position = _offsetGuessedPosition(
+ anchors[0],
+ contact,
+ radiusMeters: 330,
);
-
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
@@ -657,12 +658,25 @@ class _MapScreenState extends State {
continue; // discard implausible guesses near (0, 0)
}
} else {
- double lat = 0, lon = 0;
+ double lat = 0, lon = 0, weight = 1.0;
+ int counted = 0;
for (final a in anchors) {
- lat += a.latitude;
- lon += a.longitude;
+ if (counted == 0) {
+ lat = a.latitude;
+ lon = a.longitude;
+ } else {
+ lat += a.latitude * weight;
+ lon += a.longitude * weight;
+ }
+ // weight subsequent anchors less to create a bias towards the first (if more than 2)
+ weight = weight / 2;
+ counted++;
}
- position = LatLng(lat / anchors.length, lon / anchors.length);
+ position = _offsetGuessedPosition(
+ LatLng(lat / anchors.length, lon / anchors.length),
+ contact,
+ radiusMeters: anchors.length >= 3 ? 80 : 120,
+ );
if (!_checkLocationPlausibility(
position.latitude,
position.longitude,
@@ -682,6 +696,31 @@ class _MapScreenState extends State {
return result;
}
+ LatLng _offsetGuessedPosition(
+ LatLng anchor,
+ Contact contact, {
+ required double radiusMeters,
+ }) {
+ final seed = _guessSeed(contact.publicKey);
+ final angle = ((seed & 0xFFFF) / 0x10000) * 2 * pi;
+ final latOffsetDeg = (radiusMeters / 111320.0) * cos(angle);
+ final lonScale = max(cos(anchor.latitude * pi / 180.0).abs(), 0.2);
+ final lonOffsetDeg = (radiusMeters / (111320.0 * lonScale)) * sin(angle);
+ return LatLng(
+ anchor.latitude + latOffsetDeg,
+ anchor.longitude + lonOffsetDeg,
+ );
+ }
+
+ int _guessSeed(Uint8List publicKey) {
+ var seed = 0x811C9DC5;
+ for (final byte in publicKey) {
+ seed ^= byte;
+ seed = (seed * 0x01000193) & 0x7FFFFFFF;
+ }
+ return seed;
+ }
+
/// Estimates the free-space maximum LoRa range in km from the connected
/// device's current radio parameters. Returns null if parameters are unknown.
double? _estimateLoRaRangeKm(MeshCoreConnector connector) {
@@ -799,31 +838,70 @@ class _MapScreenState extends State {
return markers;
}
+ List _filterContactsBySettings(
+ List contacts,
+ dynamic settings, {
+ bool noLocations = false,
+ }) {
+ List filtered = [];
+ bool addContact = false;
+ for (final contact in contacts) {
+ addContact = false;
+ if (!contact.hasLocation && !noLocations) {
+ continue;
+ }
+
+ // Apply node type filters
+ if (contact.type == advTypeRepeater &&
+ (settings.mapShowRepeaters ||
+ _isBuildingPathTrace ||
+ settings.mapShowOverlaps)) {
+ addContact = true;
+ }
+ if (contact.type == advTypeChat &&
+ (settings.mapShowChatNodes || _isBuildingPathTrace)) {
+ addContact = true;
+ }
+ if (contact.type != advTypeChat &&
+ contact.type != advTypeRepeater &&
+ (settings.mapShowOtherNodes ||
+ _isBuildingPathTrace ||
+ settings.mapShowOverlaps)) {
+ 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) {
+ addContact = false;
+ }
+
+ if (addContact) {
+ filtered.add(contact);
+ }
+ }
+ return filtered;
+ }
+
List _buildMarkers(
List contacts,
settings, {
required bool showLabels,
}) {
final markers = [];
-
- for (final contact in contacts) {
- if (!contact.hasLocation) continue;
-
- // Apply node type filters
- if (contact.type == advTypeRepeater &&
- (!settings.mapShowRepeaters && !_isBuildingPathTrace)) {
- continue;
- }
- if (contact.type == advTypeChat &&
- !(settings.mapShowChatNodes && !_isBuildingPathTrace)) {
- continue;
- }
- if (contact.type != advTypeChat &&
- contact.type != advTypeRepeater &&
- (!settings.mapShowOtherNodes && !_isBuildingPathTrace)) {
- continue;
- }
-
+ final filteredContacts = _filterContactsBySettings(contacts, settings);
+ for (final contact in filteredContacts) {
final marker = Marker(
point: LatLng(contact.latitude!, contact.longitude!),
width: 35,
@@ -839,7 +917,9 @@ class _MapScreenState extends State {
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
- color: _getNodeColor(contact.type),
+ color: settings.mapShowOverlaps && !_isBuildingPathTrace
+ ? Colors.red
+ : _getNodeColor(contact.type),
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
@@ -866,7 +946,9 @@ class _MapScreenState extends State {
markers.add(
_buildNodeLabelMarker(
point: LatLng(contact.latitude!, contact.longitude!),
- label: contact.name,
+ label: settings.mapShowOverlaps && !_isBuildingPathTrace
+ ? "${contact.publicKeyHex.substring(0, 2)}:${contact.name}"
+ : contact.name,
),
);
}
@@ -941,25 +1023,25 @@ class _MapScreenState extends State {
}
Widget _buildLegend(
+ List contacts,
List contactsWithLocation,
settings,
int markerCount,
int guessedCount,
) {
- int nodeCount = 0;
- for (final contact in contactsWithLocation) {
- // Apply node type filters
- if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
- continue;
- }
- if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
- if (contact.type != advTypeChat &&
- contact.type != advTypeRepeater &&
- !settings.mapShowOtherNodes) {
- continue;
- }
- nodeCount++;
- }
+ final filteredContacts = _filterContactsBySettings(
+ contacts,
+ settings,
+ noLocations: false,
+ );
+ final filteredContactsAll = _filterContactsBySettings(
+ contacts,
+ settings,
+ noLocations: true,
+ );
+
+ final nodeCount = filteredContacts.length;
+ final nodeCountAll = filteredContactsAll.length;
return Positioned(
top: 16,
@@ -995,6 +1077,54 @@ class _MapScreenState extends State {
fontSize: 14,
),
),
+ Row(
+ children: [
+ Icon(
+ Icons.location_on,
+ size: 16,
+ color: Colors.grey,
+ ),
+ Text(
+ ": $nodeCount",
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ const Icon(
+ Icons.wrong_location,
+ size: 16,
+ color: Colors.grey,
+ ),
+ Text(
+ ": ${nodeCountAll - nodeCount}",
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ const Icon(
+ Icons.add_outlined,
+ size: 16,
+ color: Colors.grey,
+ ),
+ Text(
+ ": $nodeCountAll",
+ style: const TextStyle(
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
Text(
context.l10n.map_pinsCount(markerCount),
style: const TextStyle(
@@ -1833,6 +1963,15 @@ class _MapScreenState extends State {
},
contentPadding: EdgeInsets.zero,
),
+ CheckboxListTile(
+ title: Text(context.l10n.map_showOverlaps),
+ value: settings.mapShowOverlaps,
+ onChanged: (value) {
+ service.setMapShowOverlaps(value ?? true);
+ },
+ contentPadding: EdgeInsets.zero,
+ ),
+
const SizedBox(height: 16),
Text(
context.l10n.map_keyPrefix,
@@ -1991,12 +2130,13 @@ class _MapScreenState extends State {
});
}
- void _startPath() {
+ void _startPath(LatLng position) {
setState(() {
_isBuildingPathTrace = true;
_pathTrace.clear();
_points.clear();
_polylines.clear();
+ _points.add(position);
});
}
@@ -2042,21 +2182,25 @@ class _MapScreenState extends State {
.join(','),
style: TextStyle(fontSize: 18),
),
- const SizedBox(height: 6),
+ // const SizedBox(height: 6),
Wrap(
alignment: WrapAlignment.center,
- spacing: 8,
- runSpacing: 8,
+ spacing: 1,
+ runSpacing: 1,
children: [
if (_pathTrace.isNotEmpty)
- ElevatedButton(
+ IconButton(
onPressed: () {
+ final hashW = context
+ .read()
+ .pathHashByteWidth;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace),
+ pathHashByteWidth: hashW,
),
),
);
@@ -2064,15 +2208,37 @@ class _MapScreenState extends State {
_isBuildingPathTrace = false;
});
},
- child: Text(l10n.map_runTrace),
+ tooltip: l10n.map_runTrace,
+ icon: const Icon(Icons.arrow_forward_outlined),
),
if (_pathTrace.isNotEmpty)
- ElevatedButton(
+ IconButton(
+ onPressed: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => PathTraceMapScreen(
+ title: l10n.contacts_pathTrace,
+ path: Uint8List.fromList(_pathTrace),
+ flipPathAround: true,
+ ),
+ ),
+ );
+ setState(() {
+ _isBuildingPathTrace = false;
+ });
+ },
+ tooltip: l10n.map_runTraceWithReturnPath,
+ icon: const Icon(Icons.replay),
+ ),
+ if (_pathTrace.isNotEmpty)
+ IconButton(
onPressed: _removePath,
- child: Text(l10n.map_removeLast),
+ tooltip: l10n.map_removeLast,
+ icon: const Icon(Icons.undo),
),
if (_pathTrace.isEmpty)
- ElevatedButton(
+ IconButton(
onPressed: () {
setState(() {
_isBuildingPathTrace = false;
@@ -2084,7 +2250,8 @@ class _MapScreenState extends State {
SnackBar(content: Text(l10n.map_pathTraceCancelled)),
);
},
- child: Text(l10n.common_cancel),
+ tooltip: l10n.common_cancel,
+ icon: const Icon(Icons.close),
),
],
),
diff --git a/lib/screens/neighbors_screen.dart b/lib/screens/neighbors_screen.dart
index 5afeda4..f4c1673 100644
--- a/lib/screens/neighbors_screen.dart
+++ b/lib/screens/neighbors_screen.dart
@@ -44,6 +44,24 @@ class _NeighborsScreenState extends State {
PathSelection? _pendingStatusSelection;
List>? _parsedNeighbors;
+ int _resolveRepeaterIndex = -1;
+
+ Contact _resolveRepeater(MeshCoreConnector connector) {
+ if (_resolveRepeaterIndex >= 0 &&
+ _resolveRepeaterIndex < connector.contacts.length &&
+ connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
+ widget.repeater.publicKeyHex) {
+ return connector.contacts[_resolveRepeaterIndex];
+ }
+ _resolveRepeaterIndex = connector.contacts.indexWhere(
+ (c) => c.publicKeyHex == widget.repeater.publicKeyHex,
+ );
+ if (_resolveRepeaterIndex == -1) {
+ return widget.repeater;
+ }
+ return connector.contacts[_resolveRepeaterIndex];
+ }
+
@override
void initState() {
super.initState();
@@ -163,13 +181,6 @@ class _NeighborsScreenState extends State {
}
}
- Contact _resolveRepeater(MeshCoreConnector connector) {
- return connector.contacts.firstWhere(
- (c) => c.publicKeyHex == widget.repeater.publicKeyHex,
- orElse: () => widget.repeater,
- );
- }
-
Future _loadNeighbors() async {
if (_commandService == null) return;
diff --git a/lib/screens/path_trace_map.dart b/lib/screens/path_trace_map.dart
index e64a906..5b02931 100644
--- a/lib/screens/path_trace_map.dart
+++ b/lib/screens/path_trace_map.dart
@@ -55,6 +55,7 @@ class PathTraceMapScreen extends StatefulWidget {
final bool flipPathAround;
final bool reversePathAround;
final Contact? targetContact;
+ final int pathHashByteWidth;
const PathTraceMapScreen({
super.key,
@@ -64,6 +65,7 @@ class PathTraceMapScreen extends StatefulWidget {
this.flipPathAround = false,
this.reversePathAround = false,
this.targetContact,
+ this.pathHashByteWidth = pathHashSize,
});
@override
@@ -119,8 +121,13 @@ class _PathTraceMapScreenState extends State {
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;
}
diff --git a/lib/screens/repeater_cli_screen.dart b/lib/screens/repeater_cli_screen.dart
index 1c7ff43..52d92aa 100644
--- a/lib/screens/repeater_cli_screen.dart
+++ b/lib/screens/repeater_cli_screen.dart
@@ -77,11 +77,22 @@ class _RepeaterCliScreenState extends State {
});
}
+ int _resolveRepeaterIndex = -1;
+
Contact _resolveRepeater(MeshCoreConnector connector) {
- return connector.contacts.firstWhere(
+ if (_resolveRepeaterIndex >= 0 &&
+ _resolveRepeaterIndex < connector.contacts.length &&
+ connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
+ widget.repeater.publicKeyHex) {
+ return connector.contacts[_resolveRepeaterIndex];
+ }
+ _resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
- orElse: () => widget.repeater,
);
+ if (_resolveRepeaterIndex == -1) {
+ return widget.repeater;
+ }
+ return connector.contacts[_resolveRepeaterIndex];
}
void _handleTextMessageResponse(Uint8List frame) {
diff --git a/lib/screens/repeater_hub_screen.dart b/lib/screens/repeater_hub_screen.dart
index fd2da8e..8a14253 100644
--- a/lib/screens/repeater_hub_screen.dart
+++ b/lib/screens/repeater_hub_screen.dart
@@ -205,8 +205,7 @@ class RepeaterHubScreen extends StatelessWidget {
Navigator.push(
context,
MaterialPageRoute(
- builder: (context) =>
- TelemetryScreen(repeater: repeater, password: password),
+ builder: (context) => TelemetryScreen(contact: repeater),
),
);
},
diff --git a/lib/screens/repeater_settings_screen.dart b/lib/screens/repeater_settings_screen.dart
index bae0f50..6375e0b 100644
--- a/lib/screens/repeater_settings_screen.dart
+++ b/lib/screens/repeater_settings_screen.dart
@@ -129,11 +129,22 @@ class _RepeaterSettingsScreenState extends State {
_commandService?.handleResponse(widget.repeater, parsed.text);
}
+ int _resolveRepeaterIndex = -1;
+
Contact _resolveRepeater(MeshCoreConnector connector) {
- return connector.contacts.firstWhere(
+ if (_resolveRepeaterIndex >= 0 &&
+ _resolveRepeaterIndex < connector.contacts.length &&
+ connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
+ widget.repeater.publicKeyHex) {
+ return connector.contacts[_resolveRepeaterIndex];
+ }
+ _resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
- orElse: () => widget.repeater,
);
+ if (_resolveRepeaterIndex == -1) {
+ return widget.repeater;
+ }
+ return connector.contacts[_resolveRepeaterIndex];
}
bool _matchesRepeaterPrefix(Uint8List prefix) {
diff --git a/lib/screens/repeater_status_screen.dart b/lib/screens/repeater_status_screen.dart
index 95254f4..f938419 100644
--- a/lib/screens/repeater_status_screen.dart
+++ b/lib/screens/repeater_status_screen.dart
@@ -91,11 +91,22 @@ class _RepeaterStatusScreenState extends State {
});
}
+ int _resolveRepeaterIndex = -1;
+
Contact _resolveRepeater(MeshCoreConnector connector) {
- return connector.contacts.firstWhere(
+ if (_resolveRepeaterIndex >= 0 &&
+ _resolveRepeaterIndex < connector.contacts.length &&
+ connector.contacts[_resolveRepeaterIndex].publicKeyHex ==
+ widget.repeater.publicKeyHex) {
+ return connector.contacts[_resolveRepeaterIndex];
+ }
+ _resolveRepeaterIndex = connector.contacts.indexWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
- orElse: () => widget.repeater,
);
+ if (_resolveRepeaterIndex == -1) {
+ return widget.repeater;
+ }
+ return connector.contacts[_resolveRepeaterIndex];
}
void _handleTextMessageResponse(Uint8List frame) {
diff --git a/lib/screens/scanner_screen.dart b/lib/screens/scanner_screen.dart
index 986a598..17f26ea 100644
--- a/lib/screens/scanner_screen.dart
+++ b/lib/screens/scanner_screen.dart
@@ -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 {
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 {
}
}
+ Future _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(
+ 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(
diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart
index d6118f5..d9e0d20 100644
--- a/lib/screens/settings_screen.dart
+++ b/lib/screens/settings_screen.dart
@@ -12,6 +12,7 @@ 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';
class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key});
@@ -269,6 +270,16 @@ class _SettingsScreenState extends State {
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),
@@ -287,10 +298,10 @@ class _SettingsScreenState extends State {
const Divider(height: 1),
ListTile(
leading: const Icon(Icons.visibility_off_outlined),
- title: Text(l10n.settings_privacyMode),
- subtitle: Text(l10n.settings_privacyModeSubtitle),
+ title: Text(l10n.settings_privacy),
+ subtitle: Text(l10n.settings_privacySubtitle),
trailing: const Icon(Icons.chevron_right),
- onTap: () => _togglePrivacy(context, connector),
+ onTap: () => _privacySettings(context, connector),
),
],
),
@@ -311,10 +322,13 @@ class _SettingsScreenState extends State {
),
),
ListTile(
- leading: const Icon(Icons.cell_tower),
- title: Text(l10n.settings_sendAdvertisement),
- subtitle: Text(l10n.settings_sendAdvertisementSubtitle),
- onTap: () => _sendAdvert(context, connector),
+ leading: const Icon(Icons.delete_outline, color: Colors.red),
+ title: Text("Delete All Paths"),
+ subtitle: Text(
+ "Clear all path data from contacts.",
+ style: TextStyle(color: Colors.red[700]),
+ ),
+ onTap: () => connector.deleteAllPaths(),
),
const Divider(height: 1),
ListTile(
@@ -657,55 +671,6 @@ class _SettingsScreenState extends State {
);
}
- void _togglePrivacy(BuildContext context, MeshCoreConnector connector) {
- final l10n = context.l10n;
- showDialog(
- context: context,
- builder: (context) => AlertDialog(
- title: Text(l10n.settings_privacyMode),
- content: Text(l10n.settings_privacyModeToggle),
- actions: [
- TextButton(
- onPressed: () => Navigator.pop(context),
- child: Text(l10n.common_cancel),
- ),
- TextButton(
- onPressed: () async {
- Navigator.pop(context);
- await connector.setPrivacyMode(true);
- await connector.refreshDeviceInfo();
- if (!context.mounted) return;
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(l10n.settings_privacyModeEnabled)),
- );
- },
- child: Text(l10n.common_enable),
- ),
- TextButton(
- onPressed: () async {
- Navigator.pop(context);
- await connector.setPrivacyMode(false);
- await connector.refreshDeviceInfo();
- if (!context.mounted) return;
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(l10n.settings_privacyModeDisabled)),
- );
- },
- child: Text(l10n.common_disable),
- ),
- ],
- ),
- );
- }
-
- void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
- final l10n = context.l10n;
- connector.sendSelfAdvert(flood: true);
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
- }
-
void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.syncTime();
@@ -977,6 +942,136 @@ class _SettingsScreenState extends State {
}
}
+void _privacySettings(BuildContext context, MeshCoreConnector connector) {
+ final l10n = context.l10n;
+
+ int telemetryMode = connector.telemetryModeBase;
+ int telemetryLocMode = connector.telemetryModeLoc;
+ int telemetryEnvMode = connector.telemetryModeEnv;
+ bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
+ int multiAcks = connector.multiAcks;
+
+ final telemModeBase = [
+ DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
+ DropdownMenuItem(
+ value: teleModeAllowFlags,
+ child: Text(l10n.settings_allowByContact),
+ ),
+ DropdownMenuItem(
+ value: teleModeAllowAll,
+ child: Text(l10n.settings_allowAll),
+ ),
+ ];
+
+ showDialog(
+ context: context,
+ builder: (dialogContext) => StatefulBuilder(
+ builder: (context, setDialogState) => AlertDialog(
+ title: Text(l10n.settings_privacy),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(l10n.settings_privacySettingsDescription),
+ const SizedBox(height: 16),
+ FeatureToggleRow(
+ title: l10n.settings_advertLocation,
+ subtitle: l10n.settings_advertLocationSubtitle,
+ value: advertLocPolicy,
+ onChanged: (value) {
+ setDialogState(() => advertLocPolicy = value);
+ },
+ ),
+ const SizedBox(height: 8),
+ DropdownButtonFormField