Compare commits

...

51 Commits

Author SHA1 Message Date
zjs81 3fdd8f5eaf chore: Update version to 0.4.0+4 in pubspec.yaml 2026-01-19 20:58:11 -07:00
zjs81 f4ec732de8 feat: Add community management features with QR code scanning
- Implement Community model for managing community data, including secret handling and PSK derivation.
- Create CommunityQrScannerScreen for scanning and joining communities via QR codes.
- Develop CommunityStore for persisting community data using SharedPreferences.
- Introduce QrCodeDisplay widget for displaying QR codes with customizable options.
- Add QrScannerWidget for reusable QR code scanning functionality with validation and controls.
2026-01-19 20:56:07 -07:00
zjs81 f790604d23 Merge pull request #42 from wel97459/dev-neighbours
Added Neighbors to the repeater hub and a screen to display the Neighbors
2026-01-19 19:17:00 -07:00
zjs81 8e3b563aba revert translate.py 2026-01-19 19:14:48 -07:00
zjs81 ee3b0a3126 Add untranslated messages file and update localization keys
- Added `untranslated.json` to track untranslated messages.
- Updated localization keys in various language files to use camelCase format for consistency.
- Modified `neighbours_screen.dart` to reference updated localization keys.
2026-01-19 19:13:22 -07:00
zjs81 31d633ee0b Merge main into dev-neighbours 2026-01-19 19:09:03 -07:00
zjs81 c269365d81 Merge pull request #48 from wel97459/dev-gps
Added GPS enable and GPS interval settings.
2026-01-19 19:02:13 -07:00
zjs81 9a9f59e53f localization: update GPS settings messages for clarity and consistency across multiple languages 2026-01-19 19:00:30 -07:00
zjs81 9cb667fad0 localization: fix punctuation in GPS interval settings for Spanish and Portuguese 2026-01-19 19:00:24 -07:00
zjs81 3fef594fe5 localization: update GPS settings messages and improve handling of custom variables 2026-01-19 18:56:06 -07:00
zjs81 8387304d2a Merge main into dev-gps
- Resolved localization conflicts by keeping both GPS settings and room management strings
- Merged room management features from main
- Merged map and contacts screen updates from main
2026-01-19 18:51:02 -07:00
zjs81 2acba9eb84 Merge pull request #51 from wel97459/dev-roomManagement
Added room server management
2026-01-19 18:34:51 -07:00
zjs81 30ba1799e1 localization: update room management strings in multiple languages and refactor room login handling 2026-01-19 18:29:53 -07:00
zjs81 13f9c5058a Merge branch 'main' into dev-roomManagement 2026-01-19 18:25:00 -07:00
Winston Lowe 98fc2d6e0a Updated gps setting to follow state of companion. 2026-01-19 16:57:46 -08:00
Winston Lowe 2becbb342c Added buildGetCustomVarsFrame
And added update to refreshDeviceInfo and _requestDeviceInfo.
Added parsing of Custom Vars
2026-01-19 16:55:39 -08:00
zjs81 5b2d5a494c Merge pull request #47 from ericszimmermann/main
Disable Map rotation
2026-01-19 09:26:29 -07:00
Winston Lowe 153736d36e added roomserver management 2026-01-18 21:21:33 -08:00
Winston Lowe 6c8a149e1b fix a few translations and used _neighbourCount 2026-01-18 12:01:57 -08:00
Winston Lowe b41ccee4f9 Merge branch 'main' into dev-neighbours 2026-01-18 11:27:19 -08:00
Winston Lowe 04a713bb76 Added a basic neighbours screen for repeaters 2026-01-18 11:17:47 -08:00
Winston Lowe 714aecd7e6 Added GPS enable and interval settings 2026-01-18 01:05:46 -08:00
Winston Lowe 2e1a5e0fbf added CMD_SET_CUSTOM_VAR to BLE debug 2026-01-18 01:03:45 -08:00
Winston Lowe 1f0b7d8d7b added buildSetCustomVarFrame and setCustomVar 2026-01-18 01:02:48 -08:00
ericszimmermann dffea23ce2 Merge branch 'zjs81:main' into main 2026-01-17 20:47:56 +01:00
zjs81 e0a8fb7ec0 Merge pull request #44 from mtlynch/gh-build
Add a Github Action to build code in CI
2026-01-17 11:39:37 -07:00
zjs81 06fc08c41f Merge pull request #45 from mtlynch/flutter-analyze
Fix issues flagged by flutter analyze
2026-01-17 11:38:08 -07:00
ericz c22bfed680 Merge branch 'disable_map_rotation'
Disable Map Rotation.
2026-01-17 19:30:52 +01:00
zjs81 316c76e5b4 Merge pull request #46 from ericszimmermann/main
German translation V2
2026-01-17 11:20:54 -07:00
ericz 4b215ad574 Disable Map rotation 2026-01-17 17:14:39 +01:00
ericz 09e60cebd9 German translation V2 2026-01-17 17:03:39 +01:00
Michael Lynch 6782347cf4 Fix issues flagged by flutter analyze
This fixes code quality issues that flutter analyze catches and adds a CI step to Github Actions to flag on any future issues.
2026-01-17 11:00:34 -05:00
Michael Lynch 1726119c3e Add a Github Action to build code in CI
This adds a CI workflow in Github Actions to verify that the flutter builds compile for all supported platforms.

I tried adding Windows, but it currently fails, so I excluded it from this initial set.
2026-01-17 10:48:46 -05:00
zjs81 988806dccd Merge pull request #41 from mtlynch/show-error
Show repeater login error in login dialog
2026-01-16 19:10:15 -07:00
zjs81 14ff8250c0 Add support for private and hashtag channels in localization and channel management
- Updated Polish, Portuguese, Slovak, Slovenian, Swedish, and Chinese localization files to include new strings for creating and joining private channels, as well as joining hashtag channels.
- Enhanced the channel management UI to allow users to create and join private channels, join public channels, and join channels via hashtags.
- Implemented PSK derivation from hashtags using SHA256 in the Channel model.
- Improved the translation script to handle missing keys and translate all locales efficiently.
2026-01-16 19:06:39 -07:00
Michael Lynch 2a04ebb8b6 Show repeater login error in login dialog 2026-01-16 09:35:02 -05:00
zjs81 a14462978d Replace Column with SingleChildScrollView in RepeaterLoginDialog for better layout handling 2026-01-15 21:49:54 -07:00
zjs81 df7fb45683 Merge pull request #38 from wel97459/dev-contactsPubkey
Added public key in contacts list and in the repeater hub
2026-01-15 19:26:53 -07:00
zjs81 f01eff07ff Merge pull request #37 from wel97459/dev-map
Fix map centering
2026-01-15 19:20:20 -07:00
zjs81 7cc7183e0c Refactor map initialization and zoom calculation logic in MapScreen 2026-01-15 19:15:42 -07:00
zjs81 a6b2756d0d Ran flutter format on the file 2026-01-15 19:11:13 -07:00
zjs81 614f3d4601 Add signing configuration support in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81 7c33647119 Add key.properties support for signing configuration in build.gradle.kts 2026-01-15 18:42:20 -07:00
zjs81 fde8b686f5 Merge pull request #28 from spfmoby/better-french-translations
Replace Publicité by Annonce in the french translations
2026-01-15 18:30:55 -07:00
zjs81 9bc3a27b53 Merge pull request #30 from dennis1248/main
Update Dutch translations
2026-01-15 18:30:02 -07:00
Winston Lowe a8f387b0da Fix map centering weirdly
When nodes or markers are outside of the main area of interest.
2026-01-14 19:38:01 -08:00
Winston Lowe dd1a73c247 Repeater hub now show public key at the top 2026-01-14 19:34:41 -08:00
Winston Lowe e36f6b7eb9 changed contects list to show public keys of contect 2026-01-14 19:33:07 -08:00
Dennis ten Hoove fcef82be63 Update Dutch translations
This solves many of the most obvious errors and inconsistensies in the initial translation.
2026-01-13 11:53:54 +01:00
spfmoby 6ddb8f1a3d more fr translations / .arb and .dart synced 2026-01-13 08:27:01 +01:00
spfmoby 7a22223756 Replace Publicité by Annonce in the french translations 2026-01-12 10:18:18 +01:00
65 changed files with 11101 additions and 1359 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Build
on:
push:
pull_request:
jobs:
android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties', 'android/build.gradle', 'android/settings.gradle', 'android/app/build.gradle', 'pubspec.lock') }}
restore-keys: |
${{ runner.os }}-gradle-
- run: flutter pub get
- run: flutter build apk --release --no-pub
ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build ios --release --no-codesign --no-pub
linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- name: Install Linux build deps
run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev
- run: flutter pub get
- run: flutter build linux --release --no-pub
macos:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build macos --release --no-pub
web:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: "stable"
cache: true
- run: flutter pub get
- run: flutter build web --release --no-pub
+23
View File
@@ -0,0 +1,23 @@
name: Flutter Analyze
on:
pull_request:
push:
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
- name: Install dependencies
run: flutter pub get
- name: Analyze
run: flutter analyze --fatal-infos --fatal-warnings
+1
View File
@@ -70,6 +70,7 @@ secrets.dart
**/android/local.properties
**/android/.externalNativeBuild/
*.jks
key.properties
keystore.properties
# Generated files
+25 -3
View File
@@ -1,3 +1,5 @@
import java.util.Properties
plugins {
id("com.android.application")
id("kotlin-android")
@@ -5,6 +7,12 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
}
android {
namespace = "com.meshcore.meshcore_open"
compileSdk = flutter.compileSdkVersion
@@ -40,11 +48,25 @@ android {
// }
}
signingConfigs {
create("release") {
val storeFilePath = keystoreProperties["storeFile"] as String?
if (storeFilePath != null) {
storeFile = file(storeFilePath)
storePassword = keystoreProperties["storePassword"] as String?
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
}
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
+3
View File
@@ -16,6 +16,9 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Camera permission for QR code scanning -->
<uses-permission android:name="android.permission.CAMERA"/>
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<application
android:label="meshcore_open"
Binary file not shown.

After

Width:  |  Height:  |  Size: 579 KiB

+2
View File
@@ -53,5 +53,7 @@
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string>
</dict>
</plist>
+1
View File
@@ -3,3 +3,4 @@ template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
nullable-getter: false
untranslated-messages-file: untranslated.json
File diff suppressed because it is too large Load Diff
+65 -30
View File
@@ -20,7 +20,8 @@ class BufferReader {
Uint8List readRemainingBytes() => readBytes(remaining);
String readString() => utf8.decode(readRemainingBytes(), allowMalformed: true);
String readString() =>
utf8.decode(readRemainingBytes(), allowMalformed: true);
String readCString(int maxLength) {
final value = <int>[];
@@ -38,13 +39,19 @@ class BufferReader {
int readUInt8() => readBytes(1).buffer.asByteData().getUint8(0);
int readInt8() => readBytes(1).buffer.asByteData().getInt8(0);
int readUInt16LE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt16BE() => readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readUInt32LE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() => readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readUInt16LE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.little);
int readUInt16BE() =>
readBytes(2).buffer.asByteData().getUint16(0, Endian.big);
int readUInt32LE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.little);
int readUInt32BE() =>
readBytes(4).buffer.asByteData().getUint32(0, Endian.big);
int readInt16LE() =>
readBytes(2).buffer.asByteData().getInt16(0, Endian.little);
int readInt16BE() => readBytes(2).buffer.asByteData().getInt16(0, Endian.big);
int readInt32LE() => readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt32LE() =>
readBytes(4).buffer.asByteData().getInt32(0, Endian.little);
int readInt24BE() {
var value = (readByte() << 16) | (readByte() << 8) | readByte();
@@ -63,21 +70,25 @@ class BufferWriter {
void writeBytes(Uint8List bytes) => _builder.add(bytes);
void writeUInt16LE(int num) {
final bytes = Uint8List(2)..buffer.asByteData().setUint16(0, num, Endian.little);
final bytes = Uint8List(2)
..buffer.asByteData().setUint16(0, num, Endian.little);
writeBytes(bytes);
}
void writeUInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setUint32(0, num, Endian.little);
final bytes = Uint8List(4)
..buffer.asByteData().setUint32(0, num, Endian.little);
writeBytes(bytes);
}
void writeInt32LE(int num) {
final bytes = Uint8List(4)..buffer.asByteData().setInt32(0, num, Endian.little);
final bytes = Uint8List(4)
..buffer.asByteData().setInt32(0, num, Endian.little);
writeBytes(bytes);
}
void writeString(String string) => writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeString(String string) =>
writeBytes(Uint8List.fromList(utf8.encode(string)));
void writeCString(String string, int maxLength) {
final bytes = Uint8List(maxLength);
@@ -118,6 +129,8 @@ const int cmdGetChannel = 31;
const int cmdSetChannel = 32;
const int cmdGetRadioSettings = 57;
const int cmdGetTelemetryReq = 39;
const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50;
// Text message types
@@ -152,6 +165,7 @@ const int respCodeContactMsgRecvV3 = 16;
const int respCodeChannelMsgRecvV3 = 17;
const int respCodeChannelInfo = 18;
const int respCodeRadioSettings = 25;
const int respCodeCustomVars = 21;
// Push codes (async from device)
const int pushCodeAdvert = 0x80;
@@ -166,7 +180,6 @@ const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C;
// Contact/advertisement types
const int advTypeChat = 1;
const int advTypeRepeater = 2;
@@ -233,10 +246,7 @@ class ParsedContactText {
final Uint8List senderPrefix;
final String text;
const ParsedContactText({
required this.senderPrefix,
required this.text,
});
const ParsedContactText({required this.senderPrefix, required this.text});
}
ParsedContactText? parseContactMessageText(Uint8List frame) {
@@ -265,10 +275,17 @@ ParsedContactText? parseContactMessageText(Uint8List frame) {
return null;
}
var text = readCString(frame, baseTextOffset, frame.length - baseTextOffset).trim();
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();
text = readCString(
frame,
baseTextOffset + 4,
frame.length - (baseTextOffset + 4),
).trim();
}
if (text.isEmpty) return null;
@@ -362,7 +379,8 @@ Uint8List buildSendTextMsgFrame(
int attempt = 0,
int? timestampSeconds,
}) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypePlain);
@@ -444,7 +462,9 @@ Uint8List buildSendSelfAdvertFrame({bool flood = false}) {
// Format: [cmd][name...]
Uint8List buildSetAdvertNameFrame(String name) {
final nameBytes = utf8.encode(name);
final nameLen = nameBytes.length < maxNameSize ? nameBytes.length : maxNameSize - 1;
final nameLen = nameBytes.length < maxNameSize
? nameBytes.length
: maxNameSize - 1;
final writer = BufferWriter();
writer.writeByte(cmdSetAdvertName);
writer.writeBytes(Uint8List.fromList(nameBytes.sublist(0, nameLen)));
@@ -461,6 +481,14 @@ Uint8List buildSetAdvertLatLonFrame(double lat, double lon) {
return writer.toBytes();
}
Uint8List buildSetCustomVarFrame(String value) {
final writer = BufferWriter();
writer.writeByte(cmdSetCustomVar);
writer.writeString(value);
writer.writeByte(0);
return writer.toBytes();
}
// Build CMD_REBOOT frame
// Format: [cmd]["reboot"]
Uint8List buildRebootFrame() {
@@ -544,7 +572,9 @@ Uint8List buildUpdateContactPathFrame(
// Path data (64 bytes, zero-padded)
final pathPadded = Uint8List(maxPathSize);
if (customPath.isNotEmpty && pathLen > 0) {
final copyLen = customPath.length < maxPathSize ? customPath.length : maxPathSize;
final copyLen = customPath.length < maxPathSize
? customPath.length
: maxPathSize;
for (int i = 0; i < copyLen; i++) {
pathPadded[i] = customPath[i];
}
@@ -575,6 +605,11 @@ Uint8List buildGetRadioSettingsFrame() {
return Uint8List.fromList([cmdGetRadioSettings]);
}
//Build CMD_GET_CUSTOM_VARS frame
Uint8List buildGetCustomVarsFrame() {
return Uint8List.fromList([cmdGetCustomVar]);
}
// Calculate LoRa airtime for a packet
// Based on Semtech SX127x datasheet formula
// Returns airtime in milliseconds
@@ -598,9 +633,11 @@ int calculateLoRaAirtime({
final crc = 1; // CRC enabled
final de = lowDataRateOptimize ? 1 : 0;
final numerator = 8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final numerator =
8 * payloadBytes - 4 * spreadingFactor + 28 + 16 * crc - headerBytes;
final denominator = 4 * (spreadingFactor - 2 * de);
var payloadSymbols = 8 + ((numerator / denominator).ceil()) * (codingRate + 4);
var payloadSymbols =
8 + ((numerator / denominator).ceil()) * (codingRate + 4);
if (payloadSymbols < 0) {
payloadSymbols = 8;
@@ -647,7 +684,8 @@ Uint8List buildSendCliCommandFrame(
int attempt = 0,
int? timestampSeconds,
}) {
final timestamp = timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final timestamp =
timestampSeconds ?? (DateTime.now().millisecondsSinceEpoch ~/ 1000);
final writer = BufferWriter();
writer.writeByte(cmdSendTxtMsg);
writer.writeByte(txtTypeCliData);
@@ -661,10 +699,7 @@ Uint8List buildSendCliCommandFrame(
// Build a telemetry request frame
// Format: [cmd][pub_key x32][payload]
Uint8List buildSendBinaryReq(
Uint8List repeaterPubKey, {
Uint8List? payload,
}) {
Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
final writer = BufferWriter();
writer.writeByte(cmdSendBinaryReq);
writer.writeBytes(repeaterPubKey);
@@ -672,4 +707,4 @@ Uint8List buildSendBinaryReq(
writer.writeBytes(payload);
}
return writer.toBytes();
}
}
+150 -1
View File
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.",
"common_reload": "Презареди",
"common_clear": "Изчисти",
"path_currentPath": "Текущ път: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Повторители",
"listFilter_roomServers": "Сървъри на стая",
"listFilter_unreadOnly": "Само непрочетените",
"listFilter_newGroup": "Нова група"
"listFilter_newGroup": "Нова група",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
"repeater_neighbours": "Съседи",
"neighbors_receivedData": "Получени данни за съседи",
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
"neighbors_repeatersNeighbours": "Повторители Съседи",
"neighbors_noData": "Няма налични данни за съседи.",
"channels_createPrivateChannel": "Създай Частен Канал",
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
"channels_createPrivateChannelDesc": "Защитено с таен ключ.",
"channels_joinPrivateChannelDesc": "Ръчно въведете таен ключ.",
"channels_joinPublicChannel": "Присъединете се към Публичния канал",
"channels_joinPublicChannelDesc": "Всеки може да се присъедини към този канал.",
"channels_joinHashtagChannel": "Присъедини се към Хаштаг Канал",
"channels_joinHashtagChannelDesc": "Всеки може да се присъедини към хаштаговите канали.",
"channels_scanQrCode": "Сканирайте QR код",
"channels_scanQrCodeComingSoon": "Ще излезе скоро",
"channels_enterHashtag": "Въведете хаштаг",
"channels_hashtagHint": "напр. #отбор",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Слушано преди {time}.",
"neighbors_unknownContact": "Неизвестна {pubkey}",
"settings_locationIntervalSec": "Интервал за GPS (Секунди)",
"settings_locationGPSEnable": "Активиране на GPS",
"settings_locationGPSEnableSubtitle": "Активирайте автоматичното актуализиране на местоположението чрез GPS.",
"settings_locationIntervalInvalid": "Интервалът трябва да бъде поне 60 секунди и по-малко от 86400 секунди.",
"room_management": "Управление на сървъра за стая",
"contacts_manageRoom": "Управление на сървър за стая",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Общност",
"common_ok": "Добре",
"community_createDesc": "Създайте нова общност и я споделете чрез QR код.",
"community_create": "Създай общност",
"community_joinTitle": "Присъедини се към общността",
"community_joinConfirmation": "Искате ли да се присъедините към общността \"{name}\"?",
"community_scanQr": "Сканирайте QR кода на общността",
"community_scanInstructions": "Насочете камерата към QR код на общността",
"community_showQr": "Покажи QR код",
"community_publicChannel": "Обществено общност",
"community_hashtagChannel": "Хаштаг на общността",
"community_name": "Име на общността",
"community_enterName": "Въведете име на общността",
"community_created": "Общността \"{name}\" е създадена",
"community_joined": "Присъединено общност \"{name}\"",
"community_qrTitle": "Споделяне в общността",
"community_join": "Присъедини се",
"community_qrInstructions": "Сканирайте този QR код, за да се присъедините към {name}.",
"community_hashtagPrivacyHint": "Хаштаг каналите на общността са достъпни само за членове на общността",
"community_invalidQrCode": "Невалиден QR код на общността",
"community_alreadyMember": "Вече съм член",
"community_alreadyMemberMessage": "Вие вече сте член на \"{name}\".",
"community_addPublicChannel": "Добави публичен общностен канал",
"community_addPublicChannelHint": "Автоматично добавете публичния канал за тази общност.",
"community_noCommunities": "Няма присъединени общности още.",
"community_scanOrCreate": "Сканирайте QR код или създайте общност, за да започнете.",
"community_manageCommunities": "Управление на общности",
"community_delete": "Напусни общността",
"community_deleteConfirm": "Напускате \"{name}\"?",
"community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Остави общността \"{name}\"",
"community_addHashtagChannel": "Добави общностен хаштаг",
"community_addHashtagChannelDesc": "Добавете хаштаг канал за тази общност",
"community_selectCommunity": "Изберете общност",
"community_regularHashtag": "Обикновен хаштаг",
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
"community_communityHashtag": "Общностен хаштаг",
"community_communityHashtagDesc": "Само за членове на общността",
"community_forCommunity": "За {name}"
}
+281 -132
View File
@@ -69,7 +69,7 @@
},
"scanner_stop": "Stopp",
"scanner_scan": "Scannen",
"device_quickSwitch": "Schneller Umschalten",
"device_quickSwitch": "Schnelles Umschalten",
"device_meshcore": "MeshCore",
"settings_title": "Einstellungen",
"settings_deviceInfo": "Geräteinformationen",
@@ -78,7 +78,7 @@
"settings_nodeSettings": "Knoten-Einstellungen",
"settings_nodeName": "Knotenname",
"settings_nodeNameNotSet": "Nicht festgelegt",
"settings_nodeNameHint": "Gib den Knotenamen ein",
"settings_nodeNameHint": "Gebe den Knotenamen ein",
"settings_nodeNameUpdated": "Name aktualisiert",
"settings_radioSettings": "Funk Einstellungen",
"settings_radioSettingsSubtitle": "Frequenz, Leistung, Verbreitungsfaktor",
@@ -90,17 +90,17 @@
"settings_locationInvalid": "Ungültige Breiten- oder Längengrade.",
"settings_latitude": "Breitengrad",
"settings_longitude": "Längengrad",
"settings_privacyMode": "Privatschutzzustand",
"settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Anzeigen",
"settings_privacyModeToggle": "Aktivieren Sie den Datenschutzzustand, um Ihren Namen und Ihre Standortdaten in Anzeigen zu verbergen.",
"settings_privacyModeEnabled": "Privatschutzzustand aktiviert",
"settings_privacyMode": "Privatsphäreeinstellung",
"settings_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen",
"settings_privacyModeToggle": "Aktivieren Sie die Privatsphäreeinstellung, um Ihren Namen und Ihre Standortdaten in Ankündigungen zu verbergen.",
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
"settings_actions": "Aktionen",
"settings_sendAdvertisement": "Senden Sie Anzeige",
"settings_sendAdvertisementSubtitle": "Sendungsstatus jetzt",
"settings_advertisementSent": "Anzeige gesendet",
"settings_syncTime": "Synchronisierungszeit",
"settings_syncTimeSubtitle": "Stelle die Gerätewielfalt auf die Uhrzeit des Telefons ein",
"settings_sendAdvertisement": "Sende eine Ankündigung",
"settings_sendAdvertisementSubtitle": "Sende Ankündigung",
"settings_advertisementSent": "Ankündigung gesendet",
"settings_syncTime": "Zeitsynchronisierung",
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
"settings_timeSynchronized": "Zeit synchronisiert",
"settings_refreshContacts": "Kontakte aktualisieren",
"settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden",
@@ -128,8 +128,8 @@
"settings_infoStatus": "Status",
"settings_infoBattery": "Akku",
"settings_infoPublicKey": "Öffentlicher Schlüssel",
"settings_infoContactsCount": "Kontakte Anzahl",
"settings_infoChannelCount": "Kanalanzahl",
"settings_infoContactsCount": "Anzahl Kontakte",
"settings_infoChannelCount": "Anzahl Kanäle",
"settings_presets": "Voreinstellungen",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
@@ -139,11 +139,11 @@
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
"settings_bandwidth": "Bandbreite",
"settings_spreadingFactor": "Verteilungsfaktor",
"settings_codingRate": "Programmierpauschale",
"settings_codingRate": "Kodierungsrate",
"settings_txPower": "TX-Leistung (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
"settings_longRange": "Langreich",
"settings_longRange": "Grosse Reichweite",
"settings_fastSpeed": "Schnelle Geschwindigkeit",
"settings_error": "Fehler: {message}",
"@settings_error": {
@@ -157,7 +157,7 @@
"appSettings_appearance": "Aussehen",
"appSettings_theme": "Theme",
"appSettings_themeSystem": "Systemstandard",
"appSettings_themeLight": "Helligkeit",
"appSettings_themeLight": "Hell",
"appSettings_themeDark": "Dunkel",
"appSettings_language": "Sprache",
"appSettings_languageSystem": "Systemstandard",
@@ -176,19 +176,19 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Benachrichtigungen",
"appSettings_enableNotifications": "Benachrichtigungen aktivieren",
"appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Anzeigen",
"appSettings_enableNotificationsSubtitle": "Erhalte Benachrichtigungen für Nachrichten und Ankündigungen",
"appSettings_notificationPermissionDenied": "Erlaubnis zur Benachrichtigung verweigert",
"appSettings_notificationsEnabled": "Benachrichtigungen aktiviert",
"appSettings_notificationsDisabled": "Benachrichtigungen deaktiviert",
"appSettings_messageNotifications": "Nachrichtenbenachrichtigungen",
"appSettings_messageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfang neuer Nachrichten",
"appSettings_channelMessageNotifications": "Kanal-Nachrichten-Benachrichtigungen",
"appSettings_messageNotifications": "Direktnachrichten Benachrichtigungen",
"appSettings_messageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfang neuer Direktnachrichten",
"appSettings_channelMessageNotifications": "Kanalnachrichten Benachrichtigungen",
"appSettings_channelMessageNotificationsSubtitle": "Zeige Benachrichtigung beim Empfangen von Kanalnachrichten",
"appSettings_advertisementNotifications": "Werbeanzeigenbenachrichtigungen",
"appSettings_advertisementNotifications": "Ankündigungsbenachrichtigungen",
"appSettings_advertisementNotificationsSubtitle": "Zeige Benachrichtigung, wenn neue Knoten entdeckt werden.",
"appSettings_messaging": "Nachrichten",
"appSettings_clearPathOnMaxRetry": "Klares Pfad bei Max Wiederholungsversuch",
"appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen des Kontaktpfads nach 5 fehlgeschlagenen Sendeverboten",
"appSettings_clearPathOnMaxRetry": "Lösche Pfade bei Max Wiederholungsversuchen",
"appSettings_clearPathOnMaxRetrySubtitle": "Zurücksetzen der Kontaktpfade nach 5 fehlgeschlagenen Sendeabbrüchen",
"appSettings_pathsWillBeCleared": "Die Pfade werden nach 5 fehlgeschlagenen Versuchen gelöscht.",
"appSettings_pathsWillNotBeCleared": "Die Pfade werden nicht automatisch gelöscht.",
"appSettings_autoRouteRotation": "Automatische Routenrotation",
@@ -226,10 +226,10 @@
}
}
},
"appSettings_mapTimeFilter": "Kartent Zeitfilter",
"appSettings_mapTimeFilter": "Karten Zeitfilter",
"appSettings_showNodesDiscoveredWithin": "Zeige Knoten, die innerhalb von:",
"appSettings_allTime": "Alle Zeit",
"appSettings_lastHour": "Letzter Stunde",
"appSettings_allTime": "Ganzer Zeitverlauf",
"appSettings_lastHour": "Letzte Stunde",
"appSettings_last6Hours": "Letzte 6 Stunden",
"appSettings_last24Hours": "Letzte 24 Stunden",
"appSettings_lastWeek": "Letzte Woche",
@@ -252,13 +252,13 @@
"appSettings_appDebugLoggingEnabled": "App-Debug-Protokollierung aktiviert",
"appSettings_appDebugLoggingDisabled": "App-Debug-Protokollierung deaktiviert",
"contacts_title": "Kontakte",
"contacts_noContacts": "No Contacts noch",
"contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte Werbung machen.",
"contacts_noContacts": "Noch keine Kontakte vorhanden.",
"contacts_contactsWillAppear": "Kontakte werden angezeigt, wenn Geräte eine Ankündigung machen.",
"contacts_searchContacts": "Suche Kontakte...",
"contacts_noUnreadContacts": "Keine ungeklärten Kontakte",
"contacts_noUnreadContacts": "Keine ungesehene Kontakte",
"contacts_noContactsFound": "Keine Kontakte oder Gruppen gefunden.",
"contacts_deleteContact": "Löschen Sie Kontakt",
"contacts_removeConfirm": "Entfernen {contactName} aus den Kontakten?",
"contacts_deleteContact": "Lösche den Kontakt",
"contacts_removeConfirm": "{contactName} aus den Kontakten entfernen?",
"@contacts_removeConfirm": {
"placeholders": {
"contactName": {
@@ -266,12 +266,12 @@
}
}
},
"contacts_manageRepeater": "Wiederholung verwalten",
"contacts_manageRepeater": "Wiederholungen verwalten",
"contacts_roomLogin": "Raum-Login",
"contacts_openChat": "Öffnen Sie Chat",
"contacts_editGroup": "Gruppen bearbeiten",
"contacts_openChat": "Öffne Chat",
"contacts_editGroup": "Gruppe bearbeiten",
"contacts_deleteGroup": "Löschen Gruppe",
"contacts_deleteGroupConfirm": "Löschen Sie \"{groupName}\"?",
"contacts_deleteGroupConfirm": "Löschen von \"{groupName}\"?",
"@contacts_deleteGroupConfirm": {
"placeholders": {
"groupName": {
@@ -293,8 +293,8 @@
"contacts_filterContacts": "Filtert Kontakte...",
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
"contacts_noMembers": "Keine Mitglieder",
"contacts_lastSeenNow": "Letztes Ansehen jetzt",
"contacts_lastSeenMinsAgo": "Letzte Sichtung {minutes} Minuten her.",
"contacts_lastSeenNow": "gerade gesehen",
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -303,7 +303,7 @@
}
},
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
"contacts_lastSeenHoursAgo": "Letzte Aktivität vor {hours} Stunden.",
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
"@contacts_lastSeenHoursAgo": {
"placeholders": {
"hours": {
@@ -339,8 +339,8 @@
"channels_publicChannel": "Öffentlicher Kanal",
"channels_privateChannel": "Privater Kanal",
"channels_editChannel": "Kanal bearbeiten",
"channels_deleteChannel": "Löschen Sie Kanal",
"channels_deleteChannelConfirm": "Löschen \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
"channels_deleteChannel": "Lösche den Kanal",
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
"@channels_deleteChannelConfirm": {
"placeholders": {
"name": {
@@ -373,7 +373,7 @@
}
}
},
"channels_editChannelTitle": "Bearbeiteten Kanal {index}",
"channels_editChannelTitle": "Bearbeiteter Kanal {index}",
"@channels_editChannelTitle": {
"placeholders": {
"index": {
@@ -392,10 +392,10 @@
},
"channels_publicChannelAdded": "Öffentlicher Kanal hinzugefügt",
"channels_sortBy": "Sortiere nach",
"channels_sortManual": "Manuelle",
"channels_sortManual": "Manuell",
"channels_sortAZ": "A bis Z",
"channels_sortLatestMessages": "Letzte Nachrichten",
"channels_sortUnread": "Unlescht",
"channels_sortUnread": "Ungelesen",
"chat_noMessages": "Noch keine Nachrichten.",
"chat_sendMessageToStart": "Eine Nachricht senden, um anzufangen.",
"chat_originalMessageNotFound": "Originalmeldung nicht gefunden",
@@ -407,7 +407,7 @@
}
}
},
"chat_replyTo": "Antworten Sie {name}",
"chat_replyTo": "Antwort an {name}",
"@chat_replyTo": {
"placeholders": {
"name": {
@@ -436,7 +436,7 @@
"chat_messageCopied": "Nachricht kopiert",
"chat_messageDeleted": "Nachricht gelöscht",
"chat_retryingMessage": "Versuche es erneut.",
"chat_retryCount": "Versuchen {current}/{max}",
"chat_retryCount": "Versuche {current}/{max}",
"@chat_retryCount": {
"placeholders": {
"current": {
@@ -457,22 +457,22 @@
"emojiCategoryObjects": "Objekte",
"gifPicker_title": "Wähle ein GIF",
"gifPicker_searchHint": "Suche nach GIFs...",
"gifPicker_poweredBy": "Angetrieben von GIPHY",
"gifPicker_poweredBy": "Bereitgestellt von GIPHY",
"gifPicker_noGifsFound": "Keine GIFs gefunden",
"gifPicker_failedLoad": "GIF-Dateien konnten nicht geladen werden.",
"gifPicker_failedLoad": "GIF-Datei konnten nicht geladen werden.",
"gifPicker_failedSearch": "Suche nach GIFs fehlgeschlagen",
"gifPicker_noInternet": "Keine Internetverbindung",
"debugLog_appTitle": "App-Debug-Protokoll",
"debugLog_bleTitle": "BLE-Debug-Protokoll",
"debugLog_copyLog": "Kopieren Sie Protokoll",
"debugLog_clearLog": "Log löschen",
"debugLog_copyLog": "Kopieren des Protokolls",
"debugLog_clearLog": "Protokoll löschen",
"debugLog_copied": "Debug-Protokoll kopiert",
"debugLog_bleCopied": "BLE-Protokoll kopiert",
"debugLog_noEntries": "No Debug-Protokolle noch verfügbar",
"debugLog_enableInSettings": "Aktivieren Sie das App-Debug-Logging in den Einstellungen",
"debugLog_frames": "Rahmen",
"debugLog_rawLogRx": "Roh-Log-RX",
"debugLog_noBleActivity": "No BLE-Aktivität bisher",
"debugLog_noBleActivity": "Bisher keine BLE-Aktivität",
"debugFrame_length": "Rahmenlänge: {count} Bytes",
"@debugFrame_length": {
"placeholders": {
@@ -539,12 +539,12 @@
"chat_pathManagement": "Pfadverwaltung",
"chat_routingMode": "Routenmodus",
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"chat_forceFloodMode": "Zwangsgelände-Modus erzwingen",
"chat_forceFloodMode": "Flut-Modus erzwingen",
"chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):",
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
"chat_hopSingular": "Springe",
"chat_hopPlural": "Hops",
"chat_hopsCount": "{count} {count, plural, =1{Hop} other{Hops}}",
"chat_hopSingular": "Sprung",
"chat_hopPlural": "Sprünge",
"chat_hopsCount": "{count} {count, plural, =1{Sprung} other{Sprünge}}",
"@chat_hopsCount": {
"placeholders": {
"count": {
@@ -552,17 +552,17 @@
}
}
},
"chat_successes": "Erfolgreiche",
"chat_successes": "Erfolgreich",
"chat_removePath": "Pfad entfernen",
"chat_noPathHistoryYet": "Noe eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
"chat_pathActions": "Pfadaktionen:",
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
"chat_setCustomPathSubtitle": "Manuelle Routenpfad festlegen",
"chat_clearPath": "Klares Pfad",
"chat_clearPathSubtitle": "Zwinge bei nächster Sendung eine erneute Entdeckung durch.",
"chat_pathCleared": "Pfad freigelegt. Nächste Nachricht wird Route neu entdecken.",
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
"chat_clearPath": "Pfad zurücksetzen",
"chat_clearPathSubtitle": "Setze Pfad zurück, erkenne neuen Pfad bei nächster Sendung.",
"chat_pathCleared": "Pfad zurückgesetzt. Nächste Nachricht wird Route neu entdecken.",
"chat_floodModeSubtitle": "Verwende den Routingschalter in der App-Leiste",
"chat_floodModeEnabled": "Flutmodus aktiviert. Über den Routing-Icon in der App-Leiste wieder aktivieren.",
"chat_floodModeEnabled": "Flutmodus aktiviert.",
"chat_fullPath": "Vollständiger Pfad",
"chat_pathDetailsNotAvailable": "Die Pfaddetails sind noch nicht verfügbar. Versuchen Sie, eine Nachricht zu senden, um zu aktualisieren.",
"chat_pathSetHops": "Pfad gesetzt: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -576,15 +576,15 @@
}
}
},
"chat_pathSavedLocally": "Gespeichert lokal. Mit Verbinden zum Synchronisieren.",
"chat_pathSavedLocally": "Lokal Gespeichert. Bitte Verbinden zum Synchronisieren.",
"chat_pathDeviceConfirmed": "Gerät bestätigt.",
"chat_pathDeviceNotConfirmed": "Gerät noch nicht bestätigt.",
"chat_type": "Gib ein",
"chat_type": "Gebe ein",
"chat_path": "Pfad",
"chat_publicKey": "Öffentlicher Schlüssel",
"chat_compressOutgoingMessages": "Komprimieren ausgehende Nachrichten",
"chat_floodForced": "Überschwemmung (erzwungen)",
"chat_directForced": "Direkt (gezwungen)",
"chat_compressOutgoingMessages": "Komprimieren ausgehender Nachrichten",
"chat_floodForced": "Geflutet (erzwungen)",
"chat_directForced": "Direkt (erzwungen)",
"chat_hopsForced": "{count} Sprünge (erzwungen)",
"@chat_hopsForced": {
"placeholders": {
@@ -593,10 +593,10 @@
}
}
},
"chat_floodAuto": "Überschwemmung (automatisch)",
"chat_floodAuto": "Geflutet (automatisch)",
"chat_direct": "Direkt",
"chat_poiShared": "Gemeinsamer POI",
"chat_unread": "Unlescht: {count}",
"chat_poiShared": "Geteilter POI",
"chat_unread": "Ungelesen: {count}",
"@chat_unread": {
"placeholders": {
"count": {
@@ -604,9 +604,9 @@
}
}
},
"map_title": "Knotenkarte",
"map_title": "Karte",
"map_noNodesWithLocation": "Keine Knoten mit Standortdaten",
"map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten\nteilen,\num auf der Karte\nerscheinen.",
"map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.",
"map_nodesCount": "Knoten: {count}",
"@map_nodesCount": {
"placeholders": {
@@ -623,24 +623,24 @@
}
}
},
"map_chat": "Chat",
"map_repeater": "Wiederholung",
"map_chat": "Benutzer",
"map_repeater": "Repeater",
"map_room": "Raum",
"map_sensor": "Sensor",
"map_pinDm": "Sperren (DM)",
"map_pinPrivate": "Privat-Pin",
"map_pinPublic": "Öffentliche Taste (PIN)",
"map_pinDm": "Pin (Kontakt)",
"map_pinPrivate": "Pin (Channel)",
"map_pinPublic": "Pin (Public)",
"map_lastSeen": "Letzte Sichtung",
"map_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?",
"map_from": "Von",
"map_source": "Quelle",
"map_flags": "Flaggen",
"map_shareMarkerHere": "Teilen Sie hier das Marker.",
"map_pinLabel": "Kennzeichnungslabel",
"map_flags": "Flags",
"map_shareMarkerHere": "Teilen Sie den Marker hier.",
"map_pinLabel": "Pin Name",
"map_label": "Label",
"map_pointOfInterest": "Punkt von Interesse",
"map_sendToContact": "Senden an Kontakt",
"map_sendToChannel": "Senden Sie Kanal",
"map_sendToChannel": "Senden an Kanal",
"map_noChannelsAvailable": "Keine Kanäle verfügbar",
"map_publicLocationShare": "Öffentliche Standortfreigabe",
"map_publicLocationShareConfirm": "Sie werden kurz darauf einen Ort in {channelLabel} teilen. Dieser Kanal ist öffentlich und jeder mit dem PSK kann ihn sehen.",
@@ -652,25 +652,25 @@
}
},
"map_connectToShareMarkers": "Verbinde ein Gerät, um Marker zu teilen",
"map_filterNodes": "Filter Knoten",
"map_filterNodes": "Knotenfilter",
"map_nodeTypes": "Knotentypen",
"map_chatNodes": "Chat-Knoten",
"map_repeaters": "Wiederholer",
"map_repeaters": "Repeater",
"map_otherNodes": "Andere Knoten",
"map_keyPrefix": "Schlüsselpräfix",
"map_filterByKeyPrefix": "Filter nach Schlüsselpräfix",
"map_publicKeyPrefix": "Öffentlicher Schlüsselpräfix",
"map_publicKeyPrefix": "Schlüsselpräfix",
"map_markers": "Marker",
"map_showSharedMarkers": "Zeige gemeinsam genutzte Marker",
"map_lastSeenTime": "Letzte Sichtung",
"map_sharedPin": "Gemeinsames Passwort",
"map_joinRoom": "Beitreten Sie dem Raum",
"map_manageRepeater": "Wiederholung verwalten",
"map_manageRepeater": "Repeater verwalten",
"mapCache_title": "Offline-Karten-Cache",
"mapCache_selectAreaFirst": "Wählen Sie zuerst einen Bereich zum Zwischenspeichern aus.",
"mapCache_noTilesToDownload": "Keine Tiles für diese Region zum Herunterladen verfügbar.",
"mapCache_downloadTilesTitle": "Herunterladen von Tiles",
"mapCache_downloadTilesPrompt": "Laden {count} Tiles für den Offline-Bereich herunter?",
"mapCache_noTilesToDownload": "Keine Kacheln für diese Region zum Herunterladen verfügbar.",
"mapCache_downloadTilesTitle": "Herunterladen von Kacheln",
"mapCache_downloadTilesPrompt": "Laden {count} Kacheln für den Offline-Bereich herunter?",
"@mapCache_downloadTilesPrompt": {
"placeholders": {
"count": {
@@ -679,7 +679,7 @@
}
},
"mapCache_downloadAction": "Herunterladen",
"mapCache_cachedTiles": "Zwischengespeicherte {count} Fliesen",
"mapCache_cachedTiles": "Zwischengespeicherte {count} Kacheln",
"@mapCache_cachedTiles": {
"placeholders": {
"count": {
@@ -687,7 +687,7 @@
}
}
},
"mapCache_cachedTilesWithFailed": "Zwischengespeicherte {downloaded} Tiles ({failed} fehlgeschlagen)",
"mapCache_cachedTilesWithFailed": "Zwischengespeicherte {downloaded} Kacheln ({failed} fehlgeschlagen)",
"@mapCache_cachedTilesWithFailed": {
"placeholders": {
"downloaded": {
@@ -698,7 +698,7 @@
}
}
},
"mapCache_clearOfflineCacheTitle": "Leeren Offline-Cache",
"mapCache_clearOfflineCacheTitle": "Leere Offline-Cache",
"mapCache_clearOfflineCachePrompt": "Alle zwischengespeicherten Kartenraster entfernen?",
"mapCache_offlineCacheCleared": "Offline-Cache gelöscht",
"mapCache_noAreaSelected": "Kein Bereich ausgewählt",
@@ -724,7 +724,7 @@
}
}
},
"mapCache_downloadTilesButton": "Herunterladen von Tiles",
"mapCache_downloadTilesButton": "Herunterladen von Kacheln",
"mapCache_clearCacheButton": "Cache leeren",
"mapCache_failedDownloads": "Fehlgeschlagene Downloads: {count}",
"@mapCache_failedDownloads": {
@@ -785,10 +785,10 @@
"time_month": "Monat",
"time_months": "Monate",
"time_minutes": "Minuten",
"time_allTime": "Alle Zeit",
"time_allTime": "Ganzer Zeitraum",
"dialog_disconnect": "Trennen",
"dialog_disconnectConfirm": "Sind Sie sicher, dass Sie sich von diesem Gerät trennen möchten?",
"login_repeaterLogin": "Wiederholungseingang anmelden",
"login_repeaterLogin": "Beim Repeater anmelden",
"login_roomLogin": "Raum-Login",
"login_password": "Passwort",
"login_enterPassword": "Passwort eingeben",
@@ -799,7 +799,7 @@
"login_routing": "Routen",
"login_routingMode": "Routenmodus",
"login_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"login_forceFloodMode": "Zwangsgelände-Modus erzwingen",
"login_forceFloodMode": "Flut-Modus erzwingen",
"login_managePaths": "Pfadverwaltung",
"login_login": "Anmelden",
"login_attempt": "Versuche {current}/{max}",
@@ -821,9 +821,10 @@
}
}
},
"login_failedMessage": "Anmeldung fehlgeschlagen. Entweder ist das Passwort falsch oder der Repeater ist nicht erreichbar.",
"common_reload": "Neu laden",
"common_clear": "Löschen",
"path_currentPath": "Aktiger Pfad: {path}",
"path_currentPath": "Aktiver Pfad: {path}",
"@path_currentPath": {
"placeholders": {
"path": {
@@ -839,9 +840,9 @@
}
}
},
"path_enterCustomPath": "Gib Pfad an",
"path_enterCustomPath": "Gebe Pfad ein",
"path_currentPathLabel": "Aktueller Pfad",
"path_hexPrefixInstructions": "Gib für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
"path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
@@ -858,7 +859,7 @@
},
"path_tooLong": "Pfad zu lang. Maximal 64 Hops erlaubt.",
"path_setPath": "Pfad festlegen",
"repeater_management": "Wiederholungselement-Verwaltung",
"repeater_management": "Repeater-Verwaltung",
"repeater_managementTools": "Verwaltungs-Tools",
"repeater_status": "Status",
"repeater_statusSubtitle": "Status, Statistiken und Nachbarn anzeigen",
@@ -867,11 +868,11 @@
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Sende Befehle an den Repeater",
"repeater_settings": "Einstellungen",
"repeater_settingsSubtitle": "Wiederholungsparameter konfigurieren",
"repeater_statusTitle": "Wiederholungszustand",
"repeater_settingsSubtitle": "Repeater-parameter konfigurieren",
"repeater_statusTitle": "Repeaterstatus",
"repeater_routingMode": "Routenmodus",
"repeater_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
"repeater_forceFloodMode": "Zwangsgelände-Modus erzwingen",
"repeater_forceFloodMode": "Flut-Modus erzwingen",
"repeater_pathManagement": "Pfadverwaltung",
"repeater_refresh": "Aktualisieren",
"repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.",
@@ -963,9 +964,9 @@
}
}
},
"repeater_settingsTitle": "Wiederholungseinstellungen",
"repeater_settingsTitle": "Repeater Einstellungen",
"repeater_basicSettings": "Grundlegende Einstellungen",
"repeater_repeaterName": "Wiederholungseintrag",
"repeater_repeaterName": "Repeater Name",
"repeater_repeaterNameHelper": "Anzeigename für diesen Repeater",
"repeater_adminPassword": "Admin-Passwort",
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
@@ -978,7 +979,7 @@
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Bandbreite",
"repeater_spreadingFactor": "Verteilungsfaktor",
"repeater_codingRate": "Programmierpauschale",
"repeater_codingRate": "Kodierungsrate",
"repeater_locationSettings": "Standort Einstellungen",
"repeater_latitude": "Breitengrad",
"repeater_latitudeHelper": "Dezimalgrad (z.B. 37,7749)",
@@ -989,10 +990,10 @@
"repeater_packetForwardingSubtitle": "Aktivieren Sie den Repeater, um Pakete weiterzuleiten.",
"repeater_guestAccess": "Gastzugriff",
"repeater_guestAccessSubtitle": "Gast-Zugriff mit beschränkten Rechten zulassen",
"repeater_privacyMode": "Privatschutzzustand",
"repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Anzeigen",
"repeater_advertisementSettings": "Werbe Einstellungen",
"repeater_localAdvertInterval": "Lokaler Werbeintervall",
"repeater_privacyMode": "Privatsphäreeinstellung",
"repeater_privacyModeSubtitle": "Verstecken Sie Name/Ort in Ankündigungen",
"repeater_advertisementSettings": "Ankündigungseinstellungen",
"repeater_localAdvertInterval": "Intervall der lokalen Ankündigungen",
"repeater_localAdvertIntervalMinutes": "{minutes} Minuten",
"@repeater_localAdvertIntervalMinutes": {
"placeholders": {
@@ -1001,7 +1002,7 @@
}
}
},
"repeater_floodAdvertInterval": "Überschwemmungsanzeige-Intervall",
"repeater_floodAdvertInterval": "Intervall der gefluteten Ankündigungen",
"repeater_floodAdvertIntervalHours": "{hours} Stunden",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1010,7 +1011,7 @@
}
}
},
"repeater_encryptedAdvertInterval": "Verschlüsselte Werbeintervall",
"repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung",
"repeater_dangerZone": "Gefahrenzone",
"repeater_rebootRepeater": "Neustart Repeater",
"repeater_rebootRepeaterSubtitle": "Wiederholen Sie das Repeater-Gerät.",
@@ -1050,12 +1051,12 @@
},
"repeater_refreshBasicSettings": "Grundlegende Einstellungen aktualisieren",
"repeater_refreshRadioSettings": "Radio-Einstellungen aktualisieren",
"repeater_refreshTxPower": "Batterie-Strom aktualisieren",
"repeater_refreshTxPower": "Sendeleistung aktualisieren",
"repeater_refreshLocationSettings": "Aktualisieren Sie die Standort Einstellungen",
"repeater_refreshPacketForwarding": "Aktualisieren Paketweiterleitung",
"repeater_refreshGuestAccess": "Aktualisieren Sie den Gastzugriff",
"repeater_refreshPrivacyMode": "Wiederherstellen des Datenschutzzustands",
"repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Werbe Einstellungen",
"repeater_refreshAdvertisementSettings": "Aktualisieren Sie die Ankündigungseinstellungen",
"repeater_refreshed": "{label} wurde aktualisiert",
"@repeater_refreshed": {
"placeholders": {
@@ -1072,10 +1073,10 @@
}
}
},
"repeater_cliTitle": "Wiederholung CLI",
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Fehlersuche Nächster Befehl",
"repeater_commandHelp": "Hilfe",
"repeater_clearHistory": "Löschung der Historie",
"repeater_clearHistory": "Löschen der Historie",
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
"repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle",
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
@@ -1096,37 +1097,37 @@
"repeater_cliQuickGetTx": "Erhalte TX",
"repeater_cliQuickNeighbors": "Nachbarn",
"repeater_cliQuickVersion": "Version",
"repeater_cliQuickAdvertise": "Werben",
"repeater_cliQuickAdvertise": "Ankündigungen",
"repeater_cliQuickClock": "Uhr",
"repeater_cliHelpAdvert": "Sendet ein Werbepaket",
"repeater_cliHelpAdvert": "Sendet eine Ankündigung",
"repeater_cliHelpReboot": "Startet das Gerät neu. (Beachten Sie, dass es möglicherweise zu einer 'Timeout'-Situation kommt, was normal ist.)",
"repeater_cliHelpClock": "Zeigt die aktuelle Uhrzeit pro Gerät an.",
"repeater_cliHelpPassword": "Legt ein neues Administrator-Passwort für das Gerät fest.",
"repeater_cliHelpVersion": "Zeigt die Geräteversion und das Datum des Firmware-Builds an.",
"repeater_cliHelpClearStats": "Setzt verschiedene Statistikkalkulate auf Null zurück.",
"repeater_cliHelpClearStats": "Setzt verschiedene Statistikberechnungen auf Null zurück.",
"repeater_cliHelpSetAf": "Legt den Luftzeitfaktor fest.",
"repeater_cliHelpSetTx": "Legt die LoRa-Übertragungspower in dBm (bezogen auf 1 Watt) fest. (Neustart erforderlich, um die Änderungen anzuwenden)",
"repeater_cliHelpSetRepeat": "Aktiviert oder deaktiviert die Repeater-Rolle für diesen Knoten.",
"repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber kann nicht in den Raum geschickt werden. (nur lesen möglich).",
"repeater_cliHelpSetAllowReadOnly": "(Raumspeicher) Wenn 'an', dann wird die Anmeldung mit einem leeren Passwort erlaubt sein, aber es kann nicht in den Raum gesendet werden. (nur lesen möglich).",
"repeater_cliHelpSetFloodMax": "Legt die maximale Anzahl an Hops für Pakete der eingehenden Flut (wenn >= max, wird das Paket nicht weitergeleitet)",
"repeater_cliHelpSetIntThresh": "Legt den Interferenzeniveau (in dB) fest. Der Standardwert ist 14. Auf 0 setzen, um die Erkennung von Kanalinterferenzen zu deaktivieren.",
"repeater_cliHelpSetAgcResetInterval": "Legt das Intervall für das Zurücksetzen des Auto Gain Controllers fest. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetMultiAcks": "Aktiviert oder deaktiviert die Funktion 'Doppel-ACKs'.",
"repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Werbe-Paket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Werbungspakets fest. Auf 0 setzen, um es zu deaktivieren.",
"repeater_cliHelpSetAdvertInterval": "Legt das Timer-Intervall in Minuten fest, um ein lokales (ohne-Weiterleitung) Ankündigungspaket zu senden. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetFloodAdvertInterval": "Legt das Timer-Intervall in Stunden für den Versand eines Flut-Ankündigungspacket fest. Auf 0 setzen, um es zu deaktivieren.",
"repeater_cliHelpSetGuestPassword": "Legt/aktualisiert das Gastpasswort fest. (für Repeater können Gast-Logins die \"Get Stats\"-Anfrage senden)",
"repeater_cliHelpSetName": "Legt den Anzeigenamen fest.",
"repeater_cliHelpSetLat": "Legt die Breitengrad-Angabe der Werbekarte fest. (dezimale Grad)",
"repeater_cliHelpSetLon": "Legt die Längengrade der Werbe-Map fest. (dezimale Grad)",
"repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)",
"repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.",
"repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
"repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).",
"repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.",
"repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.",
"repeater_cliHelpSetBridgeDelay": "Setze Verzögerung vor erneuter Übertragung von Paketen.",
"repeater_cliHelpSetBridgeSource": "Wählen Sie, ob die Brücke empfangene oder gesendete Pakete erneut übertragen soll.",
"repeater_cliHelpSetBridgeSource": "Wählen Sie, ob über die Brücke empfangene oder gesendete Pakete erneut übertragen soll.",
"repeater_cliHelpSetBridgeBaud": "Setze die serielle Link-Baudrate für RS232-Brücken.",
"repeater_cliHelpSetBridgeSecret": "Richte das Espnow-Brücken-Geheimnis ein.",
"repeater_cliHelpSetBridgeSecret": "Richte das Brückenpassword ein.",
"repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).",
"repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).",
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
@@ -1134,9 +1135,9 @@
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Werbung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.",
"repeater_cliHelpRegion": "(Serien nur) Listet alle definierten Regionen und aktuelle Hochwassermissungen auf.",
"repeater_cliHelpRegion": "Listet alle definierten Regionen auf.",
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.",
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
@@ -1228,12 +1229,12 @@
"channelPath_title": "Paketpfad",
"channelPath_viewMap": "Karte anzeigen",
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
"channelPath_repeaterHops": "Wiederholungs-Sprünge",
"channelPath_repeaterHops": "Repeater-Sprünge",
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
"channelPath_messageDetails": "Nachrichtsdetails",
"channelPath_senderLabel": "Sender",
"channelPath_timeLabel": "Zeit",
"channelPath_repeatsLabel": "Wiederholung",
"channelPath_repeatsLabel": "Wiederholungen",
"channelPath_pathLabel": "Pfad {index}",
"channelPath_observedLabel": "Beobachtet",
"channelPath_observedPathTitle": "Beobachteter Pfad {index} • {hops}",
@@ -1271,7 +1272,7 @@
}
},
"channelPath_unknownPath": "Unbekannt",
"channelPath_floodPath": "Überschwemmung",
"channelPath_floodPath": "Geflutet",
"channelPath_directPath": "Direkt",
"channelPath_observedZeroOf": "0 von {total} Sprüngen",
"@channelPath_observedZeroOf": {
@@ -1327,13 +1328,161 @@
"listFilter_tooltip": "Filteren und sortieren",
"listFilter_sortBy": "Sortiere nach",
"listFilter_latestMessages": "Letzte Nachrichten",
"listFilter_heardRecently": "Hörte kürzlich",
"listFilter_heardRecently": "Kürzlich gehört",
"listFilter_az": "A-Z",
"listFilter_filters": "Filtere",
"listFilter_all": "Alle",
"listFilter_users": "Benutzer",
"listFilter_repeaters": "Wiederholer",
"listFilter_repeaters": "Repeater",
"listFilter_roomServers": "Raumserver",
"listFilter_unreadOnly": "Nur nicht gelesen",
"listFilter_newGroup": "Neue Gruppe"
"listFilter_newGroup": "Neue Gruppe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Nachbarn",
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
"neighbors_receivedData": "Empfangene Nachbarendaten",
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
"neighbors_repeatersNeighbours": "Wiederholer Nachbarn",
"neighbors_noData": "Keine Nachbardaten verfügbar.",
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
"channels_createPrivateChannelDesc": "Verschlüsselt mit einem geheimen Schlüssel.",
"channels_joinPublicChannel": "Tritt dem öffentlichen Kanal bei",
"channels_joinPublicChannelDesc": "Jeder kann diesem Kanal beitreten.",
"channels_joinHashtagChannel": "Treten Sie einem Hashtag-Kanal bei",
"channels_joinHashtagChannelDesc": "Jeder kann sich bei Hashtag-Kanälen beteiligen.",
"channels_scanQrCode": "Scannen Sie einen QR-Code",
"channels_scanQrCodeComingSoon": "Bald verfügbar",
"channels_enterHashtag": "Gib Hashtag ein",
"channels_hashtagHint": "z.B. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Hörte: {time} vor her.",
"neighbors_unknownContact": "Unbekannte {pubkey}",
"settings_locationGPSEnable": "GPS aktivieren",
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
"settings_locationIntervalInvalid": "Das Intervall muss mindestens 60 Sekunden und weniger als 86400 Sekunden betragen.",
"contacts_manageRoom": "Raum-Server verwalten",
"room_management": "Raum-Server-Verwaltung",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_create": "Erstelle Community",
"community_createDesc": "Erstelle eine neue Community und teile sie über den QR-Code.",
"community_join": "Beitreten",
"community_joinTitle": "Tritt der Community bei",
"community_joinConfirmation": "Möchten Sie sich der Community \"{name}\" anschließen?",
"community_scanQr": "Scannen Sie die Community QR-Code",
"community_scanInstructions": "Richten Sie die Kamera auf einen Community-QR-Code.",
"community_showQr": "Zeige QR-Code",
"community_publicChannel": "Community Öffentlich",
"community_enterName": "Bitte Community-Name eingeben",
"community_title": "Community",
"community_created": "Community \"{name}\" wurde erstellt",
"community_joined": "Community \"{name}\" beigetreten",
"community_qrTitle": "Teile Community",
"community_qrInstructions": "Scannen Sie diesen QR-Code, um sich \"{name}\" anzuschließen.",
"community_hashtagPrivacyHint": "Community-Hashtag-Kanäle können nur von Mitgliedern der Community betreten werden",
"community_hashtagChannel": "Community Hashtag",
"community_name": "Community Name",
"community_invalidQrCode": "Ungültiger Community-QR-Code",
"community_alreadyMember": "Bereits registriert",
"community_alreadyMemberMessage": "Sie sind bereits Mitglied von \"{name}\".",
"community_addPublicChannel": "Füge einen öffentlichen Community-Kanal hinzu",
"community_addPublicChannelHint": "Automatisch den öffentlichen Kanal für diese Community hinzufügen",
"community_noCommunities": "Noch keiner Community beigetreten",
"community_scanOrCreate": "Scannen Sie einen QR-Code oder eine Community erstellen, um loszulegen.",
"community_manageCommunities": "Verwalten von Communities",
"community_delete": "Verlasse Community",
"community_deleteConfirm": "\"{name}\" verlassen?",
"community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Community \"{name}\" verlassen",
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
"community_selectCommunity": "Wählen Sie Community",
"community_regularHashtag": "Regulärer Hashtag",
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
"community_forCommunity": "Für {name}",
"community_communityHashtag": "Community Hashtag"
}
+161 -3
View File
@@ -8,6 +8,7 @@
"nav_map": "Map",
"common_cancel": "Cancel",
"common_ok": "OK",
"common_connect": "Connect",
"common_unknownDevice": "Unknown Device",
"common_save": "Save",
@@ -83,9 +84,13 @@
"settings_radioSettingsUpdated": "Radio settings updated",
"settings_location": "Location",
"settings_locationSubtitle": "GPS coordinates",
"settings_locationUpdated": "Location updated",
"settings_locationUpdated": "Location and GPS settings updated",
"settings_locationBothRequired": "Enter both latitude and longitude.",
"settings_locationInvalid": "Invalid latitude or longitude.",
"settings_locationGPSEnable": "GPS Enable",
"settings_locationGPSEnableSubtitle": "Enables GPS to automatically update location.",
"settings_locationIntervalSec": "Interval for GPS (Seconds)",
"settings_locationIntervalInvalid": "Interval must be at least 60 seconds, and less than 86400 seconds.",
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_privacyMode": "Privacy Mode",
@@ -253,7 +258,8 @@
}
},
"contacts_manageRepeater": "Manage Repeater",
"contacts_roomLogin": "Room Login",
"contacts_manageRoom": "Manage Room Server",
"contacts_roomLogin": "Room Server Login",
"contacts_openChat": "Open Chat",
"contacts_editGroup": "Edit Group",
"contacts_deleteGroup": "Delete Group",
@@ -361,6 +367,18 @@
"channels_sortAZ": "A-Z",
"channels_sortLatestMessages": "Latest messages",
"channels_sortUnread": "Unread",
"channels_createPrivateChannel": "Create a Private Channel",
"channels_createPrivateChannelDesc": "Secured with a secret key.",
"channels_joinPrivateChannel": "Join a Private Channel",
"channels_joinPrivateChannelDesc": "Manually enter a secret key.",
"channels_joinPublicChannel": "Join the Public Channel",
"channels_joinPublicChannelDesc": "Anyone can join this channel.",
"channels_joinHashtagChannel": "Join a Hashtag Channel",
"channels_joinHashtagChannelDesc": "Anyone can join hashtag channels.",
"channels_scanQrCode": "Scan a QR Code",
"channels_scanQrCodeComingSoon": "Coming soon",
"channels_enterHashtag": "Enter hashtag",
"channels_hashtagHint": "e.g. #team",
"chat_noMessages": "No messages yet",
"chat_sendMessageToStart": "Send a message to get started",
@@ -685,7 +703,7 @@
"dialog_disconnectConfirm": "Are you sure you want to disconnect from this device?",
"login_repeaterLogin": "Repeater Login",
"login_roomLogin": "Room Login",
"login_roomLogin": "Room Server Login",
"login_password": "Password",
"login_enterPassword": "Enter password",
"login_savePassword": "Save password",
@@ -711,6 +729,8 @@
"error": {"type": "String"}
}
},
"login_failedMessage": "Login failed. Either the password is incorrect or the repeater is unreachable.",
"common_reload": "Reload",
"common_clear": "Clear",
@@ -746,6 +766,7 @@
"path_setPath": "Set Path",
"repeater_management": "Repeater Management",
"room_management": "Room Server Management",
"repeater_managementTools": "Management Tools",
"repeater_status": "Status",
"repeater_statusSubtitle": "View repeater status, stats, and neighbors",
@@ -753,6 +774,8 @@
"repeater_telemetrySubtitle": "View telemetry of sensors and system stats",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Send commands to the repeater",
"repeater_neighbours": "Neighbors",
"repeater_neighboursSubtitle": "View zero hop neighbors.",
"repeater_settings": "Settings",
"repeater_settingsSubtitle": "Configure repeater parameters",
@@ -1055,6 +1078,29 @@
"fahrenheit": {"type": "String"}
}
},
"neighbors_receivedData": "Received Neighbours Data",
"neighbors_requestTimedOut": "Neighbours request timed out.",
"neighbors_errorLoading": "Error loading neighbors: {error}",
"@neighbors_errorLoading": {
"placeholders": {
"error": {"type": "String"}
}
},
"neighbors_repeatersNeighbours": "Repeaters Neighbours",
"neighbors_noData": "No neighbours data available.",
"neighbors_unknownContact": "Unknown {pubkey}",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {"type": "String"}
}
},
"neighbors_heardAgo": "Heard: {time} ago",
"@neighbors_heardAgo": {
"placeholders": {
"time": {"type": "String"}
}
},
"channelPath_title": "Packet Path",
"channelPath_viewMap": "View map",
"channelPath_otherObservedPaths": "Other Observed Paths",
@@ -1129,6 +1175,118 @@
},
"channelPath_noHopDetailsAvailable": "No hop details available for this packet.",
"channelPath_unknownRepeater": "Unknown Repeater",
"community_title": "Community",
"community_create": "Create Community",
"community_createDesc": "Create a new community and share via QR code.",
"community_join": "Join",
"community_joinTitle": "Join Community",
"community_joinConfirmation": "Do you want to join the community \"{name}\"?",
"@community_joinConfirmation": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_scanQr": "Scan Community QR",
"community_scanInstructions": "Point the camera at a community QR code",
"community_showQr": "Show QR Code",
"community_publicChannel": "Community Public",
"community_hashtagChannel": "Community Hashtag",
"community_name": "Community Name",
"community_enterName": "Enter community name",
"community_created": "Community \"{name}\" created",
"@community_created": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_joined": "Joined community \"{name}\"",
"@community_joined": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_qrTitle": "Share Community",
"community_qrInstructions": "Scan this QR code to join \"{name}\"",
"@community_qrInstructions": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_hashtagPrivacyHint": "Community hashtag channels are only joinable by members of the community",
"community_invalidQrCode": "Invalid community QR code",
"community_alreadyMember": "Already a Member",
"community_alreadyMemberMessage": "You are already a member of \"{name}\".",
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_addPublicChannel": "Add Community Public Channel",
"community_addPublicChannelHint": "Automatically add the public channel for this community",
"community_noCommunities": "No communities joined yet",
"community_scanOrCreate": "Scan a QR code or create a community to get started",
"community_manageCommunities": "Manage Communities",
"community_delete": "Leave Community",
"community_deleteConfirm": "Leave \"{name}\"?",
"@community_deleteConfirm": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_deleteChannelsWarning": "This will also delete {count} channel(s) and their messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Left community \"{name}\"",
"@community_deleted": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_regenerateSecret": "Regenerate Secret",
"community_regenerateSecretConfirm": "Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.",
"@community_regenerateSecretConfirm": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_regenerate": "Regenerate",
"community_secretRegenerated": "Secret regenerated for \"{name}\"",
"@community_secretRegenerated": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_updateSecret": "Update Secret",
"community_secretUpdated": "Secret updated for \"{name}\"",
"@community_secretUpdated": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_scanToUpdateSecret": "Scan the new QR code to update the secret for \"{name}\"",
"@community_scanToUpdateSecret": {
"placeholders": {
"name": {"type": "String"}
}
},
"community_addHashtagChannel": "Add Community Hashtag",
"community_addHashtagChannelDesc": "Add a hashtag channel for this community",
"community_selectCommunity": "Select Community",
"community_regularHashtag": "Regular Hashtag",
"community_regularHashtagDesc": "Public hashtag (anyone can join)",
"community_communityHashtag": "Community Hashtag",
"community_communityHashtagDesc": "Private to community members",
"community_forCommunity": "For {name}",
"@community_forCommunity": {
"placeholders": {
"name": {"type": "String"}
}
},
"listFilter_tooltip": "Filter and sort",
"listFilter_sortBy": "Sort by",
"listFilter_latestMessages": "Latest messages",
+150 -1
View File
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.",
"common_reload": "Recargar",
"common_clear": "Borrar",
"path_currentPath": "Ruta actual: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de la sala",
"listFilter_unreadOnly": "Solo sin leer",
"listFilter_newGroup": "Nuevo grupo"
"listFilter_newGroup": "Nuevo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vecinos",
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
"neighbors_receivedData": "Recibidas Datos de Vecinos",
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
"neighbors_noData": "No hay datos de vecinos disponibles.",
"channels_joinPrivateChannel": "Únete a un Canal Privado",
"channels_createPrivateChannel": "Crear un Canal Privado",
"channels_createPrivateChannelDesc": "Cifrado con una clave secreta.",
"channels_joinPrivateChannelDesc": "Introducir manualmente una clave secreta.",
"channels_joinPublicChannel": "Únete al Canal Público",
"channels_joinPublicChannelDesc": "Cualquiera puede unirse a este canal.",
"channels_joinHashtagChannel": "Únete a un Canal con Hashtag",
"channels_joinHashtagChannelDesc": "Cualquiera puede unirse a los canales de hashtag.",
"channels_scanQrCode": "Escanear un Código QR",
"channels_scanQrCodeComingSoon": "Próximamente",
"channels_enterHashtag": "Introducir hashtag",
"channels_hashtagHint": "ej. #equipo",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clave pública desconocida {pubkey}",
"neighbors_heardAgo": "Escuchado: {time} hace atrás",
"settings_locationGPSEnable": "Habilitar GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Crear Comunidad",
"community_createDesc": "Crear una nueva comunidad y compartir a través de código QR.",
"community_title": "Comunidad",
"community_join": "Únete",
"community_joinTitle": "Únete a la comunidad",
"community_joinConfirmation": "¿Quieres unirte a la comunidad \"{name}\"?",
"community_scanQr": "Escanear Código QR de la Comunidad",
"community_scanInstructions": "Apunte la cámara a un código QR de la comunidad",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidad Pública",
"community_hashtagChannel": "Hashtag de la Comunidad",
"community_name": "Nombre de la comunidad",
"common_ok": "De acuerdo",
"community_enterName": "Introducir nombre de comunidad",
"community_created": "Comunidad \"{name}\" creada",
"community_joined": "Se unió a la comunidad \"{name}\"",
"community_qrTitle": "Compartir Comunidad",
"community_qrInstructions": "Escanear este código QR para unirte a {name}",
"community_hashtagPrivacyHint": "Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad",
"community_invalidQrCode": "Código QR de comunidad no válido",
"community_alreadyMember": "Ya eres Miembro",
"community_alreadyMemberMessage": "Ya eres miembro de \"{name}\".",
"community_addPublicChannel": "Añadir Canal Público de la Comunidad",
"community_addPublicChannelHint": "Añade automáticamente el canal público para esta comunidad.",
"community_noCommunities": "Aún no se han unido comunidades.",
"community_scanOrCreate": "Escanear un código QR o crear una comunidad para comenzar",
"community_manageCommunities": "Gestionar Comunidades",
"community_delete": "Salir de la Comunidad",
"community_deleteConfirm": "¿Salir de \"{name}\"?",
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Has salido de la comunidad \"{name}\"",
"community_addHashtagChannel": "Añadir Hashtag de la Comunidad",
"community_addHashtagChannelDesc": "Añadir un canal con hashtag para esta comunidad",
"community_selectCommunity": "Seleccionar Comunidad",
"community_regularHashtag": "Etiqueta de Hashtag Regular",
"community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)",
"community_communityHashtag": "Hashtag de la Comunidad",
"community_communityHashtagDesc": "Exclusivo para miembros de la comunidad",
"community_forCommunity": "Para {name}"
}
+177 -28
View File
@@ -91,12 +91,12 @@
"settings_latitude": "Latitude",
"settings_longitude": "Longitude",
"settings_privacyMode": "Mode de confidentialité",
"settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les publicités",
"settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les publicités.",
"settings_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces",
"settings_privacyModeToggle": "Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.",
"settings_privacyModeEnabled": "Mode de confidentialité activé",
"settings_privacyModeDisabled": "Mode de confidentialité désactivé",
"settings_actions": "Actions",
"settings_sendAdvertisement": "Envoyer la publicité",
"settings_sendAdvertisement": "S'annoncer",
"settings_sendAdvertisementSubtitle": "Présence diffusée maintenant",
"settings_advertisementSent": "Annonce envoyée",
"settings_syncTime": "Temps de synchronisation",
@@ -176,7 +176,7 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Notifications",
"appSettings_enableNotifications": "Activer les Notifications",
"appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les publicités",
"appSettings_enableNotificationsSubtitle": "Recevoir des notifications pour les messages et les annonces",
"appSettings_notificationPermissionDenied": "Permission de notification refusée",
"appSettings_notificationsEnabled": "Notifications activées",
"appSettings_notificationsDisabled": "Notifications désactivées",
@@ -184,7 +184,7 @@
"appSettings_messageNotificationsSubtitle": "Afficher une notification lors de la réception de nouveaux messages",
"appSettings_channelMessageNotifications": "Notifications des Messages de Canal",
"appSettings_channelMessageNotificationsSubtitle": "Afficher une notification lors de la réception des messages de canal",
"appSettings_advertisementNotifications": "Notifications publicitaires",
"appSettings_advertisementNotifications": "Notifications d'annonces",
"appSettings_advertisementNotificationsSubtitle": "Afficher une notification lors de la découverte de nouveaux nœuds",
"appSettings_messaging": "Messagerie",
"appSettings_clearPathOnMaxRetry": "Effacer le chemin sur Max Retry",
@@ -192,7 +192,7 @@
"appSettings_pathsWillBeCleared": "Les chemins seront effacés après 5 tentatives infructueuses.",
"appSettings_pathsWillNotBeCleared": "Les chemins ne seront pas effacés automatiquement.",
"appSettings_autoRouteRotation": "Rotation de l'itinéraire automatique",
"appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode inondation",
"appSettings_autoRouteRotationSubtitle": "Alterner entre les meilleurs chemins et le mode d'envoi sur tout le réseau (flood)",
"appSettings_autoRouteRotationEnabled": "Rotation du routage automatique activée",
"appSettings_autoRouteRotationDisabled": "Rotation de l'itinéraire automatique désactivée",
"appSettings_battery": "Batterie",
@@ -539,7 +539,7 @@
"chat_pathManagement": "Gestion des chemins",
"chat_routingMode": "Mode de routage",
"chat_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"chat_forceFloodMode": "Mode Inondation Forcée",
"chat_forceFloodMode": "Mode tout le réseau forcé",
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
"chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
"chat_hopSingular": "Sautez",
@@ -562,7 +562,7 @@
"chat_clearPathSubtitle": "Forcer la redécouverte lors de la prochaine envoi",
"chat_pathCleared": "Le chemin est dégagé. Le prochain message redécouvrira le tracé.",
"chat_floodModeSubtitle": "Utiliser le commutateur de routage dans la barre d'application",
"chat_floodModeEnabled": "Le mode inondation est activé. Réactiver via l'icône de routage dans la barre d'outils.",
"chat_floodModeEnabled": "Le mode envoi à tout le réseau est activé. Changer via l'icône de routage dans la barre d'outils.",
"chat_fullPath": "Chemin complet",
"chat_pathDetailsNotAvailable": "Les détails du chemin ne sont pas encore disponibles. Essayez d'envoyer un message pour rafraîchir.",
"chat_pathSetHops": "Chemin défini : {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -583,7 +583,7 @@
"chat_path": "Chemin",
"chat_publicKey": "Clé Publique",
"chat_compressOutgoingMessages": "Compresser les messages sortants",
"chat_floodForced": "Inondation (forcée)",
"chat_floodForced": "Tout le réseau (forcée)",
"chat_directForced": "Direct (forcé)",
"chat_hopsForced": "{count} sauts (forcés)",
"@chat_hopsForced": {
@@ -593,7 +593,7 @@
}
}
},
"chat_floodAuto": "Inondation (auto)",
"chat_floodAuto": "Tout le réseau (auto)",
"chat_direct": "Afficher",
"chat_poiShared": "Point d'intérêt Partagé",
"chat_unread": "Non lu : {count}",
@@ -799,7 +799,7 @@
"login_routing": "Redirection",
"login_routingMode": "Mode de routage",
"login_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"login_forceFloodMode": "Mode Inondation Forcée",
"login_forceFloodMode": "Mode tout le réseau forcé",
"login_managePaths": "Gérer les chemins",
"login_login": "Connexion",
"login_attempt": "Essayer {current}/{max}",
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.",
"common_reload": "Recharger",
"common_clear": "Effacer",
"path_currentPath": "Chemin actuel : {path}",
@@ -871,7 +872,7 @@
"repeater_statusTitle": "État du répétiteur",
"repeater_routingMode": "Mode de routage",
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
"repeater_forceFloodMode": "Mode de submersion forcée",
"repeater_forceFloodMode": "Mode tout le réseau forcé",
"repeater_pathManagement": "Gestion des chemins",
"repeater_refresh": "Rafraîchir",
"repeater_statusRequestTimeout": "Demande de statut délai dépassé.",
@@ -916,7 +917,7 @@
}
}
},
"repeater_packetTxTotal": "Total : {total}, Inondation : {flood}, Direct : {direct}",
"repeater_packetTxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -930,7 +931,7 @@
}
}
},
"repeater_packetRxTotal": "Total : {total}, Inondation : {flood}, Direct : {direct}",
"repeater_packetRxTotal": "Total : {total}, Tout le réseau : {flood}, Direct : {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -944,7 +945,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Inondation : {flood}, Direct : {direct}",
"repeater_duplicatesFloodDirect": "Tout le réseau : {flood}, Direct : {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -990,9 +991,9 @@
"repeater_guestAccess": "Accès Invité",
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
"repeater_privacyMode": "Mode de confidentialité",
"repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les publicités",
"repeater_advertisementSettings": "Paramètres de Publicité",
"repeater_localAdvertInterval": "Intervalle Publicité Locale",
"repeater_privacyModeSubtitle": "Cacher le nom/l'emplacement dans les annonces",
"repeater_advertisementSettings": "Paramètres d'annonces",
"repeater_localAdvertInterval": "Intervalle des annonces Locale (0 saut)",
"repeater_localAdvertIntervalMinutes": "{minutes} minutes",
"@repeater_localAdvertIntervalMinutes": {
"placeholders": {
@@ -1001,7 +1002,7 @@
}
}
},
"repeater_floodAdvertInterval": "Intervalle de Publicité Inondation",
"repeater_floodAdvertInterval": "Intervalle des annonces à tout le réseau (flood)",
"repeater_floodAdvertIntervalHours": "{hours} heures",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1010,7 +1011,7 @@
}
}
},
"repeater_encryptedAdvertInterval": "Intervalle publicitaire crypté",
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
"repeater_dangerZone": "Zone d'alerte",
"repeater_rebootRepeater": "Redémarrer Répéteur",
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur",
@@ -1055,7 +1056,7 @@
"repeater_refreshPacketForwarding": "Rafraîchir le routage des paquets",
"repeater_refreshGuestAccess": "Rafraîchir l'accès invité",
"repeater_refreshPrivacyMode": "Rafraîchir le Mode Confidentialité",
"repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres de la Publicité",
"repeater_refreshAdvertisementSettings": "Rafraîchir les Paramètres des annonces",
"repeater_refreshed": "{label} rafraîchi",
"@repeater_refreshed": {
"placeholders": {
@@ -1098,7 +1099,7 @@
"repeater_cliQuickVersion": "Version",
"repeater_cliQuickAdvertise": "Publier",
"repeater_cliQuickClock": "Horloge",
"repeater_cliHelpAdvert": "Envoie un paquet publicitaire",
"repeater_cliHelpAdvert": "Envoie un paquet d'annonce",
"repeater_cliHelpReboot": "Redémarre l'appareil. (Note, vous risquez d'obtenir 'Timeout' ce qui est normal)",
"repeater_cliHelpClock": "Affiche l'heure actuelle par l'horloge de chaque appareil.",
"repeater_cliHelpPassword": "Définit un nouveau mot de passe administrateur pour l'appareil.",
@@ -1115,12 +1116,12 @@
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.",
"repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.",
"repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")",
"repeater_cliHelpSetName": "Définit le nom de la publicité.",
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
"repeater_cliHelpSetLat": "Définit la latitude de la carte des annonces. (degrés décimaux)",
"repeater_cliHelpSetLon": "Définit la longitude de la carte de l'annonce. (degrés décimaux)",
"repeater_cliHelpSetRadio": "Définit complètement de nouveaux paramètres de radio et les enregistre dans les préférences. Nécessite une commande \"redémarrage\" pour les appliquer.",
"repeater_cliHelpSetRxDelay": "Paramètres (expérimental) de base pour appliquer un léger délai aux paquets reçus, en fonction de la force du signal/score. Définir sur 0 pour désactiver.",
"repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode inondation pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).",
"repeater_cliHelpSetTxDelay": "Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).",
"repeater_cliHelpSetDirectTxDelay": "Identique à txdelay, mais pour appliquer un délai aléatoire au transfert des paquets en mode direct.",
"repeater_cliHelpSetBridgeEnabled": "Activer/Désactiver le pont.",
"repeater_cliHelpSetBridgeDelay": "Définir le délai avant de renvoyer les paquets.",
@@ -1134,9 +1135,9 @@
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
"repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.",
"repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des publicités sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations de débordement actuelles.",
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
"repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
"repeater_cliHelpRegionGet": "Recherche la région avec le préfixe de nom donné (ou \"\" pour l'étendue globale). Répond avec \"-> nom-de-région (nom-parent) 'F'\"",
"repeater_cliHelpRegionPut": "Ajoute ou met à jour une définition de région avec le nom donné.",
@@ -1271,7 +1272,7 @@
}
},
"channelPath_unknownPath": "Inconnu",
"channelPath_floodPath": "Inondation",
"channelPath_floodPath": "Tout le réseau",
"channelPath_directPath": "Afficher",
"channelPath_observedZeroOf": "0 de {total} sauts",
"@channelPath_observedZeroOf": {
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Répéteurs",
"listFilter_roomServers": "Serveurs de pièce",
"listFilter_unreadOnly": "Messages non lus seulement",
"listFilter_newGroup": "Nouvelle groupe"
"listFilter_newGroup": "Nouvelle groupe",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Voisins",
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
"neighbors_receivedData": "Données des voisins reçues",
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
"neighbors_errorLoading": "Erreur lors du chargement des voisins : {error}",
"neighbors_repeatersNeighbours": "Répéteurs Voisins",
"neighbors_noData": "Aucune donnée concernant les voisins disponible.",
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
"channels_createPrivateChannel": "Créer un Canal Privé",
"channels_joinPrivateChannelDesc": "Entrer manuellement une clé secrète.",
"channels_joinPublicChannel": "Rejoindre le canal public",
"channels_joinPublicChannelDesc": "Tout le monde peut rejoindre ce canal.",
"channels_joinHashtagChannel": "Rejoindre un Canal Hashtag",
"channels_joinHashtagChannelDesc": "N'importe qui peut rejoindre les canaux #hashtag.",
"channels_scanQrCode": "Scanner un code QR",
"channels_scanQrCodeComingSoon": "Bientôt disponible",
"channels_enterHashtag": "Entrez le hashtag",
"channels_hashtagHint": "ex. #équipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Clé publique inconnue {pubkey}",
"neighbors_heardAgo": "Écouté : {time} auparavant",
"settings_locationGPSEnable": "Habilita GPS",
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
"settings_locationIntervalSec": "Intervalo pour GPS (Segundos)",
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
"contacts_manageRoom": "Gestionar Servidor de Habitación",
"room_management": "Administración del Servidor de Habitación",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Communauté",
"community_create": "Créer une Communauté",
"community_createDesc": "Créer une nouvelle communauté et la partager via QR code.",
"community_join": "Rejoindre",
"community_joinTitle": "Rejoindre la communauté",
"community_joinConfirmation": "Souhaitez-vous rejoindre la communauté \"{name}\" ?",
"community_scanQr": "Scanner la communauté QR",
"community_scanInstructions": "Pointez l'appareil photo vers un code QR communautaire.",
"community_showQr": "Afficher le QR Code",
"community_publicChannel": "Communauté Publique",
"community_hashtagChannel": "Hashtag Communauté",
"community_name": "Nom de la communauté",
"community_enterName": "Entrez le nom de la communauté",
"community_created": "Communauté \"{name}\" créée",
"community_joined": "Rejoint la communauté \"{name}\"",
"community_qrTitle": "Partager Communauté",
"community_qrInstructions": "Scanner ce QR code pour rejoindre {name}",
"community_hashtagPrivacyHint": "Les canaux hashtag de la communauté ne sont accessibles qu'aux membres de la communauté",
"community_invalidQrCode": "Code QR de communauté non valide",
"community_alreadyMember": "Déjà membre",
"community_alreadyMemberMessage": "Vous êtes déjà membre de \"{name}\".",
"community_addPublicChannel": "Ajouter un Canal Public de la Communauté",
"community_addPublicChannelHint": "Ajouter automatiquement le canal public pour cette communauté",
"community_noCommunities": "Aucun groupe n'a été rejoint pour le moment.",
"community_scanOrCreate": "Scanner un code QR ou créer une communauté pour commencer",
"community_manageCommunities": "Gérer les Communautés",
"community_delete": "Quitter la communauté",
"community_deleteConfirm": "Quitter \"{name}\" ?",
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Communauté \"{name}\" quittée",
"community_addHashtagChannel": "Ajouter un Hashtag Communauté",
"community_addHashtagChannelDesc": "Ajouter un canal hachage pour cette communauté",
"community_selectCommunity": "Sélectionner Communauté",
"community_regularHashtag": "Hashtag régulier",
"community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)",
"community_communityHashtag": "Hashtag de la communauté",
"community_communityHashtagDesc": "Exclusif aux membres de la communauté",
"community_forCommunity": "Pour {name}"
}
+150 -1
View File
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.",
"common_reload": "Ricaricare",
"common_clear": "Cancella",
"path_currentPath": "Percorso corrente: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Ripetitori",
"listFilter_roomServers": "Server della stanza",
"listFilter_unreadOnly": "Solo non letto",
"listFilter_newGroup": "Nuovo gruppo"
"listFilter_newGroup": "Nuovo gruppo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vicini",
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
"neighbors_receivedData": "Ricevute dati vicini",
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
"channels_createPrivateChannel": "Crea un Canale Privato",
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
"channels_joinPrivateChannel": "Unisciti a un Canale Privato",
"channels_joinPrivateChannelDesc": "Inserire manualmente una chiave segreta.",
"channels_joinPublicChannel": "Unisciti al Canale Pubblico",
"channels_joinPublicChannelDesc": "Chiunque può unirsi a questo canale.",
"channels_joinHashtagChannel": "Unisciti a un Canale con Hashtag",
"channels_joinHashtagChannelDesc": "Chiunque può unirsi ai canali hashtag.",
"channels_scanQrCode": "Scansiona un codice QR",
"channels_scanQrCodeComingSoon": "Arriverà presto",
"channels_enterHashtag": "Inserisci hashtag",
"channels_hashtagHint": "es. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Sentito: {time} fa",
"neighbors_unknownContact": "Chiave pubblica sconosciuta {pubkey}",
"settings_locationGPSEnable": "Abilita GPS",
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
"contacts_manageRoom": "Gestisci Server Camera",
"room_management": "Gestione del Server di Camera",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"common_ok": "OK",
"community_title": "Comunità",
"community_create": "Crea Comunità",
"community_createDesc": "Crea una nuova comunità e condividila tramite codice QR.",
"community_join": "Unisciti",
"community_joinTitle": "Unisciti alla Community",
"community_joinConfirmation": "Vuoi unirti alla community \"{name}\"?",
"community_scanQr": "Scansiona il QR Code della Community",
"community_scanInstructions": "Punta la fotocamera su un codice QR della comunità",
"community_showQr": "Mostra il codice QR",
"community_publicChannel": "Comunità Pubblica",
"community_hashtagChannel": "Hashtag della Comunità",
"community_name": "Nome della Comunità",
"community_enterName": "Inserisci il nome della comunità",
"community_created": "Comunità \"{name}\" creata",
"community_joined": "Unito alla comunità \"{name}\"",
"community_qrTitle": "Condividi Comunità",
"community_qrInstructions": "Scansiona questo codice QR per unirti a {name}",
"community_hashtagPrivacyHint": "I canali hashtag della community sono accessibili solo ai membri della community",
"community_invalidQrCode": "Codice QR della community non valido",
"community_alreadyMember": "Già membro",
"community_alreadyMemberMessage": "Sei già un membro di \"{name}\".",
"community_addPublicChannel": "Aggiungi Canale Pubblico della Comunità",
"community_addPublicChannelHint": "Aggiungi automaticamente il canale pubblico per questa community",
"community_noCommunities": "Nessun gruppo aggiunto finora",
"community_scanOrCreate": "Scansiona un codice QR o crea una community per iniziare.",
"community_manageCommunities": "Gestisci Comunità",
"community_delete": "Lascia la Comunità",
"community_deleteConfirm": "Uscire da \"{name}\"?",
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Hai lasciato la comunità \"{name}\"",
"community_addHashtagChannel": "Aggiungi Hashtag della Community",
"community_addHashtagChannelDesc": "Aggiungi un canale con hashtag per questa community",
"community_selectCommunity": "Seleziona Comunità",
"community_regularHashtag": "Hashtag regolare",
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
"community_communityHashtag": "Hashtag della Comunità",
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
"community_forCommunity": "Per {name}"
}
+447 -3
View File
@@ -150,6 +150,12 @@ abstract class AppLocalizations {
/// **'Cancel'**
String get common_cancel;
/// No description provided for @common_ok.
///
/// In en, this message translates to:
/// **'OK'**
String get common_ok;
/// No description provided for @common_connect.
///
/// In en, this message translates to:
@@ -465,7 +471,7 @@ abstract class AppLocalizations {
/// No description provided for @settings_locationUpdated.
///
/// In en, this message translates to:
/// **'Location updated'**
/// **'Location and GPS settings updated'**
String get settings_locationUpdated;
/// No description provided for @settings_locationBothRequired.
@@ -480,6 +486,30 @@ abstract class AppLocalizations {
/// **'Invalid latitude or longitude.'**
String get settings_locationInvalid;
/// No description provided for @settings_locationGPSEnable.
///
/// In en, this message translates to:
/// **'GPS Enable'**
String get settings_locationGPSEnable;
/// No description provided for @settings_locationGPSEnableSubtitle.
///
/// In en, this message translates to:
/// **'Enables GPS to automatically update location.'**
String get settings_locationGPSEnableSubtitle;
/// No description provided for @settings_locationIntervalSec.
///
/// In en, this message translates to:
/// **'Interval for GPS (Seconds)'**
String get settings_locationIntervalSec;
/// No description provided for @settings_locationIntervalInvalid.
///
/// In en, this message translates to:
/// **'Interval must be at least 60 seconds, and less than 86400 seconds.'**
String get settings_locationIntervalInvalid;
/// No description provided for @settings_latitude.
///
/// In en, this message translates to:
@@ -1284,10 +1314,16 @@ abstract class AppLocalizations {
/// **'Manage Repeater'**
String get contacts_manageRepeater;
/// No description provided for @contacts_manageRoom.
///
/// In en, this message translates to:
/// **'Manage Room Server'**
String get contacts_manageRoom;
/// No description provided for @contacts_roomLogin.
///
/// In en, this message translates to:
/// **'Room Login'**
/// **'Room Server Login'**
String get contacts_roomLogin;
/// No description provided for @contacts_openChat.
@@ -1596,6 +1632,78 @@ abstract class AppLocalizations {
/// **'Unread'**
String get channels_sortUnread;
/// No description provided for @channels_createPrivateChannel.
///
/// In en, this message translates to:
/// **'Create a Private Channel'**
String get channels_createPrivateChannel;
/// No description provided for @channels_createPrivateChannelDesc.
///
/// In en, this message translates to:
/// **'Secured with a secret key.'**
String get channels_createPrivateChannelDesc;
/// No description provided for @channels_joinPrivateChannel.
///
/// In en, this message translates to:
/// **'Join a Private Channel'**
String get channels_joinPrivateChannel;
/// No description provided for @channels_joinPrivateChannelDesc.
///
/// In en, this message translates to:
/// **'Manually enter a secret key.'**
String get channels_joinPrivateChannelDesc;
/// No description provided for @channels_joinPublicChannel.
///
/// In en, this message translates to:
/// **'Join the Public Channel'**
String get channels_joinPublicChannel;
/// No description provided for @channels_joinPublicChannelDesc.
///
/// In en, this message translates to:
/// **'Anyone can join this channel.'**
String get channels_joinPublicChannelDesc;
/// No description provided for @channels_joinHashtagChannel.
///
/// In en, this message translates to:
/// **'Join a Hashtag Channel'**
String get channels_joinHashtagChannel;
/// No description provided for @channels_joinHashtagChannelDesc.
///
/// In en, this message translates to:
/// **'Anyone can join hashtag channels.'**
String get channels_joinHashtagChannelDesc;
/// No description provided for @channels_scanQrCode.
///
/// In en, this message translates to:
/// **'Scan a QR Code'**
String get channels_scanQrCode;
/// No description provided for @channels_scanQrCodeComingSoon.
///
/// In en, this message translates to:
/// **'Coming soon'**
String get channels_scanQrCodeComingSoon;
/// No description provided for @channels_enterHashtag.
///
/// In en, this message translates to:
/// **'Enter hashtag'**
String get channels_enterHashtag;
/// No description provided for @channels_hashtagHint.
///
/// In en, this message translates to:
/// **'e.g. #team'**
String get channels_hashtagHint;
/// No description provided for @chat_noMessages.
///
/// In en, this message translates to:
@@ -2600,7 +2708,7 @@ abstract class AppLocalizations {
/// No description provided for @login_roomLogin.
///
/// In en, this message translates to:
/// **'Room Login'**
/// **'Room Server Login'**
String get login_roomLogin;
/// No description provided for @login_password.
@@ -2687,6 +2795,12 @@ abstract class AppLocalizations {
/// **'Login failed: {error}'**
String login_failed(String error);
/// No description provided for @login_failedMessage.
///
/// In en, this message translates to:
/// **'Login failed. Either the password is incorrect or the repeater is unreachable.'**
String get login_failedMessage;
/// No description provided for @common_reload.
///
/// In en, this message translates to:
@@ -2789,6 +2903,12 @@ abstract class AppLocalizations {
/// **'Repeater Management'**
String get repeater_management;
/// No description provided for @room_management.
///
/// In en, this message translates to:
/// **'Room Server Management'**
String get room_management;
/// No description provided for @repeater_managementTools.
///
/// In en, this message translates to:
@@ -2831,6 +2951,18 @@ abstract class AppLocalizations {
/// **'Send commands to the repeater'**
String get repeater_cliSubtitle;
/// No description provided for @repeater_neighbours.
///
/// In en, this message translates to:
/// **'Neighbors'**
String get repeater_neighbours;
/// No description provided for @repeater_neighboursSubtitle.
///
/// In en, this message translates to:
/// **'View zero hop neighbors.'**
String get repeater_neighboursSubtitle;
/// No description provided for @repeater_settings.
///
/// In en, this message translates to:
@@ -3970,6 +4102,48 @@ abstract class AppLocalizations {
/// **'{celsius}°C / {fahrenheit}°F'**
String telemetry_temperatureValue(String celsius, String fahrenheit);
/// No description provided for @neighbors_receivedData.
///
/// In en, this message translates to:
/// **'Received Neighbours Data'**
String get neighbors_receivedData;
/// No description provided for @neighbors_requestTimedOut.
///
/// In en, this message translates to:
/// **'Neighbours request timed out.'**
String get neighbors_requestTimedOut;
/// No description provided for @neighbors_errorLoading.
///
/// In en, this message translates to:
/// **'Error loading neighbors: {error}'**
String neighbors_errorLoading(String error);
/// No description provided for @neighbors_repeatersNeighbours.
///
/// In en, this message translates to:
/// **'Repeaters Neighbours'**
String get neighbors_repeatersNeighbours;
/// No description provided for @neighbors_noData.
///
/// In en, this message translates to:
/// **'No neighbours data available.'**
String get neighbors_noData;
/// No description provided for @neighbors_unknownContact.
///
/// In en, this message translates to:
/// **'Unknown {pubkey}'**
String neighbors_unknownContact(String pubkey);
/// No description provided for @neighbors_heardAgo.
///
/// In en, this message translates to:
/// **'Heard: {time} ago'**
String neighbors_heardAgo(String time);
/// No description provided for @channelPath_title.
///
/// In en, this message translates to:
@@ -4138,6 +4312,276 @@ abstract class AppLocalizations {
/// **'Unknown Repeater'**
String get channelPath_unknownRepeater;
/// No description provided for @community_title.
///
/// In en, this message translates to:
/// **'Community'**
String get community_title;
/// No description provided for @community_create.
///
/// In en, this message translates to:
/// **'Create Community'**
String get community_create;
/// No description provided for @community_createDesc.
///
/// In en, this message translates to:
/// **'Create a new community and share via QR code.'**
String get community_createDesc;
/// No description provided for @community_join.
///
/// In en, this message translates to:
/// **'Join'**
String get community_join;
/// No description provided for @community_joinTitle.
///
/// In en, this message translates to:
/// **'Join Community'**
String get community_joinTitle;
/// No description provided for @community_joinConfirmation.
///
/// In en, this message translates to:
/// **'Do you want to join the community \"{name}\"?'**
String community_joinConfirmation(String name);
/// No description provided for @community_scanQr.
///
/// In en, this message translates to:
/// **'Scan Community QR'**
String get community_scanQr;
/// No description provided for @community_scanInstructions.
///
/// In en, this message translates to:
/// **'Point the camera at a community QR code'**
String get community_scanInstructions;
/// No description provided for @community_showQr.
///
/// In en, this message translates to:
/// **'Show QR Code'**
String get community_showQr;
/// No description provided for @community_publicChannel.
///
/// In en, this message translates to:
/// **'Community Public'**
String get community_publicChannel;
/// No description provided for @community_hashtagChannel.
///
/// In en, this message translates to:
/// **'Community Hashtag'**
String get community_hashtagChannel;
/// No description provided for @community_name.
///
/// In en, this message translates to:
/// **'Community Name'**
String get community_name;
/// No description provided for @community_enterName.
///
/// In en, this message translates to:
/// **'Enter community name'**
String get community_enterName;
/// No description provided for @community_created.
///
/// In en, this message translates to:
/// **'Community \"{name}\" created'**
String community_created(String name);
/// No description provided for @community_joined.
///
/// In en, this message translates to:
/// **'Joined community \"{name}\"'**
String community_joined(String name);
/// No description provided for @community_qrTitle.
///
/// In en, this message translates to:
/// **'Share Community'**
String get community_qrTitle;
/// No description provided for @community_qrInstructions.
///
/// In en, this message translates to:
/// **'Scan this QR code to join \"{name}\"'**
String community_qrInstructions(String name);
/// No description provided for @community_hashtagPrivacyHint.
///
/// In en, this message translates to:
/// **'Community hashtag channels are only joinable by members of the community'**
String get community_hashtagPrivacyHint;
/// No description provided for @community_invalidQrCode.
///
/// In en, this message translates to:
/// **'Invalid community QR code'**
String get community_invalidQrCode;
/// No description provided for @community_alreadyMember.
///
/// In en, this message translates to:
/// **'Already a Member'**
String get community_alreadyMember;
/// No description provided for @community_alreadyMemberMessage.
///
/// In en, this message translates to:
/// **'You are already a member of \"{name}\".'**
String community_alreadyMemberMessage(String name);
/// No description provided for @community_addPublicChannel.
///
/// In en, this message translates to:
/// **'Add Community Public Channel'**
String get community_addPublicChannel;
/// No description provided for @community_addPublicChannelHint.
///
/// In en, this message translates to:
/// **'Automatically add the public channel for this community'**
String get community_addPublicChannelHint;
/// No description provided for @community_noCommunities.
///
/// In en, this message translates to:
/// **'No communities joined yet'**
String get community_noCommunities;
/// No description provided for @community_scanOrCreate.
///
/// In en, this message translates to:
/// **'Scan a QR code or create a community to get started'**
String get community_scanOrCreate;
/// No description provided for @community_manageCommunities.
///
/// In en, this message translates to:
/// **'Manage Communities'**
String get community_manageCommunities;
/// No description provided for @community_delete.
///
/// In en, this message translates to:
/// **'Leave Community'**
String get community_delete;
/// No description provided for @community_deleteConfirm.
///
/// In en, this message translates to:
/// **'Leave \"{name}\"?'**
String community_deleteConfirm(String name);
/// No description provided for @community_deleteChannelsWarning.
///
/// In en, this message translates to:
/// **'This will also delete {count} channel(s) and their messages.'**
String community_deleteChannelsWarning(int count);
/// No description provided for @community_deleted.
///
/// In en, this message translates to:
/// **'Left community \"{name}\"'**
String community_deleted(String name);
/// No description provided for @community_regenerateSecret.
///
/// In en, this message translates to:
/// **'Regenerate Secret'**
String get community_regenerateSecret;
/// No description provided for @community_regenerateSecretConfirm.
///
/// In en, this message translates to:
/// **'Regenerate the secret key for \"{name}\"? All members will need to scan the new QR code to continue communicating.'**
String community_regenerateSecretConfirm(String name);
/// No description provided for @community_regenerate.
///
/// In en, this message translates to:
/// **'Regenerate'**
String get community_regenerate;
/// No description provided for @community_secretRegenerated.
///
/// In en, this message translates to:
/// **'Secret regenerated for \"{name}\"'**
String community_secretRegenerated(String name);
/// No description provided for @community_updateSecret.
///
/// In en, this message translates to:
/// **'Update Secret'**
String get community_updateSecret;
/// No description provided for @community_secretUpdated.
///
/// In en, this message translates to:
/// **'Secret updated for \"{name}\"'**
String community_secretUpdated(String name);
/// No description provided for @community_scanToUpdateSecret.
///
/// In en, this message translates to:
/// **'Scan the new QR code to update the secret for \"{name}\"'**
String community_scanToUpdateSecret(String name);
/// No description provided for @community_addHashtagChannel.
///
/// In en, this message translates to:
/// **'Add Community Hashtag'**
String get community_addHashtagChannel;
/// No description provided for @community_addHashtagChannelDesc.
///
/// In en, this message translates to:
/// **'Add a hashtag channel for this community'**
String get community_addHashtagChannelDesc;
/// No description provided for @community_selectCommunity.
///
/// In en, this message translates to:
/// **'Select Community'**
String get community_selectCommunity;
/// No description provided for @community_regularHashtag.
///
/// In en, this message translates to:
/// **'Regular Hashtag'**
String get community_regularHashtag;
/// No description provided for @community_regularHashtagDesc.
///
/// In en, this message translates to:
/// **'Public hashtag (anyone can join)'**
String get community_regularHashtagDesc;
/// No description provided for @community_communityHashtag.
///
/// In en, this message translates to:
/// **'Community Hashtag'**
String get community_communityHashtag;
/// No description provided for @community_communityHashtagDesc.
///
/// In en, this message translates to:
/// **'Private to community members'**
String get community_communityHashtagDesc;
/// No description provided for @community_forCommunity.
///
/// In en, this message translates to:
/// **'For {name}'**
String community_forCommunity(String name);
/// No description provided for @listFilter_tooltip.
///
/// In en, this message translates to:
+268
View File
@@ -23,6 +23,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get common_cancel => 'Отказ';
@override
String get common_ok => 'Добре';
@override
String get common_connect => 'Свържи се';
@@ -201,6 +204,20 @@ class AppLocalizationsBg extends AppLocalizations {
@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 => 'Широчина';
@@ -650,6 +667,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Управление на Повтарящ се Елемент';
@override
String get contacts_manageRoom => 'Управление на сървър за стая';
@override
String get contacts_roomLogin => 'Вход в стаята';
@@ -830,6 +850,45 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get channels_sortUnread => 'Непрочетено';
@override
String get channels_createPrivateChannel => 'Създай Частен Канал';
@override
String get channels_createPrivateChannelDesc => 'Защитено с таен ключ.';
@override
String get channels_joinPrivateChannel => 'Присъедини се към Частен Канал';
@override
String get channels_joinPrivateChannelDesc => 'Ръчно въведете таен ключ.';
@override
String get channels_joinPublicChannel =>
'Присъединете се към Публичния канал';
@override
String get channels_joinPublicChannelDesc =>
'Всеки може да се присъедини към този канал.';
@override
String get channels_joinHashtagChannel => 'Присъедини се към Хаштаг Канал';
@override
String get channels_joinHashtagChannelDesc =>
'Всеки може да се присъедини към хаштаговите канали.';
@override
String get channels_scanQrCode => 'Сканирайте QR код';
@override
String get channels_scanQrCodeComingSoon => 'Ще излезе скоро';
@override
String get channels_enterHashtag => 'Въведете хаштаг';
@override
String get channels_hashtagHint => 'напр. #отбор';
@override
String get chat_noMessages => 'Няма съобщения.';
@@ -1475,6 +1534,10 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Входът не беше успешен: $error';
}
@override
String get login_failedMessage =>
'Входът не беше успешен. Или паролата е грешна, или повторителят е недостъпен.';
@override
String get common_reload => 'Презареди';
@@ -1544,6 +1607,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_management => 'Управление на повторители';
@override
String get room_management => 'Управление на сървъра за стая';
@override
String get repeater_managementTools => 'Инструменти за управление';
@@ -1567,6 +1633,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
@override
String get repeater_neighbours => 'Съседи';
@override
String get repeater_neighboursSubtitle =>
'Преглед на съседни възли с нулев скок.';
@override
String get repeater_settings => 'Настройки';
@@ -2252,6 +2325,33 @@ class AppLocalizationsBg extends AppLocalizations {
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_repeatersNeighbours => 'Повторители Съседи';
@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 => 'Пътеки пъзел';
@@ -2355,6 +2455,174 @@ class AppLocalizationsBg extends AppLocalizations {
@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 => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$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 => 'Филтрирайте и сортирайте';
File diff suppressed because it is too large Load Diff
+267 -3
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get common_cancel => 'Cancel';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connect';
@@ -190,7 +193,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get settings_locationSubtitle => 'GPS coordinates';
@override
String get settings_locationUpdated => 'Location updated';
String get settings_locationUpdated => 'Location and GPS settings updated';
@override
String get settings_locationBothRequired =>
@@ -199,6 +202,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get settings_locationInvalid => 'Invalid latitude or longitude.';
@override
String get settings_locationGPSEnable => 'GPS Enable';
@override
String get settings_locationGPSEnableSubtitle =>
'Enables GPS to automatically update location.';
@override
String get settings_locationIntervalSec => 'Interval for GPS (Seconds)';
@override
String get settings_locationIntervalInvalid =>
'Interval must be at least 60 seconds, and less than 86400 seconds.';
@override
String get settings_latitude => 'Latitude';
@@ -641,7 +658,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get contacts_manageRepeater => 'Manage Repeater';
@override
String get contacts_roomLogin => 'Room Login';
String get contacts_manageRoom => 'Manage Room Server';
@override
String get contacts_roomLogin => 'Room Server Login';
@override
String get contacts_openChat => 'Open Chat';
@@ -818,6 +838,43 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channels_sortUnread => 'Unread';
@override
String get channels_createPrivateChannel => 'Create a Private Channel';
@override
String get channels_createPrivateChannelDesc => 'Secured with a secret key.';
@override
String get channels_joinPrivateChannel => 'Join a Private Channel';
@override
String get channels_joinPrivateChannelDesc => 'Manually enter a secret key.';
@override
String get channels_joinPublicChannel => 'Join the Public Channel';
@override
String get channels_joinPublicChannelDesc => 'Anyone can join this channel.';
@override
String get channels_joinHashtagChannel => 'Join a Hashtag Channel';
@override
String get channels_joinHashtagChannelDesc =>
'Anyone can join hashtag channels.';
@override
String get channels_scanQrCode => 'Scan a QR Code';
@override
String get channels_scanQrCodeComingSoon => 'Coming soon';
@override
String get channels_enterHashtag => 'Enter hashtag';
@override
String get channels_hashtagHint => 'e.g. #team';
@override
String get chat_noMessages => 'No messages yet';
@@ -1402,7 +1459,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get login_repeaterLogin => 'Repeater Login';
@override
String get login_roomLogin => 'Room Login';
String get login_roomLogin => 'Room Server Login';
@override
String get login_password => 'Password';
@@ -1453,6 +1510,10 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Login failed: $error';
}
@override
String get login_failedMessage =>
'Login failed. Either the password is incorrect or the repeater is unreachable.';
@override
String get common_reload => 'Reload';
@@ -1520,6 +1581,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_management => 'Repeater Management';
@override
String get room_management => 'Room Server Management';
@override
String get repeater_managementTools => 'Management Tools';
@@ -1543,6 +1607,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Send commands to the repeater';
@override
String get repeater_neighbours => 'Neighbors';
@override
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
@override
String get repeater_settings => 'Settings';
@@ -2216,6 +2286,33 @@ class AppLocalizationsEn extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Received Neighbours Data';
@override
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
@override
String neighbors_errorLoading(String error) {
return 'Error loading neighbors: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
@override
String get neighbors_noData => 'No neighbours data available.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Unknown $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Heard: $time ago';
}
@override
String get channelPath_title => 'Packet Path';
@@ -2319,6 +2416,173 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Unknown Repeater';
@override
String get community_title => 'Community';
@override
String get community_create => 'Create Community';
@override
String get community_createDesc =>
'Create a new community and share via QR code.';
@override
String get community_join => 'Join';
@override
String get community_joinTitle => 'Join Community';
@override
String community_joinConfirmation(String name) {
return 'Do you want to join the community \"$name\"?';
}
@override
String get community_scanQr => 'Scan Community QR';
@override
String get community_scanInstructions =>
'Point the camera at a community QR code';
@override
String get community_showQr => 'Show QR Code';
@override
String get community_publicChannel => 'Community Public';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Community Name';
@override
String get community_enterName => 'Enter community name';
@override
String community_created(String name) {
return 'Community \"$name\" created';
}
@override
String community_joined(String name) {
return 'Joined community \"$name\"';
}
@override
String get community_qrTitle => 'Share Community';
@override
String community_qrInstructions(String name) {
return 'Scan this QR code to join \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community hashtag channels are only joinable by members of the community';
@override
String get community_invalidQrCode => 'Invalid community QR code';
@override
String get community_alreadyMember => 'Already a Member';
@override
String community_alreadyMemberMessage(String name) {
return 'You are already a member of \"$name\".';
}
@override
String get community_addPublicChannel => 'Add Community Public Channel';
@override
String get community_addPublicChannelHint =>
'Automatically add the public channel for this community';
@override
String get community_noCommunities => 'No communities joined yet';
@override
String get community_scanOrCreate =>
'Scan a QR code or create a community to get started';
@override
String get community_manageCommunities => 'Manage Communities';
@override
String get community_delete => 'Leave Community';
@override
String community_deleteConfirm(String name) {
return 'Leave \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'This will also delete $count channel(s) and their messages.';
}
@override
String community_deleted(String name) {
return 'Left community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Add Community Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Add a hashtag channel for this community';
@override
String get community_selectCommunity => 'Select Community';
@override
String get community_regularHashtag => 'Regular Hashtag';
@override
String get community_regularHashtagDesc => 'Public hashtag (anyone can join)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Private to community members';
@override
String community_forCommunity(String name) {
return 'For $name';
}
@override
String get listFilter_tooltip => 'Filter and sort';
+271
View File
@@ -23,6 +23,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'De acuerdo';
@override
String get common_connect => 'Conectar';
@@ -200,6 +203,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitud o longitud inválidos.';
@override
String get settings_locationGPSEnable => 'Habilitar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
@override
String get settings_latitude => 'Latitud';
@@ -648,6 +665,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestionar Repetidor';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
@override
String get contacts_roomLogin => 'Inicio de Sala';
@@ -829,6 +849,46 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channels_sortUnread => 'Sin leer';
@override
String get channels_createPrivateChannel => 'Crear un Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Cifrado con una clave secreta.';
@override
String get channels_joinPrivateChannel => 'Únete a un Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Introducir manualmente una clave secreta.';
@override
String get channels_joinPublicChannel => 'Únete al Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Cualquiera puede unirse a este canal.';
@override
String get channels_joinHashtagChannel => 'Únete a un Canal con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Cualquiera puede unirse a los canales de hashtag.';
@override
String get channels_scanQrCode => 'Escanear un Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Próximamente';
@override
String get channels_enterHashtag => 'Introducir hashtag';
@override
String get channels_hashtagHint => 'ej. #equipo';
@override
String get chat_noMessages => 'Aún no hay mensajes';
@@ -1472,6 +1532,10 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Inicio fallido: $error';
}
@override
String get login_failedMessage =>
'Inicio fallido. La contraseña es incorrecta o el repetidor no está disponible.';
@override
String get common_reload => 'Recargar';
@@ -1541,6 +1605,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_management => 'Gestión de Repetidores';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_managementTools => 'Herramientas de Gestión';
@@ -1564,6 +1631,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
@override
String get repeater_neighbours => 'Vecinos';
@override
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
@override
String get repeater_settings => 'Configuración';
@@ -2248,6 +2321,34 @@ class AppLocalizationsEs extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Recibidas Datos de Vecinos';
@override
String get neighbors_requestTimedOut =>
'Los vecinos solicitan que se desconecte.';
@override
String neighbors_errorLoading(String error) {
return 'Error al cargar vecinos: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
@override
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Clave pública desconocida $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Escuchado: $time hace atrás';
}
@override
String get channelPath_title => 'Ruta del Paquete';
@@ -2351,6 +2452,176 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconocido';
@override
String get community_title => 'Comunidad';
@override
String get community_create => 'Crear Comunidad';
@override
String get community_createDesc =>
'Crear una nueva comunidad y compartir a través de código QR.';
@override
String get community_join => 'Únete';
@override
String get community_joinTitle => 'Únete a la comunidad';
@override
String community_joinConfirmation(String name) {
return '¿Quieres unirte a la comunidad \"$name\"?';
}
@override
String get community_scanQr => 'Escanear Código QR de la Comunidad';
@override
String get community_scanInstructions =>
'Apunte la cámara a un código QR de la comunidad';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidad Pública';
@override
String get community_hashtagChannel => 'Hashtag de la Comunidad';
@override
String get community_name => 'Nombre de la comunidad';
@override
String get community_enterName => 'Introducir nombre de comunidad';
@override
String community_created(String name) {
return 'Comunidad \"$name\" creada';
}
@override
String community_joined(String name) {
return 'Se unió a la comunidad \"$name\"';
}
@override
String get community_qrTitle => 'Compartir Comunidad';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para unirte a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Los canales de hashtag de la comunidad solo son accesibles para los miembros de la comunidad';
@override
String get community_invalidQrCode => 'Código QR de comunidad no válido';
@override
String get community_alreadyMember => 'Ya eres Miembro';
@override
String community_alreadyMemberMessage(String name) {
return 'Ya eres miembro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Añadir Canal Público de la Comunidad';
@override
String get community_addPublicChannelHint =>
'Añade automáticamente el canal público para esta comunidad.';
@override
String get community_noCommunities => 'Aún no se han unido comunidades.';
@override
String get community_scanOrCreate =>
'Escanear un código QR o crear una comunidad para comenzar';
@override
String get community_manageCommunities => 'Gestionar Comunidades';
@override
String get community_delete => 'Salir de la Comunidad';
@override
String community_deleteConfirm(String name) {
return '¿Salir de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Esto también eliminará $count canal(es) y sus mensajes.';
}
@override
String community_deleted(String name) {
return 'Has salido de la comunidad \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Añadir Hashtag de la Comunidad';
@override
String get community_addHashtagChannelDesc =>
'Añadir un canal con hashtag para esta comunidad';
@override
String get community_selectCommunity => 'Seleccionar Comunidad';
@override
String get community_regularHashtag => 'Etiqueta de Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (cualquiera puede unirse)';
@override
String get community_communityHashtag => 'Hashtag de la Comunidad';
@override
String get community_communityHashtagDesc =>
'Exclusivo para miembros de la comunidad';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar y ordenar';
+301 -27
View File
@@ -23,6 +23,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get common_cancel => 'Annuler';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connecter';
@@ -200,6 +203,20 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitude ou longitude invalide.';
@override
String get settings_locationGPSEnable => 'Habilita GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita la actualización automática de la ubicación mediante GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo pour GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.';
@override
String get settings_latitude => 'Latitude';
@@ -211,11 +228,11 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get settings_privacyModeSubtitle =>
'Cacher le nom/l\'emplacement dans les publicités';
'Cacher le nom/l\'emplacement dans les annonces';
@override
String get settings_privacyModeToggle =>
'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les publicités.';
'Activer le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.';
@override
String get settings_privacyModeEnabled => 'Mode de confidentialité activé';
@@ -228,7 +245,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_actions => 'Actions';
@override
String get settings_sendAdvertisement => 'Envoyer la publicité';
String get settings_sendAdvertisement => 'S\'annoncer';
@override
String get settings_sendAdvertisementSubtitle =>
@@ -438,7 +455,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_enableNotificationsSubtitle =>
'Recevoir des notifications pour les messages et les publicités';
'Recevoir des notifications pour les messages et les annonces';
@override
String get appSettings_notificationPermissionDenied =>
@@ -467,7 +484,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_advertisementNotifications =>
'Notifications publicitaires';
'Notifications d\'annonces';
@override
String get appSettings_advertisementNotificationsSubtitle =>
@@ -498,7 +515,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get appSettings_autoRouteRotationSubtitle =>
'Alterner entre les meilleurs chemins et le mode inondation';
'Alterner entre les meilleurs chemins et le mode d\'envoi sur tout le réseau (flood)';
@override
String get appSettings_autoRouteRotationEnabled =>
@@ -649,6 +666,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gérer le répétiteur';
@override
String get contacts_manageRoom => 'Gestionar Servidor de Habitación';
@override
String get contacts_roomLogin => 'Connexion Salle';
@@ -830,6 +850,46 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channels_sortUnread => 'Non lu';
@override
String get channels_createPrivateChannel => 'Créer un Canal Privé';
@override
String get channels_createPrivateChannelDesc =>
'Sécurisé avec une clé secrète.';
@override
String get channels_joinPrivateChannel => 'Rejoindre un Canal Privé';
@override
String get channels_joinPrivateChannelDesc =>
'Entrer manuellement une clé secrète.';
@override
String get channels_joinPublicChannel => 'Rejoindre le canal public';
@override
String get channels_joinPublicChannelDesc =>
'Tout le monde peut rejoindre ce canal.';
@override
String get channels_joinHashtagChannel => 'Rejoindre un Canal Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'N\'importe qui peut rejoindre les canaux #hashtag.';
@override
String get channels_scanQrCode => 'Scanner un code QR';
@override
String get channels_scanQrCodeComingSoon => 'Bientôt disponible';
@override
String get channels_enterHashtag => 'Entrez le hashtag';
@override
String get channels_hashtagHint => 'ex. #équipe';
@override
String get chat_noMessages => 'Aucun message pour le moment.';
@@ -1016,7 +1076,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get chat_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)';
@override
String get chat_forceFloodMode => 'Mode Inondation Forcée';
String get chat_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get chat_recentAckPaths =>
@@ -1080,7 +1140,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get chat_floodModeEnabled =>
'Le mode inondation est activé. Réactiver via l\'icône de routage dans la barre d\'outils.';
'Le mode envoi à tout le réseau est activé. Changer via l\'icône de routage dans la barre d\'outils.';
@override
String get chat_fullPath => 'Chemin complet';
@@ -1125,7 +1185,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Compresser les messages sortants';
@override
String get chat_floodForced => 'Inondation (forcée)';
String get chat_floodForced => 'Tout le réseau (forcée)';
@override
String get chat_directForced => 'Direct (forcé)';
@@ -1136,7 +1196,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get chat_floodAuto => 'Inondation (auto)';
String get chat_floodAuto => 'Tout le réseau (auto)';
@override
String get chat_direct => 'Afficher';
@@ -1460,7 +1520,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get login_autoUseSavedPath => 'Auto (utiliser le chemin sauvegardé)';
@override
String get login_forceFloodMode => 'Mode Inondation Forcée';
String get login_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get login_managePaths => 'Gérer les chemins';
@@ -1478,6 +1538,10 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Connexion échouée : $error';
}
@override
String get login_failedMessage =>
'Connexion échouée. Soit le mot de passe est incorrect, soit le relais est injoignable.';
@override
String get common_reload => 'Recharger';
@@ -1547,6 +1611,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_management => 'Gestion des répétiteurs';
@override
String get room_management => 'Administración del Servidor de Habitación';
@override
String get repeater_managementTools => 'Outils de Gestion';
@@ -1570,6 +1637,13 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Envoyer des commandes au répétiteur';
@override
String get repeater_neighbours => 'Voisins';
@override
String get repeater_neighboursSubtitle =>
'Afficher les voisins de saut nuls.';
@override
String get repeater_settings => 'Paramètres';
@@ -1588,7 +1662,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Auto (utiliser le chemin sauvegardé)';
@override
String get repeater_forceFloodMode => 'Mode de submersion forcée';
String get repeater_forceFloodMode => 'Mode tout le réseau forcé';
@override
String get repeater_pathManagement => 'Gestion des chemins';
@@ -1665,17 +1739,17 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String repeater_packetTxTotal(int total, String flood, String direct) {
return 'Total : $total, Inondation : $flood, Direct : $direct';
return 'Total : $total, Tout le réseau : $flood, Direct : $direct';
}
@override
String repeater_packetRxTotal(int total, String flood, String direct) {
return 'Total : $total, Inondation : $flood, Direct : $direct';
return 'Total : $total, Tout le réseau : $flood, Direct : $direct';
}
@override
String repeater_duplicatesFloodDirect(String flood, String direct) {
return 'Inondation : $flood, Direct : $direct';
return 'Tout le réseau : $flood, Direct : $direct';
}
@override
@@ -1771,13 +1845,14 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_privacyModeSubtitle =>
'Cacher le nom/l\'emplacement dans les publicités';
'Cacher le nom/l\'emplacement dans les annonces';
@override
String get repeater_advertisementSettings => 'Paramètres de Publicité';
String get repeater_advertisementSettings => 'Paramètres d\'annonces';
@override
String get repeater_localAdvertInterval => 'Intervalle Publicité Locale';
String get repeater_localAdvertInterval =>
'Intervalle des annonces Locale (0 saut)';
@override
String repeater_localAdvertIntervalMinutes(int minutes) {
@@ -1786,7 +1861,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_floodAdvertInterval =>
'Intervalle de Publicité Inondation';
'Intervalle des annonces à tout le réseau (flood)';
@override
String repeater_floodAdvertIntervalHours(int hours) {
@@ -1795,7 +1870,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_encryptedAdvertInterval =>
'Intervalle publicitaire crypté';
'Intervalle d\'annonces cryptées';
@override
String get repeater_dangerZone => 'Zone d\'alerte';
@@ -1886,7 +1961,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_refreshAdvertisementSettings =>
'Rafraîchir les Paramètres de la Publicité';
'Rafraîchir les Paramètres des annonces';
@override
String repeater_refreshed(String label) {
@@ -1960,7 +2035,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_cliQuickClock => 'Horloge';
@override
String get repeater_cliHelpAdvert => 'Envoie un paquet publicitaire';
String get repeater_cliHelpAdvert => 'Envoie un paquet d\'annonce';
@override
String get repeater_cliHelpReboot =>
@@ -2026,7 +2101,7 @@ class AppLocalizationsFr extends AppLocalizations {
'Définit/met à jour le mot de passe de l\'invité. (pour les répéteurs, les connexions d\'invités peuvent envoyer la requête \"Get Stats\")';
@override
String get repeater_cliHelpSetName => 'Définit le nom de la publicité.';
String get repeater_cliHelpSetName => 'Définit le nom de l\'annonce.';
@override
String get repeater_cliHelpSetLat =>
@@ -2046,7 +2121,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpSetTxDelay =>
'Définit un facteur multiplié par le temps de fonctionnement en mode inondation pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).';
'Définit un facteur multiplié par le temps de fonctionnement en mode vers tout le réseau (flood) pour un paquet et avec un système de slot aléatoire, afin de retarder son envoi (pour diminuer la probabilité de collisions).';
@override
String get repeater_cliHelpSetDirectTxDelay =>
@@ -2101,7 +2176,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpNeighbors =>
'Affiche une liste d\'autres nœuds répétiteurs entendus via des publicités sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
'Affiche une liste d\'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4';
@override
String get repeater_cliHelpNeighborRemove =>
@@ -2109,7 +2184,7 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get repeater_cliHelpRegion =>
'(série uniquement) Liste toutes les régions définies et les autorisations de débordement actuelles.';
'(série uniquement) Liste toutes les régions définies et les autorisations actuelles d\'annonces sur tout le réseau (flood).';
@override
String get repeater_cliHelpRegionLoad =>
@@ -2261,6 +2336,34 @@ class AppLocalizationsFr extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Données des voisins reçues';
@override
String get neighbors_requestTimedOut => 'Les voisins demandent un délai.';
@override
String neighbors_errorLoading(String error) {
return 'Erreur lors du chargement des voisins : $error';
}
@override
String get neighbors_repeatersNeighbours => 'Répéteurs Voisins';
@override
String get neighbors_noData =>
'Aucune donnée concernant les voisins disponible.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Clé publique inconnue $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Écouté : $time auparavant';
}
@override
String get channelPath_title => 'Chemin de paquet';
@@ -2319,7 +2422,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get channelPath_unknownPath => 'Inconnu';
@override
String get channelPath_floodPath => 'Inondation';
String get channelPath_floodPath => 'Tout le réseau';
@override
String get channelPath_directPath => 'Afficher';
@@ -2364,6 +2467,177 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Répéteur Inconnu';
@override
String get community_title => 'Communauté';
@override
String get community_create => 'Créer une Communauté';
@override
String get community_createDesc =>
'Créer une nouvelle communauté et la partager via QR code.';
@override
String get community_join => 'Rejoindre';
@override
String get community_joinTitle => 'Rejoindre la communauté';
@override
String community_joinConfirmation(String name) {
return 'Souhaitez-vous rejoindre la communauté \"$name\" ?';
}
@override
String get community_scanQr => 'Scanner la communauté QR';
@override
String get community_scanInstructions =>
'Pointez l\'appareil photo vers un code QR communautaire.';
@override
String get community_showQr => 'Afficher le QR Code';
@override
String get community_publicChannel => 'Communauté Publique';
@override
String get community_hashtagChannel => 'Hashtag Communauté';
@override
String get community_name => 'Nom de la communauté';
@override
String get community_enterName => 'Entrez le nom de la communauté';
@override
String community_created(String name) {
return 'Communauté \"$name\" créée';
}
@override
String community_joined(String name) {
return 'Rejoint la communauté \"$name\"';
}
@override
String get community_qrTitle => 'Partager Communauté';
@override
String community_qrInstructions(String name) {
return 'Scanner ce QR code pour rejoindre $name';
}
@override
String get community_hashtagPrivacyHint =>
'Les canaux hashtag de la communauté ne sont accessibles qu\'aux membres de la communauté';
@override
String get community_invalidQrCode => 'Code QR de communauté non valide';
@override
String get community_alreadyMember => 'Déjà membre';
@override
String community_alreadyMemberMessage(String name) {
return 'Vous êtes déjà membre de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Ajouter un Canal Public de la Communauté';
@override
String get community_addPublicChannelHint =>
'Ajouter automatiquement le canal public pour cette communauté';
@override
String get community_noCommunities =>
'Aucun groupe n\'a été rejoint pour le moment.';
@override
String get community_scanOrCreate =>
'Scanner un code QR ou créer une communauté pour commencer';
@override
String get community_manageCommunities => 'Gérer les Communautés';
@override
String get community_delete => 'Quitter la communauté';
@override
String community_deleteConfirm(String name) {
return 'Quitter \"$name\" ?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Cela supprimera également $count canal/canaux et leurs messages.';
}
@override
String community_deleted(String name) {
return 'Communauté \"$name\" quittée';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Ajouter un Hashtag Communauté';
@override
String get community_addHashtagChannelDesc =>
'Ajouter un canal hachage pour cette communauté';
@override
String get community_selectCommunity => 'Sélectionner Communauté';
@override
String get community_regularHashtag => 'Hashtag régulier';
@override
String get community_regularHashtagDesc =>
'Hashtag public (tout le monde peut rejoindre)';
@override
String get community_communityHashtag => 'Hashtag de la communauté';
@override
String get community_communityHashtagDesc =>
'Exclusif aux membres de la communauté';
@override
String community_forCommunity(String name) {
return 'Pour $name';
}
@override
String get listFilter_tooltip => 'Filtrer et trier';
+271
View File
@@ -23,6 +23,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get common_cancel => 'Annulla';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Connetti';
@@ -200,6 +203,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitudine o longitudine non valida.';
@override
String get settings_locationGPSEnable => 'Abilita GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Abilita l\'aggiornamento automatico della posizione tramite GPS.';
@override
String get settings_locationIntervalSec => 'Intervallo GPS (Secondi)';
@override
String get settings_locationIntervalInvalid =>
'L\'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.';
@override
String get settings_latitude => 'Latitudine';
@@ -646,6 +663,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gestisci Ripetitore';
@override
String get contacts_manageRoom => 'Gestisci Server Camera';
@override
String get contacts_roomLogin => 'Login Camera';
@@ -827,6 +847,46 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channels_sortUnread => 'Non letto';
@override
String get channels_createPrivateChannel => 'Crea un Canale Privato';
@override
String get channels_createPrivateChannelDesc =>
'Protetta con una chiave segreta.';
@override
String get channels_joinPrivateChannel => 'Unisciti a un Canale Privato';
@override
String get channels_joinPrivateChannelDesc =>
'Inserire manualmente una chiave segreta.';
@override
String get channels_joinPublicChannel => 'Unisciti al Canale Pubblico';
@override
String get channels_joinPublicChannelDesc =>
'Chiunque può unirsi a questo canale.';
@override
String get channels_joinHashtagChannel => 'Unisciti a un Canale con Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Chiunque può unirsi ai canali hashtag.';
@override
String get channels_scanQrCode => 'Scansiona un codice QR';
@override
String get channels_scanQrCodeComingSoon => 'Arriverà presto';
@override
String get channels_enterHashtag => 'Inserisci hashtag';
@override
String get channels_hashtagHint => 'es. #team';
@override
String get chat_noMessages => 'Nessun messaggio ancora';
@@ -1470,6 +1530,10 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Accesso fallito: $error';
}
@override
String get login_failedMessage =>
'Accesso fallito. La password non è corretta oppure il ripetitore non è raggiungibile.';
@override
String get common_reload => 'Ricaricare';
@@ -1539,6 +1603,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_management => 'Gestione Ripetitori';
@override
String get room_management => 'Gestione del Server di Camera';
@override
String get repeater_managementTools => 'Strumenti di Gestione';
@@ -1562,6 +1629,13 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
@override
String get repeater_neighbours => 'Vicini';
@override
String get repeater_neighboursSubtitle =>
'Visualizza vicini di salto pari a zero.';
@override
String get repeater_settings => 'Impostazioni';
@@ -2248,6 +2322,33 @@ class AppLocalizationsIt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Ricevute dati vicini';
@override
String get neighbors_requestTimedOut => 'I vicini richiedono un timeout.';
@override
String neighbors_errorLoading(String error) {
return 'Errore nel caricamento dei vicini: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
@override
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Chiave pubblica sconosciuta $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Sentito: $time fa';
}
@override
String get channelPath_title => 'Percorso Pacchetto';
@@ -2351,6 +2452,176 @@ class AppLocalizationsIt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Ripetitore sconosciuto';
@override
String get community_title => 'Comunità';
@override
String get community_create => 'Crea Comunità';
@override
String get community_createDesc =>
'Crea una nuova comunità e condividila tramite codice QR.';
@override
String get community_join => 'Unisciti';
@override
String get community_joinTitle => 'Unisciti alla Community';
@override
String community_joinConfirmation(String name) {
return 'Vuoi unirti alla community \"$name\"?';
}
@override
String get community_scanQr => 'Scansiona il QR Code della Community';
@override
String get community_scanInstructions =>
'Punta la fotocamera su un codice QR della comunità';
@override
String get community_showQr => 'Mostra il codice QR';
@override
String get community_publicChannel => 'Comunità Pubblica';
@override
String get community_hashtagChannel => 'Hashtag della Comunità';
@override
String get community_name => 'Nome della Comunità';
@override
String get community_enterName => 'Inserisci il nome della comunità';
@override
String community_created(String name) {
return 'Comunità \"$name\" creata';
}
@override
String community_joined(String name) {
return 'Unito alla comunità \"$name\"';
}
@override
String get community_qrTitle => 'Condividi Comunità';
@override
String community_qrInstructions(String name) {
return 'Scansiona questo codice QR per unirti a $name';
}
@override
String get community_hashtagPrivacyHint =>
'I canali hashtag della community sono accessibili solo ai membri della community';
@override
String get community_invalidQrCode => 'Codice QR della community non valido';
@override
String get community_alreadyMember => 'Già membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Sei già un membro di \"$name\".';
}
@override
String get community_addPublicChannel =>
'Aggiungi Canale Pubblico della Comunità';
@override
String get community_addPublicChannelHint =>
'Aggiungi automaticamente il canale pubblico per questa community';
@override
String get community_noCommunities => 'Nessun gruppo aggiunto finora';
@override
String get community_scanOrCreate =>
'Scansiona un codice QR o crea una community per iniziare.';
@override
String get community_manageCommunities => 'Gestisci Comunità';
@override
String get community_delete => 'Lascia la Comunità';
@override
String community_deleteConfirm(String name) {
return 'Uscire da \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Questo eliminerà anche $count canale/i e i loro messaggi.';
}
@override
String community_deleted(String name) {
return 'Hai lasciato la comunità \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Aggiungi Hashtag della Community';
@override
String get community_addHashtagChannelDesc =>
'Aggiungi un canale con hashtag per questa community';
@override
String get community_selectCommunity => 'Seleziona Comunità';
@override
String get community_regularHashtag => 'Hashtag regolare';
@override
String get community_regularHashtagDesc =>
'Hashtag pubblico (chiunque può unirsi)';
@override
String get community_communityHashtag => 'Hashtag della Comunità';
@override
String get community_communityHashtagDesc =>
'Visibile solo ai membri della comunità';
@override
String community_forCommunity(String name) {
return 'Per $name';
}
@override
String get listFilter_tooltip => 'Filtra e ordina';
+361 -92
View File
@@ -15,7 +15,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get nav_contacts => 'Contacten';
@override
String get nav_channels => 'Kanaal';
String get nav_channels => 'Kanalen';
@override
String get nav_map => 'Kaart';
@@ -23,6 +23,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get common_cancel => 'Annuleren';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Verbinden';
@@ -39,7 +42,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_close => 'Sluiten';
@override
String get common_edit => 'Bewerk';
String get common_edit => 'Bewerken';
@override
String get common_add => 'Toevoegen';
@@ -48,13 +51,13 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_settings => 'Instellingen';
@override
String get common_disconnect => 'Verbinden verbreken';
String get common_disconnect => 'Verbinding verbreken';
@override
String get common_connected => 'Verbonden';
@override
String get common_disconnected => 'Ontkoppeld';
String get common_disconnected => 'Verbinding verbroken';
@override
String get common_create => 'Maak';
@@ -78,7 +81,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_remove => 'Verwijderen';
@override
String get common_enable => 'Aktivatie';
String get common_enable => 'Activeren';
@override
String get common_disable => 'Uitschakelen';
@@ -87,7 +90,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get common_reboot => 'Herstarten';
@override
String get common_loading => 'Laad...';
String get common_loading => 'Laden...';
@override
String get common_notAvailable => '';
@@ -168,7 +171,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_nodeNameNotSet => 'Niet ingesteld';
@override
String get settings_nodeNameHint => 'Voer knooppuntnaam in';
String get settings_nodeNameHint => 'Voer nodenaam in';
@override
String get settings_nodeNameUpdated => 'Naam bijgewerkt';
@@ -200,6 +203,20 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_locationInvalid =>
'Ongeldige breedtegraad of lengtegraad.';
@override
String get settings_locationGPSEnable => 'GPS inschakelen';
@override
String get settings_locationGPSEnableSubtitle =>
'Activeer automatisch locatieupdates via GPS.';
@override
String get settings_locationIntervalSec => 'Interval voor GPS (Seconden)';
@override
String get settings_locationIntervalInvalid =>
'De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.';
@override
String get settings_latitude => 'Breedtegraad';
@@ -312,7 +329,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_infoContactsCount => 'Aantal Contacten';
@override
String get settings_infoChannelCount => 'Kanaal Aantal';
String get settings_infoChannelCount => 'Aantal Kanalen';
@override
String get settings_presets => 'Presets';
@@ -354,10 +371,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
@override
String get settings_longRange => 'Lang Bereik';
String get settings_longRange => 'Lange Afstand';
@override
String get settings_fastSpeed => 'Snelle Snelheid';
String get settings_fastSpeed => 'Hoge Snelheid';
@override
String settings_error(String message) {
@@ -377,7 +394,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_themeSystem => 'Standaardinstelling';
@override
String get appSettings_themeLight => 'Helder';
String get appSettings_themeLight => 'Licht';
@override
String get appSettings_themeDark => 'Donker';
@@ -469,13 +486,13 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_advertisementNotificationsSubtitle =>
'Toon notificatie wanneer nieuwe knooppunten worden ontdekt';
'Toon notificatie wanneer nieuwe nodes worden ontdekt';
@override
String get appSettings_messaging => 'Berichten';
@override
String get appSettings_clearPathOnMaxRetry => 'Duidelijke Pad op Max Retry';
String get appSettings_clearPathOnMaxRetry => 'Wis Pad op Max Retry';
@override
String get appSettings_clearPathOnMaxRetrySubtitle =>
@@ -490,19 +507,19 @@ class AppLocalizationsNl extends AppLocalizations {
'Padoms worden niet automatisch verwijderd';
@override
String get appSettings_autoRouteRotation => 'Automatische Route Rotatie';
String get appSettings_autoRouteRotation => 'Route Automatisch Roteren';
@override
String get appSettings_autoRouteRotationSubtitle =>
'Wissel tussen de beste paden en floodmodus over.';
'Verwissel tussen beste pad en floodmodus.';
@override
String get appSettings_autoRouteRotationEnabled =>
'Automatische routeplanning rotatie ingeschakeld';
'Automatische route rotatie ingeschakeld';
@override
String get appSettings_autoRouteRotationDisabled =>
'Automatische routeplanning rotatie is uitgeschakeld';
'Automatische route rotatie is uitgeschakeld';
@override
String get appSettings_battery => 'Batterij';
@@ -532,11 +549,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get appSettings_mapDisplay => 'Kaartweergave';
@override
String get appSettings_showRepeaters => 'Toon Herhalingen';
String get appSettings_showRepeaters => 'Toon Repeaters';
@override
String get appSettings_showRepeatersSubtitle =>
'Toon herhalende knoopjes op de kaart';
'Toon repeaternodes op de kaart';
@override
String get appSettings_showChatNodes => 'Chat Nodes tonen';
@@ -546,21 +563,21 @@ class AppLocalizationsNl extends AppLocalizations {
'Chatnodes weergeven op de kaart';
@override
String get appSettings_showOtherNodes => 'Toon Andere Knopen';
String get appSettings_showOtherNodes => 'Toon Andere Nodes';
@override
String get appSettings_showOtherNodesSubtitle =>
'Toon andere knooptypes op de kaart';
'Toon andere nodetypes op de kaart';
@override
String get appSettings_timeFilter => 'Filter op tijd';
@override
String get appSettings_timeFilterShowAll => 'Alle knooppunten tonen';
String get appSettings_timeFilterShowAll => 'Alle nodes tonen';
@override
String appSettings_timeFilterShowLast(int hours) {
return 'Toon knopen van de laatste $hours uur';
return 'Toon nodes van de laatste $hours uur';
}
@override
@@ -568,10 +585,10 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get appSettings_showNodesDiscoveredWithin =>
'Toon knooppunten ontdekt binnen:';
'Toon nodes ontdekt binnen:';
@override
String get appSettings_allTime => 'Alle tijd';
String get appSettings_allTime => 'Altijd';
@override
String get appSettings_lastHour => 'Laat uur';
@@ -642,7 +659,10 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get contacts_manageRepeater => 'Beheer Herhaling';
String get contacts_manageRepeater => 'Beheer Repeater';
@override
String get contacts_manageRoom => 'Beheer Ruimte Server';
@override
String get contacts_roomLogin => 'Ruimte Inloggen';
@@ -690,7 +710,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String contacts_lastSeenMinsAgo(int minutes) {
return 'Laast gezien $minutes minuten geleden';
return 'Laatst gezien $minutes minuten geleden';
}
@override
@@ -824,6 +844,46 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channels_sortUnread => 'Ongelezen';
@override
String get channels_createPrivateChannel => 'Maak een Privé Kanaal';
@override
String get channels_createPrivateChannelDesc =>
'Beveiligd met een geheime sleutel.';
@override
String get channels_joinPrivateChannel => 'Sluit een Privé Kanaal aan';
@override
String get channels_joinPrivateChannelDesc =>
'Handmatig een geheime sleutel invoeren.';
@override
String get channels_joinPublicChannel => 'Sluit het Open Kanaal';
@override
String get channels_joinPublicChannelDesc =>
'Iedereen kan dit kanaal aanmelden.';
@override
String get channels_joinHashtagChannel => 'Sluit een Hashtag Kanaal';
@override
String get channels_joinHashtagChannelDesc =>
'Iedereen kan lid worden van hashtag-kanalen.';
@override
String get channels_scanQrCode => 'Scan een QR-code';
@override
String get channels_scanQrCodeComingSoon => 'Komt later';
@override
String get channels_enterHashtag => 'Voer hashtag in';
@override
String get channels_hashtagHint => 'bijv. #team';
@override
String get chat_noMessages => 'Nog geen berichten.';
@@ -1009,7 +1069,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get chat_autoUseSavedPath => 'Automatisch (gebruik opgeslagen pad)';
@override
String get chat_forceFloodMode => 'Dwing Overstromingsmodus';
String get chat_forceFloodMode => 'Dwing Floodsmodus';
@override
String get chat_recentAckPaths => 'Recente ACK Paden (tik om te gebruiken):';
@@ -1071,7 +1131,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get chat_floodModeEnabled =>
'Overstromingsmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.';
'Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.';
@override
String get chat_fullPath => 'Volledige Pad';
@@ -1115,18 +1175,18 @@ class AppLocalizationsNl extends AppLocalizations {
'Verzenden van uitgaande berichten comprimeren';
@override
String get chat_floodForced => 'Overstroming (gedwongen)';
String get chat_floodForced => 'Flood (afgedwongen)';
@override
String get chat_directForced => 'Direct (gedwongen)';
String get chat_directForced => 'Direct (afgedwongen)';
@override
String chat_hopsForced(int count) {
return '$count sprongen (gedwongen)';
return '$count hops (afgedwongen)';
}
@override
String get chat_floodAuto => 'Overstroming (auto)';
String get chat_floodAuto => 'Flood (auto)';
@override
String get chat_direct => 'Direct';
@@ -1143,7 +1203,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_title => 'Node Map';
@override
String get map_noNodesWithLocation => 'Geen knopen met locatiegegevens';
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
@override
String get map_nodesNeedGps =>
@@ -1163,7 +1223,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_chat => 'Chat';
@override
String get map_repeater => 'Herhaling';
String get map_repeater => 'Repeater';
@override
String get map_room => 'Ruimte';
@@ -1230,19 +1290,19 @@ class AppLocalizationsNl extends AppLocalizations {
'Verbind met een apparaat om markers te delen';
@override
String get map_filterNodes => 'Filter Knopen';
String get map_filterNodes => 'Filter Nodes';
@override
String get map_nodeTypes => 'Node Types';
String get map_nodeTypes => 'Nodetypes';
@override
String get map_chatNodes => 'Chat Nodes';
String get map_chatNodes => 'Chatnodes';
@override
String get map_repeaters => 'Herhalingen';
String get map_repeaters => 'Repeaters';
@override
String get map_otherNodes => 'Andere knooppunten';
String get map_otherNodes => 'Andere Nodes';
@override
String get map_keyPrefix => 'Prefix sleutel';
@@ -1269,7 +1329,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get map_joinRoom => 'Sluit Kamer';
@override
String get map_manageRepeater => 'Beheer Herhaling';
String get map_manageRepeater => 'Beheer Repeater';
@override
String get mapCache_title => 'Offline Kaarten Cache';
@@ -1412,7 +1472,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?';
@override
String get login_repeaterLogin => 'Herhaalders Inloggen';
String get login_repeaterLogin => 'Inloggen Repeater';
@override
String get login_roomLogin => 'Ruimte Inloggen';
@@ -1432,7 +1492,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get login_repeaterDescription =>
'Voer het wachtwoord van de herhaling in om instellingen en status te openen.';
'Voer het wachtwoord van de repeater in om instellingen en status te openen.';
@override
String get login_roomDescription =>
@@ -1448,7 +1508,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get login_autoUseSavedPath => 'Automatisch (gebruik opgeslagen pad)';
@override
String get login_forceFloodMode => 'Dwing Overstromingsmodus';
String get login_forceFloodMode => 'Dwing Floodmodus Af';
@override
String get login_managePaths => 'Padbeheer';
@@ -1466,6 +1526,10 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Inloggen mislukt: $error';
}
@override
String get login_failedMessage =>
'Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.';
@override
String get common_reload => 'Opnieuw laden';
@@ -1513,8 +1577,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get path_selectFromContacts => 'Of select contacten:';
@override
String get path_noRepeatersFound =>
'Geen herhalingen of zaalservers gevonden.';
String get path_noRepeatersFound => 'Geen repeaters of roomservers gevonden.';
@override
String get path_customPathsRequire =>
@@ -1533,7 +1596,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get path_setPath => 'Stel Pad in';
@override
String get repeater_management => 'Beheer Herhalingen';
String get repeater_management => 'Beheer Repeaters';
@override
String get room_management => 'Beheer Server Kamer';
@override
String get repeater_managementTools => 'Beheerinstrumenten';
@@ -1556,7 +1622,13 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_cli => 'CLI';
@override
String get repeater_cliSubtitle => 'Verzend commando\'s naar de herhaaldere';
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
@override
String get repeater_neighbours => 'Buren';
@override
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
@override
String get repeater_settings => 'Instellingen';
@@ -1565,7 +1637,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_settingsSubtitle => 'Configureer repeaterparameters';
@override
String get repeater_statusTitle => 'Status herhalen';
String get repeater_statusTitle => 'Status repeater';
@override
String get repeater_routingMode => 'Routeerwijze';
@@ -1575,7 +1647,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Automatisch (gebruik opgeslagen pad)';
@override
String get repeater_forceFloodMode => 'Dwing Overloopmodus';
String get repeater_forceFloodMode => 'Dwing Floodmodus Af';
@override
String get repeater_pathManagement => 'Beheer van paden';
@@ -1592,7 +1664,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_systemInformation => 'Systeem Informatie';
String get repeater_systemInformation => 'Systeeminformatie';
@override
String get repeater_battery => 'Batterij';
@@ -1607,10 +1679,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_queueLength => 'Wachttijd';
@override
String get repeater_debugFlags => 'Debug Flags';
String get repeater_debugFlags => 'Debugvlaggen';
@override
String get repeater_radioStatistics => 'Radio Statistieken';
String get repeater_radioStatistics => 'Radiostatistieken';
@override
String get repeater_lastRssi => 'Laatste RSSI';
@@ -1619,7 +1691,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_lastSnr => 'Laatste SNR';
@override
String get repeater_noiseFloor => 'Ruishoordniveau';
String get repeater_noiseFloor => 'Ruisvloer';
@override
String get repeater_txAirtime => 'TX Airtime';
@@ -1628,7 +1700,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_rxAirtime => 'RX Airtime';
@override
String get repeater_packetStatistics => 'Pakket Statistieken';
String get repeater_packetStatistics => 'Pakketstatistieken';
@override
String get repeater_sent => 'Verzonden';
@@ -1637,7 +1709,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_received => 'Ontvangen';
@override
String get repeater_duplicates => 'Dubbele';
String get repeater_duplicates => 'Duplicaat';
@override
String repeater_daysHoursMinsSecs(
@@ -1651,17 +1723,17 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String repeater_packetTxTotal(int total, String flood, String direct) {
return 'Totaal: $total, Overstroming: $flood, Direct: $direct';
return 'Totaal: $total, Flood: $flood, Direct: $direct';
}
@override
String repeater_packetRxTotal(int total, String flood, String direct) {
return 'Totaal: $total, Overstroming: $flood, Direct: $direct';
return 'Totaal: $total, Flood: $flood, Direct: $direct';
}
@override
String repeater_duplicatesFloodDirect(String flood, String direct) {
return 'Overstroming: $flood, Direct: $direct';
return 'Flood: $flood, Direct: $direct';
}
@override
@@ -1670,16 +1742,16 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_settingsTitle => 'Herstelinstellingen';
String get repeater_settingsTitle => 'Repeater Instellingen';
@override
String get repeater_basicSettings => 'Basisinstellingen';
@override
String get repeater_repeaterName => 'Herhaalnaam';
String get repeater_repeaterName => 'Repeaternaam';
@override
String get repeater_repeaterNameHelper => 'Weergave naam voor deze herhaling';
String get repeater_repeaterNameHelper => 'Weergave naam voor deze repeater';
@override
String get repeater_adminPassword => 'Admin wachtwoord';
@@ -1712,7 +1784,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_bandwidth => 'Bandbreedte';
@override
String get repeater_spreadingFactor => 'Spreadsnelheid';
String get repeater_spreadingFactor => 'Spreidingsfactor';
@override
String get repeater_codingRate => 'Codeertarief';
@@ -1736,11 +1808,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_features => 'Kenmerken';
@override
String get repeater_packetForwarding => 'Pakketdoorstrooming';
String get repeater_packetForwarding => 'Pakketdoorvoering';
@override
String get repeater_packetForwardingSubtitle =>
'Herstel activeren om pakketten door te sturen';
'Repeater instellen om pakketten door te sturen';
@override
String get repeater_guestAccess => 'Toegang voor Gasten';
@@ -1750,7 +1822,7 @@ class AppLocalizationsNl extends AppLocalizations {
'Toegestane leesbeheer toegang voor gasten.';
@override
String get repeater_privacyMode => 'Privacy Mode';
String get repeater_privacyMode => 'Privacy Modus';
@override
String get repeater_privacyModeSubtitle =>
@@ -1768,8 +1840,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_floodAdvertInterval =>
'Advertentie Interval bij overstroming';
String get repeater_floodAdvertInterval => 'Flood Advertentie Interval';
@override
String repeater_floodAdvertIntervalHours(int hours) {
@@ -1784,11 +1855,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_dangerZone => 'Gevaarzone';
@override
String get repeater_rebootRepeater => 'Herstart Herhaalder';
String get repeater_rebootRepeater => 'Herstart Repeater';
@override
String get repeater_rebootRepeaterSubtitle =>
'De herstart van het herhalerapparaat';
String get repeater_rebootRepeaterSubtitle => 'Herstart het Repeaterapparaat';
@override
String get repeater_rebootRepeaterConfirm =>
@@ -1804,18 +1874,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_regenerateIdentityKeyConfirm =>
'Dit genereert een nieuwe identiteit voor de herhaling. Doorgaan?';
'Dit genereert een nieuwe identiteit voor de repeater. Doorgaan?';
@override
String get repeater_eraseFileSystem => 'Verwijder Besturingssysteem';
@override
String get repeater_eraseFileSystemSubtitle =>
'Formateer het herhalende bestandsysteem';
'Formateer het bestandsysteem van de repeater';
@override
String get repeater_eraseFileSystemConfirm =>
'WAARSCHUWING: Dit zal alle gegevens op de herhaling wissen. Dit kan niet worden teruggedraaid!';
'WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!';
@override
String get repeater_eraseSerialOnly =>
@@ -1847,7 +1917,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_refreshRadioSettings =>
'Radiozenders Instellingen Bijwerken';
'Radiozender Instellingen Verversen';
@override
String get repeater_refreshTxPower => 'Nieuw laden TX-vermogen';
@@ -1881,7 +1951,7 @@ class AppLocalizationsNl extends AppLocalizations {
}
@override
String get repeater_cliTitle => 'Herhaling CLI';
String get repeater_cliTitle => 'Repeater CLI';
@override
String get repeater_debugNextCommand => 'Debug Volgende Commando';
@@ -1972,7 +2042,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetRepeat =>
'Activeert of deactiveert de herhalerrol voor dit knoop.';
'Activeert of deactiveert de repeater rol van deze node.';
@override
String get repeater_cliHelpSetAllowReadOnly =>
@@ -1980,7 +2050,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetFloodMax =>
'Stelt het maximale aantal hops van een inkomend overlastpakket in (indien >= max, wordt het pakket niet doorgestuurd)';
'Stelt het maximale aantal hops van een inkomend floodpakket in (indien >= max, wordt het pakket niet doorgestuurd)';
@override
String get repeater_cliHelpSetIntThresh =>
@@ -1992,7 +2062,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetMultiAcks =>
'Activeert of deactiveert de functie \'dubbele ACKs\'.';
'Activeert of deactiveert de functie \'duplicate ACKs\'.';
@override
String get repeater_cliHelpSetAdvertInterval =>
@@ -2000,7 +2070,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpSetFloodAdvertInterval =>
'Stelt het timerinterval in uren in om een overstromingsadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.';
'Stelt het timerinterval in uren in om een floodadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.';
@override
String get repeater_cliHelpSetGuestPassword =>
@@ -2091,7 +2161,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get repeater_cliHelpRegion =>
'(reeks alleen) Lijst alle gedefinieerde regio\'s en huidige overstromingsrechten.';
'(Alleen Serieel) Lijst alle gedefinieerde regio\'s en huidige floodrechten.';
@override
String get repeater_cliHelpRegionLoad =>
@@ -2136,11 +2206,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_cliHelpGpsOnOff => 'Schakel de GPS-standby aan/uit.';
@override
String get repeater_cliHelpGpsSync => 'Synchroniseer knooptime met GPS-klok.';
String get repeater_cliHelpGpsSync => 'Synchroniseer node met GPS-klok.';
@override
String get repeater_cliHelpGpsSetLoc =>
'Stel de positie van het knoop vast naar GPS-coördinaten en sla de voorkeuren op.';
'Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.';
@override
String get repeater_cliHelpGpsAdvert =>
@@ -2170,11 +2240,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get repeater_logging => 'Logging';
@override
String get repeater_neighborsRepeaterOnly => 'Buren (Alleen herhaald)';
String get repeater_neighborsRepeaterOnly => 'Buren (Alleen repeaters)';
@override
String get repeater_regionManagementRepeaterOnly =>
'Regiobeheer (Alleen voor Repeater)';
'Regiobeheer (Alleen Repeater)';
@override
String get repeater_regionNote =>
@@ -2241,6 +2311,34 @@ class AppLocalizationsNl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Ontvangen Buurdata';
@override
String get neighbors_requestTimedOut =>
'Buren vragen om tijdelijk uitgeschakeld.';
@override
String neighbors_errorLoading(String error) {
return 'Fout bij het laden van buren: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
@override
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Onbekende $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Horen: $time geleden';
}
@override
String get channelPath_title => 'Pakketpad';
@@ -2251,7 +2349,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channelPath_otherObservedPaths => 'Overige Waargenomen Paden';
@override
String get channelPath_repeaterHops => 'Herhalingstapjes';
String get channelPath_repeaterHops => 'Repeater Hops';
@override
String get channelPath_noHopDetails =>
@@ -2267,7 +2365,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channelPath_timeLabel => 'Tijd';
@override
String get channelPath_repeatsLabel => 'Herhalen';
String get channelPath_repeatsLabel => 'Repeats';
@override
String channelPath_pathLabel(int index) {
@@ -2299,7 +2397,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get channelPath_unknownPath => 'Onbekend';
@override
String get channelPath_floodPath => 'Overstroming';
String get channelPath_floodPath => 'Flood';
@override
String get channelPath_directPath => 'Direct';
@@ -2319,7 +2417,7 @@ class AppLocalizationsNl extends AppLocalizations {
@override
String get channelPath_noRepeaterLocations =>
'Geen herhaler locaties beschikbaar voor deze route.';
'Geen repeaters beschikbaar voor deze route.';
@override
String channelPath_primaryPath(int index) {
@@ -2342,7 +2440,178 @@ class AppLocalizationsNl extends AppLocalizations {
'Geen details beschikbaar voor dit pakket.';
@override
String get channelPath_unknownRepeater => 'Onbekend Herhaalaar';
String get channelPath_unknownRepeater => 'Onbekend Repeater';
@override
String get community_title => 'Gemeenschap';
@override
String get community_create => 'Maak Gemeenschap';
@override
String get community_createDesc =>
'Maak een nieuwe community en deel deze via QR-code.';
@override
String get community_join => 'Sluit aan';
@override
String get community_joinTitle => 'Worden lid van de community';
@override
String community_joinConfirmation(String name) {
return 'Wil je je aansluiten bij de community \"$name\"?';
}
@override
String get community_scanQr => 'Scan Gemeenschap QR';
@override
String get community_scanInstructions =>
'Richt de camera op een gemeenschappelijke QR-code';
@override
String get community_showQr => 'Toon QR-code';
@override
String get community_publicChannel => 'Gemeenschap Openbaar';
@override
String get community_hashtagChannel => 'Gemeenschappelijk Hashtag';
@override
String get community_name => 'Gemeenschapnaam';
@override
String get community_enterName => 'Voer de gemeenschapsnaam in';
@override
String community_created(String name) {
return 'Gemeenschap \"$name\" is aangemaakt';
}
@override
String community_joined(String name) {
return 'Gevonden in de community \"$name\"';
}
@override
String get community_qrTitle => 'Deel Gemeenschap';
@override
String community_qrInstructions(String name) {
return 'Scan deze QR-code om je aan te sluiten bij $name';
}
@override
String get community_hashtagPrivacyHint =>
'Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community';
@override
String get community_invalidQrCode => 'Ongeldige community QR-code';
@override
String get community_alreadyMember => 'Alleen al lid';
@override
String community_alreadyMemberMessage(String name) {
return 'U bent al lid van \"$name\".';
}
@override
String get community_addPublicChannel =>
'Voeg een Openbaar Gemeenschapskanaal toe';
@override
String get community_addPublicChannelHint =>
'Automatisch de publieke kanaal toevoegen voor deze community';
@override
String get community_noCommunities =>
'Nog geen gemeenschappen zijn bijgesloten.';
@override
String get community_scanOrCreate =>
'Scan een QR-code of een community aanmaken om te beginnen';
@override
String get community_manageCommunities => 'Beheer Gemeenschappen';
@override
String get community_delete => 'Laat Gemeenschap';
@override
String community_deleteConfirm(String name) {
return '\"$name\" verlaten?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Dit verwijdert ook $count kanaal/kanalen en hun berichten.';
}
@override
String community_deleted(String name) {
return 'Community \"$name\" verlaten';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Voeg Community Hashtag toe';
@override
String get community_addHashtagChannelDesc =>
'Voeg een hashtag-kanaal toe aan deze community';
@override
String get community_selectCommunity => 'Selecteer Gemeenschap';
@override
String get community_regularHashtag => 'Gewone Hashtag';
@override
String get community_regularHashtagDesc =>
'Open hashtag (iedereen kan deelnemen)';
@override
String get community_communityHashtag => 'Gemeenschappelijk Hashtag';
@override
String get community_communityHashtagDesc =>
'Alleen zichtbaar voor leden van de community';
@override
String community_forCommunity(String name) {
return 'Voor $name';
}
@override
String get listFilter_tooltip => 'Filteren en sorteren';
@@ -2369,10 +2638,10 @@ class AppLocalizationsNl extends AppLocalizations {
String get listFilter_users => 'Gebruikers';
@override
String get listFilter_repeaters => 'Herhalingen';
String get listFilter_repeaters => 'Repeaters';
@override
String get listFilter_roomServers => 'Kamervirtualisatie';
String get listFilter_roomServers => 'Roomservers';
@override
String get listFilter_unreadOnly => 'Alleen ongelezen';
+272
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get common_cancel => 'Anuluj';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Połącz';
@@ -202,6 +205,20 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_locationInvalid =>
'Nieprawidłowa szerokość geograficzna lub długość geograficzna.';
@override
String get settings_locationGPSEnable => 'Włącz GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Włącza automatyczne aktualizowanie pozycji za pomocą GPS.';
@override
String get settings_locationIntervalSec => 'Interwał dla GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.';
@override
String get settings_latitude => 'Szerokość';
@@ -649,6 +666,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Zarządzaj Powtórzami';
@override
String get contacts_manageRoom => 'Zarządzaj Serwerem Pokoju';
@override
String get contacts_roomLogin => 'Logowanie do pokoju';
@@ -828,6 +848,46 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channels_sortUnread => 'Niezgłoszone';
@override
String get channels_createPrivateChannel => 'Utwórz Prywatny Kanał';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpieczone kluczem szyfrowym.';
@override
String get channels_joinPrivateChannel => 'Dołącz do Prywatnego Kanału';
@override
String get channels_joinPrivateChannelDesc => 'Ręcznie wprowadź klucz tajny.';
@override
String get channels_joinPublicChannel => 'Dołącz do kanału publicznego.';
@override
String get channels_joinPublicChannelDesc =>
'Każdy może dołączyć do tego kanału.';
@override
String get channels_joinHashtagChannel =>
'Dołącz do kanału oznaczanego hashtagiem';
@override
String get channels_joinHashtagChannelDesc =>
'Każdy może dołączyć do kanałów z hashtagami.';
@override
String get channels_scanQrCode => 'Skanuj kod QR';
@override
String get channels_scanQrCodeComingSoon => 'Wkrótce';
@override
String get channels_enterHashtag => 'Wprowadź hashtag';
@override
String get channels_hashtagHint => 'np. #zespół';
@override
String get chat_noMessages => 'Brak jeszcze wiadomości';
@@ -1474,6 +1534,10 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Zalogowanie się nie powiodło: $error';
}
@override
String get login_failedMessage =>
'Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.';
@override
String get common_reload => 'Ponownie załadować';
@@ -1543,6 +1607,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_management => 'Zarządzanie Powtórzami';
@override
String get room_management => 'Zarządzanie Serwerem Pokoju';
@override
String get repeater_managementTools => 'Narzędzia Zarządzania';
@@ -1566,6 +1633,13 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
@override
String get repeater_neighbours => 'Sąsiedzi';
@override
String get repeater_neighboursSubtitle =>
'Wyświetl sąsiedztwo zerowych hopów.';
@override
String get repeater_settings => 'Ustawienia';
@@ -2246,6 +2320,34 @@ class AppLocalizationsPl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Otrzymano dane sąsiedztwa';
@override
String get neighbors_requestTimedOut =>
'Sąsiedzi proszą o wyłączenie timingu.';
@override
String neighbors_errorLoading(String error) {
return 'Błąd podczas ładowania sąsiadów: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
@override
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Nieznana $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Usłyszano: $time temu';
}
@override
String get channelPath_title => 'Ścieżka pakietu';
@@ -2349,6 +2451,176 @@ class AppLocalizationsPl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Nieznany Powtarzacz';
@override
String get community_title => 'Społeczność';
@override
String get community_create => 'Utwórz Społeczność';
@override
String get community_createDesc =>
'Utwórz nową społeczność i udostępnij za pomocą kodu QR.';
@override
String get community_join => 'Dołącz';
@override
String get community_joinTitle => 'Dołącz do społeczności';
@override
String community_joinConfirmation(String name) {
return 'Czy chcesz dołączyć do społeczności \"$name\"?';
}
@override
String get community_scanQr => 'Skanuj QR kod społeczności';
@override
String get community_scanInstructions =>
'Skieruj kamerę w kierunku kodu QR społeczności.';
@override
String get community_showQr => 'Pokaż kod QR';
@override
String get community_publicChannel => 'Społeczność Publiczna';
@override
String get community_hashtagChannel => 'Hashtag Społeczności';
@override
String get community_name => 'Nazwa Społeczności';
@override
String get community_enterName => 'Wprowadź nazwę społeczności';
@override
String community_created(String name) {
return 'Społeczność \"$name\" została utworzona';
}
@override
String community_joined(String name) {
return 'Dołączył do społeczności \"$name\"';
}
@override
String get community_qrTitle => 'Dziel się Społecznością';
@override
String community_qrInstructions(String name) {
return 'Skanuj ten kod QR, aby dołączyć $name';
}
@override
String get community_hashtagPrivacyHint =>
'Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności';
@override
String get community_invalidQrCode => 'Nieprawidłowy kod QR społeczności.';
@override
String get community_alreadyMember => 'Już jesteś członkiem.';
@override
String community_alreadyMemberMessage(String name) {
return 'Jesteś już członkiem \"$name\".';
}
@override
String get community_addPublicChannel => 'Dodaj Kanał Publiczny Społeczności';
@override
String get community_addPublicChannelHint =>
'Automatycznie dodaj kanał publiczny dla tej społeczności.';
@override
String get community_noCommunities =>
'Nie dołączono jeszcze żadnych społeczności.';
@override
String get community_scanOrCreate =>
'Skanuj kod QR lub utwórz społeczność, aby zacząć.';
@override
String get community_manageCommunities => 'Zarządzaj Grupami';
@override
String get community_delete => 'Opuszczenie Społeczności';
@override
String community_deleteConfirm(String name) {
return 'Opuścić \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Spowoduje to również usunięcie $count kanału/kanałów i ich wiadomości.';
}
@override
String community_deleted(String name) {
return 'Opuszczono społeczność \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Dodaj hashtag społeczności';
@override
String get community_addHashtagChannelDesc =>
'Dodaj kanał z hashtagiem dla tej społeczności';
@override
String get community_selectCommunity => 'Wybierz społeczność';
@override
String get community_regularHashtag => 'Hashtag regular';
@override
String get community_regularHashtagDesc =>
'Publiczny hashtag (każdy może dołączyć)';
@override
String get community_communityHashtag => 'Hashtag Społeczności';
@override
String get community_communityHashtagDesc =>
'Dostępne tylko dla członków społeczności';
@override
String community_forCommunity(String name) {
return 'Dla $name';
}
@override
String get listFilter_tooltip => 'Filtruj i sortuj';
+273
View File
@@ -23,6 +23,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get common_cancel => 'Cancelar';
@override
String get common_ok => 'OK';
@override
String get common_connect => 'Conectar';
@@ -201,6 +204,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get settings_locationInvalid => 'Latitude ou longitude inválidos.';
@override
String get settings_locationGPSEnable => 'Ativar GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Habilita a atualização automática da localização via GPS.';
@override
String get settings_locationIntervalSec => 'Intervalo para GPS (Segundos)';
@override
String get settings_locationIntervalInvalid =>
'O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.';
@override
String get settings_latitude => 'Latitude';
@@ -649,6 +666,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Gerenciar Repetidor';
@override
String get contacts_manageRoom => 'Gerenciar Servidor de Sala';
@override
String get contacts_roomLogin => 'Login no Quarto';
@@ -829,6 +849,46 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channels_sortUnread => 'Não lido';
@override
String get channels_createPrivateChannel => 'Criar um Canal Privado';
@override
String get channels_createPrivateChannelDesc =>
'Protegido com uma chave secreta.';
@override
String get channels_joinPrivateChannel => 'Junte-se a um Canal Privado';
@override
String get channels_joinPrivateChannelDesc =>
'Inserir uma chave secreta manualmente.';
@override
String get channels_joinPublicChannel => 'Junte-se ao Canal Público';
@override
String get channels_joinPublicChannelDesc =>
'Qualquer pessoa pode entrar neste canal.';
@override
String get channels_joinHashtagChannel => 'Junte-se a um Canal com Hashtag';
@override
String get channels_joinHashtagChannelDesc =>
'Qualquer pessoa pode participar de canais com hashtag.';
@override
String get channels_scanQrCode => 'Digitalizar um Código QR';
@override
String get channels_scanQrCodeComingSoon => 'Em breve';
@override
String get channels_enterHashtag => 'Insira hashtag';
@override
String get channels_hashtagHint => 'ex. #equipe';
@override
String get chat_noMessages => 'Ainda não existem mensagens.';
@@ -1472,6 +1532,10 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Login falhou: $error';
}
@override
String get login_failedMessage =>
'Falha no login. A senha está incorreta ou o repetidor está inacessível.';
@override
String get common_reload => 'Recarregar';
@@ -1541,6 +1605,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_management => 'Gerenciamento de Repetidor';
@override
String get room_management => 'Gerenciamento de Servidor de Sala';
@override
String get repeater_managementTools => 'Ferramentas de Gerenciamento';
@@ -1564,6 +1631,13 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
@override
String get repeater_neighbours => 'Vizinhos';
@override
String get repeater_neighboursSubtitle =>
'Visualizar vizinhos de salto zero.';
@override
String get repeater_settings => 'Configurações';
@@ -2248,6 +2322,34 @@ class AppLocalizationsPt extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Dados dos Vizinhos Recebidos';
@override
String get neighbors_requestTimedOut =>
'Vizinhos solicitam tempo limite esgotado.';
@override
String neighbors_errorLoading(String error) {
return 'Erro ao carregar vizinhos: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
@override
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
@override
String neighbors_unknownContact(String pubkey) {
return '$pubkey Desconhecido';
}
@override
String neighbors_heardAgo(String time) {
return 'Ouvido: $time atrás';
}
@override
String get channelPath_title => 'Rótulo de Caminho de Pacote';
@@ -2351,6 +2453,177 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Repetidor Desconhecido';
@override
String get community_title => 'Comunidade';
@override
String get community_create => 'Criar Comunidade';
@override
String get community_createDesc =>
'Crie uma nova comunidade e compartilhe via código QR.';
@override
String get community_join => 'Junte-se';
@override
String get community_joinTitle => 'Junte-se à Comunidade';
@override
String community_joinConfirmation(String name) {
return 'Você gostaria de se juntar à comunidade \"$name\"?';
}
@override
String get community_scanQr => 'Digitalizar a QR Code da Comunidade';
@override
String get community_scanInstructions =>
'Aponte a câmera para um código QR da comunidade';
@override
String get community_showQr => 'Mostrar Código QR';
@override
String get community_publicChannel => 'Comunidade Pública';
@override
String get community_hashtagChannel => 'Hashtag da Comunidade';
@override
String get community_name => 'Nome da Comunidade';
@override
String get community_enterName => 'Insira o nome da comunidade';
@override
String community_created(String name) {
return 'Comunidade \"$name\" criada';
}
@override
String community_joined(String name) {
return 'Juntou-se à comunidade \"$name\"';
}
@override
String get community_qrTitle => 'Partilhar Comunidade';
@override
String community_qrInstructions(String name) {
return 'Escanear este código QR para juntar-se a $name';
}
@override
String get community_hashtagPrivacyHint =>
'Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade';
@override
String get community_invalidQrCode => 'Código QR da comunidade inválido';
@override
String get community_alreadyMember => 'Já é Membro';
@override
String community_alreadyMemberMessage(String name) {
return 'Você já é membro de \"$name\".';
}
@override
String get community_addPublicChannel =>
'Adicionar Canal Público da Comunidade';
@override
String get community_addPublicChannelHint =>
'Adicionar automaticamente o canal público para esta comunidade';
@override
String get community_noCommunities =>
'Ainda não foram adicionadas comunidades.';
@override
String get community_scanOrCreate =>
'Escaneie um código QR ou crie uma comunidade para começar.';
@override
String get community_manageCommunities => 'Gerenciar Comunidades';
@override
String get community_delete => 'Deixar Comunidade';
@override
String community_deleteConfirm(String name) {
return 'Sair de \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Isso também excluirá $count canal/canais e suas mensagens.';
}
@override
String community_deleted(String name) {
return 'Saiu da comunidade \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Adicionar Hashtag da Comunidade';
@override
String get community_addHashtagChannelDesc =>
'Adicionar um canal de hashtag para esta comunidade';
@override
String get community_selectCommunity => 'Selecione Comunidade';
@override
String get community_regularHashtag => 'Hashtag Regular';
@override
String get community_regularHashtagDesc =>
'Hashtag público (qualquer pessoa pode participar)';
@override
String get community_communityHashtag => 'Hashtag da Comunidade';
@override
String get community_communityHashtagDesc =>
'Apenas para membros da comunidade';
@override
String community_forCommunity(String name) {
return 'Para $name';
}
@override
String get listFilter_tooltip => 'Filtrar e ordenar';
+270 -1
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get common_cancel => 'Zrušiť';
@override
String get common_ok => 'OK\nDobre';
@override
String get common_connect => 'Pripojiť';
@@ -200,6 +203,20 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get settings_locationInvalid => 'Neplatná šírka alebo dĺžka.';
@override
String get settings_locationGPSEnable => 'Aktivovať GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Povolí automatické aktualizovanie polohy pomocou GPS.';
@override
String get settings_locationIntervalSec => 'Interval pre GPS (Sekundy)';
@override
String get settings_locationIntervalInvalid =>
'Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.';
@override
String get settings_latitude => 'Súradnica';
@@ -642,6 +659,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Spravovať opakované zoznamy';
@override
String get contacts_manageRoom => 'Spravovať server miestnosti';
@override
String get contacts_roomLogin => 'Prihlásenie do miestnosti';
@@ -824,6 +844,45 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channels_sortUnread => 'Nezriadené';
@override
String get channels_createPrivateChannel => 'Vytvorte súkromný kanál';
@override
String get channels_createPrivateChannelDesc =>
'Zabezpečené pomocou tajného kľúča.';
@override
String get channels_joinPrivateChannel => 'Pripojiť sa k súkromnému kanálu';
@override
String get channels_joinPrivateChannelDesc => 'Ručne zadajte tajný kľúč.';
@override
String get channels_joinPublicChannel => 'Pripojte sa k verejnému kanálu';
@override
String get channels_joinPublicChannelDesc =>
'Któvek sátó na tutó kanalizovát.';
@override
String get channels_joinHashtagChannel => 'Pripojte sa k Hashtag Kanálu';
@override
String get channels_joinHashtagChannelDesc =>
'Ktoekolikoľvek sa môže pridať do hashtag kanálov.';
@override
String get channels_scanQrCode => 'Skenujte QR kód';
@override
String get channels_scanQrCodeComingSoon => 'Čoskoro';
@override
String get channels_enterHashtag => 'Zadajte hashtag';
@override
String get channels_hashtagHint => 'napr. #tím';
@override
String get chat_noMessages => 'Zatiaľ žiadne správy.';
@@ -1061,7 +1120,7 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get chat_clearPathSubtitle =>
'Znovu nájsť vynútene pri nasledujacej pošlite';
'Znovu nájsť vynútene pri nasledujúcej pošlite';
@override
String get chat_pathCleared =>
@@ -1468,6 +1527,10 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Prihlásenie zlyhalo: $error';
}
@override
String get login_failedMessage =>
'Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.';
@override
String get common_reload => 'Načítať';
@@ -1537,6 +1600,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_management => 'Správa opakérov';
@override
String get room_management => 'Správa servera miestnosti';
@override
String get repeater_managementTools => 'Nástroje na správu';
@@ -1560,6 +1626,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
@override
String get repeater_neighbours => 'Súsezný';
@override
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
@override
String get repeater_settings => 'Nastavenia';
@@ -2237,6 +2309,34 @@ class AppLocalizationsSk extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Obdielo dáta suseda';
@override
String get neighbors_requestTimedOut => 'Súďia žiadajú o časové ukončenie.';
@override
String neighbors_errorLoading(String error) {
return 'Chyba pri načítaní susedov: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
@override
String get neighbors_noData =>
'Nie je dostupná žiadna informácia o susedoch.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Neznáma $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Počuli sme to: $time dozadu';
}
@override
String get channelPath_title => 'Cesta balíka';
@@ -2340,6 +2440,175 @@ class AppLocalizationsSk extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Neznáme opakovače';
@override
String get community_title => 'Komunita';
@override
String get community_create => 'Vytvoriť komunitu';
@override
String get community_createDesc =>
'Vytvorte novú komunitu a zdieľajte cez QR kód.';
@override
String get community_join => 'Pripojiť';
@override
String get community_joinTitle => 'Pripojiť sa k spoločenstvu';
@override
String community_joinConfirmation(String name) {
return 'Chceš sa pridať do komunity \"$name\"?';
}
@override
String get community_scanQr => 'Skontrolujte komunitný QR kód';
@override
String get community_scanInstructions =>
'Zamerte kameru na komunitný QR kód.';
@override
String get community_showQr => 'Zobraziť QR kód';
@override
String get community_publicChannel => 'Komunita verejná';
@override
String get community_hashtagChannel => 'Komunitný Hashtag';
@override
String get community_name => 'Komunita';
@override
String get community_enterName => 'Zadajte názov komunity';
@override
String community_created(String name) {
return 'Komunita \"$name\" vytvorená';
}
@override
String community_joined(String name) {
return 'Pripojená komunita \"$name\"';
}
@override
String get community_qrTitle => 'Zdieľť komunitu';
@override
String community_qrInstructions(String name) {
return 'Skenejte tento QR kód, aby ste sa pripojili k $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Hashtagové kanály komunity sú prístupné len členom komunity';
@override
String get community_invalidQrCode => 'Neplatná QR kód komunity.';
@override
String get community_alreadyMember => 'Už ste členom.';
@override
String community_alreadyMemberMessage(String name) {
return 'Vy ste už členom \"$name\".';
}
@override
String get community_addPublicChannel => 'Pridať verejný komunikačný kanál';
@override
String get community_addPublicChannelHint =>
'Automaticky prida verejný kanál pre túto komunitu.';
@override
String get community_noCommunities =>
'Zatiaľ ste sa nepripojili k žiadnej komunite';
@override
String get community_scanOrCreate =>
'Skene QR kód alebo vytvor komunitu na začiatok.';
@override
String get community_manageCommunities => 'Spravovať komunity';
@override
String get community_delete => 'Nechajte komunitu';
@override
String community_deleteConfirm(String name) {
return 'Opustiť \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Tým sa tiež vymaže $count kanál/kanálov a ich správy.';
}
@override
String community_deleted(String name) {
return 'Opustená komunita \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Pridať komunitný hashtag';
@override
String get community_addHashtagChannelDesc =>
'Pridajte hashtagový kanál pre túto komunitu.';
@override
String get community_selectCommunity => 'Vyberte komunitu';
@override
String get community_regularHashtag => 'Zvyčajný hashtag';
@override
String get community_regularHashtagDesc =>
'Veľký hashtag (ktočokoľvek sa môže pridať)';
@override
String get community_communityHashtag => 'Komunitný Hashtag';
@override
String get community_communityHashtagDesc => 'Špecifické pre členov komunity';
@override
String community_forCommunity(String name) {
return 'Pre $name';
}
@override
String get listFilter_tooltip => 'Filtrovať a triediť';
+273 -4
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get common_cancel => 'Prekliči';
@override
String get common_ok => 'V redu';
@override
String get common_connect => 'Poveži se';
@@ -200,6 +203,20 @@ class AppLocalizationsSl extends AppLocalizations {
String get settings_locationInvalid =>
'Neveljna zemeljska širina ali dolžina.';
@override
String get settings_locationGPSEnable => 'Omogoči GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Omogoči samodejno posodabljanje lokacije z GPS-jem.';
@override
String get settings_locationIntervalSec => 'Interval za GPS (Sekunde)';
@override
String get settings_locationIntervalInvalid =>
'Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.';
@override
String get settings_latitude => 'Širina';
@@ -435,7 +452,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get appSettings_enableNotificationsSubtitle =>
'Prejmujte obvestila o sporočilih in oglasih';
'Prejmite obvestila o sporočilih in oglasih';
@override
String get appSettings_notificationPermissionDenied =>
@@ -631,7 +648,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_noContactsFound =>
'Niti ena osebe ali skupine ni najdena.';
'Niti ena oseba ali skupine ni najdena.';
@override
String get contacts_deleteContact => 'Izbrisati Kontakt';
@@ -644,6 +661,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Upravljajte Ponovitve';
@override
String get contacts_manageRoom => 'Upravljajte strežnik sobe';
@override
String get contacts_roomLogin => 'Vnos v sobo';
@@ -680,7 +700,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get contacts_noContactsMatchFilter =>
'Niti ena osebe ne ustreza vašemu kriteriju.';
'Niti ena oseba ne ustreza vašemu kriteriju.';
@override
String get contacts_noMembers => 'Nič članov.';
@@ -824,6 +844,45 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channels_sortUnread => 'Nerešeno';
@override
String get channels_createPrivateChannel => 'Ustvari zasebno kanal.';
@override
String get channels_createPrivateChannelDesc =>
'Varno zaklenjeno s skrivnim ključem.';
@override
String get channels_joinPrivateChannel => 'Pridružite se zasebni skupini';
@override
String get channels_joinPrivateChannelDesc => 'Ročno vnesite zaporni ključ.';
@override
String get channels_joinPublicChannel => 'Pridružite se javnemu kanalu';
@override
String get channels_joinPublicChannelDesc =>
'Kdor karkoli je, lahko se pridruži tej skupini.';
@override
String get channels_joinHashtagChannel => 'Pridružite se Kanalu z Hashtagom';
@override
String get channels_joinHashtagChannelDesc =>
'Kdor karkoli, lahko se pridruži hashtag kanalom.';
@override
String get channels_scanQrCode => 'Skeniraj QR kodo';
@override
String get channels_scanQrCodeComingSoon => 'Prihajajoča';
@override
String get channels_enterHashtag => 'Vnesite hashtag';
@override
String get channels_hashtagHint => 'npr. #ekipa';
@override
String get chat_noMessages => 'Še ni sporočil.';
@@ -1147,7 +1206,7 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get map_nodesNeedGps =>
'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.';
'Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.';
@override
String map_nodesCount(int count) {
@@ -1469,6 +1528,10 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Prijava je bila neuspešna: $error';
}
@override
String get login_failedMessage =>
'Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.';
@override
String get common_reload => 'Ponovno naloži';
@@ -1537,6 +1600,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get repeater_management => 'Upravljanje ponovitve';
@override
String get room_management => 'Upravljanje stremlišča';
@override
String get repeater_managementTools => 'Upravne orodje';
@@ -1561,6 +1627,12 @@ class AppLocalizationsSl extends AppLocalizations {
String get repeater_cliSubtitle =>
'Pošlji ukazne povelje na ponovitveno enoto.';
@override
String get repeater_neighbours => 'Sosedi';
@override
String get repeater_neighboursSubtitle => 'Pogledati nič sosednjih hopjev.';
@override
String get repeater_settings => 'Nastavitve';
@@ -2242,6 +2314,34 @@ class AppLocalizationsSl extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Prejeto podatke o sosedih';
@override
String get neighbors_requestTimedOut =>
'Sosedi zahtevajo izklop po dogovoru.';
@override
String neighbors_errorLoading(String error) {
return 'Napaka pri obnašanju sosedov: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Ponovitve Sosedi';
@override
String get neighbors_noData => 'Niso na voljo podatki o sosedih.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Nepoznano $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Udeleženec je prejel sporočilo $time nazaj.';
}
@override
String get channelPath_title => 'Pot do paketa';
@@ -2345,6 +2445,175 @@ class AppLocalizationsSl extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Nepoznati ponovitelj';
@override
String get community_title => 'Skupnost';
@override
String get community_create => 'Ustvari skupnost';
@override
String get community_createDesc =>
'Ustvari novo skupnost in jo deli preko QR kode.';
@override
String get community_join => 'Pridružiti se';
@override
String get community_joinTitle => 'Pridružite se skupnosti';
@override
String community_joinConfirmation(String name) {
return 'Želiš se pridružiti skupnosti \"$name\"?';
}
@override
String get community_scanQr => 'Skeniraj QR kode skupnosti';
@override
String get community_scanInstructions =>
'Nasmerite kamero s skupnostnim QR kodom.';
@override
String get community_showQr => 'Pokaži QR kodo';
@override
String get community_publicChannel => 'Skupnostna javna';
@override
String get community_hashtagChannel => 'Skupnostni hashtag';
@override
String get community_name => 'Komunitarne ime';
@override
String get community_enterName => 'Vnesite ime skupnosti';
@override
String community_created(String name) {
return 'Skupnost \"$name\" je bila ustvarila.';
}
@override
String community_joined(String name) {
return 'Prilojen k skupnosti \"$name\"';
}
@override
String get community_qrTitle => 'Delite skupnost';
@override
String community_qrInstructions(String name) {
return 'Skenirajte to QR kodo za vključitev $name.';
}
@override
String get community_hashtagPrivacyHint =>
'Hashtag kanali skupnosti so dostopni samo članom skupnosti';
@override
String get community_invalidQrCode => 'Neveljaven QR koden skupnosti';
@override
String get community_alreadyMember => 'Že član';
@override
String community_alreadyMemberMessage(String name) {
return 'Kljub temu ste že član/ka $name.';
}
@override
String get community_addPublicChannel => 'Dodaj Objavni Kanal Komunitarja';
@override
String get community_addPublicChannelHint =>
'Samodejno dodaj javni kanal za to skupnost.';
@override
String get community_noCommunities => 'Še nobena skupnost se ni pridružila.';
@override
String get community_scanOrCreate =>
'Skenirajte QR kodo ali ustvarite skupnost za začetek.';
@override
String get community_manageCommunities => 'Upravljajte skupnosti';
@override
String get community_delete => 'Opusti skupnost';
@override
String community_deleteConfirm(String name) {
return 'Zapustiti \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'To bo izbrisalo tudi $count kanal/kanalov in njihova sporočila.';
}
@override
String community_deleted(String name) {
return 'Zapustil skupnost \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Dodaj Oznako Obštnine';
@override
String get community_addHashtagChannelDesc =>
'Dodajte hashtag kanal za to skupnost.';
@override
String get community_selectCommunity => 'Izberi skupnost';
@override
String get community_regularHashtag => 'Oznaka s hashtagom';
@override
String get community_regularHashtagDesc =>
'javna oznaka (kateri koli lahko sodelujejo)';
@override
String get community_communityHashtag => 'Skupnostni hashtag';
@override
String get community_communityHashtagDesc =>
'Izključeno za uporabnike skupnosti';
@override
String community_forCommunity(String name) {
return 'Za $name';
}
@override
String get listFilter_tooltip => 'Filtri in vrstiči';
+269
View File
@@ -23,6 +23,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get common_cancel => 'Avbryt';
@override
String get common_ok => 'Okej';
@override
String get common_connect => 'Anslut';
@@ -199,6 +202,20 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get settings_locationInvalid => 'Ogiltig latitud eller longitud.';
@override
String get settings_locationGPSEnable => 'Aktivera GPS';
@override
String get settings_locationGPSEnableSubtitle =>
'Aktivera automatiska uppdateringar av platsen med hjälp av GPS.';
@override
String get settings_locationIntervalSec => 'Interval för GPS (Sekunder)';
@override
String get settings_locationIntervalInvalid =>
'Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.';
@override
String get settings_latitude => 'Latitud';
@@ -638,6 +655,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get contacts_manageRepeater => 'Hantera Upprepare';
@override
String get contacts_manageRoom => 'Hantera Rumserver';
@override
String get contacts_roomLogin => 'Rum Inloggning';
@@ -817,6 +837,46 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channels_sortUnread => 'Oläst';
@override
String get channels_createPrivateChannel => 'Skapa en privat kanal';
@override
String get channels_createPrivateChannelDesc =>
'Skyddat med en hemlig nyckel.';
@override
String get channels_joinPrivateChannel => 'Gå med i en Privat Kanal';
@override
String get channels_joinPrivateChannelDesc =>
'Ange en hemlig nyckel manuellt.';
@override
String get channels_joinPublicChannel => 'Gå med i den Offentliga Kanalen';
@override
String get channels_joinPublicChannelDesc =>
'Vem som helst kan gå med i denna kanal.';
@override
String get channels_joinHashtagChannel => 'Gå med i en Hashtagkanal';
@override
String get channels_joinHashtagChannelDesc =>
'Väldigt enkelt att gå med i hashtag-kanaler.';
@override
String get channels_scanQrCode => 'Skanna en QR-kod';
@override
String get channels_scanQrCodeComingSoon => 'Kommer snart';
@override
String get channels_enterHashtag => 'Ange hashtag';
@override
String get channels_hashtagHint => 't.ex. #team';
@override
String get chat_noMessages => 'Inga meddelanden ännu';
@@ -1457,6 +1517,10 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Inloggning misslyckades: $error';
}
@override
String get login_failedMessage =>
'Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.';
@override
String get common_reload => 'Ladda om';
@@ -1525,6 +1589,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_management => 'Återuppspelarens Hantering';
@override
String get room_management => 'Rumserverhantering';
@override
String get repeater_managementTools => 'Administrationsverktyg';
@@ -1548,6 +1615,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
@override
String get repeater_neighbours => 'Grannar';
@override
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
@override
String get repeater_settings => 'Inställningar';
@@ -2225,6 +2298,33 @@ class AppLocalizationsSv extends AppLocalizations {
return '$celsius°C / $fahrenheit°F';
}
@override
String get neighbors_receivedData => 'Mottagna grannars data';
@override
String get neighbors_requestTimedOut => 'Grannar begär tidsinställd utskick.';
@override
String neighbors_errorLoading(String error) {
return 'Fel vid inläsning av grannar: $error';
}
@override
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
@override
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
@override
String neighbors_unknownContact(String pubkey) {
return 'Okänd $pubkey';
}
@override
String neighbors_heardAgo(String time) {
return 'Hördes: $time sedan';
}
@override
String get channelPath_title => 'Paketväg';
@@ -2328,6 +2428,175 @@ class AppLocalizationsSv extends AppLocalizations {
@override
String get channelPath_unknownRepeater => 'Okänt Upprepare';
@override
String get community_title => 'Gemenskap';
@override
String get community_create => 'Skapa Gemenskap';
@override
String get community_createDesc =>
'Skapa en ny gemenskap och dela via QR-kod.';
@override
String get community_join => 'Gå med';
@override
String get community_joinTitle => 'Gå med i gemenskapen';
@override
String community_joinConfirmation(String name) {
return 'Vill du gå med i communityn \"$name\"?';
}
@override
String get community_scanQr => 'Skanna Gemenskapens QR';
@override
String get community_scanInstructions =>
'Rikta kameran mot en QR-kod i communityn';
@override
String get community_showQr => 'Visa QR-kod';
@override
String get community_publicChannel => 'Föreningens Offentliga';
@override
String get community_hashtagChannel => 'Community Hashtag';
@override
String get community_name => 'Gemenskapens namn';
@override
String get community_enterName => 'Ange communities namn';
@override
String community_created(String name) {
return 'Community \"$name\" har skapats';
}
@override
String community_joined(String name) {
return 'Medlem i communityn \"$name\"';
}
@override
String get community_qrTitle => 'Dela Gemenskap';
@override
String community_qrInstructions(String name) {
return 'Skanna denna QR-kod för att gå med i \"$name\"';
}
@override
String get community_hashtagPrivacyHint =>
'Community-hashtagkanaler kan endast nås av medlemmar i communityn';
@override
String get community_invalidQrCode => 'Ogiltig community QR-kod';
@override
String get community_alreadyMember => 'Är redan medlem';
@override
String community_alreadyMemberMessage(String name) {
return 'Du är redan medlem av \"$name\".';
}
@override
String get community_addPublicChannel =>
'Lägg till Gemenskapskanal (Offentlig)';
@override
String get community_addPublicChannelHint =>
'Lägg automatiskt till den offentliga kanalen för denna community';
@override
String get community_noCommunities => 'Inga gemenskaper har anslutats ännu';
@override
String get community_scanOrCreate =>
'Skanna en QR-kod eller skapa en community för att komma igång';
@override
String get community_manageCommunities => 'Hantera Gemenskaper';
@override
String get community_delete => 'Lämna Gemenskap';
@override
String community_deleteConfirm(String name) {
return 'Lämna \"$name\"?';
}
@override
String community_deleteChannelsWarning(int count) {
return 'Detta kommer också att radera $count kanal/kanaler och deras meddelanden.';
}
@override
String community_deleted(String name) {
return 'Lämnade community \"$name\"';
}
@override
String get community_regenerateSecret => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$name\"';
}
@override
String get community_addHashtagChannel => 'Lägg till Gemenskapens Hashtag';
@override
String get community_addHashtagChannelDesc =>
'Lägg till en hashtag-kanal för denna community';
@override
String get community_selectCommunity => 'Välj Gemenskap';
@override
String get community_regularHashtag => 'Vanlig Hash Tag';
@override
String get community_regularHashtagDesc =>
'Offentlig hashtag (alla kan gå med)';
@override
String get community_communityHashtag => 'Community Hashtag';
@override
String get community_communityHashtagDesc => 'Endast för medlemmar';
@override
String community_forCommunity(String name) {
return 'För $name';
}
@override
String get listFilter_tooltip => 'Filtrera och sortera';
+254
View File
@@ -23,6 +23,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get common_cancel => '取消';
@override
String get common_ok => '好的';
@override
String get common_connect => '连接';
@@ -196,6 +199,18 @@ class AppLocalizationsZh extends AppLocalizations {
@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 => '纬度';
@@ -611,6 +626,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get contacts_manageRepeater => '管理重复项';
@override
String get contacts_manageRoom => '管理房间服务器';
@override
String get contacts_roomLogin => '房间登录';
@@ -789,6 +807,42 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get channels_sortUnread => '未读';
@override
String get channels_createPrivateChannel => '创建私聊频道';
@override
String get channels_createPrivateChannelDesc => '使用密钥保护。';
@override
String get channels_joinPrivateChannel => '加入私密频道';
@override
String get channels_joinPrivateChannelDesc => '手动输入密钥。';
@override
String get channels_joinPublicChannel => '加入公共频道';
@override
String get channels_joinPublicChannelDesc => '任何人都可以加入这个频道。';
@override
String get channels_joinHashtagChannel => '加入标签频道';
@override
String get channels_joinHashtagChannelDesc => '任何人都可以加入话题频道。';
@override
String get channels_scanQrCode => '扫描二维码';
@override
String get channels_scanQrCodeComingSoon => '即将到来';
@override
String get channels_enterHashtag => '输入标签';
@override
String get channels_hashtagHint => '例如 #团队';
@override
String get chat_noMessages => '目前还没有消息';
@@ -1411,6 +1465,9 @@ class AppLocalizationsZh extends AppLocalizations {
return '登录失败:$error';
}
@override
String get login_failedMessage => '登录失败。密码不正确或中继器不可达。';
@override
String get common_reload => '重新加载';
@@ -1474,6 +1531,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_management => '重复器管理';
@override
String get room_management => '房间服务器管理';
@override
String get repeater_managementTools => '管理工具';
@@ -1495,6 +1555,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get repeater_cliSubtitle => '发送命令到重复器';
@override
String get repeater_neighbours => '邻居';
@override
String get repeater_neighboursSubtitle => '查看零跳邻居。';
@override
String get repeater_settings => '设置';
@@ -2125,6 +2191,33 @@ class AppLocalizationsZh extends AppLocalizations {
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_repeatersNeighbours => '重复器邻居';
@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 => '数据包路径';
@@ -2225,6 +2318,167 @@ class AppLocalizationsZh extends AppLocalizations {
@override
String get channelPath_unknownRepeater => '未知重复器';
@override
String get community_title => '社区';
@override
String get community_create => '创建社区';
@override
String get community_createDesc => '创建新的社区并可通过二维码分享。';
@override
String get community_join => '加入';
@override
String get community_joinTitle => '加入社区';
@override
String community_joinConfirmation(String name) {
return '您想加入社区 \"$name\" 吗?';
}
@override
String get community_scanQr => '扫描社区二维码';
@override
String get community_scanInstructions => '将相机对准社区二维码';
@override
String get community_showQr => '显示二维码';
@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 '扫描此二维码加入$name';
}
@override
String get community_hashtagPrivacyHint => '社区标签频道仅社区成员可加入';
@override
String get community_invalidQrCode => '无效的社区二维码';
@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 => '扫描二维码或创建社区开始';
@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 => 'Regenerate Secret';
@override
String community_regenerateSecretConfirm(String name) {
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
}
@override
String get community_regenerate => 'Regenerate';
@override
String community_secretRegenerated(String name) {
return 'Secret regenerated for \"$name\"';
}
@override
String get community_updateSecret => 'Update Secret';
@override
String community_secretUpdated(String name) {
return 'Secret updated for \"$name\"';
}
@override
String community_scanToUpdateSecret(String name) {
return 'Scan the new QR code to update the secret for \"$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 => '筛选和排序';
+239 -90
View File
@@ -2,7 +2,7 @@
"@@locale": "nl",
"appTitle": "MeshCore Open",
"nav_contacts": "Contacten",
"nav_channels": "Kanaal",
"nav_channels": "Kanalen",
"nav_map": "Kaart",
"common_cancel": "Annuleren",
"common_connect": "Verbinden",
@@ -10,12 +10,12 @@
"common_save": "Opslaan",
"common_delete": "Verwijderen",
"common_close": "Sluiten",
"common_edit": "Bewerk",
"common_edit": "Bewerken",
"common_add": "Toevoegen",
"common_settings": "Instellingen",
"common_disconnect": "Verbinden verbreken",
"common_disconnect": "Verbinding verbreken",
"common_connected": "Verbonden",
"common_disconnected": "Ontkoppeld",
"common_disconnected": "Verbinding verbroken",
"common_create": "Maak",
"common_continue": "Doorgaan",
"common_share": "Delen",
@@ -23,10 +23,10 @@
"common_retry": "Nogmaals proberen",
"common_hide": "Verbergen",
"common_remove": "Verwijderen",
"common_enable": "Aktivatie",
"common_enable": "Activeren",
"common_disable": "Uitschakelen",
"common_reboot": "Herstarten",
"common_loading": "Laad...",
"common_loading": "Laden...",
"common_notAvailable": "—",
"common_voltageValue": "{volts} V",
"@common_voltageValue": {
@@ -78,7 +78,7 @@
"settings_nodeSettings": "Node Instellingen",
"settings_nodeName": "Node Naam",
"settings_nodeNameNotSet": "Niet ingesteld",
"settings_nodeNameHint": "Voer knooppuntnaam in",
"settings_nodeNameHint": "Voer nodenaam in",
"settings_nodeNameUpdated": "Naam bijgewerkt",
"settings_radioSettings": "Radio Instellingen",
"settings_radioSettingsSubtitle": "Frequentie, vermogen, spredfactor",
@@ -129,7 +129,7 @@
"settings_infoBattery": "Batterij",
"settings_infoPublicKey": "Openbare Sleutel",
"settings_infoContactsCount": "Aantal Contacten",
"settings_infoChannelCount": "Kanaal Aantal",
"settings_infoChannelCount": "Aantal Kanalen",
"settings_presets": "Presets",
"settings_preset915Mhz": "915 MHz",
"settings_preset868Mhz": "868 MHz",
@@ -143,8 +143,8 @@
"settings_txPower": "TX Vermogen (dBm)",
"settings_txPowerHelper": "0 - 22",
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
"settings_longRange": "Lang Bereik",
"settings_fastSpeed": "Snelle Snelheid",
"settings_longRange": "Lange Afstand",
"settings_fastSpeed": "Hoge Snelheid",
"settings_error": "Fout: {message}",
"@settings_error": {
"placeholders": {
@@ -157,7 +157,7 @@
"appSettings_appearance": "Uiterlijk",
"appSettings_theme": "Thema",
"appSettings_themeSystem": "Standaardinstelling",
"appSettings_themeLight": "Helder",
"appSettings_themeLight": "Licht",
"appSettings_themeDark": "Donker",
"appSettings_language": "Taal",
"appSettings_languageSystem": "Standaardinstelling",
@@ -185,16 +185,16 @@
"appSettings_channelMessageNotifications": "Kanaal Bericht Meldingen",
"appSettings_channelMessageNotificationsSubtitle": "Toon notificatie bij het ontvangen van kanaalberichten",
"appSettings_advertisementNotifications": "Advertentie-meldingen",
"appSettings_advertisementNotificationsSubtitle": "Toon notificatie wanneer nieuwe knooppunten worden ontdekt",
"appSettings_advertisementNotificationsSubtitle": "Toon notificatie wanneer nieuwe nodes worden ontdekt",
"appSettings_messaging": "Berichten",
"appSettings_clearPathOnMaxRetry": "Duidelijke Pad op Max Retry",
"appSettings_clearPathOnMaxRetry": "Wis Pad op Max Retry",
"appSettings_clearPathOnMaxRetrySubtitle": "Reset contactpad na 5 mislukte verzendpogingen",
"appSettings_pathsWillBeCleared": "De paden worden na 5 mislukte pogingen leeggehaald.",
"appSettings_pathsWillNotBeCleared": "Padoms worden niet automatisch verwijderd",
"appSettings_autoRouteRotation": "Automatische Route Rotatie",
"appSettings_autoRouteRotationSubtitle": "Wissel tussen de beste paden en floodmodus over.",
"appSettings_autoRouteRotationEnabled": "Automatische routeplanning rotatie ingeschakeld",
"appSettings_autoRouteRotationDisabled": "Automatische routeplanning rotatie is uitgeschakeld",
"appSettings_autoRouteRotation": "Route Automatisch Roteren",
"appSettings_autoRouteRotationSubtitle": "Verwissel tussen beste pad en floodmodus.",
"appSettings_autoRouteRotationEnabled": "Automatische route rotatie ingeschakeld",
"appSettings_autoRouteRotationDisabled": "Automatische route rotatie is uitgeschakeld",
"appSettings_battery": "Batterij",
"appSettings_batteryChemistry": "Batterijchemie",
"appSettings_batteryChemistryPerDevice": "Instellen per apparaat ({deviceName})",
@@ -210,15 +210,15 @@
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
"appSettings_mapDisplay": "Kaartweergave",
"appSettings_showRepeaters": "Toon Herhalingen",
"appSettings_showRepeatersSubtitle": "Toon herhalende knoopjes op de kaart",
"appSettings_showRepeaters": "Toon Repeaters",
"appSettings_showRepeatersSubtitle": "Toon repeaternodes op de kaart",
"appSettings_showChatNodes": "Chat Nodes tonen",
"appSettings_showChatNodesSubtitle": "Chatnodes weergeven op de kaart",
"appSettings_showOtherNodes": "Toon Andere Knopen",
"appSettings_showOtherNodesSubtitle": "Toon andere knooptypes op de kaart",
"appSettings_showOtherNodes": "Toon Andere Nodes",
"appSettings_showOtherNodesSubtitle": "Toon andere nodetypes op de kaart",
"appSettings_timeFilter": "Filter op tijd",
"appSettings_timeFilterShowAll": "Alle knooppunten tonen",
"appSettings_timeFilterShowLast": "Toon knopen van de laatste {hours} uur",
"appSettings_timeFilterShowAll": "Alle nodes tonen",
"appSettings_timeFilterShowLast": "Toon nodes van de laatste {hours} uur",
"@appSettings_timeFilterShowLast": {
"placeholders": {
"hours": {
@@ -227,8 +227,8 @@
}
},
"appSettings_mapTimeFilter": "Filter tijd op kaart",
"appSettings_showNodesDiscoveredWithin": "Toon knooppunten ontdekt binnen:",
"appSettings_allTime": "Alle tijd",
"appSettings_showNodesDiscoveredWithin": "Toon nodes ontdekt binnen:",
"appSettings_allTime": "Altijd",
"appSettings_lastHour": "Laat uur",
"appSettings_last6Hours": "laatste 6 uur",
"appSettings_last24Hours": "De laatste 24 uur",
@@ -266,7 +266,7 @@
}
}
},
"contacts_manageRepeater": "Beheer Herhaling",
"contacts_manageRepeater": "Beheer Repeater",
"contacts_roomLogin": "Ruimte Inloggen",
"contacts_openChat": "Open Chat",
"contacts_editGroup": "Groep bewerken",
@@ -294,7 +294,7 @@
"contacts_noContactsMatchFilter": "Geen contacten matchen met uw filter",
"contacts_noMembers": "Geen leden",
"contacts_lastSeenNow": "Laatste keer gezien nu",
"contacts_lastSeenMinsAgo": "Laast gezien {minutes} minuten geleden",
"contacts_lastSeenMinsAgo": "Laatst gezien {minutes} minuten geleden",
"@contacts_lastSeenMinsAgo": {
"placeholders": {
"minutes": {
@@ -539,7 +539,7 @@
"chat_pathManagement": "Beheer van Paden",
"chat_routingMode": "Routeerwijze",
"chat_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"chat_forceFloodMode": "Dwing Overstromingsmodus",
"chat_forceFloodMode": "Dwing Floodsmodus",
"chat_recentAckPaths": "Recente ACK Paden (tik om te gebruiken):",
"chat_pathHistoryFull": "De voorgeschiedenis is vol. Verwijder vermeldingen om er nieuwe aan toe te voegen.",
"chat_hopSingular": "Hop",
@@ -562,7 +562,7 @@
"chat_clearPathSubtitle": "Dwing herontdekking bij volgende verzending",
"chat_pathCleared": "Pad is vrijgegeven. Volgende bericht herontdekt route.",
"chat_floodModeSubtitle": "Gebruik de route-schakelaar in de app-balk",
"chat_floodModeEnabled": "Overstromingsmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.",
"chat_floodModeEnabled": "Floodmodus is ingeschakeld. Schakel dit uit via het route-icoon in de app-balk.",
"chat_fullPath": "Volledige Pad",
"chat_pathDetailsNotAvailable": "De paddetails zijn nog niet beschikbaar. Probeer een bericht te sturen om te vernieuwen.",
"chat_pathSetHops": "Pad ingesteld: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
@@ -583,9 +583,9 @@
"chat_path": "Pad",
"chat_publicKey": "Openbare Sleutel",
"chat_compressOutgoingMessages": "Verzenden van uitgaande berichten comprimeren",
"chat_floodForced": "Overstroming (gedwongen)",
"chat_directForced": "Direct (gedwongen)",
"chat_hopsForced": "{count} sprongen (gedwongen)",
"chat_floodForced": "Flood (afgedwongen)",
"chat_directForced": "Direct (afgedwongen)",
"chat_hopsForced": "{count} hops (afgedwongen)",
"@chat_hopsForced": {
"placeholders": {
"count": {
@@ -593,7 +593,7 @@
}
}
},
"chat_floodAuto": "Overstroming (auto)",
"chat_floodAuto": "Flood (auto)",
"chat_direct": "Direct",
"chat_poiShared": "Gedeelde POI",
"chat_unread": "Nieuw: {count}",
@@ -605,7 +605,7 @@
}
},
"map_title": "Node Map",
"map_noNodesWithLocation": "Geen knopen met locatiegegevens",
"map_noNodesWithLocation": "Geen nodes met locatiegegevens",
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
"map_nodesCount": "Nodes: {count}",
"@map_nodesCount": {
@@ -624,7 +624,7 @@
}
},
"map_chat": "Chat",
"map_repeater": "Herhaling",
"map_repeater": "Repeater",
"map_room": "Ruimte",
"map_sensor": "Sensor",
"map_pinDm": "Verzenden als bericht (DM)",
@@ -652,11 +652,11 @@
}
},
"map_connectToShareMarkers": "Verbind met een apparaat om markers te delen",
"map_filterNodes": "Filter Knopen",
"map_nodeTypes": "Node Types",
"map_chatNodes": "Chat Nodes",
"map_repeaters": "Herhalingen",
"map_otherNodes": "Andere knooppunten",
"map_filterNodes": "Filter Nodes",
"map_nodeTypes": "Nodetypes",
"map_chatNodes": "Chatnodes",
"map_repeaters": "Repeaters",
"map_otherNodes": "Andere Nodes",
"map_keyPrefix": "Prefix sleutel",
"map_filterByKeyPrefix": "Filteren op sleutelvoorgemeld",
"map_publicKeyPrefix": "Openbare sleutelvoorgemeld",
@@ -665,7 +665,7 @@
"map_lastSeenTime": "Laatste Bekeken Tijd",
"map_sharedPin": "Gedeelde pin",
"map_joinRoom": "Sluit Kamer",
"map_manageRepeater": "Beheer Herhaling",
"map_manageRepeater": "Beheer Repeater",
"mapCache_title": "Offline Kaarten Cache",
"mapCache_selectAreaFirst": "Select een gebied om eerst in de cache op te slaan",
"mapCache_noTilesToDownload": "Geen tiles te downloaden voor dit gebied.",
@@ -788,18 +788,18 @@
"time_allTime": "Alle tijd",
"dialog_disconnect": "Verbinden verbreken",
"dialog_disconnectConfirm": "Ben je er zeker van dat je verbinding met dit apparaat wilt verbreken?",
"login_repeaterLogin": "Herhaalders Inloggen",
"login_repeaterLogin": "Inloggen Repeater",
"login_roomLogin": "Ruimte Inloggen",
"login_password": "Wachtwoord",
"login_enterPassword": "Wachtwoord invoeren",
"login_savePassword": "Wachtwoord opslaan",
"login_savePasswordSubtitle": "Het wachtwoord wordt veilig op dit apparaat opgeslagen.",
"login_repeaterDescription": "Voer het wachtwoord van de herhaling in om instellingen en status te openen.",
"login_repeaterDescription": "Voer het wachtwoord van de repeater in om instellingen en status te openen.",
"login_roomDescription": "Voer het wachtwoord van de kamer in om toegang te krijgen tot instellingen en status.",
"login_routing": "Routing",
"login_routingMode": "Routeerwijze",
"login_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"login_forceFloodMode": "Dwing Overstromingsmodus",
"login_forceFloodMode": "Dwing Floodmodus Af",
"login_managePaths": "Padbeheer",
"login_login": "Inloggen",
"login_attempt": "Poging {current}/{max}",
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Inloggen mislukt. Het wachtwoord is onjuist of de repeater is niet bereikbaar.",
"common_reload": "Opnieuw laden",
"common_clear": "Schoonmaken",
"path_currentPath": "Huidige pad: {path}",
@@ -846,7 +847,7 @@
"path_labelHexPrefixes": "Pad (hex-voorkeursletters)",
"path_helperMaxHops": "Maximaal 64 sprongen. Elke prefix is 2 hexadecimale tekens (1 byte)",
"path_selectFromContacts": "Of select contacten:",
"path_noRepeatersFound": "Geen herhalingen of zaalservers gevonden.",
"path_noRepeatersFound": "Geen repeaters of roomservers gevonden.",
"path_customPathsRequire": "Aangepaste paden vereisen tussentse overstappen die berichten kunnen doorgeven.",
"path_invalidHexPrefixes": "Ongeldige hex-voorkeursletters: {prefixes}",
"@path_invalidHexPrefixes": {
@@ -858,20 +859,20 @@
},
"path_tooLong": "Pad is te lang. Maximaal 64 sprongen zijn toegestaan.",
"path_setPath": "Stel Pad in",
"repeater_management": "Beheer Herhalingen",
"repeater_management": "Beheer Repeaters",
"repeater_managementTools": "Beheerinstrumenten",
"repeater_status": "Status",
"repeater_statusSubtitle": "Status, statistieken en buren bekijken",
"repeater_telemetry": "Telemetry",
"repeater_telemetrySubtitle": "Bekijk telemetrie van sensoren en systeemgegevens",
"repeater_cli": "CLI",
"repeater_cliSubtitle": "Verzend commando's naar de herhaaldere",
"repeater_cliSubtitle": "Verzend commando's naar de repeater",
"repeater_settings": "Instellingen",
"repeater_settingsSubtitle": "Configureer repeaterparameters",
"repeater_statusTitle": "Status herhalen",
"repeater_statusTitle": "Status repeater",
"repeater_routingMode": "Routeerwijze",
"repeater_autoUseSavedPath": "Automatisch (gebruik opgeslagen pad)",
"repeater_forceFloodMode": "Dwing Overloopmodus",
"repeater_forceFloodMode": "Dwing Floodmodus Af",
"repeater_pathManagement": "Beheer van paden",
"repeater_refresh": "Vernieuwen",
"repeater_statusRequestTimeout": "Statusverzoek is uitgevallen.",
@@ -883,22 +884,22 @@
}
}
},
"repeater_systemInformation": "Systeem Informatie",
"repeater_systemInformation": "Systeeminformatie",
"repeater_battery": "Batterij",
"repeater_clockAtLogin": "Tijd (bij aanmelden)",
"repeater_uptime": "Beschikbaarheid",
"repeater_queueLength": "Wachttijd",
"repeater_debugFlags": "Debug Flags",
"repeater_radioStatistics": "Radio Statistieken",
"repeater_debugFlags": "Debugvlaggen",
"repeater_radioStatistics": "Radiostatistieken",
"repeater_lastRssi": "Laatste RSSI",
"repeater_lastSnr": "Laatste SNR",
"repeater_noiseFloor": "Ruishoordniveau",
"repeater_noiseFloor": "Ruisvloer",
"repeater_txAirtime": "TX Airtime",
"repeater_rxAirtime": "RX Airtime",
"repeater_packetStatistics": "Pakket Statistieken",
"repeater_packetStatistics": "Pakketstatistieken",
"repeater_sent": "Verzonden",
"repeater_received": "Ontvangen",
"repeater_duplicates": "Dubbele",
"repeater_duplicates": "Duplicaat",
"repeater_daysHoursMinsSecs": "{days} dagen {hours} uur {minutes} minuten {seconds} seconden",
"@repeater_daysHoursMinsSecs": {
"placeholders": {
@@ -916,7 +917,7 @@
}
}
},
"repeater_packetTxTotal": "Totaal: {total}, Overstroming: {flood}, Direct: {direct}",
"repeater_packetTxTotal": "Totaal: {total}, Flood: {flood}, Direct: {direct}",
"@repeater_packetTxTotal": {
"placeholders": {
"total": {
@@ -930,7 +931,7 @@
}
}
},
"repeater_packetRxTotal": "Totaal: {total}, Overstroming: {flood}, Direct: {direct}",
"repeater_packetRxTotal": "Totaal: {total}, Flood: {flood}, Direct: {direct}",
"@repeater_packetRxTotal": {
"placeholders": {
"total": {
@@ -944,7 +945,7 @@
}
}
},
"repeater_duplicatesFloodDirect": "Overstroming: {flood}, Direct: {direct}",
"repeater_duplicatesFloodDirect": "Flood: {flood}, Direct: {direct}",
"@repeater_duplicatesFloodDirect": {
"placeholders": {
"flood": {
@@ -963,10 +964,10 @@
}
}
},
"repeater_settingsTitle": "Herstelinstellingen",
"repeater_settingsTitle": "Repeater Instellingen",
"repeater_basicSettings": "Basisinstellingen",
"repeater_repeaterName": "Herhaalnaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze herhaling",
"repeater_repeaterName": "Repeaternaam",
"repeater_repeaterNameHelper": "Weergave naam voor deze repeater",
"repeater_adminPassword": "Admin wachtwoord",
"repeater_adminPasswordHelper": "Volledige toegangspaswoord",
"repeater_guestPassword": "Wachtwoord Gast",
@@ -977,7 +978,7 @@
"repeater_txPower": "TX Power",
"repeater_txPowerHelper": "1-30 dBm",
"repeater_bandwidth": "Bandbreedte",
"repeater_spreadingFactor": "Spreadsnelheid",
"repeater_spreadingFactor": "Spreidingsfactor",
"repeater_codingRate": "Codeertarief",
"repeater_locationSettings": "Locatie Instellingen",
"repeater_latitude": "Breedtegraad",
@@ -985,11 +986,11 @@
"repeater_longitude": "Lengtegraad",
"repeater_longitudeHelper": "Graadseconden (bijv. -122.4194)",
"repeater_features": "Kenmerken",
"repeater_packetForwarding": "Pakketdoorstrooming",
"repeater_packetForwardingSubtitle": "Herstel activeren om pakketten door te sturen",
"repeater_packetForwarding": "Pakketdoorvoering",
"repeater_packetForwardingSubtitle": "Repeater instellen om pakketten door te sturen",
"repeater_guestAccess": "Toegang voor Gasten",
"repeater_guestAccessSubtitle": "Toegestane leesbeheer toegang voor gasten.",
"repeater_privacyMode": "Privacy Mode",
"repeater_privacyMode": "Privacy Modus",
"repeater_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"repeater_advertisementSettings": "Advertentie Instellingen",
"repeater_localAdvertInterval": "Lokale Advertentie Interval",
@@ -1001,7 +1002,7 @@
}
}
},
"repeater_floodAdvertInterval": "Advertentie Interval bij overstroming",
"repeater_floodAdvertInterval": "Flood Advertentie Interval",
"repeater_floodAdvertIntervalHours": "{hours} uur",
"@repeater_floodAdvertIntervalHours": {
"placeholders": {
@@ -1012,15 +1013,15 @@
},
"repeater_encryptedAdvertInterval": "Versleutelde Advertentie Interval",
"repeater_dangerZone": "Gevaarzone",
"repeater_rebootRepeater": "Herstart Herhaalder",
"repeater_rebootRepeaterSubtitle": "De herstart van het herhalerapparaat",
"repeater_rebootRepeater": "Herstart Repeater",
"repeater_rebootRepeaterSubtitle": "Herstart het Repeaterapparaat",
"repeater_rebootRepeaterConfirm": "Ben je er zeker van dat je deze repeater opnieuw wilt opstarten?",
"repeater_regenerateIdentityKey": "Identiteit sleutel opnieuw genereren",
"repeater_regenerateIdentityKeySubtitle": "Nieuwe publieke/private sleutelpaar genereren",
"repeater_regenerateIdentityKeyConfirm": "Dit genereert een nieuwe identiteit voor de herhaling. Doorgaan?",
"repeater_regenerateIdentityKeyConfirm": "Dit genereert een nieuwe identiteit voor de repeater. Doorgaan?",
"repeater_eraseFileSystem": "Verwijder Besturingssysteem",
"repeater_eraseFileSystemSubtitle": "Formateer het herhalende bestandsysteem",
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de herhaling wissen. Dit kan niet worden teruggedraaid!",
"repeater_eraseFileSystemSubtitle": "Formateer het bestandsysteem van de repeater",
"repeater_eraseFileSystemConfirm": "WAARSCHUWING: Dit zal alle gegevens op de repeater wissen. Dit kan niet worden teruggedraaid!",
"repeater_eraseSerialOnly": "Verwijderen is alleen beschikbaar via de seriële console.",
"repeater_commandSent": "Commando verzonden: {command}",
"@repeater_commandSent": {
@@ -1049,7 +1050,7 @@
}
},
"repeater_refreshBasicSettings": "Basisinstellingen vernieuwen",
"repeater_refreshRadioSettings": "Radiozenders Instellingen Bijwerken",
"repeater_refreshRadioSettings": "Radiozender Instellingen Verversen",
"repeater_refreshTxPower": "Nieuw laden TX-vermogen",
"repeater_refreshLocationSettings": "Instellingen Locatie Vernieuwen",
"repeater_refreshPacketForwarding": "Vernieuwen Pakket Doorversturing",
@@ -1072,7 +1073,7 @@
}
}
},
"repeater_cliTitle": "Herhaling CLI",
"repeater_cliTitle": "Repeater CLI",
"repeater_debugNextCommand": "Debug Volgende Commando",
"repeater_commandHelp": "Help",
"repeater_clearHistory": "Verwijder Geschiedenis",
@@ -1106,14 +1107,14 @@
"repeater_cliHelpClearStats": "Reset verschillende statistiek-tellers naar nul.",
"repeater_cliHelpSetAf": "Stelt de luchtvaartfactor in.",
"repeater_cliHelpSetTx": "Stelt LoRa zendvermogen in dBm. (om te wijzigen)",
"repeater_cliHelpSetRepeat": "Activeert of deactiveert de herhalerrol voor dit knoop.",
"repeater_cliHelpSetRepeat": "Activeert of deactiveert de repeater rol van deze node.",
"repeater_cliHelpSetAllowReadOnly": "(Kamervisie) Als 'aan', dan wordt inloggen met een blanco wachtwoord toegestaan, maar kan niet naar de kamervisie Posten. (alleen lezen mogelijk).",
"repeater_cliHelpSetFloodMax": "Stelt het maximale aantal hops van een inkomend overlastpakket in (indien >= max, wordt het pakket niet doorgestuurd)",
"repeater_cliHelpSetFloodMax": "Stelt het maximale aantal hops van een inkomend floodpakket in (indien >= max, wordt het pakket niet doorgestuurd)",
"repeater_cliHelpSetIntThresh": "Stelt de Interferentiewaarde (in dB) in. Standaardwaarde is 14. Stel in op 0 om het detecteren van kanaalinterferentie uit te schakelen.",
"repeater_cliHelpSetAgcResetInterval": "Stelt het interval in om de Auto Gain Controller te resetten. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetMultiAcks": "Activeert of deactiveert de functie 'dubbele ACKs'.",
"repeater_cliHelpSetMultiAcks": "Activeert of deactiveert de functie 'duplicate ACKs'.",
"repeater_cliHelpSetAdvertInterval": "Stelt het timerinterval in minuten in om een lokale (zero-hop) advertentiepakket te versturen. Stel in op 0 om uit te schakelen.",
"repeater_cliHelpSetFloodAdvertInterval": "Stelt het timerinterval in uren in om een overstromingsadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetFloodAdvertInterval": "Stelt het timerinterval in uren in om een floodadvertentiepakket te versturen. Stel in op 0 om dit uit te schakelen.",
"repeater_cliHelpSetGuestPassword": "Stelt/past de gastenwacht aan of wijzigt deze. (voor herstelcontacten kunnen gastelogins de \"Get Stats\" verzoek verzenden)",
"repeater_cliHelpSetName": "Stelt de advertentietitel in.",
"repeater_cliHelpSetLat": "Stelt de breedtegraad van de advertentiekaart in. (graadrijssysteem)",
@@ -1136,7 +1137,7 @@
"repeater_cliHelpLogErase": "Verwijdert de pakketlogs uit het bestandssysteem.",
"repeater_cliHelpNeighbors": "Toont een lijst met andere repeater nodes die via nul-hop advertenties zijn gehoord. Elke regel is id-prefix-hex:timestamp:snr-times-4",
"repeater_cliHelpNeighborRemove": "Verwijdert de eerste overeenkomende vermelding (via pubkey prefix (hex)) uit de lijst van buren.",
"repeater_cliHelpRegion": "(reeks alleen) Lijst alle gedefinieerde regio's en huidige overstromingsrechten.",
"repeater_cliHelpRegion": "(Alleen Serieel) Lijst alle gedefinieerde regio's en huidige floodrechten.",
"repeater_cliHelpRegionLoad": "LET OP: dit is een speciale multi-command aanroep. Elke volgende opdracht is een regiortaak (uitgelijnd met spaties om de ouderhiërarchie aan te duiden, met minimaal één spatie). Beëindigd door een lege regel/opdracht te sturen.",
"repeater_cliHelpRegionGet": "Zoekt naar regio met gegeven naam voorvoegsel (of \"\" voor de globale scope). Antwoordt met \"-> regio-naam (ouder-naam) 'F'\"",
"repeater_cliHelpRegionPut": "Voegt of wijzigt een regio-definitie met de gegeven naam.",
@@ -1148,8 +1149,8 @@
"repeater_cliHelpRegionSave": "Bewaar de lijst/kaart van de regio's naar de opslag.",
"repeater_cliHelpGps": "Geeft de status van de GPS. Wanneer de GPS uit staat, antwoordt het alleen met \"uit\", als het aan staat, antwoordt het met \"aan\", status, fix, sat count.",
"repeater_cliHelpGpsOnOff": "Schakel de GPS-standby aan/uit.",
"repeater_cliHelpGpsSync": "Synchroniseer knooptime met GPS-klok.",
"repeater_cliHelpGpsSetLoc": "Stel de positie van het knoop vast naar GPS-coördinaten en sla de voorkeuren op.",
"repeater_cliHelpGpsSync": "Synchroniseer node met GPS-klok.",
"repeater_cliHelpGpsSetLoc": "Stel de positie van de node vast als GPS-coördinaten en sla de voorkeuren op.",
"repeater_cliHelpGpsAdvert": "Geeft de locatie advertentieconfiguratie van de node:\n- none: locatie niet in advertenties opnemen\n- share: gps locatie delen (van SensorManager)\n- prefs: locatie adverteren die in de voorkeuren is opgeslagen",
"repeater_cliHelpGpsAdvertSet": "Stelt advertentie locatie configuratie in.",
"repeater_commandsListTitle": "Commandenlijst",
@@ -1158,8 +1159,8 @@
"repeater_settingsCategory": "Instellingen",
"repeater_bridge": "Bruggen",
"repeater_logging": "Logging",
"repeater_neighborsRepeaterOnly": "Buren (Alleen herhaald)",
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen voor Repeater)",
"repeater_neighborsRepeaterOnly": "Buren (Alleen repeaters)",
"repeater_regionManagementRepeaterOnly": "Regiobeheer (Alleen Repeater)",
"repeater_regionNote": "Regio-commando's zijn geïntroduceerd om regio-definities en permissies te beheren.",
"repeater_gpsManagement": "Beheer GPS",
"repeater_gpsNote": "De GPS-commando is geïntroduceerd om locatiegerelateerde onderwerpen te beheren.",
@@ -1228,12 +1229,12 @@
"channelPath_title": "Pakketpad",
"channelPath_viewMap": "Kaart bekijken",
"channelPath_otherObservedPaths": "Overige Waargenomen Paden",
"channelPath_repeaterHops": "Herhalingstapjes",
"channelPath_repeaterHops": "Repeater Hops",
"channelPath_noHopDetails": "De details van de pakket zijn niet verstrekt.",
"channelPath_messageDetails": "Details Bericht",
"channelPath_senderLabel": "Afzender",
"channelPath_timeLabel": "Tijd",
"channelPath_repeatsLabel": "Herhalen",
"channelPath_repeatsLabel": "Repeats",
"channelPath_pathLabel": "Pad {index}",
"channelPath_observedLabel": "Waargenomen",
"channelPath_observedPathTitle": "Waargenomen pad {index} • {hops}",
@@ -1271,7 +1272,7 @@
}
},
"channelPath_unknownPath": "Onbekend",
"channelPath_floodPath": "Overstroming",
"channelPath_floodPath": "Flood",
"channelPath_directPath": "Direct",
"channelPath_observedZeroOf": "0 van {total} sprongen",
"@channelPath_observedZeroOf": {
@@ -1293,7 +1294,7 @@
}
},
"channelPath_mapTitle": "Padkaart",
"channelPath_noRepeaterLocations": "Geen herhaler locaties beschikbaar voor deze route.",
"channelPath_noRepeaterLocations": "Geen repeaters beschikbaar voor deze route.",
"channelPath_primaryPath": "Pad {index} (Hoofdtype)",
"@channelPath_primaryPath": {
"placeholders": {
@@ -1323,7 +1324,7 @@
}
},
"channelPath_noHopDetailsAvailable": "Geen details beschikbaar voor dit pakket.",
"channelPath_unknownRepeater": "Onbekend Herhaalaar",
"channelPath_unknownRepeater": "Onbekend Repeater",
"listFilter_tooltip": "Filteren en sorteren",
"listFilter_sortBy": "Sorteren door",
"listFilter_latestMessages": "Recente berichten",
@@ -1332,8 +1333,156 @@
"listFilter_filters": "Filters",
"listFilter_all": "Alles",
"listFilter_users": "Gebruikers",
"listFilter_repeaters": "Herhalingen",
"listFilter_roomServers": "Kamervirtualisatie",
"listFilter_repeaters": "Repeaters",
"listFilter_roomServers": "Roomservers",
"listFilter_unreadOnly": "Alleen ongelezen",
"listFilter_newGroup": "Nieuwe groep"
"listFilter_newGroup": "Nieuwe groep",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Buren",
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
"neighbors_receivedData": "Ontvangen Buurdata",
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
"neighbors_repeatersNeighbours": "Herhalingen Buren",
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
"channels_createPrivateChannel": "Maak een Privé Kanaal",
"channels_joinPrivateChannel": "Sluit een Privé Kanaal aan",
"channels_joinPrivateChannelDesc": "Handmatig een geheime sleutel invoeren.",
"channels_joinPublicChannel": "Sluit het Open Kanaal",
"channels_joinPublicChannelDesc": "Iedereen kan dit kanaal aanmelden.",
"channels_joinHashtagChannel": "Sluit een Hashtag Kanaal",
"channels_joinHashtagChannelDesc": "Iedereen kan lid worden van hashtag-kanalen.",
"channels_scanQrCode": "Scan een QR-code",
"channels_scanQrCodeComingSoon": "Komt later",
"channels_enterHashtag": "Voer hashtag in",
"channels_hashtagHint": "bijv. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Onbekende {pubkey}",
"neighbors_heardAgo": "Horen: {time} geleden",
"settings_locationGPSEnable": "GPS inschakelen",
"settings_locationGPSEnableSubtitle": "Activeer automatisch locatieupdates via GPS.",
"settings_locationIntervalSec": "Interval voor GPS (Seconden)",
"settings_locationIntervalInvalid": "De intervallen moeten minstens 60 seconden zijn en minder dan 86400 seconden.",
"contacts_manageRoom": "Beheer Ruimte Server",
"room_management": "Beheer Server Kamer",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Gemeenschap",
"common_ok": "OK",
"community_createDesc": "Maak een nieuwe community en deel deze via QR-code.",
"community_create": "Maak Gemeenschap",
"community_join": "Sluit aan",
"community_joinTitle": "Worden lid van de community",
"community_joinConfirmation": "Wil je je aansluiten bij de community \"{name}\"?",
"community_scanQr": "Scan Gemeenschap QR",
"community_scanInstructions": "Richt de camera op een gemeenschappelijke QR-code",
"community_showQr": "Toon QR-code",
"community_publicChannel": "Gemeenschap Openbaar",
"community_hashtagChannel": "Gemeenschappelijk Hashtag",
"community_name": "Gemeenschapnaam",
"community_enterName": "Voer de gemeenschapsnaam in",
"community_created": "Gemeenschap \"{name}\" is aangemaakt",
"community_joined": "Gevonden in de community \"{name}\"",
"community_qrTitle": "Deel Gemeenschap",
"community_qrInstructions": "Scan deze QR-code om je aan te sluiten bij {name}",
"community_hashtagPrivacyHint": "Community hashtag-kanalen zijn alleen toegankelijk voor leden van de community",
"community_invalidQrCode": "Ongeldige community QR-code",
"community_alreadyMember": "Alleen al lid",
"community_alreadyMemberMessage": "U bent al lid van \"{name}\".",
"community_addPublicChannel": "Voeg een Openbaar Gemeenschapskanaal toe",
"community_addPublicChannelHint": "Automatisch de publieke kanaal toevoegen voor deze community",
"community_noCommunities": "Nog geen gemeenschappen zijn bijgesloten.",
"community_scanOrCreate": "Scan een QR-code of een community aanmaken om te beginnen",
"community_manageCommunities": "Beheer Gemeenschappen",
"community_delete": "Laat Gemeenschap",
"community_deleteConfirm": "\"{name}\" verlaten?",
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Community \"{name}\" verlaten",
"community_addHashtagChannel": "Voeg Community Hashtag toe",
"community_addHashtagChannelDesc": "Voeg een hashtag-kanaal toe aan deze community",
"community_selectCommunity": "Selecteer Gemeenschap",
"community_regularHashtag": "Gewone Hashtag",
"community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)",
"community_communityHashtag": "Gemeenschappelijk Hashtag",
"community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community",
"community_forCommunity": "Voor {name}"
}
+150 -1
View File
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Logowanie nie powiodło się. Hasło jest nieprawidłowe albo repeater jest nieosiągalny.",
"common_reload": "Ponownie załadować",
"common_clear": "Wyczyść",
"path_currentPath": "Aktualny ścieżka: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Powtarzacze",
"listFilter_roomServers": "Serwery pokoju",
"listFilter_unreadOnly": "Tylko nieprzeczytane",
"listFilter_newGroup": "Nowa grupa"
"listFilter_newGroup": "Nowa grupa",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Sąsiedzi",
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
"channels_createPrivateChannelDesc": "Zabezpieczone kluczem szyfrowym.",
"channels_joinPrivateChannel": "Dołącz do Prywatnego Kanału",
"channels_joinPublicChannel": "Dołącz do kanału publicznego.",
"channels_joinPublicChannelDesc": "Każdy może dołączyć do tego kanału.",
"channels_joinHashtagChannel": "Dołącz do kanału oznaczanego hashtagiem",
"channels_joinHashtagChannelDesc": "Każdy może dołączyć do kanałów z hashtagami.",
"channels_scanQrCode": "Skanuj kod QR",
"channels_scanQrCodeComingSoon": "Wkrótce",
"channels_enterHashtag": "Wprowadź hashtag",
"channels_hashtagHint": "np. #zespół",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Usłyszano: {time} temu",
"neighbors_unknownContact": "Nieznana {pubkey}",
"settings_locationGPSEnable": "Włącz GPS",
"settings_locationGPSEnableSubtitle": "Włącza automatyczne aktualizowanie pozycji za pomocą GPS.",
"settings_locationIntervalSec": "Interwał dla GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interwał musi wynosić co najmniej 60 sekund i mniej niż 86400 sekund.",
"contacts_manageRoom": "Zarządzaj Serwerem Pokoju",
"room_management": "Zarządzanie Serwerem Pokoju",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Utwórz nową społeczność i udostępnij za pomocą kodu QR.",
"community_title": "Społeczność",
"community_create": "Utwórz Społeczność",
"common_ok": "OK",
"community_join": "Dołącz",
"community_joinTitle": "Dołącz do społeczności",
"community_joinConfirmation": "Czy chcesz dołączyć do społeczności \"{name}\"?",
"community_scanQr": "Skanuj QR kod społeczności",
"community_scanInstructions": "Skieruj kamerę w kierunku kodu QR społeczności.",
"community_showQr": "Pokaż kod QR",
"community_publicChannel": "Społeczność Publiczna",
"community_hashtagChannel": "Hashtag Społeczności",
"community_name": "Nazwa Społeczności",
"community_enterName": "Wprowadź nazwę społeczności",
"community_created": "Społeczność \"{name}\" została utworzona",
"community_joined": "Dołączył do społeczności \"{name}\"",
"community_qrTitle": "Dziel się Społecznością",
"community_qrInstructions": "Skanuj ten kod QR, aby dołączyć {name}",
"community_hashtagPrivacyHint": "Kanały hashtagowe społeczności są dostępne tylko dla członków społeczności",
"community_invalidQrCode": "Nieprawidłowy kod QR społeczności.",
"community_alreadyMember": "Już jesteś członkiem.",
"community_alreadyMemberMessage": "Jesteś już członkiem \"{name}\".",
"community_addPublicChannel": "Dodaj Kanał Publiczny Społeczności",
"community_addPublicChannelHint": "Automatycznie dodaj kanał publiczny dla tej społeczności.",
"community_noCommunities": "Nie dołączono jeszcze żadnych społeczności.",
"community_scanOrCreate": "Skanuj kod QR lub utwórz społeczność, aby zacząć.",
"community_manageCommunities": "Zarządzaj Grupami",
"community_delete": "Opuszczenie Społeczności",
"community_deleteConfirm": "Opuścić \"{name}\"?",
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Opuszczono społeczność \"{name}\"",
"community_addHashtagChannel": "Dodaj hashtag społeczności",
"community_addHashtagChannelDesc": "Dodaj kanał z hashtagiem dla tej społeczności",
"community_selectCommunity": "Wybierz społeczność",
"community_regularHashtag": "Hashtag regular",
"community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)",
"community_communityHashtag": "Hashtag Społeczności",
"community_communityHashtagDesc": "Dostępne tylko dla członków społeczności",
"community_forCommunity": "Dla {name}"
}
+150 -1
View File
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Falha no login. A senha está incorreta ou o repetidor está inacessível.",
"common_reload": "Recarregar",
"common_clear": "Limpar",
"path_currentPath": "Caminho atual: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Repetidores",
"listFilter_roomServers": "Servidores de sala",
"listFilter_unreadOnly": "Apenas não lido",
"listFilter_newGroup": "Novo grupo"
"listFilter_newGroup": "Novo grupo",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Vizinhos",
"neighbors_receivedData": "Dados dos Vizinhos Recebidos",
"repeater_neighboursSubtitle": "Visualizar vizinhos de salto zero.",
"neighbors_requestTimedOut": "Vizinhos solicitam tempo limite esgotado.",
"neighbors_errorLoading": "Erro ao carregar vizinhos: {error}",
"neighbors_repeatersNeighbours": "Repetidores Vizinhos",
"neighbors_noData": "Não estão disponíveis dados de vizinhos.",
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
"channels_createPrivateChannel": "Criar um Canal Privado",
"channels_joinPrivateChannel": "Junte-se a um Canal Privado",
"channels_joinPublicChannel": "Junte-se ao Canal Público",
"channels_joinPublicChannelDesc": "Qualquer pessoa pode entrar neste canal.",
"channels_joinHashtagChannel": "Junte-se a um Canal com Hashtag",
"channels_joinHashtagChannelDesc": "Qualquer pessoa pode participar de canais com hashtag.",
"channels_scanQrCode": "Digitalizar um Código QR",
"channels_scanQrCodeComingSoon": "Em breve",
"channels_enterHashtag": "Insira hashtag",
"channels_hashtagHint": "ex. #equipe",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Ouvido: {time} atrás",
"neighbors_unknownContact": "{pubkey} Desconhecido",
"settings_locationGPSEnable": "Ativar GPS",
"settings_locationGPSEnableSubtitle": "Habilita a atualização automática da localização via GPS.",
"settings_locationIntervalInvalid": "O intervalo deve ser de pelo menos 60 segundos e inferior a 86400 segundos.",
"settings_locationIntervalSec": "Intervalo para GPS (Segundos)",
"contacts_manageRoom": "Gerenciar Servidor de Sala",
"room_management": "Gerenciamento de Servidor de Sala",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_title": "Comunidade",
"community_createDesc": "Crie uma nova comunidade e compartilhe via código QR.",
"common_ok": "OK",
"community_create": "Criar Comunidade",
"community_join": "Junte-se",
"community_joinTitle": "Junte-se à Comunidade",
"community_joinConfirmation": "Você gostaria de se juntar à comunidade \"{name}\"?",
"community_scanQr": "Digitalizar a QR Code da Comunidade",
"community_scanInstructions": "Aponte a câmera para um código QR da comunidade",
"community_showQr": "Mostrar Código QR",
"community_publicChannel": "Comunidade Pública",
"community_hashtagChannel": "Hashtag da Comunidade",
"community_name": "Nome da Comunidade",
"community_enterName": "Insira o nome da comunidade",
"community_created": "Comunidade \"{name}\" criada",
"community_joined": "Juntou-se à comunidade \"{name}\"",
"community_qrTitle": "Partilhar Comunidade",
"community_qrInstructions": "Escanear este código QR para juntar-se a {name}",
"community_hashtagPrivacyHint": "Os canais de hashtag da comunidade só podem ser acessados por membros da comunidade",
"community_invalidQrCode": "Código QR da comunidade inválido",
"community_alreadyMember": "Já é Membro",
"community_alreadyMemberMessage": "Você já é membro de \"{name}\".",
"community_addPublicChannel": "Adicionar Canal Público da Comunidade",
"community_addPublicChannelHint": "Adicionar automaticamente o canal público para esta comunidade",
"community_noCommunities": "Ainda não foram adicionadas comunidades.",
"community_scanOrCreate": "Escaneie um código QR ou crie uma comunidade para começar.",
"community_manageCommunities": "Gerenciar Comunidades",
"community_delete": "Deixar Comunidade",
"community_deleteConfirm": "Sair de \"{name}\"?",
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Saiu da comunidade \"{name}\"",
"community_addHashtagChannel": "Adicionar Hashtag da Comunidade",
"community_addHashtagChannelDesc": "Adicionar um canal de hashtag para esta comunidade",
"community_selectCommunity": "Selecione Comunidade",
"community_regularHashtag": "Hashtag Regular",
"community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)",
"community_communityHashtag": "Hashtag da Comunidade",
"community_communityHashtagDesc": "Apenas para membros da comunidade",
"community_forCommunity": "Para {name}"
}
+151 -2
View File
@@ -559,7 +559,7 @@
"chat_setCustomPath": "Nastaviť vlastnú cestu",
"chat_setCustomPathSubtitle": "Ručne zadajte trasu.",
"chat_clearPath": "Vyčistiš cestu",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujacej pošlite",
"chat_clearPathSubtitle": "Znovu nájsť vynútene pri nasledujúcej pošlite",
"chat_pathCleared": "Cesta vyčistená. Nasledujúce prepočetné získa trasu znova.",
"chat_floodModeSubtitle": "Použite prepínanie trasy v navigačnom paneli.",
"chat_floodModeEnabled": "Odosporňovacia prevádzka je zapnutá. Vypnite ju znova cez ikonu routovania v navigačnom páse.",
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Prihlásenie zlyhalo. Heslo je nesprávne alebo je opakovač nedostupný.",
"common_reload": "Načítať",
"common_clear": "Zmazať",
"path_currentPath": "Aktívna cesta: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Opakovadlá",
"listFilter_roomServers": "Servéry miestnosti",
"listFilter_unreadOnly": "Nezaregistrované len",
"listFilter_newGroup": "Nová skupina"
"listFilter_newGroup": "Nová skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
"neighbors_receivedData": "Obdielo dáta suseda",
"repeater_neighbours": "Súsezný",
"neighbors_errorLoading": "Chyba pri načítaní susedov: {error}",
"neighbors_repeatersNeighbours": "Opakovadlá Súsezná",
"neighbors_noData": "Nie je dostupná žiadna informácia o susedoch.",
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
"channels_joinPrivateChannelDesc": "Ručne zadajte tajný kľúč.",
"channels_createPrivateChannelDesc": "Zabezpečené pomocou tajného kľúča.",
"channels_joinPublicChannel": "Pripojte sa k verejnému kanálu",
"channels_joinPublicChannelDesc": "Któvek sátó na tutó kanalizovát.",
"channels_joinHashtagChannel": "Pripojte sa k Hashtag Kanálu",
"channels_joinHashtagChannelDesc": "Ktoekolikoľvek sa môže pridať do hashtag kanálov.",
"channels_scanQrCode": "Skenujte QR kód",
"channels_scanQrCodeComingSoon": "Čoskoro",
"channels_enterHashtag": "Zadajte hashtag",
"channels_hashtagHint": "napr. #tím",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Počuli sme to: {time} dozadu",
"neighbors_unknownContact": "Neznáma {pubkey}",
"settings_locationGPSEnable": "Aktivovať GPS",
"settings_locationGPSEnableSubtitle": "Povolí automatické aktualizovanie polohy pomocou GPS.",
"settings_locationIntervalSec": "Interval pre GPS (Sekundy)",
"settings_locationIntervalInvalid": "Interval musí byť aspoň 60 sekúnd a menej ako 86400 sekúnd.",
"contacts_manageRoom": "Spravovať server miestnosti",
"room_management": "Správa servera miestnosti",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Vytvoriť komunitu",
"community_title": "Komunita",
"community_createDesc": "Vytvorte novú komunitu a zdieľajte cez QR kód.",
"community_join": "Pripojiť",
"community_joinTitle": "Pripojiť sa k spoločenstvu",
"community_joinConfirmation": "Chceš sa pridať do komunity \"{name}\"?",
"community_scanQr": "Skontrolujte komunitný QR kód",
"community_scanInstructions": "Zamerte kameru na komunitný QR kód.",
"community_showQr": "Zobraziť QR kód",
"common_ok": "OK\nDobre",
"community_publicChannel": "Komunita verejná",
"community_hashtagChannel": "Komunitný Hashtag",
"community_name": "Komunita",
"community_enterName": "Zadajte názov komunity",
"community_created": "Komunita \"{name}\" vytvorená",
"community_joined": "Pripojená komunita \"{name}\"",
"community_qrTitle": "Zdieľť komunitu",
"community_qrInstructions": "Skenejte tento QR kód, aby ste sa pripojili k {name}.",
"community_hashtagPrivacyHint": "Hashtagové kanály komunity sú prístupné len členom komunity",
"community_invalidQrCode": "Neplatná QR kód komunity.",
"community_alreadyMember": "Už ste členom.",
"community_alreadyMemberMessage": "Vy ste už členom \"{name}\".",
"community_addPublicChannel": "Pridať verejný komunikačný kanál",
"community_addPublicChannelHint": "Automaticky prida verejný kanál pre túto komunitu.",
"community_noCommunities": "Zatiaľ ste sa nepripojili k žiadnej komunite",
"community_scanOrCreate": "Skene QR kód alebo vytvor komunitu na začiatok.",
"community_manageCommunities": "Spravovať komunity",
"community_delete": "Nechajte komunitu",
"community_deleteConfirm": "Opustiť \"{name}\"?",
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Opustená komunita \"{name}\"",
"community_addHashtagChannel": "Pridať komunitný hashtag",
"community_addHashtagChannelDesc": "Pridajte hashtagový kanál pre túto komunitu.",
"community_selectCommunity": "Vyberte komunitu",
"community_regularHashtag": "Zvyčajný hashtag",
"community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)",
"community_communityHashtag": "Komunitný Hashtag",
"community_communityHashtagDesc": "Špecifické pre členov komunity",
"community_forCommunity": "Pre {name}"
}
+154 -5
View File
@@ -176,7 +176,7 @@
"appSettings_languageBg": "Български",
"appSettings_notifications": "Obveščanja",
"appSettings_enableNotifications": "Omogoči obveščanje",
"appSettings_enableNotificationsSubtitle": "Prejmujte obvestila o sporočilih in oglasih",
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
"appSettings_notificationsEnabled": "Obvestila omogočena",
"appSettings_notificationsDisabled": "Obvestila so izklopljena",
@@ -256,7 +256,7 @@
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
"contacts_searchContacts": "Iskanje kontaktov...",
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
"contacts_noContactsFound": "Niti ena osebe ali skupine ni najdena.",
"contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
"contacts_deleteContact": "Izbrisati Kontakt",
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
"@contacts_removeConfirm": {
@@ -291,7 +291,7 @@
}
},
"contacts_filterContacts": "Filtri kontakt\\,...",
"contacts_noContactsMatchFilter": "Niti ena osebe ne ustreza vašemu kriteriju.",
"contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.",
"contacts_noMembers": "Nič članov.",
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
@@ -606,7 +606,7 @@
},
"map_title": "Mapa omrežja",
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazajo na zemljeobrazniku.",
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
"map_nodesCount": "Omize: {count}",
"@map_nodesCount": {
"placeholders": {
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Prijava je bila neuspešna. Geslo je napačno ali pa je repetitor nedosegljiv.",
"common_reload": "Ponovno naloži",
"common_clear": "Ponoviti",
"path_currentPath": "Trenutna pot: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Ponovitve",
"listFilter_roomServers": "Smeti za prostore",
"listFilter_unreadOnly": "Nezbrani samo",
"listFilter_newGroup": "Nova skupina"
"listFilter_newGroup": "Nova skupina",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
"repeater_neighbours": "Sosedi",
"neighbors_receivedData": "Prejeto podatke o sosedih",
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
"neighbors_noData": "Niso na voljo podatki o sosedih.",
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
"channels_joinPrivateChannelDesc": "Ročno vnesite zaporni ključ.",
"channels_createPrivateChannel": "Ustvari zasebno kanal.",
"channels_joinPublicChannel": "Pridružite se javnemu kanalu",
"channels_joinPublicChannelDesc": "Kdor karkoli je, lahko se pridruži tej skupini.",
"channels_joinHashtagChannel": "Pridružite se Kanalu z Hashtagom",
"channels_joinHashtagChannelDesc": "Kdor karkoli, lahko se pridruži hashtag kanalom.",
"channels_scanQrCode": "Skeniraj QR kodo",
"channels_scanQrCodeComingSoon": "Prihajajoča",
"channels_enterHashtag": "Vnesite hashtag",
"channels_hashtagHint": "npr. #ekipa",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_unknownContact": "Nepoznano {pubkey}",
"neighbors_heardAgo": "Udeleženec je prejel sporočilo {time} nazaj.",
"settings_locationGPSEnable": "Omogoči GPS",
"settings_locationGPSEnableSubtitle": "Omogoči samodejno posodabljanje lokacije z GPS-jem.",
"settings_locationIntervalSec": "Interval za GPS (Sekunde)",
"settings_locationIntervalInvalid": "Intervallo mora biti vsaj 60 sekund in manj kot 86400 sekund.",
"contacts_manageRoom": "Upravljajte strežnik sobe",
"room_management": "Upravljanje stremlišča",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_createDesc": "Ustvari novo skupnost in jo deli preko QR kode.",
"community_title": "Skupnost",
"common_ok": "V redu",
"community_create": "Ustvari skupnost",
"community_joinTitle": "Pridružite se skupnosti",
"community_joinConfirmation": "Želiš se pridružiti skupnosti \"{name}\"?",
"community_scanQr": "Skeniraj QR kode skupnosti",
"community_scanInstructions": "Nasmerite kamero s skupnostnim QR kodom.",
"community_showQr": "Pokaži QR kodo",
"community_publicChannel": "Skupnostna javna",
"community_hashtagChannel": "Skupnostni hashtag",
"community_name": "Komunitarne ime",
"community_enterName": "Vnesite ime skupnosti",
"community_join": "Pridružiti se",
"community_created": "Skupnost \"{name}\" je bila ustvarila.",
"community_joined": "Prilojen k skupnosti \"{name}\"",
"community_qrTitle": "Delite skupnost",
"community_qrInstructions": "Skenirajte to QR kodo za vključitev {name}.",
"community_hashtagPrivacyHint": "Hashtag kanali skupnosti so dostopni samo članom skupnosti",
"community_invalidQrCode": "Neveljaven QR koden skupnosti",
"community_alreadyMember": "Že član",
"community_alreadyMemberMessage": "Kljub temu ste že član/ka {name}.",
"community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja",
"community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.",
"community_noCommunities": "Še nobena skupnost se ni pridružila.",
"community_scanOrCreate": "Skenirajte QR kodo ali ustvarite skupnost za začetek.",
"community_manageCommunities": "Upravljajte skupnosti",
"community_delete": "Opusti skupnost",
"community_deleteConfirm": "Zapustiti \"{name}\"?",
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Zapustil skupnost \"{name}\"",
"community_addHashtagChannel": "Dodaj Oznako Obštnine",
"community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.",
"community_selectCommunity": "Izberi skupnost",
"community_regularHashtag": "Oznaka s hashtagom",
"community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)",
"community_communityHashtag": "Skupnostni hashtag",
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
"community_forCommunity": "Za {name}"
}
+150 -1
View File
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "Inloggning misslyckades. Antingen är lösenordet fel eller så går det inte att nå repeatern.",
"common_reload": "Ladda om",
"common_clear": "Rensa",
"path_currentPath": "Nuvarande sökväg: {path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "Upprepare",
"listFilter_roomServers": "Rumservrar",
"listFilter_unreadOnly": "Endast oinlästa",
"listFilter_newGroup": "Ny grupp"
"listFilter_newGroup": "Ny grupp",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighbours": "Grannar",
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
"neighbors_receivedData": "Mottagna grannars data",
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
"neighbors_repeatersNeighbours": "Upprepar grannar",
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
"channels_createPrivateChannel": "Skapa en privat kanal",
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
"channels_joinPrivateChannelDesc": "Ange en hemlig nyckel manuellt.",
"channels_createPrivateChannelDesc": "Skyddat med en hemlig nyckel.",
"channels_joinPublicChannel": "Gå med i den Offentliga Kanalen",
"channels_joinPublicChannelDesc": "Vem som helst kan gå med i denna kanal.",
"channels_joinHashtagChannel": "Gå med i en Hashtagkanal",
"channels_joinHashtagChannelDesc": "Väldigt enkelt att gå med i hashtag-kanaler.",
"channels_scanQrCode": "Skanna en QR-kod",
"channels_scanQrCodeComingSoon": "Kommer snart",
"channels_enterHashtag": "Ange hashtag",
"channels_hashtagHint": "t.ex. #team",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "Hördes: {time} sedan",
"neighbors_unknownContact": "Okänd {pubkey}",
"settings_locationGPSEnable": "Aktivera GPS",
"settings_locationGPSEnableSubtitle": "Aktivera automatiska uppdateringar av platsen med hjälp av GPS.",
"settings_locationIntervalSec": "Interval för GPS (Sekunder)",
"settings_locationIntervalInvalid": "Intervalet måste vara minst 60 sekunder och mindre än 86400 sekunder.",
"contacts_manageRoom": "Hantera Rumserver",
"room_management": "Rumserverhantering",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "Skapa Gemenskap",
"community_createDesc": "Skapa en ny gemenskap och dela via QR-kod.",
"common_ok": "Okej",
"community_title": "Gemenskap",
"community_join": "Gå med",
"community_joinTitle": "Gå med i gemenskapen",
"community_joinConfirmation": "Vill du gå med i communityn \"{name}\"?",
"community_scanQr": "Skanna Gemenskapens QR",
"community_scanInstructions": "Rikta kameran mot en QR-kod i communityn",
"community_showQr": "Visa QR-kod",
"community_publicChannel": "Föreningens Offentliga",
"community_name": "Gemenskapens namn",
"community_enterName": "Ange communities namn",
"community_created": "Community \"{name}\" har skapats",
"community_joined": "Medlem i communityn \"{name}\"",
"community_qrTitle": "Dela Gemenskap",
"community_qrInstructions": "Skanna denna QR-kod för att gå med i \"{name}\"",
"community_hashtagPrivacyHint": "Community-hashtagkanaler kan endast nås av medlemmar i communityn",
"community_hashtagChannel": "Community Hashtag",
"community_invalidQrCode": "Ogiltig community QR-kod",
"community_alreadyMember": "Är redan medlem",
"community_alreadyMemberMessage": "Du är redan medlem av \"{name}\".",
"community_addPublicChannel": "Lägg till Gemenskapskanal (Offentlig)",
"community_addPublicChannelHint": "Lägg automatiskt till den offentliga kanalen för denna community",
"community_noCommunities": "Inga gemenskaper har anslutats ännu",
"community_scanOrCreate": "Skanna en QR-kod eller skapa en community för att komma igång",
"community_manageCommunities": "Hantera Gemenskaper",
"community_delete": "Lämna Gemenskap",
"community_deleteConfirm": "Lämna \"{name}\"?",
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "Lämnade community \"{name}\"",
"community_addHashtagChannel": "Lägg till Gemenskapens Hashtag",
"community_addHashtagChannelDesc": "Lägg till en hashtag-kanal för denna community",
"community_selectCommunity": "Välj Gemenskap",
"community_regularHashtag": "Vanlig Hash Tag",
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
"community_communityHashtagDesc": "Endast för medlemmar",
"community_forCommunity": "För {name}",
"community_communityHashtag": "Community Hashtag"
}
+150 -1
View File
@@ -821,6 +821,7 @@
}
}
},
"login_failedMessage": "登录失败。密码不正确或中继器不可达。",
"common_reload": "重新加载",
"common_clear": "清除",
"path_currentPath": "当前路径:{path}",
@@ -1335,5 +1336,153 @@
"listFilter_repeaters": "重复器",
"listFilter_roomServers": "房间服务器",
"listFilter_unreadOnly": "未读消息",
"listFilter_newGroup": "新组"
"listFilter_newGroup": "新组",
"@neighbors_errorLoading": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"repeater_neighboursSubtitle": "查看零跳邻居。",
"repeater_neighbours": "邻居",
"neighbors_receivedData": "收到邻居数据",
"neighbors_requestTimedOut": "邻居请求超时处理。",
"neighbors_errorLoading": "加载邻居时出错:{error}",
"neighbors_repeatersNeighbours": "重复器邻居",
"neighbors_noData": "没有可用的邻居数据。",
"channels_joinPrivateChannel": "加入私密频道",
"channels_createPrivateChannelDesc": "使用密钥保护。",
"channels_joinPrivateChannelDesc": "手动输入密钥。",
"channels_createPrivateChannel": "创建私聊频道",
"channels_joinPublicChannel": "加入公共频道",
"channels_joinPublicChannelDesc": "任何人都可以加入这个频道。",
"channels_joinHashtagChannel": "加入标签频道",
"channels_joinHashtagChannelDesc": "任何人都可以加入话题频道。",
"channels_scanQrCode": "扫描二维码",
"channels_scanQrCodeComingSoon": "即将到来",
"channels_enterHashtag": "输入标签",
"channels_hashtagHint": "例如 #团队",
"@neighbors_unknownContact": {
"placeholders": {
"pubkey": {
"type": "String"
}
}
},
"@neighbors_heardAgo": {
"placeholders": {
"time": {
"type": "String"
}
}
},
"neighbors_heardAgo": "听到的时间:{time}前",
"neighbors_unknownContact": "未知{pubkey}",
"settings_locationGPSEnable": "启用GPS",
"settings_locationGPSEnableSubtitle": "启用GPS自动更新位置。",
"settings_locationIntervalSec": "GPS 间隔(秒)",
"settings_locationIntervalInvalid": "时间间隔必须至少为60秒,且小于86400秒。",
"contacts_manageRoom": "管理房间服务器",
"room_management": "房间服务器管理",
"@community_joinConfirmation": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_created": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_joined": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_qrInstructions": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_alreadyMemberMessage": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleteConfirm": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_deleted": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"@community_forCommunity": {
"placeholders": {
"name": {
"type": "String"
}
}
},
"community_create": "创建社区",
"community_title": "社区",
"community_createDesc": "创建新的社区并可通过二维码分享。",
"common_ok": "好的",
"community_join": "加入",
"community_joinTitle": "加入社区",
"community_joinConfirmation": "您想加入社区 \"{name}\" 吗?",
"community_scanQr": "扫描社区二维码",
"community_scanInstructions": "将相机对准社区二维码",
"community_showQr": "显示二维码",
"community_publicChannel": "社区公开",
"community_hashtagChannel": "社区标签",
"community_name": "社区名称",
"community_enterName": "请输入社区名称",
"community_created": "社区“{name}”已创建",
"community_joined": "加入社区 \"{name}\"",
"community_qrTitle": "分享社区",
"community_qrInstructions": "扫描此二维码加入{name}",
"community_hashtagPrivacyHint": "社区标签频道仅社区成员可加入",
"community_invalidQrCode": "无效的社区二维码",
"community_alreadyMember": "已经是会员了",
"community_alreadyMemberMessage": "您已经是 \"{name}\" 的会员。",
"community_addPublicChannel": "添加社区公共频道",
"community_addPublicChannelHint": "自动添加该社区的公共频道",
"community_noCommunities": "尚未加入任何社区",
"community_scanOrCreate": "扫描二维码或创建社区开始",
"community_manageCommunities": "管理社群",
"community_delete": "退出社区",
"community_deleteConfirm": "退出 \"{name}\"",
"community_deleteChannelsWarning": "这也将删除 {count} 个频道及其消息。",
"@community_deleteChannelsWarning": {
"placeholders": {
"count": {"type": "int"}
}
},
"community_deleted": "已退出社区 \"{name}\"",
"community_addHashtagChannel": "添加社区标签",
"community_addHashtagChannelDesc": "添加一个话题频道给此社区",
"community_selectCommunity": "选择社区",
"community_regularHashtag": "常规话题标签",
"community_regularHashtagDesc": "公共话题(任何人都可以加入)",
"community_communityHashtag": "社区标签",
"community_communityHashtagDesc": "仅限社区成员使用",
"community_forCommunity": "对于 {name}"
}
+1 -1
View File
@@ -27,7 +27,7 @@ void main() async {
final storage = StorageService();
final connector = MeshCoreConnector();
final pathHistoryService = PathHistoryService(storage);
final retryService = MessageRetryService(storage);
final retryService = MessageRetryService();
final appSettingsService = AppSettingsService();
final bleDebugLogService = BleDebugLogService();
final appDebugLogService = AppDebugLogService();
+41
View File
@@ -1,5 +1,8 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import '../connector/meshcore_protocol.dart';
class Channel {
@@ -61,6 +64,44 @@ class Channel {
return bytes;
}
/// Derive PSK from hashtag name using SHA256.
/// The hashtag is normalized to include '#' prefix.
/// Returns first 16 bytes of SHA256 hash as PSK.
static Uint8List derivePskFromHashtag(String hashtag) {
final name = hashtag.startsWith('#') ? hashtag : '#$hashtag';
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
return Uint8List.fromList(hash.sublist(0, 16));
}
/// Derive PSK for community public channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
///
/// This creates a channel that is "public" only to members who have
/// the community secret. Outsiders see only opaque IDs.
static Uint8List deriveCommunityPublicPsk(Uint8List secret) {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel using HMAC-SHA256.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
///
/// Community hashtag channels are deterministic for all members
/// (same name => same id) but impossible to enumerate/guess without K.
static Uint8List deriveCommunityHashtagPsk(Uint8List secret, String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Normalize a hashtag name for consistent community PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
static String formatPskHex(Uint8List psk) {
return _bytesToHex(psk);
}
+243
View File
@@ -0,0 +1,243 @@
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
/// Represents a community with a shared secret for deriving channel PSKs.
///
/// A Community is a namespace with a shared secret K (32 random bytes),
/// distributed via QR code. Members can create Community Public Channels
/// and Community Hashtag Channels that are opaque to outsiders.
class Community {
/// Unique identifier for local storage
final String id;
/// Display name for the community
final String name;
/// The 32-byte shared secret (K)
final Uint8List secret;
/// Timestamp when the community was created/joined
final DateTime createdAt;
/// List of hashtag channel names (without #) that have been added
final List<String> hashtagChannels;
Community({
required this.id,
required this.name,
required this.secret,
required this.createdAt,
List<String>? hashtagChannels,
}) : hashtagChannels = hashtagChannels ?? [];
/// Generate a new community with a random 32-byte secret
factory Community.create({
required String id,
required String name,
}) {
final random = Random.secure();
final secret = Uint8List(32);
for (int i = 0; i < 32; i++) {
secret[i] = random.nextInt(256);
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: DateTime.now(),
);
}
/// Parse a community from QR code JSON data
factory Community.fromQrData(String id, String qrData) {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') {
throw const FormatException('Invalid QR code type');
}
if (json['v'] != 1) {
throw const FormatException('Unsupported QR code version');
}
final name = json['name'] as String;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) {
throw const FormatException('Invalid secret length');
}
return Community(
id: id,
name: name,
secret: Uint8List.fromList(secret),
createdAt: DateTime.now(),
);
}
/// Parse a community from storage JSON
factory Community.fromJson(Map<String, dynamic> json) {
return Community(
id: json['id'] as String,
name: json['name'] as String,
secret: base64Decode(json['secret'] as String),
createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
hashtagChannels: (json['hashtag_channels'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
[],
);
}
/// Convert to JSON for storage
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'secret': base64Encode(secret),
'created_at': createdAt.millisecondsSinceEpoch,
'hashtag_channels': hashtagChannels,
};
}
/// Generate QR code JSON payload for sharing
String toQrJson() {
return jsonEncode({
'v': 1,
'type': 'meshcore_community',
'name': name,
'k': base64Url.encode(secret),
});
}
/// Derive the public Community ID from the secret.
/// This is safe to display/log since it's one-way derived.
/// CID = SHA256("community:v1" || K)
String get communityId {
final data = utf8.encode('community:v1') + secret;
final hash = crypto.sha256.convert(data).bytes;
return _bytesToHex(Uint8List.fromList(hash));
}
/// Short version of community ID for display (first 8 chars)
String get shortCommunityId => communityId.substring(0, 8);
/// Derive PSK for community public channel.
/// PSK = HMAC-SHA256(K, "channel:v1:__public__")[:16]
Uint8List deriveCommunityPublicPsk() {
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:__public__'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Derive PSK for community hashtag channel.
/// PSK = HMAC-SHA256(K, "channel:v1:" + normalized_name)[:16]
Uint8List deriveCommunityHashtagPsk(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
final hmac = crypto.Hmac(crypto.sha256, secret);
final digest = hmac.convert(utf8.encode('channel:v1:$normalized'));
return Uint8List.fromList(digest.bytes.sublist(0, 16));
}
/// Check if QR data is valid community data
static bool isValidQrData(String data) {
try {
final json = jsonDecode(data) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return false;
if (json['v'] != 1) return false;
if (json['name'] == null || (json['name'] as String).isEmpty) {
return false;
}
if (json['k'] == null) return false;
final secret = base64Url.decode(json['k'] as String);
return secret.length == 32;
} catch (_) {
return false;
}
}
/// Normalize a hashtag name for consistent PSK derivation.
/// Strips leading #, converts to lowercase, trims whitespace.
static String _normalizeCommunityHashtag(String hashtag) {
return hashtag.replaceFirst(RegExp(r'^#'), '').toLowerCase().trim();
}
/// Add a hashtag channel to this community's list
Community addHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
if (hashtagChannels.contains(normalized)) {
return this;
}
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: [...hashtagChannels, normalized],
);
}
/// Remove a hashtag channel from this community's list
Community removeHashtagChannel(String hashtag) {
final normalized = _normalizeCommunityHashtag(hashtag);
return Community(
id: id,
name: name,
secret: secret,
createdAt: createdAt,
hashtagChannels: hashtagChannels.where((h) => h != normalized).toList(),
);
}
/// Create a copy of this community with a new secret
Community withNewSecret(Uint8List newSecret) {
return Community(
id: id,
name: name,
secret: newSecret,
createdAt: createdAt,
hashtagChannels: hashtagChannels,
);
}
/// Create a copy of this community with a regenerated random secret
Community withRegeneratedSecret() {
final random = Random.secure();
final newSecret = Uint8List(32);
for (int i = 0; i < 32; i++) {
newSecret[i] = random.nextInt(256);
}
return withNewSecret(newSecret);
}
/// Extract secret from QR data (for updating existing community)
static Uint8List? extractSecretFromQrData(String qrData) {
try {
final json = jsonDecode(qrData) as Map<String, dynamic>;
if (json['type'] != 'meshcore_community') return null;
if (json['v'] != 1) return null;
final secretBase64 = json['k'] as String;
final secret = base64Url.decode(secretBase64);
if (secret.length != 32) return null;
return Uint8List.fromList(secret);
} catch (_) {
return null;
}
}
static String _bytesToHex(Uint8List bytes) {
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Community &&
runtimeType == other.runtimeType &&
id == other.id;
@override
int get hashCode => id.hashCode;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:uuid/uuid.dart';
import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../models/community.dart';
import '../storage/community_store.dart';
import '../widgets/qr_scanner_widget.dart';
/// Screen for scanning community QR codes to join communities.
///
/// After successful scan, the user can:
/// 1. Join the community (saves to local storage)
/// 2. Optionally add the Community Public Channel to the device
class CommunityQrScannerScreen extends StatefulWidget {
const CommunityQrScannerScreen({super.key});
@override
State<CommunityQrScannerScreen> createState() =>
_CommunityQrScannerScreenState();
}
class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
final CommunityStore _communityStore = CommunityStore();
bool _isProcessing = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.community_scanQr),
centerTitle: true,
),
body: _isProcessing
? const Center(child: CircularProgressIndicator())
: QrScannerWidget(
onScanned: (data) => _handleScannedData(context, data),
validator: Community.isValidQrData,
onValidationFailed: (_) => _showInvalidQrError(context),
instructions: context.l10n.community_scanInstructions,
),
);
}
Future<void> _handleScannedData(BuildContext context, String data) async {
if (_isProcessing) return;
setState(() {
_isProcessing = true;
});
try {
// Parse the community data
final community = Community.fromQrData(const Uuid().v4(), data);
// Check if this community already exists
final existing = await _communityStore.findByCommunityId(
community.communityId,
);
if (existing != null) {
if (context.mounted) {
_showAlreadyMemberDialog(context, existing);
}
return;
}
// Show confirmation dialog
if (context.mounted) {
await _showJoinConfirmationDialog(context, community);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() {
_isProcessing = false;
});
}
}
}
void _showInvalidQrError(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_invalidQrCode),
backgroundColor: Colors.orange,
duration: const Duration(seconds: 2),
),
);
}
void _showAlreadyMemberDialog(BuildContext context, Community community) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.community_alreadyMember),
content: Text(
context.l10n.community_alreadyMemberMessage(community.name),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
Navigator.pop(context);
},
child: Text(context.l10n.common_ok),
),
],
),
);
}
Future<void> _showJoinConfirmationDialog(
BuildContext context,
Community community,
) async {
bool addPublicChannel = true;
final result = await showDialog<bool>(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (dialogContext, setDialogState) => AlertDialog(
title: Text(context.l10n.community_joinTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.community_joinConfirmation(community.name)),
const SizedBox(height: 16),
Row(
children: [
Icon(
Icons.groups,
color: Theme.of(dialogContext).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
community.name,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
'ID: ${community.shortCommunityId}...',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
],
),
const SizedBox(height: 16),
const Divider(),
const SizedBox(height: 8),
CheckboxListTile(
value: addPublicChannel,
onChanged: (value) {
setDialogState(() {
addPublicChannel = value ?? true;
});
},
title: Text(context.l10n.community_addPublicChannel),
subtitle: Text(context.l10n.community_addPublicChannelHint),
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
child: Text(context.l10n.common_cancel),
),
FilledButton(
onPressed: () => Navigator.pop(dialogContext, true),
child: Text(context.l10n.community_join),
),
],
),
),
);
if (result == true && context.mounted) {
await _joinCommunity(context, community, addPublicChannel);
} else if (context.mounted) {
// User cancelled - go back
Navigator.pop(context);
}
}
Future<void> _joinCommunity(
BuildContext context,
Community community,
bool addPublicChannel,
) async {
// Save community to local storage
await _communityStore.addCommunity(community);
// Optionally add the community public channel to the device
if (addPublicChannel && context.mounted) {
final connector = context.read<MeshCoreConnector>();
final nextIndex = _findNextAvailableChannelIndex(connector);
if (nextIndex != null) {
final psk = community.deriveCommunityPublicPsk();
final channelName = '${community.name} Public';
connector.setChannel(nextIndex, channelName, psk);
}
}
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.community_joined(community.name)),
backgroundColor: Colors.green,
),
);
// Return to previous screen
Navigator.pop(context, community);
}
}
int? _findNextAvailableChannelIndex(MeshCoreConnector connector) {
final usedIndices = connector.channels.map((c) => c.index).toSet();
for (int i = 0; i < connector.maxChannels; i++) {
if (!usedIndices.contains(i)) return i;
}
return null;
}
}
+181 -79
View File
@@ -27,13 +27,15 @@ import 'map_screen.dart';
import 'repeater_hub_screen.dart';
import 'settings_screen.dart';
enum RoomLoginDestination {
chat,
management,
}
class ContactsScreen extends StatefulWidget {
final bool hideBackButton;
const ContactsScreen({
super.key,
this.hideBackButton = false,
});
const ContactsScreen({super.key, this.hideBackButton = false});
@override
State<ContactsScreen> createState() => _ContactsScreenState();
@@ -114,7 +116,8 @@ class _ContactsScreenState extends State<ContactsScreen>
top: false,
child: QuickSwitchBar(
selectedIndex: 0,
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
),
),
),
@@ -168,8 +171,9 @@ class _ContactsScreenState extends State<ContactsScreen>
}
final filteredAndSorted = _filterAndSortContacts(contacts, connector);
final filteredGroups =
_showUnreadOnly ? const <ContactGroup>[] : _filterAndSortGroups(_groups, contacts);
final filteredGroups = _showUnreadOnly
? const <ContactGroup>[]
: _filterAndSortGroups(_groups, contacts);
return Column(
children: [
@@ -199,7 +203,10 @@ class _ContactsScreenState extends State<ContactsScreen>
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
_searchDebounce?.cancel();
@@ -238,14 +245,18 @@ class _ContactsScreenState extends State<ContactsScreen>
final group = filteredGroups[index];
return _buildGroupTile(context, group, contacts);
}
final contact = filteredAndSorted[index - filteredGroups.length];
final unreadCount = connector.getUnreadCountForContact(contact);
final contact =
filteredAndSorted[index - filteredGroups.length];
final unreadCount = connector.getUnreadCountForContact(
contact,
);
return _ContactTile(
contact: contact,
lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount,
onTap: () => _openChat(context, contact),
onLongPress: () => _showContactOptions(context, connector, contact),
onLongPress: () =>
_showContactOptions(context, connector, contact),
);
},
),
@@ -255,35 +266,48 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
List<ContactGroup> _filterAndSortGroups(List<ContactGroup> groups, List<Contact> contacts) {
List<ContactGroup> _filterAndSortGroups(
List<ContactGroup> groups,
List<Contact> contacts,
) {
final query = _searchQuery.trim().toLowerCase();
final contactsByKey = <String, Contact>{};
for (final contact in contacts) {
contactsByKey[contact.publicKeyHex] = contact;
}
final filtered = groups.where((group) {
if (query.isEmpty) return true;
if (group.name.toLowerCase().contains(query)) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && matchesContactQuery(contact, query)) return true;
}
return false;
}).where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
}
return false;
}).toList();
final filtered = groups
.where((group) {
if (query.isEmpty) return true;
if (group.name.toLowerCase().contains(query)) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && matchesContactQuery(contact, query)) {
return true;
}
}
return false;
})
.where((group) {
if (_typeFilter == ContactTypeFilter.all) return true;
for (final key in group.memberKeys) {
final contact = contactsByKey[key];
if (contact != null && _matchesTypeFilter(contact)) return true;
}
return false;
})
.toList();
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return filtered;
}
List<Contact> _filterAndSortContacts(List<Contact> contacts, MeshCoreConnector connector) {
List<Contact> _filterAndSortContacts(
List<Contact> contacts,
MeshCoreConnector connector,
) {
var filtered = contacts.where((contact) {
if (_searchQuery.isEmpty) return true;
return matchesContactQuery(contact, _searchQuery);
@@ -301,19 +325,27 @@ class _ContactsScreenState extends State<ContactsScreen>
switch (_sortOption) {
case ContactSortOption.lastSeen:
filtered.sort((a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)));
filtered.sort(
(a, b) => _resolveLastSeen(b).compareTo(_resolveLastSeen(a)),
);
break;
case ContactSortOption.recentMessages:
filtered.sort((a, b) {
final aMessages = connector.getMessages(a);
final bMessages = connector.getMessages(b);
final aLastMsg = aMessages.isEmpty ? DateTime(1970) : aMessages.last.timestamp;
final bLastMsg = bMessages.isEmpty ? DateTime(1970) : bMessages.last.timestamp;
final aLastMsg = aMessages.isEmpty
? DateTime(1970)
: aMessages.last.timestamp;
final bLastMsg = bMessages.isEmpty
? DateTime(1970)
: bMessages.last.timestamp;
return bLastMsg.compareTo(aLastMsg);
});
break;
case ContactSortOption.name:
filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
filtered.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
break;
}
@@ -340,7 +372,11 @@ class _ContactsScreenState extends State<ContactsScreen>
: contact.lastSeen;
}
Widget _buildGroupTile(BuildContext context, ContactGroup group, List<Contact> contacts) {
Widget _buildGroupTile(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final memberContacts = _resolveGroupContacts(group, contacts);
final subtitle = _formatGroupMembers(context, memberContacts);
return ListTile(
@@ -359,7 +395,10 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
List<Contact> _resolveGroupContacts(ContactGroup group, List<Contact> contacts) {
List<Contact> _resolveGroupContacts(
ContactGroup group,
List<Contact> contacts,
) {
final byKey = <String, Contact>{};
for (final contact in contacts) {
byKey[contact.publicKeyHex] = contact;
@@ -371,7 +410,9 @@ class _ContactsScreenState extends State<ContactsScreen>
resolved.add(contact);
}
}
resolved.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
resolved.sort(
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
);
return resolved;
}
@@ -387,7 +428,7 @@ class _ContactsScreenState extends State<ContactsScreen>
if (contact.type == advTypeRepeater) {
_showRepeaterLogin(context, contact);
} else if (contact.type == advTypeRoom) {
_showRoomLogin(context, contact);
_showRoomLogin(context, contact, RoomLoginDestination.chat);
} else {
context.read<MeshCoreConnector>().markContactRead(contact.publicKeyHex);
Navigator.push(
@@ -403,17 +444,13 @@ class _ContactsScreenState extends State<ContactsScreen>
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ChannelsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
case 2:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const MapScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
);
break;
}
@@ -429,10 +466,8 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
),
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
),
);
},
@@ -440,18 +475,23 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showRoomLogin(BuildContext context, Contact room) {
void _showRoomLogin(
BuildContext context,
Contact room,
RoomLoginDestination destination,
) {
showDialog(
context: context,
builder: (context) => RoomLoginDialog(
room: room,
onLogin: (password) {
// Navigate to chat screen after successful login
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(contact: room),
builder: (context) => destination == RoomLoginDestination.management
? RepeaterHubScreen(repeater: room, password: password)
: ChatScreen(contact: room),
),
);
},
@@ -459,7 +499,11 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
void _showGroupOptions(BuildContext context, ContactGroup group, List<Contact> contacts) {
void _showGroupOptions(
BuildContext context,
ContactGroup group,
List<Contact> contacts,
) {
final members = _resolveGroupContacts(group, contacts);
showModalBottomSheet(
context: context,
@@ -478,7 +522,10 @@ class _ContactsScreenState extends State<ContactsScreen>
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteGroup, style: const TextStyle(color: Colors.red)),
title: Text(
context.l10n.contacts_deleteGroup,
style: const TextStyle(color: Colors.red),
),
onTap: () {
Navigator.pop(sheetContext);
_confirmDeleteGroup(context, group);
@@ -522,7 +569,10 @@ class _ContactsScreenState extends State<ContactsScreen>
});
await _saveGroups();
},
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
),
],
),
@@ -548,10 +598,16 @@ class _ContactsScreenState extends State<ContactsScreen>
final filteredContacts = filterQuery.isEmpty
? sortedContacts
: sortedContacts
.where((contact) => matchesContactQuery(contact, filterQuery))
.toList();
.where(
(contact) => matchesContactQuery(contact, filterQuery),
)
.toList();
return AlertDialog(
title: Text(isEditing ? context.l10n.contacts_editGroup : context.l10n.contacts_newGroup),
title: Text(
isEditing
? context.l10n.contacts_editGroup
: context.l10n.contacts_newGroup,
),
content: SizedBox(
width: double.maxFinite,
child: Column(
@@ -582,12 +638,18 @@ class _ContactsScreenState extends State<ContactsScreen>
SizedBox(
height: 240,
child: filteredContacts.isEmpty
? Center(child: Text(context.l10n.contacts_noContactsMatchFilter))
? Center(
child: Text(
context.l10n.contacts_noContactsMatchFilter,
),
)
: ListView.builder(
itemCount: filteredContacts.length,
itemBuilder: (context, index) {
final contact = filteredContacts[index];
final isSelected = selectedKeys.contains(contact.publicKeyHex);
final isSelected = selectedKeys.contains(
contact.publicKeyHex,
);
return CheckboxListTile(
value: isSelected,
title: Text(contact.name),
@@ -618,7 +680,9 @@ class _ContactsScreenState extends State<ContactsScreen>
final name = nameController.text.trim();
if (name.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupNameRequired)),
SnackBar(
content: Text(context.l10n.contacts_groupNameRequired),
),
);
return;
}
@@ -628,13 +692,19 @@ class _ContactsScreenState extends State<ContactsScreen>
});
if (exists) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.contacts_groupAlreadyExists(name))),
SnackBar(
content: Text(
context.l10n.contacts_groupAlreadyExists(name),
),
),
);
return;
}
setState(() {
if (isEditing) {
final index = _groups.indexWhere((g) => g.name == group.name);
final index = _groups.indexWhere(
(g) => g.name == group.name,
);
if (index != -1) {
_groups[index] = ContactGroup(
name: name,
@@ -642,7 +712,12 @@ class _ContactsScreenState extends State<ContactsScreen>
);
}
} else {
_groups.add(ContactGroup(name: name, memberKeys: selectedKeys.toList()));
_groups.add(
ContactGroup(
name: name,
memberKeys: selectedKeys.toList(),
),
);
}
});
await _saveGroups();
@@ -650,7 +725,11 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext);
}
},
child: Text(isEditing ? context.l10n.common_save : context.l10n.common_create),
child: Text(
isEditing
? context.l10n.common_save
: context.l10n.common_create,
),
),
],
);
@@ -682,16 +761,24 @@ class _ContactsScreenState extends State<ContactsScreen>
_showRepeaterLogin(context, contact);
},
)
else if (isRoom)
else if (isRoom) ...[
ListTile(
leading: const Icon(Icons.room, color: Colors.blue),
title: Text(context.l10n.contacts_roomLogin),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact);
_showRoomLogin(context, contact, RoomLoginDestination.chat);
},
)
else
),
ListTile(
leading: const Icon(Icons.room_preferences, color: Colors.orange),
title: Text(context.l10n.room_management),
onTap: () {
Navigator.pop(sheetContext);
_showRoomLogin(context, contact, RoomLoginDestination.management);
},
),
] else
ListTile(
leading: const Icon(Icons.chat),
title: Text(context.l10n.contacts_openChat),
@@ -702,7 +789,10 @@ class _ContactsScreenState extends State<ContactsScreen>
),
ListTile(
leading: const Icon(Icons.delete, color: Colors.red),
title: Text(context.l10n.contacts_deleteContact, style: const TextStyle(color: Colors.red)),
title: Text(
context.l10n.contacts_deleteContact,
style: const TextStyle(color: Colors.red),
),
onTap: () {
Navigator.pop(sheetContext);
_confirmDelete(context, connector, contact);
@@ -734,7 +824,10 @@ class _ContactsScreenState extends State<ContactsScreen>
Navigator.pop(dialogContext);
connector.removeContact(contact);
},
child: Text(context.l10n.common_delete, style: const TextStyle(color: Colors.red)),
child: Text(
context.l10n.common_delete,
style: const TextStyle(color: Colors.red),
),
),
],
),
@@ -759,13 +852,17 @@ class _ContactTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
final shotPublicKey =
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
return ListTile(
leading: CircleAvatar(
backgroundColor: _getTypeColor(contact.type),
child: _buildContactAvatar(contact),
),
title: Text(contact.name),
subtitle: Text('${contact.typeLabel}${contact.pathLabel}'),
subtitle: Text(
'${contact.typeLabel}${contact.pathLabel} $shotPublicKey',
),
trailing: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
@@ -790,10 +887,7 @@ class _ContactTile extends StatelessWidget {
Widget _buildContactAvatar(Contact contact) {
final emoji = firstEmoji(contact.name);
if (emoji != null) {
return Text(
emoji,
style: const TextStyle(fontSize: 18),
);
return Text(emoji, style: const TextStyle(fontSize: 18));
}
return Icon(_getTypeIcon(contact.type), color: Colors.white, size: 20);
}
@@ -832,13 +926,21 @@ class _ContactTile extends StatelessWidget {
final now = DateTime.now();
final diff = now.difference(lastSeen);
if (diff.isNegative || diff.inMinutes < 5) return context.l10n.contacts_lastSeenNow;
if (diff.inMinutes < 60) return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
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);
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);
return days == 1
? context.l10n.contacts_lastSeenDayAgo
: context.l10n.contacts_lastSeenDaysAgo(days);
}
}
+291 -130
View File
@@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
@@ -47,6 +49,8 @@ class _MapScreenState extends State<MapScreen> {
final Set<String> _hiddenMarkerIds = {};
Set<String> _removedMarkerIds = {};
bool _isSelectingPoi = false;
bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false;
@override
void initState() {
@@ -67,9 +71,39 @@ class _MapScreenState extends State<MapScreen> {
if (!mounted) return;
setState(() {
_removedMarkerIds = ids;
_removedMarkersLoaded = true;
});
}
double _standardDeviation(List<double> values) {
if (values.length <= 1) {
return 0.0;
}
final mean = values.reduce((a, b) => a + b) / values.length;
double sumSquaredDiff = 0.0;
for (final value in values) {
final diff = value - mean;
sumSquaredDiff += diff * diff;
}
// Sample standard deviation (n-1) most appropriate here
final variance = sumSquaredDiff / (values.length - 1);
return sqrt(variance);
}
// Calculate zoom level based on the spread of points (std deviation in degrees)
double _zoomFromStdDev(double latStdDev, double lonStdDev) {
final maxSpread = max(latStdDev, lonStdDev);
if (maxSpread <= 0) return 13.0;
// Approzimate: each zoom level halves the visible area
// ~0.01 degrees spread -> zoom 13, ~0.1 -> zoom 10, ~1.0 -> zoom 7
final zoom = 10.0 - log(maxSpread * 10 + 1) / ln10 * 3;
return zoom.clamp(4.0, 15.0);
}
@override
Widget build(BuildContext context) {
return Consumer2<MeshCoreConnector, AppSettingsService>(
@@ -80,10 +114,12 @@ class _MapScreenState extends State<MapScreen> {
final highlightPosition = widget.highlightPosition;
final sharedMarkers = settings.mapShowMarkers
? _collectSharedMarkers(connector)
.where((marker) =>
!_hiddenMarkerIds.contains(marker.id) &&
!_removedMarkerIds.contains(marker.id))
.toList()
.where(
(marker) =>
!_hiddenMarkerIds.contains(marker.id) &&
!_removedMarkerIds.contains(marker.id),
)
.toList()
: <_SharedMarker>[];
// Filter by time
@@ -91,16 +127,18 @@ class _MapScreenState extends State<MapScreen> {
final filteredByTime = settings.mapTimeFilterHours == 0
? contacts
: contacts.where((c) {
final hoursSinceLastSeen =
now.difference(c.lastSeen).inHours;
final hoursSinceLastSeen = now.difference(c.lastSeen).inHours;
return hoursSinceLastSeen <= settings.mapTimeFilterHours;
}).toList();
// Filter by key prefix
final keyPrefix = settings.mapKeyPrefix.trim();
final filteredByKeyPrefix = (settings.mapKeyPrefixEnabled && keyPrefix.isNotEmpty)
final filteredByKeyPrefix =
(settings.mapKeyPrefixEnabled && keyPrefix.isNotEmpty)
? filteredByTime.where((c) {
return c.publicKeyHex.toLowerCase().startsWith(keyPrefix.toLowerCase());
return c.publicKeyHex.toLowerCase().startsWith(
keyPrefix.toLowerCase(),
);
}).toList()
: filteredByTime;
@@ -109,30 +147,91 @@ class _MapScreenState extends State<MapScreen> {
.where((c) => c.hasLocation)
.toList();
// Calculate center of all nodes, or default to (0, 0)
// Calculate center and zoom of all nodes, or default to (0, 0)
LatLng center = const LatLng(0, 0);
final hasMapContent = contactsWithLocation.isNotEmpty ||
double initialZoom = 10.0;
final hasMapContent =
contactsWithLocation.isNotEmpty ||
sharedMarkers.isNotEmpty ||
_isSelectingPoi ||
highlightPosition != null;
if (contactsWithLocation.isNotEmpty || sharedMarkers.isNotEmpty) {
double avgLat = contactsWithLocation
.map((c) => c.latitude!)
.fold<double>(0, (sum, lat) => sum + lat);
double avgLon = contactsWithLocation
.map((c) => c.longitude!)
.fold<double>(0, (sum, lon) => sum + lon);
for (final marker in sharedMarkers) {
avgLat += marker.position.latitude;
avgLon += marker.position.longitude;
}
final total = contactsWithLocation.length + sharedMarkers.length;
if (total > 0) {
center = LatLng(avgLat / total, avgLon / total);
final allPoints = [
...contactsWithLocation.map(
(c) => LatLng(c.latitude!, c.longitude!),
),
...sharedMarkers.map((m) => m.position),
];
if (allPoints.length >= 3) {
final latValues = allPoints.map((p) => p.latitude).toList();
final lonValues = allPoints.map((p) => p.longitude).toList();
final meanLat =
latValues.reduce((a, b) => a + b) / latValues.length;
final meanLon =
lonValues.reduce((a, b) => a + b) / lonValues.length;
final latStdDev = _standardDeviation(latValues);
final lonStdDev = _standardDeviation(lonValues);
final filteredPoints = allPoints
.where(
(p) =>
(p.latitude - meanLat).abs() <= latStdDev * 2 &&
(p.longitude - meanLon).abs() <= lonStdDev * 2,
)
.toList();
if (filteredPoints.isNotEmpty) {
final filteredLatValues = filteredPoints
.map((p) => p.latitude)
.toList();
final filteredLonValues = filteredPoints
.map((p) => p.longitude)
.toList();
final avgLat = filteredLatValues.reduce((a, b) => a + b);
final avgLon = filteredLonValues.reduce((a, b) => a + b);
center = LatLng(
avgLat / filteredPoints.length,
avgLon / filteredPoints.length,
);
// Use std deviation of filtered points for zoom
final filteredLatStdDev = _standardDeviation(filteredLatValues);
final filteredLonStdDev = _standardDeviation(filteredLonValues);
initialZoom = _zoomFromStdDev(
filteredLatStdDev,
filteredLonStdDev,
);
} else {
center = LatLng(meanLat, meanLon);
initialZoom = _zoomFromStdDev(latStdDev, lonStdDev);
}
} else {
double avgLat = 0.0;
double avgLon = 0.0;
for (final point in allPoints) {
avgLat += point.latitude;
avgLon += point.longitude;
}
center = LatLng(
avgLat / allPoints.length,
avgLon / allPoints.length,
);
initialZoom = 12.0;
}
}
if (highlightPosition != null) {
center = highlightPosition;
initialZoom = widget.highlightZoom;
}
// Re center map after removed markers have loaded
if (!_hasInitializedMap && _removedMarkersLoaded && hasMapContent) {
_hasInitializedMap = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_mapController.move(center, initialZoom);
}
});
}
final allowBack = !connector.isConnected;
@@ -156,7 +255,9 @@ class _MapScreenState extends State<MapScreen> {
tooltip: context.l10n.common_settings,
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsScreen()),
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
),
),
],
@@ -169,9 +270,12 @@ class _MapScreenState extends State<MapScreen> {
mapController: _mapController,
options: MapOptions(
initialCenter: center,
initialZoom: 13.0,
initialZoom: initialZoom,
minZoom: 2.0,
maxZoom: 18.0,
interactionOptions: InteractionOptions(
flags: ~InteractiveFlag.rotate
),
onTap: (_, latLng) {
if (_isSelectingPoi) {
setState(() {
@@ -234,14 +338,18 @@ class _MapScreenState extends State<MapScreen> {
),
],
),
_buildLegend(contactsWithLocation.length, sharedMarkers.length),
_buildLegend(
contactsWithLocation.length,
sharedMarkers.length,
),
],
),
bottomNavigationBar: SafeArea(
top: false,
child: QuickSwitchBar(
selectedIndex: 2,
onDestinationSelected: (index) => _handleQuickSwitch(index, context),
onDestinationSelected: (index) =>
_handleQuickSwitch(index, context),
),
),
floatingActionButton: FloatingActionButton(
@@ -259,27 +367,17 @@ class _MapScreenState extends State<MapScreen> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_off,
size: 64,
color: Colors.grey[400],
),
Icon(Icons.location_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
context.l10n.map_noNodesWithLocation,
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
context.l10n.map_nodesNeedGps,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
),
],
),
@@ -293,7 +391,9 @@ class _MapScreenState extends State<MapScreen> {
if (!contact.hasLocation) continue;
// Apply node type filters
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) continue;
if (contact.type == advTypeRepeater && !settings.mapShowRepeaters) {
continue;
}
if (contact.type == advTypeChat && !settings.mapShowChatNodes) continue;
if (contact.type != advTypeChat &&
contact.type != advTypeRepeater &&
@@ -396,13 +496,37 @@ class _MapScreenState extends State<MapScreen> {
),
),
const SizedBox(height: 8),
_buildLegendItem(Icons.person, context.l10n.map_chat, Colors.blue),
_buildLegendItem(Icons.router, context.l10n.map_repeater, Colors.green),
_buildLegendItem(Icons.meeting_room, context.l10n.map_room, Colors.purple),
_buildLegendItem(Icons.sensors, context.l10n.map_sensor, Colors.orange),
_buildLegendItem(
Icons.person,
context.l10n.map_chat,
Colors.blue,
),
_buildLegendItem(
Icons.router,
context.l10n.map_repeater,
Colors.green,
),
_buildLegendItem(
Icons.meeting_room,
context.l10n.map_room,
Colors.purple,
),
_buildLegendItem(
Icons.sensors,
context.l10n.map_sensor,
Colors.orange,
),
_buildLegendItem(Icons.flag, context.l10n.map_pinDm, Colors.blue),
_buildLegendItem(Icons.flag, context.l10n.map_pinPrivate, Colors.purple),
_buildLegendItem(Icons.flag, context.l10n.map_pinPublic, Colors.orange),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPrivate,
Colors.purple,
),
_buildLegendItem(
Icons.flag,
context.l10n.map_pinPublic,
Colors.orange,
),
],
),
),
@@ -418,10 +542,7 @@ class _MapScreenState extends State<MapScreen> {
children: [
Icon(icon, size: 16, color: color),
const SizedBox(width: 8),
Text(
label,
style: const TextStyle(fontSize: 12),
),
Text(label, style: const TextStyle(fontSize: 12)),
],
),
);
@@ -475,7 +596,9 @@ class _MapScreenState extends State<MapScreen> {
label: payload.label,
flags: payload.flags,
fromName: message.senderName,
sourceLabel: channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name,
sourceLabel: channel.name.isEmpty
? 'Channel ${channel.index}'
: channel.name,
isChannel: true,
isPublicChannel: isPublic,
),
@@ -541,11 +664,7 @@ class _MapScreenState extends State<MapScreen> {
),
],
),
child: const Icon(
Icons.flag,
color: Colors.white,
size: 20,
),
child: const Icon(Icons.flag, color: Colors.white, size: 20),
),
],
),
@@ -563,10 +682,8 @@ class _MapScreenState extends State<MapScreen> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterHubScreen(
repeater: repeater,
password: password,
),
builder: (context) =>
RepeaterHubScreen(repeater: repeater, password: password),
),
);
},
@@ -584,9 +701,7 @@ class _MapScreenState extends State<MapScreen> {
context.read<MeshCoreConnector>().markContactRead(room.publicKeyHex);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ChatScreen(contact: room),
),
MaterialPageRoute(builder: (context) => ChatScreen(contact: room)),
);
},
),
@@ -613,9 +728,14 @@ class _MapScreenState extends State<MapScreen> {
children: [
_buildInfoRow('Type', contact.typeLabel),
_buildInfoRow('Path', contact.pathLabel),
_buildInfoRow('Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}'),
_buildInfoRow(context.l10n.map_lastSeen, _formatLastSeen(contact.lastSeen)),
_buildInfoRow(
'Location',
'${contact.latitude!.toStringAsFixed(6)}, ${contact.longitude!.toStringAsFixed(6)}',
),
_buildInfoRow(
context.l10n.map_lastSeen,
_formatLastSeen(contact.lastSeen),
),
_buildInfoRow('Public Key', contact.publicKeyHex),
],
),
@@ -624,7 +744,8 @@ class _MapScreenState extends State<MapScreen> {
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_close),
),
if (contact.type == advTypeChat) // Only show chat button for chat nodes
if (contact.type ==
advTypeChat) // Only show chat button for chat nodes
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
@@ -637,22 +758,22 @@ class _MapScreenState extends State<MapScreen> {
},
child: Text(context.l10n.contacts_openChat),
),
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
if (contact.type == advTypeRepeater)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRepeaterLogin(context, contact);
},
child: Text(context.l10n.map_manageRepeater),
),
if (contact.type == advTypeRoom)
TextButton(
onPressed: () {
Navigator.pop(dialogContext);
_showRoomLogin(context, contact);
},
child: Text(context.l10n.map_joinRoom),
),
],
),
);
@@ -664,17 +785,13 @@ class _MapScreenState extends State<MapScreen> {
case 0:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ContactsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
);
break;
case 1:
Navigator.pushReplacement(
context,
buildQuickSwitchRoute(
const ChannelsScreen(hideBackButton: true),
),
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
);
break;
}
@@ -722,7 +839,8 @@ class _MapScreenState extends State<MapScreen> {
'Location',
'${marker.position.latitude.toStringAsFixed(6)}, ${marker.position.longitude.toStringAsFixed(6)}',
),
if (marker.flags.isNotEmpty) _buildInfoRow(context.l10n.map_flags, marker.flags),
if (marker.flags.isNotEmpty)
_buildInfoRow(context.l10n.map_flags, marker.flags),
],
),
actions: [
@@ -772,10 +890,7 @@ class _MapScreenState extends State<MapScreen> {
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(fontSize: 14),
),
Text(value, style: const TextStyle(fontSize: 14)),
],
),
);
@@ -860,7 +975,10 @@ class _MapScreenState extends State<MapScreen> {
);
}
Future<String?> _promptForLabel(BuildContext context, String defaultLabel) async {
Future<String?> _promptForLabel(
BuildContext context,
String defaultLabel,
) async {
final controller = TextEditingController(text: defaultLabel);
return showDialog<String>(
context: context,
@@ -881,7 +999,10 @@ class _MapScreenState extends State<MapScreen> {
TextButton(
onPressed: () {
final label = controller.text.trim().replaceAll('|', '/');
Navigator.pop(dialogContext, label.isEmpty ? defaultLabel : label);
Navigator.pop(
dialogContext,
label.isEmpty ? defaultLabel : label,
);
},
child: Text(context.l10n.common_continue),
),
@@ -913,8 +1034,11 @@ class _MapScreenState extends State<MapScreen> {
return Consumer<MeshCoreConnector>(
builder: (consumerContext, liveConnector, child) {
final allContacts = liveConnector.contacts
.where((contact) =>
contact.type != advTypeRepeater && contact.type != advTypeRoom)
.where(
(contact) =>
contact.type != advTypeRepeater &&
contact.type != advTypeRoom,
)
.toList();
return SafeArea(
child: SingleChildScrollView(
@@ -924,7 +1048,10 @@ class _MapScreenState extends State<MapScreen> {
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(context.l10n.map_sendToContact, style: const TextStyle(fontWeight: FontWeight.bold)),
child: Text(
context.l10n.map_sendToContact,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
@@ -935,7 +1062,10 @@ class _MapScreenState extends State<MapScreen> {
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
setSheetState(() {
@@ -945,50 +1075,73 @@ class _MapScreenState extends State<MapScreen> {
),
),
...allContacts
.where((contact) =>
query.isEmpty || matchesContactQuery(contact, query))
.where(
(contact) =>
query.isEmpty ||
matchesContactQuery(contact, query),
)
.map((contact) {
return ListTile(
leading: const Icon(Icons.person),
title: Text(contact.name),
onTap: () {
Navigator.pop(sheetContext);
liveConnector.sendMessage(contact, markerText);
},
);
}),
return ListTile(
leading: const Icon(Icons.person),
title: Text(contact.name),
onTap: () {
Navigator.pop(sheetContext);
liveConnector.sendMessage(contact, markerText);
},
);
}),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(context.l10n.map_sendToChannel, style: const TextStyle(fontWeight: FontWeight.bold)),
child: Text(
context.l10n.map_sendToChannel,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
if (liveConnector.isLoadingChannels)
const Padding(
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: LinearProgressIndicator(),
)
else if (liveConnector.channels.where((c) => !c.isEmpty).isEmpty)
else if (liveConnector.channels
.where((c) => !c.isEmpty)
.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: Text(context.l10n.map_noChannelsAvailable),
)
else
...liveConnector.channels.where((c) => !c.isEmpty).map((channel) {
...liveConnector.channels.where((c) => !c.isEmpty).map((
channel,
) {
final isPublic = _isPublicChannel(channel);
final label = channel.name.isEmpty ? 'Channel ${channel.index}' : channel.name;
final label = channel.name.isEmpty
? 'Channel ${channel.index}'
: channel.name;
return ListTile(
leading: Icon(
isPublic ? Icons.public : Icons.tag,
color: isPublic ? Colors.orange : Colors.blue,
),
title: Text(label),
subtitle: isPublic ? Text(context.l10n.channels_publicChannel) : null,
subtitle: isPublic
? Text(context.l10n.channels_publicChannel)
: null,
onTap: () async {
Navigator.pop(sheetContext);
final canSend = isPublic
? await _confirmPublicShare(context, label)
: true;
if (canSend) {
liveConnector.sendChannelMessage(channel, markerText);
liveConnector.sendChannelMessage(
channel,
markerText,
);
}
},
);
@@ -1008,12 +1161,17 @@ class _MapScreenState extends State<MapScreen> {
return channel.isPublicChannel;
}
Future<bool> _confirmPublicShare(BuildContext context, String channelLabel) async {
Future<bool> _confirmPublicShare(
BuildContext context,
String channelLabel,
) async {
final result = await showDialog<bool>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.map_publicLocationShare),
content: Text(context.l10n.map_publicLocationShareConfirm(channelLabel)),
content: Text(
context.l10n.map_publicLocationShareConfirm(channelLabel),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext, false),
@@ -1029,7 +1187,10 @@ class _MapScreenState extends State<MapScreen> {
return result ?? false;
}
void _showFilterDialog(BuildContext context, AppSettingsService settingsService) {
void _showFilterDialog(
BuildContext context,
AppSettingsService settingsService,
) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
@@ -1133,10 +1294,7 @@ class _MapScreenState extends State<MapScreen> {
const SizedBox(height: 8),
Text(
_getTimeFilterLabel(settings.mapTimeFilterHours),
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
),
Slider(
value: _hoursToSliderValue(settings.mapTimeFilterHours),
@@ -1176,11 +1334,14 @@ class _MapScreenState extends State<MapScreen> {
if (hours <= 24) {
return (hours / 24) * 40;
} else if (hours <= 168) { // 7 days
} else if (hours <= 168) {
// 7 days
return 40 + ((hours - 24) / (168 - 24)) * 20;
} else if (hours <= 720) { // 30 days
} else if (hours <= 720) {
// 30 days
return 60 + ((hours - 168) / (720 - 168)) * 20;
} else if (hours <= 4380) { // 6 months
} else if (hours <= 4380) {
// 6 months
return 80 + ((hours - 720) / (4380 - 720)) * 19;
} else {
return 100;
+456
View File
@@ -0,0 +1,456 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import '../models/path_selection.dart';
import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart';
import '../services/repeater_command_service.dart';
import '../widgets/path_management_dialog.dart';
import '../widgets/snr_indicator.dart';
class NeighboursScreen extends StatefulWidget {
final Contact repeater;
final String password;
const NeighboursScreen({
super.key,
required this.repeater,
required this.password,
});
@override
State<NeighboursScreen> createState() => _NeighboursScreenState();
}
class _NeighboursScreenState extends State<NeighboursScreen> {
static const int _reqNeighboursKeyLen = 4;
static const int _statusPayloadOffset = 8;
static const int _statusStatsSize = 52;
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _neighbourCount = 0;
bool _isLoading = false;
bool _isLoaded = false;
bool _hasData = false;
Timer? _statusTimeout;
StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService;
PathSelection? _pendingStatusSelection;
List<Map<String, dynamic>>? _parsedNeighbours;
@override
void initState() {
super.initState();
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
_commandService = RepeaterCommandService(connector);
_setupMessageListener();
_loadNeighbours();
_hasData = false;
}
void _setupMessageListener() {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
// Listen for incoming text messages from the repeater
_frameSubscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty) return;
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleNeighboursResponse(connector, frame.sublist(6));
}
});
}
String fmtDuration(double seconds) {
if (seconds < 60) {
return '${seconds.toStringAsFixed(1)}s';
}
final int m = (seconds ~/ 60).toInt();
final double s = seconds - (60 * m);
if (m < 60) {
return '${m}m ${s.toStringAsFixed(0)}s';
}
final int h = m ~/ 60;
final int m2 = m % 60;
return '${h}h ${m2}m';
}
static List<Map<String, dynamic>> parseNeighboursData(
BufferReader buffer,
int resultsCount,
) {
final Map<int, Map<String, dynamic>> neighbours = {};
for (var i = 0; i < resultsCount; i++) {
final neighbourData = neighbours.putIfAbsent(
i,
() => {
'contact': null,
'publicKey': <Uint8List>{},
'lastHeard': <int>{},
'snr': <double>{},
},
);
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
neighbourData['lastHeard'] = buffer.readUInt32LE();
neighbourData['snr'] = buffer.readInt8() / 4.0;
}
return neighbours.values.toList();
}
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
final buffer = BufferReader(frame);
final neighbourCount = buffer.readUInt16LE();
final parsedNeighbours = parseNeighboursData(buffer, buffer.readUInt16LE());
connector.contacts.where((c) => c.type == advTypeRepeater).forEach((
repeater,
) {
for (var neighbourData in parsedNeighbours) {
final publicKey = neighbourData['publicKey'];
if (listEquals(
repeater.publicKey.sublist(0, _reqNeighboursKeyLen),
publicKey,
)) {
neighbourData['contact'] = repeater;
}
}
});
setState(() {
_parsedNeighbours = parsedNeighbours;
_neighbourCount = neighbourCount;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_receivedData),
backgroundColor: Colors.green,
),
);
_statusTimeout?.cancel();
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = true;
_hasData = true;
});
}
Contact _resolveRepeater(MeshCoreConnector connector) {
return connector.contacts.firstWhere(
(c) => c.publicKeyHex == widget.repeater.publicKeyHex,
orElse: () => widget.repeater,
);
}
Future<void> _loadNeighbours() async {
if (_commandService == null) return;
setState(() {
_isLoading = true;
_isLoaded = false;
});
try {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
final selection = await connector.preparePathForContactSend(repeater);
_pendingStatusSelection = selection;
//[version][number of requested neighbours][offset_16bit][order by][len of public key]
final frame = buildSendBinaryReq(
repeater.publicKey,
payload: Uint8List.fromList([
reqTypeGetNeighbours,
0x00,
0x0F,
0x00,
0x00,
0x00,
_reqNeighboursKeyLen,
]),
);
await connector.sendFrame(frame);
final pathLengthValue = selection.useFlood ? -1 : selection.hopCount;
final messageBytes = frame.length >= _statusResponseBytes
? frame.length
: _statusResponseBytes;
final timeoutMs = connector.calculateTimeout(
pathLength: pathLengthValue,
messageBytes: messageBytes,
);
_statusTimeout?.cancel();
_statusTimeout = Timer(Duration(milliseconds: timeoutMs), () {
if (!mounted) return;
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_requestTimedOut),
backgroundColor: Colors.red,
),
);
_recordStatusResult(false);
});
} catch (e) {
if (mounted) {
setState(() {
_isLoading = false;
_isLoaded = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.neighbors_errorLoading(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
void _recordStatusResult(bool success) {
final selection = _pendingStatusSelection;
if (selection == null) return;
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final repeater = _resolveRepeater(connector);
connector.recordRepeaterPathResult(repeater, selection, success, null);
_pendingStatusSelection = null;
}
@override
void dispose() {
_frameSubscription?.cancel();
_commandService?.dispose();
_statusTimeout?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
final connector = context.watch<MeshCoreConnector>();
final repeater = _resolveRepeater(connector);
final isFloodMode = repeater.pathOverride == -1;
return Scaffold(
appBar: AppBar(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
l10n.neighbors_repeatersNeighbours,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Text(
repeater.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
centerTitle: false,
actions: [
PopupMenuButton<String>(
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
tooltip: l10n.repeater_routingMode,
onSelected: (mode) async {
if (mode == 'flood') {
await connector.setPathOverride(repeater, pathLen: -1);
} else {
await connector.setPathOverride(repeater, pathLen: null);
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'auto',
child: Row(
children: [
Icon(
Icons.auto_mode,
size: 20,
color: !isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_autoUseSavedPath,
style: TextStyle(
fontWeight: !isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
PopupMenuItem(
value: 'flood',
child: Row(
children: [
Icon(
Icons.waves,
size: 20,
color: isFloodMode
? Theme.of(context).primaryColor
: null,
),
const SizedBox(width: 8),
Text(
l10n.repeater_forceFloodMode,
style: TextStyle(
fontWeight: isFloodMode
? FontWeight.bold
: FontWeight.normal,
),
),
],
),
),
],
),
IconButton(
icon: const Icon(Icons.timeline),
tooltip: l10n.repeater_pathManagement,
onPressed: () =>
PathManagementDialog.show(context, contact: repeater),
),
IconButton(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
onPressed: _isLoading ? null : _loadNeighbours,
tooltip: l10n.repeater_refresh,
),
],
),
body: SafeArea(
top: false,
child: RefreshIndicator(
onRefresh: _loadNeighbours,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
if (!_isLoaded &&
!_hasData &&
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
Center(
child: Text(
l10n.neighbors_noData,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
if (_isLoaded ||
_hasData &&
!(_parsedNeighbours == null ||
_parsedNeighbours!.isEmpty))
_buildNeighboursInfoCard(
"${l10n.repeater_neighbours} - $_neighbourCount",
),
],
),
),
),
);
}
Widget _buildNeighboursInfoCard(String title) {
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
color: Theme.of(context).textTheme.headlineSmall?.color,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const Divider(),
for (final entry in _parsedNeighbours!.asMap().entries)
_buildInfoRow(
entry.value['contact'] != null
? entry.value['contact'].name
: context.l10n.neighbors_unknownContact(
"<${pubKeyToHex(entry.value['publicKey'])}>",
),
context.l10n.neighbors_heardAgo(
fmtDuration(entry.value['lastHeard'] + 0.0),
),
entry.value['snr'],
connector.currentSf!,
),
],
),
),
);
}
Widget _buildInfoRow(
String label,
String value,
double snr,
int spreadingFactor,
) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: ListTile(
contentPadding: EdgeInsets.zero,
title: Text(
label,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(value),
trailing: SNRIcon(
snr: snr,
snrLevels: getSNRfromSF(spreadingFactor),
),
),
),
],
),
);
}
}
+163 -120
View File
@@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/contact.dart';
import 'repeater_status_screen.dart';
import 'repeater_cli_screen.dart';
import 'repeater_settings_screen.dart';
import 'telemetry_screen.dart';
import 'neighbours_screen.dart';
class RepeaterHubScreen extends StatelessWidget {
final Contact repeater;
@@ -25,10 +27,17 @@ class RepeaterHubScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(l10n.repeater_management),
Text(
repeater.type == advTypeRepeater
? l10n.repeater_management
: l10n.room_management,
),
Text(
repeater.name,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.normal,
),
),
],
),
@@ -39,130 +48,167 @@ class RepeaterHubScreen extends StatelessWidget {
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(Icons.cell_tower, size: 40, color: Colors.white),
// Repeater info card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
CircleAvatar(
radius: 40,
backgroundColor: Colors.orange,
child: const Icon(
Icons.cell_tower,
size: 40,
color: Colors.white,
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
repeater.name,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.location_on, size: 14, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
const SizedBox(height: 8),
Text(
repeater.pathLabel,
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
if (repeater.hasLocation) ...[
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.location_on,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
'${repeater.latitude?.toStringAsFixed(4)}, ${repeater.longitude?.toStringAsFixed(4)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
],
),
],
),
],
),
],
),
],
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
),
const SizedBox(height: 24),
Text(
l10n.repeater_managementTools,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// Status button
_buildManagementCard(
context,
icon: Icons.analytics,
title: l10n.repeater_status,
subtitle: l10n.repeater_statusSubtitle,
color: Colors.blue,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterStatusScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TelemetryScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 16),
// Telemetry button
_buildManagementCard(
context,
icon: Icons.bar_chart_sharp,
title: l10n.repeater_telemetry,
subtitle: l10n.repeater_telemetrySubtitle,
color: Colors.teal,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
TelemetryScreen(repeater: repeater, password: password),
),
);
},
),
const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 12),
// CLI button
_buildManagementCard(
context,
icon: Icons.terminal,
title: l10n.repeater_cli,
subtitle: l10n.repeater_cliSubtitle,
color: Colors.green,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterCliScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 12),
// Neighbors button
_buildManagementCard(
context,
icon: Icons.group,
title: l10n.repeater_neighbours,
subtitle: l10n.repeater_neighboursSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NeighboursScreen(
repeater: repeater,
password: password,
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.orange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
),
);
},
),
const SizedBox(height: 12),
// Settings button
_buildManagementCard(
context,
icon: Icons.settings,
title: l10n.repeater_settings,
subtitle: l10n.repeater_settingsSubtitle,
color: Colors.deepOrange,
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepeaterSettingsScreen(
repeater: repeater,
password: password,
),
);
},
),
),
);
},
),
],
),
),
@@ -209,10 +255,7 @@ class RepeaterHubScreen extends StatelessWidget {
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
),
],
),
+3 -3
View File
@@ -895,7 +895,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _bandwidth,
initialValue: _bandwidth,
decoration: InputDecoration(
labelText: l10n.repeater_bandwidth,
border: const OutlineInputBorder(),
@@ -917,7 +917,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _spreadingFactor,
initialValue: _spreadingFactor,
decoration: InputDecoration(
labelText: l10n.repeater_spreadingFactor,
border: const OutlineInputBorder(),
@@ -939,7 +939,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
),
const SizedBox(height: 16),
DropdownButtonFormField<int>(
value: _codingRate,
initialValue: _codingRate,
decoration: InputDecoration(
labelText: l10n.repeater_codingRate,
border: const OutlineInputBorder(),
+227 -122
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:meshcore_open/widgets/elements_ui.dart';
import 'package:provider/provider.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -38,10 +39,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings_title),
centerTitle: true,
),
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
body: SafeArea(
top: false,
child: Consumer<MeshCoreConnector>(
@@ -68,7 +66,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _buildDeviceInfoCard(BuildContext context, MeshCoreConnector connector) {
Widget _buildDeviceInfoCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Padding(
@@ -83,21 +84,38 @@ class _SettingsScreenState extends State<SettingsScreen> {
const SizedBox(height: 16),
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
_buildInfoRow(l10n.settings_infoStatus, connector.isConnected ? l10n.common_connected : l10n.common_disconnected),
_buildInfoRow(
l10n.settings_infoStatus,
connector.isConnected
? l10n.common_connected
: l10n.common_disconnected,
),
_buildBatteryInfoRow(context, connector),
if (connector.selfName != null)
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
if (connector.selfPublicKey != null)
_buildInfoRow(l10n.settings_infoPublicKey, '${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...'),
_buildInfoRow(l10n.settings_infoContactsCount, '${connector.contacts.length}'),
_buildInfoRow(l10n.settings_infoChannelCount, '${connector.channels.length}'),
_buildInfoRow(
l10n.settings_infoPublicKey,
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
),
_buildInfoRow(
l10n.settings_infoContactsCount,
'${connector.contacts.length}',
),
_buildInfoRow(
l10n.settings_infoChannelCount,
'${connector.channels.length}',
),
],
),
),
);
}
Widget _buildBatteryInfoRow(BuildContext context, MeshCoreConnector connector) {
Widget _buildBatteryInfoRow(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
final percent = connector.batteryPercent;
final millivolts = connector.batteryMillivolts;
@@ -167,7 +185,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
Widget _buildNodeSettingsCard(BuildContext context, MeshCoreConnector connector) {
Widget _buildNodeSettingsCard(
BuildContext context,
MeshCoreConnector connector,
) {
final l10n = context.l10n;
return Card(
child: Column(
@@ -298,7 +319,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const BleDebugLogScreen()),
MaterialPageRoute(
builder: (context) => const BleDebugLogScreen(),
),
);
},
),
@@ -311,7 +334,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const AppDebugLogScreen()),
MaterialPageRoute(
builder: (context) => const AppDebugLogScreen(),
),
);
},
),
@@ -334,20 +359,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
children: [
Row(
children: [
if (leading != null) ...[
leading,
const SizedBox(width: 8),
],
if (leading != null) ...[leading, const SizedBox(width: 8)],
Text(label, style: TextStyle(color: Colors.grey[600])),
],
),
Flexible(
child: Text(
value,
style: TextStyle(
fontWeight: FontWeight.w500,
color: valueColor,
),
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
overflow: TextOverflow.ellipsis,
),
),
@@ -413,75 +432,154 @@ class _SettingsScreenState extends State<SettingsScreen> {
final l10n = context.l10n;
final latController = TextEditingController();
final lonController = TextEditingController();
final intervalController = TextEditingController();
latController.text = connector.selfLatitude?.toStringAsFixed(6) ?? '';
lonController.text = connector.selfLongitude?.toStringAsFixed(6) ?? '';
// Safe access to custom vars - may be null before device responds
final customVars = connector.currentCustomVars ?? {};
final bool hasGPS = customVars.containsKey("gps");
bool isGPSEnabled = customVars["gps"] == "1";
// Read current interval or default to 900 (15 minutes)
final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
intervalController.text = currentInterval.toString();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_location),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: latController,
decoration: InputDecoration(
labelText: l10n.settings_latitude,
border: const OutlineInputBorder(),
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.settings_location),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: latController,
decoration: InputDecoration(
labelText: l10n.settings_latitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
const SizedBox(height: 16),
TextField(
controller: lonController,
decoration: InputDecoration(
labelText: l10n.settings_longitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
signed: true,
),
),
if (hasGPS) ...[
const SizedBox(height: 16),
TextField(
controller: intervalController,
decoration: InputDecoration(
labelText: l10n.settings_locationIntervalSec,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(
decimal: false,
signed: false,
),
),
const SizedBox(height: 16),
FeatureToggleRow(
title: l10n.settings_locationGPSEnable,
subtitle: l10n.settings_locationGPSEnableSubtitle,
value: isGPSEnabled,
onChanged: (value) async {
setDialogState(() => isGPSEnabled = value);
if (value) {
await connector.setCustomVar("gps:1");
} else {
await connector.setCustomVar("gps:0");
}
},
),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
const SizedBox(height: 16),
TextField(
controller: lonController,
decoration: InputDecoration(
labelText: l10n.settings_longitude,
border: const OutlineInputBorder(),
),
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
TextButton(
onPressed: () async {
Navigator.pop(context);
if (hasGPS) {
final intervalText = intervalController.text.trim();
if (intervalText.isEmpty) {
return;
}
final interval = int.tryParse(intervalText);
if (interval == null || interval < 60 || interval >= 86400) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.settings_locationIntervalInvalid),
),
);
return;
}
await connector.setCustomVar("gps_interval:$interval");
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
}
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty
? double.tryParse(latText)
: currentLat;
final lon = lonText.isNotEmpty
? double.tryParse(lonText)
: currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)),
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () async {
Navigator.pop(context);
final latText = latController.text.trim();
final lonText = lonController.text.trim();
if (latText.isEmpty && lonText.isEmpty) {
return;
}
final currentLat = connector.selfLatitude;
final currentLon = connector.selfLongitude;
final lat = latText.isNotEmpty ? double.tryParse(latText) : currentLat;
final lon = lonText.isNotEmpty ? double.tryParse(lonText) : currentLon;
if (lat == null || lon == null) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationBothRequired)),
);
return;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationInvalid)),
);
return;
}
await connector.setNodeLocation(lat: lat, lon: lon);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_locationUpdated)),
);
},
child: Text(l10n.common_save),
),
],
),
);
}
@@ -530,17 +628,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _sendAdvert(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.sendSelfAdvert(flood: true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_advertisementSent)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_advertisementSent)));
}
void _syncTime(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
connector.syncTime();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_timeSynchronized)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_timeSynchronized)));
}
void _confirmReboot(BuildContext context, MeshCoreConnector connector) {
@@ -560,7 +658,10 @@ class _SettingsScreenState extends State<SettingsScreen> {
Navigator.pop(context);
connector.rebootDevice();
},
child: Text(l10n.common_reboot, style: const TextStyle(color: Colors.orange)),
child: Text(
l10n.common_reboot,
style: const TextStyle(color: Colors.orange),
),
),
],
),
@@ -572,7 +673,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
showAboutDialog(
context: context,
applicationName: l10n.appTitle,
applicationVersion: _appVersion.isEmpty ? l10n.common_loading : _appVersion,
applicationVersion: _appVersion.isEmpty
? l10n.common_loading
: _appVersion,
applicationLegalese: l10n.settings_aboutLegalese,
children: [
const SizedBox(height: 16),
@@ -604,7 +707,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
// Populate with current settings if available
if (widget.connector.currentFreqHz != null) {
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0).toStringAsFixed(3);
_frequencyController.text = (widget.connector.currentFreqHz! / 1000.0)
.toStringAsFixed(3);
} else {
_frequencyController.text = '915.0';
}
@@ -670,26 +774,31 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
final txPower = int.tryParse(_txPowerController.text);
if (freqMHz == null || freqMHz < 300 || freqMHz > 2500) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_frequencyInvalid)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_frequencyInvalid)));
return;
}
if (txPower == null || txPower < 0 || txPower > 22) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(l10n.settings_txPowerInvalid)),
);
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
return;
}
final freqHz = (freqMHz * 1000).round();
final bwHz = _bandwidth.hz;
final sf = _spreadingFactor.value;
final cr = _toDeviceCodingRate(_codingRate.value, widget.connector.currentCr);
final cr = _toDeviceCodingRate(
_codingRate.value,
widget.connector.currentCr,
);
try {
await widget.connector.sendFrame(buildSetRadioParamsFrame(freqHz, bwHz, sf, cr));
await widget.connector.sendFrame(
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
);
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
await widget.connector.refreshDeviceInfo();
@@ -727,7 +836,10 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.settings_presets, style: const TextStyle(fontWeight: FontWeight.bold)),
Text(
l10n.settings_presets,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
@@ -762,7 +874,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
helperText: l10n.settings_frequencyHelper,
),
keyboardType: const TextInputType.numberWithOptions(decimal: true),
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
),
const SizedBox(height: 16),
DropdownButtonFormField<LoRaBandwidth>(
@@ -772,10 +886,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaBandwidth.values
.map((bw) => DropdownMenuItem(
value: bw,
child: Text(bw.label),
))
.map(
(bw) => DropdownMenuItem(value: bw, child: Text(bw.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _bandwidth = value);
@@ -789,10 +902,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaSpreadingFactor.values
.map((sf) => DropdownMenuItem(
value: sf,
child: Text(sf.label),
))
.map(
(sf) => DropdownMenuItem(value: sf, child: Text(sf.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _spreadingFactor = value);
@@ -806,10 +918,9 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
border: const OutlineInputBorder(),
),
items: LoRaCodingRate.values
.map((cr) => DropdownMenuItem(
value: cr,
child: Text(cr.label),
))
.map(
(cr) => DropdownMenuItem(value: cr, child: Text(cr.label)),
)
.toList(),
onChanged: (value) {
if (value != null) setState(() => _codingRate = value);
@@ -833,10 +944,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
FilledButton(
onPressed: _saveSettings,
child: Text(l10n.common_save),
),
FilledButton(onPressed: _saveSettings, child: Text(l10n.common_save)),
],
);
}
@@ -850,9 +958,6 @@ class _PresetChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ActionChip(
label: Text(label),
onPressed: onTap,
);
return ActionChip(label: Text(label), onPressed: onTap);
}
}
+4 -7
View File
@@ -1,7 +1,4 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -34,7 +31,6 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
static const int _statusResponseBytes =
_statusPayloadOffset + _statusStatsSize;
Uint8List _tagData = Uint8List(4);
int _timeEstment = 0;
bool _isLoading = false;
bool _isLoaded = false;
@@ -64,18 +60,19 @@ class _TelemetryScreenState extends State<TelemetryScreen> {
if (frame[0] == respCodeSent) {
_tagData = frame.sublist(2, 6);
_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
}
// Check if it's a binary response
if (frame[0] == pushCodeBinaryResponse &&
listEquals(frame.sublist(2, 6), _tagData)) {
_handleStatusResponse(context, frame.sublist(6));
if (!mounted) return;
_handleStatusResponse(frame.sublist(6));
}
});
}
void _handleStatusResponse(BuildContext context, Uint8List frame) {
void _handleStatusResponse(Uint8List frame) {
if (!mounted) return;
setState(() {
_parsedTelemetry = CayenneLpp.parseByChannel(frame);
});
+46 -11
View File
@@ -16,7 +16,9 @@ class BleDebugLogEntry {
String get hexPreview {
const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex;
}
@@ -26,14 +28,13 @@ class BleRawLogRxEntry {
final DateTime timestamp;
final Uint8List payload;
BleRawLogRxEntry({
required this.timestamp,
required this.payload,
});
BleRawLogRxEntry({required this.timestamp, required this.payload});
String get hexPreview {
const maxBytes = 64;
final bytes = payload.length > maxBytes ? payload.sublist(0, maxBytes) : payload;
final bytes = payload.length > maxBytes
? payload.sublist(0, maxBytes)
: payload;
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
return payload.length > maxBytes ? '$hex' : hex;
}
@@ -45,7 +46,8 @@ class BleDebugLogService extends ChangeNotifier {
final List<BleRawLogRxEntry> _rawLogRxEntries = [];
List<BleDebugLogEntry> get entries => List.unmodifiable(_entries);
List<BleRawLogRxEntry> get rawLogRxEntries => List.unmodifiable(_rawLogRxEntries);
List<BleRawLogRxEntry> get rawLogRxEntries =>
List.unmodifiable(_rawLogRxEntries);
void logFrame(Uint8List frame, {required bool outgoing, String? note}) {
if (frame.isEmpty) return;
@@ -85,15 +87,32 @@ class BleDebugLogService extends ChangeNotifier {
notifyListeners();
}
String _describeFrame(int code, Uint8List frame, bool outgoing, String? note) {
final label = _codeLabel(code);
String _describeFrame(
int code,
Uint8List frame,
bool outgoing,
String? note,
) {
final label = _codeLabel(code, outgoing: outgoing);
final prefix = outgoing ? 'TX' : 'RX';
final extra = _frameDetail(code, frame);
final noteText = note != null ? '$note' : '';
return '$prefix $label$extra$noteText';
}
String _codeLabel(int code) {
String _codeLabel(int code, {required bool outgoing}) {
if (outgoing) {
return _commandLabel(code) ?? 'CODE_$code';
}
final pushLabel = _pushLabel(code);
if (pushLabel != null) return pushLabel;
final responseLabel = _responseLabel(code);
if (responseLabel != null) return responseLabel;
return 'CODE_$code';
}
String? _commandLabel(int code) {
switch (code) {
case cmdAppStart:
return 'CMD_APP_START';
@@ -135,6 +154,15 @@ class BleDebugLogService extends ChangeNotifier {
return 'CMD_SET_CHANNEL';
case cmdGetRadioSettings:
return 'CMD_GET_RADIO_SETTINGS';
case cmdSetCustomVar:
return 'CMD_SET_CUSTOM_VAR';
default:
return null;
}
}
String? _responseLabel(int code) {
switch (code) {
case respCodeOk:
return 'RESP_CODE_OK';
case respCodeErr:
@@ -167,6 +195,13 @@ class BleDebugLogService extends ChangeNotifier {
return 'RESP_CODE_CHANNEL_INFO';
case respCodeRadioSettings:
return 'RESP_CODE_RADIO_SETTINGS';
default:
return null;
}
}
String? _pushLabel(int code) {
switch (code) {
case pushCodeAdvert:
return 'PUSH_CODE_ADVERT';
case pushCodePathUpdated:
@@ -184,7 +219,7 @@ class BleDebugLogService extends ChangeNotifier {
case pushCodeNewAdvert:
return 'PUSH_CODE_NEW_ADVERT';
default:
return 'CODE_$code';
return null;
}
}
+1 -3
View File
@@ -6,7 +6,6 @@ import 'package:crypto/crypto.dart';
import '../models/contact.dart';
import '../models/message.dart';
import '../models/path_selection.dart';
import 'storage_service.dart';
import 'app_settings_service.dart';
import 'app_debug_log_service.dart';
@@ -36,7 +35,6 @@ class MessageRetryService extends ChangeNotifier {
static const int maxRetries = 5;
static const int maxAckHistorySize = 100;
final StorageService _storage;
final Map<String, Timer> _timeoutTimers = {};
final Map<String, Message> _pendingMessages = {};
final Map<String, Contact> _pendingContacts = {};
@@ -59,7 +57,7 @@ class MessageRetryService extends ChangeNotifier {
AppDebugLogService? _debugLogService;
Function(String, PathSelection, bool, int?)? _recordPathResultCallback;
MessageRetryService(this._storage);
MessageRetryService();
void initialize({
required Function(Contact, String, int, int) sendMessageCallback,
+117
View File
@@ -0,0 +1,117 @@
import 'dart:convert';
import '../models/community.dart';
import 'prefs_manager.dart';
/// Persists communities to local storage using SharedPreferences.
///
/// Communities are stored as a JSON array under a single key.
/// Each community contains its secret K, so this data should
/// be considered sensitive (though device encryption handles security).
class CommunityStore {
static const String _communitiesKey = 'communities_v1';
/// Load all communities from storage
Future<List<Community>> loadCommunities() async {
final prefs = PrefsManager.instance;
final jsonString = prefs.getString(_communitiesKey);
if (jsonString == null || jsonString.isEmpty) {
return [];
}
try {
final jsonList = jsonDecode(jsonString) as List<dynamic>;
return jsonList
.map((json) => Community.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
// If JSON is corrupted, return empty list
return [];
}
}
/// Save all communities to storage
Future<void> saveCommunities(List<Community> communities) async {
final prefs = PrefsManager.instance;
final jsonList = communities.map((c) => c.toJson()).toList();
await prefs.setString(_communitiesKey, jsonEncode(jsonList));
}
/// Add a new community
Future<void> addCommunity(Community community) async {
final communities = await loadCommunities();
// Check if community with same ID already exists
final existingIndex = communities.indexWhere((c) => c.id == community.id);
if (existingIndex >= 0) {
// Replace existing
communities[existingIndex] = community;
} else {
communities.add(community);
}
await saveCommunities(communities);
}
/// Update an existing community
Future<void> updateCommunity(Community community) async {
final communities = await loadCommunities();
final index = communities.indexWhere((c) => c.id == community.id);
if (index >= 0) {
communities[index] = community;
await saveCommunities(communities);
}
}
/// Remove a community by ID
Future<void> removeCommunity(String communityId) async {
final communities = await loadCommunities();
communities.removeWhere((c) => c.id == communityId);
await saveCommunities(communities);
}
/// Get a community by ID
Future<Community?> getCommunity(String communityId) async {
final communities = await loadCommunities();
try {
return communities.firstWhere((c) => c.id == communityId);
} catch (_) {
return null;
}
}
/// Check if a community with the same secret already exists
/// (to prevent duplicate imports from QR scanning)
Future<Community?> findByCommunityId(String cid) async {
final communities = await loadCommunities();
try {
return communities.firstWhere((c) => c.communityId == cid);
} catch (_) {
return null;
}
}
/// Add a hashtag channel to a community
Future<void> addHashtagChannel(
String communityId,
String hashtag,
) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.addHashtagChannel(hashtag);
await updateCommunity(updated);
}
}
/// Remove a hashtag channel from a community
Future<void> removeHashtagChannel(
String communityId,
String hashtag,
) async {
final community = await getCommunity(communityId);
if (community != null) {
final updated = community.removeHashtagChannel(hashtag);
await updateCommunity(updated);
}
}
}
+59
View File
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
class FeatureToggleRow extends StatefulWidget {
final String title;
final String subtitle;
final bool value;
final bool hasRefreshing;
final bool isRefreshing;
final ValueChanged<bool>? onChanged;
final VoidCallback? onRefresh;
final String? refreshTooltip;
const FeatureToggleRow({
super.key,
required this.title,
required this.subtitle,
required this.value,
this.hasRefreshing = false,
this.isRefreshing = false,
this.onChanged,
this.onRefresh,
this.refreshTooltip,
});
@override
State<FeatureToggleRow> createState() => _FeatureToggleRow();
}
class _FeatureToggleRow extends State<FeatureToggleRow> {
@override
Widget build(BuildContext context) {
return Row(
children: [
Expanded(
child: SwitchListTile(
title: Text(widget.title),
subtitle: Text(widget.subtitle),
value: widget.value,
onChanged: widget.onChanged,
contentPadding: EdgeInsets.zero,
),
),
if (widget.hasRefreshing)
IconButton(
icon: widget.isRefreshing
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh, size: 20),
onPressed: widget.isRefreshing ? null : widget.onRefresh,
tooltip: widget.refreshTooltip,
visualDensity: VisualDensity.compact,
),
],
);
}
}
+233
View File
@@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:qr_flutter/qr_flutter.dart';
/// A reusable QR code display widget for sharing data.
///
/// Features:
/// - Configurable size and colors
/// - Optional logo/icon in center
/// - Automatic theming (light/dark mode aware)
/// - Title and instructions
class QrCodeDisplay extends StatelessWidget {
/// The data to encode in the QR code
final String data;
/// Size of the QR code (width and height)
final double size;
/// Optional widget to display in the center (e.g., app logo)
final Widget? embeddedImage;
/// Size of the embedded image (if provided)
final double embeddedImageSize;
/// Title displayed above the QR code
final String? title;
/// Instructions displayed below the QR code
final String? instructions;
/// Background color of the QR code (defaults to white)
final Color? backgroundColor;
/// Foreground color of the QR code modules (defaults to black)
final Color? foregroundColor;
/// Padding around the QR code
final EdgeInsets padding;
/// Error correction level
final int errorCorrectionLevel;
const QrCodeDisplay({
super.key,
required this.data,
this.size = 200,
this.embeddedImage,
this.embeddedImageSize = 50,
this.title,
this.instructions,
this.backgroundColor,
this.foregroundColor,
this.padding = const EdgeInsets.all(16),
this.errorCorrectionLevel = QrErrorCorrectLevel.M,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
// Default colors based on theme
final bgColor = backgroundColor ?? Colors.white;
final fgColor = foregroundColor ?? Colors.black;
return Padding(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (title != null) ...[
Text(
title!,
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
],
// QR code container with rounded corners
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(16),
boxShadow: isDark
? null
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: embeddedImage != null
? _buildQrWithEmbeddedImage(fgColor, bgColor)
: _buildSimpleQr(fgColor, bgColor),
),
if (instructions != null) ...[
const SizedBox(height: 16),
Text(
instructions!,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
],
],
),
);
}
Widget _buildSimpleQr(Color fgColor, Color bgColor) {
return QrImageView(
data: data,
version: QrVersions.auto,
size: size,
backgroundColor: bgColor,
errorCorrectionLevel: errorCorrectionLevel,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
),
);
}
Widget _buildQrWithEmbeddedImage(Color fgColor, Color bgColor) {
return Stack(
alignment: Alignment.center,
children: [
QrImageView(
data: data,
version: QrVersions.auto,
size: size,
backgroundColor: bgColor,
// Use higher error correction when embedding image
errorCorrectionLevel: QrErrorCorrectLevel.H,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: fgColor,
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: fgColor,
),
),
Container(
width: embeddedImageSize,
height: embeddedImageSize,
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(4),
child: embeddedImage,
),
],
);
}
}
/// Dialog to display a QR code for sharing
class QrCodeShareDialog extends StatelessWidget {
final String data;
final String? title;
final String? instructions;
final Widget? embeddedImage;
const QrCodeShareDialog({
super.key,
required this.data,
this.title,
this.instructions,
this.embeddedImage,
});
@override
Widget build(BuildContext context) {
return Dialog(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
QrCodeDisplay(
data: data,
size: 250,
title: title,
instructions: instructions,
embeddedImage: embeddedImage,
padding: EdgeInsets.zero,
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: () => Navigator.pop(context),
child: const Text('Done'),
),
),
],
),
),
);
}
/// Show the dialog
static Future<void> show({
required BuildContext context,
required String data,
String? title,
String? instructions,
Widget? embeddedImage,
}) {
return showDialog(
context: context,
builder: (context) => QrCodeShareDialog(
data: data,
title: title,
instructions: instructions,
embeddedImage: embeddedImage,
),
);
}
}
+391
View File
@@ -0,0 +1,391 @@
import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
/// A reusable QR code scanner widget that can be embedded anywhere.
///
/// Features:
/// - Configurable scan window overlay
/// - Flash toggle button
/// - Camera switch button (front/back)
/// - Customizable callbacks for scan results
/// - Optional validation function for QR data
/// - Automatic pause when not visible
/// - Debouncing to prevent duplicate scans
class QrScannerWidget extends StatefulWidget {
/// Called when a valid QR code is scanned
final void Function(String data) onScanned;
/// Optional validator - return true if the QR data is valid
final bool Function(String data)? validator;
/// Optional error callback when validation fails
final void Function(String data)? onValidationFailed;
/// Whether to show the flash toggle button
final bool showFlashButton;
/// Whether to show the camera switch button
final bool showCameraSwitchButton;
/// Custom overlay widget (defaults to scan window frame)
final Widget? overlay;
/// Instructions text shown below the scan window
final String? instructions;
/// Whether to continue scanning after first successful scan
final bool continuousScanning;
/// Debounce duration to prevent duplicate scans
final Duration debounceDuration;
const QrScannerWidget({
super.key,
required this.onScanned,
this.validator,
this.onValidationFailed,
this.showFlashButton = true,
this.showCameraSwitchButton = true,
this.overlay,
this.instructions,
this.continuousScanning = false,
this.debounceDuration = const Duration(milliseconds: 500),
});
@override
State<QrScannerWidget> createState() => _QrScannerWidgetState();
}
class _QrScannerWidgetState extends State<QrScannerWidget>
with WidgetsBindingObserver {
late MobileScannerController _controller;
bool _hasScanned = false;
String? _lastScannedData;
DateTime? _lastScanTime;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_controller = MobileScannerController(
detectionSpeed: DetectionSpeed.normal,
facing: CameraFacing.back,
);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_controller.dispose();
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// Handle app lifecycle changes - pause/resume scanner
if (!_controller.value.hasCameraPermission) return;
switch (state) {
case AppLifecycleState.resumed:
_controller.start();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
_controller.stop();
break;
}
}
void _handleDetection(BarcodeCapture capture) {
// Prevent duplicate scans
if (_hasScanned && !widget.continuousScanning) return;
final List<Barcode> barcodes = capture.barcodes;
for (final barcode in barcodes) {
final String? rawValue = barcode.rawValue;
if (rawValue == null || rawValue.isEmpty) continue;
// Debounce - ignore if same data scanned too quickly
final now = DateTime.now();
if (_lastScannedData == rawValue &&
_lastScanTime != null &&
now.difference(_lastScanTime!) < widget.debounceDuration) {
continue;
}
_lastScannedData = rawValue;
_lastScanTime = now;
// Validate if validator provided
if (widget.validator != null && !widget.validator!(rawValue)) {
widget.onValidationFailed?.call(rawValue);
continue;
}
// Mark as scanned to prevent duplicates
if (!widget.continuousScanning) {
setState(() {
_hasScanned = true;
});
_controller.stop();
}
// Notify callback
widget.onScanned(rawValue);
return;
}
}
/// Reset the scanner to allow scanning again
void resetScanner() {
setState(() {
_hasScanned = false;
_lastScannedData = null;
_lastScanTime = null;
});
_controller.start();
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
// Scanner view
MobileScanner(
controller: _controller,
onDetect: _handleDetection,
errorBuilder: (context, error, child) {
return _buildErrorWidget(context, error);
},
),
// Overlay
widget.overlay ?? _buildDefaultOverlay(context),
// Control buttons
Positioned(
bottom: 16,
left: 0,
right: 0,
child: _buildControls(context),
),
],
);
}
Widget _buildDefaultOverlay(BuildContext context) {
return ColorFiltered(
colorFilter: ColorFilter.mode(
Colors.black.withValues(alpha: 0.5),
BlendMode.srcOut,
),
child: Stack(
fit: StackFit.expand,
children: [
Container(
decoration: const BoxDecoration(
color: Colors.black,
backgroundBlendMode: BlendMode.dstOut,
),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 250,
width: 250,
decoration: BoxDecoration(
color: Colors.red, // This color is used for cutout
borderRadius: BorderRadius.circular(16),
),
),
if (widget.instructions != null) ...[
const SizedBox(height: 24),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.7),
borderRadius: BorderRadius.circular(8),
),
child: Text(
widget.instructions!,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
],
],
),
),
],
),
);
}
Widget _buildControls(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.showFlashButton)
ValueListenableBuilder(
valueListenable: _controller,
builder: (context, state, child) {
return IconButton.filled(
onPressed: () => _controller.toggleTorch(),
icon: Icon(
state.torchState == TorchState.on
? Icons.flash_on
: Icons.flash_off,
),
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
),
);
},
),
if (widget.showFlashButton && widget.showCameraSwitchButton)
const SizedBox(width: 24),
if (widget.showCameraSwitchButton)
IconButton.filled(
onPressed: () => _controller.switchCamera(),
icon: const Icon(Icons.cameraswitch),
style: IconButton.styleFrom(
backgroundColor: Colors.black54,
foregroundColor: Colors.white,
),
),
],
);
}
Widget _buildErrorWidget(BuildContext context, MobileScannerException error) {
String message;
IconData icon;
switch (error.errorCode) {
case MobileScannerErrorCode.permissionDenied:
message = 'Camera permission denied.\nPlease enable camera access in settings.';
icon = Icons.no_photography;
break;
case MobileScannerErrorCode.unsupported:
message = 'Camera not supported on this device.';
icon = Icons.videocam_off;
break;
default:
message = 'Failed to start camera.\n${error.errorDetails?.message ?? ''}';
icon = Icons.error_outline;
}
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
),
);
}
}
/// A simpler scanner overlay with just corner brackets
class ScannerCornerOverlay extends StatelessWidget {
final double scanWindowSize;
final Color borderColor;
final double borderWidth;
final double cornerLength;
const ScannerCornerOverlay({
super.key,
this.scanWindowSize = 250,
this.borderColor = Colors.white,
this.borderWidth = 3,
this.cornerLength = 30,
});
@override
Widget build(BuildContext context) {
return Center(
child: SizedBox(
width: scanWindowSize,
height: scanWindowSize,
child: CustomPaint(
painter: _CornerPainter(
color: borderColor,
strokeWidth: borderWidth,
cornerLength: cornerLength,
),
),
),
);
}
}
class _CornerPainter extends CustomPainter {
final Color color;
final double strokeWidth;
final double cornerLength;
_CornerPainter({
required this.color,
required this.strokeWidth,
required this.cornerLength,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color
..strokeWidth = strokeWidth
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final path = Path();
// Top-left corner
path.moveTo(0, cornerLength);
path.lineTo(0, 0);
path.lineTo(cornerLength, 0);
// Top-right corner
path.moveTo(size.width - cornerLength, 0);
path.lineTo(size.width, 0);
path.lineTo(size.width, cornerLength);
// Bottom-right corner
path.moveTo(size.width, size.height - cornerLength);
path.lineTo(size.width, size.height);
path.lineTo(size.width - cornerLength, size.height);
// Bottom-left corner
path.moveTo(cornerLength, size.height);
path.lineTo(0, size.height);
path.lineTo(0, size.height - cornerLength);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
+43 -12
View File
@@ -31,6 +31,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
bool _savePassword = false;
bool _isLoading = true;
bool _obscurePassword = true;
String? _loginError;
late MeshCoreConnector _connector;
int _currentAttempt = 0;
static const int _maxAttempts = 5;
@@ -79,6 +80,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
setState(() {
_isLoggingIn = true;
_currentAttempt = 0;
_loginError = null;
});
try {
@@ -134,7 +136,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
'Login failed for ${repeater.name}',
tag: 'RepeaterLogin',
);
throw Exception('Wrong password or node is unreachable');
break;
}
appLogger.warn(
'Login attempt ${attempt + 1} timed out after ${timeoutSeconds}s',
@@ -156,7 +158,13 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
}
if (loginResult != true) {
throw Exception('Wrong password or node is unreachable');
if (mounted) {
setState(() {
_isLoggingIn = false;
_loginError = context.l10n.login_failedMessage;
});
}
return;
}
// If we got a response, login succeeded
@@ -182,13 +190,8 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
if (mounted) {
setState(() {
_isLoggingIn = false;
_loginError = context.l10n.login_failedMessage;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(context.l10n.login_failed(e.toString())),
backgroundColor: Colors.red,
),
);
}
}
}
@@ -261,15 +264,35 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
child: CircularProgressIndicator(),
),
)
: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.login_repeaterDescription,
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
if (_loginError != null) ...[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.error, size: 18, color: Theme.of(context).colorScheme.error),
const SizedBox(width: 8),
Expanded(
child: Text(
_loginError!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 13,
),
),
),
],
),
const SizedBox(height: 12),
],
TextField(
controller: _passwordController,
obscureText: _obscurePassword,
@@ -291,6 +314,13 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
},
),
),
onChanged: (_) {
if (_loginError != null && mounted) {
setState(() {
_loginError = null;
});
}
},
onSubmitted: (_) => _handleLogin(),
autofocus: _passwordController.text.isEmpty,
),
@@ -382,6 +412,7 @@ class _RepeaterLoginDialogState extends State<RepeaterLoginDialog> {
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
+62
View File
@@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
List<double> getSNRfromSF(int spreadingFactor) {
switch (spreadingFactor) {
case 7:
return [4.0, -2.0, -4.0, -6.0];
case 8:
return [4.0, -4.0, -6.0, -8.0];
case 9:
return [4.0, -6.0, -8.0, -10.0];
case 10:
return [4.0, -8.0, -10.0, -13.0];
case 11:
return [4.0, -10.0, -12.5, -15.0];
case 12:
return [4.0, -12.5, -15.0, -18.0];
default:
return []; // Or throw Exception('Invalid SF: $spreadingFactor');
}
}
class SNRIcon extends StatelessWidget {
final double snr;
final List<double> snrLevels;
const SNRIcon({
super.key,
required this.snr,
this.snrLevels = const [4.0, -2.0, -4.0, -6.0],
});
@override
Widget build(BuildContext context) {
IconData icon;
Color color;
if (snr >= snrLevels[0]) {
icon = Icons.signal_cellular_alt;
color = Colors.green;
} else if (snr >= snrLevels[1]) {
icon = Icons.signal_cellular_alt;
color = Colors.lightGreen;
} else if (snr >= snrLevels[2]) {
icon = Icons.signal_cellular_alt;
color = Colors.yellow;
} else if (snr >= snrLevels[3]) {
icon = Icons.signal_cellular_alt_2_bar;
color = Colors.orange;
} else {
icon = Icons.signal_cellular_alt_1_bar;
color = Colors.red;
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Text('$snr dB', style: TextStyle(fontSize: 10, color: color)),
],
);
}
}
@@ -7,6 +7,7 @@ import Foundation
import flutter_blue_plus_darwin
import flutter_local_notifications
import mobile_scanner
import package_info_plus
import path_provider_foundation
import shared_preferences_foundation
@@ -16,6 +17,7 @@ import wakelock_plus
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+2
View File
@@ -12,5 +12,7 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
+2
View File
@@ -30,5 +30,7 @@
<string>NSApplication</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>MeshCore needs Bluetooth to communicate with LoRa mesh devices</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string>
</dict>
</plist>
+2
View File
@@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.device.bluetooth</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
+24
View File
@@ -453,6 +453,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
mobile_scanner:
dependency: "direct main"
description:
name: mobile_scanner
sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173"
url: "https://pub.dev"
source: hosted
version: "6.0.11"
nested:
dependency: transitive
description:
@@ -605,6 +613,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
qr:
dependency: transitive
description:
name: qr
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: transitive
description:
+6 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 0.3.0+3
version: 0.4.0+4
environment:
sdk: ^3.9.2
@@ -53,6 +53,8 @@ dependencies:
wakelock_plus: ^1.2.8
characters: ^1.4.0
package_info_plus: ^8.0.0
mobile_scanner: ^6.0.0 # QR/barcode scanning
qr_flutter: ^4.1.0 # QR code generation
dev_dependencies:
flutter_test:
@@ -78,6 +80,9 @@ flutter:
# the material Icons class.
uses-material-design: true
assets:
- assets/images/
flutter_launcher_icons:
android: true
ios: true
+190 -19
View File
@@ -10,6 +10,7 @@ Translates ARB/JSON localization values using a local Ollama model, while:
- printing progress as it runs
Usage:
# Translate all strings:
python translate_arb_with_ollama.py \
--in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \
--out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \
@@ -17,12 +18,28 @@ Usage:
--model ministral-3:latest \
--temperature 0 \
--concurrency 4
# Translate only missing/untranslated strings:
python translate_arb_with_ollama.py \
--in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \
--out /home/zjs81/Desktop/meshcore-open/lib/l10n/app_es.arb \
--to-locale es \
--missing-only \
--model ministral-3:latest
# Translate all locales (missing strings only):
python translate_arb_with_ollama.py \
--in /home/zjs81/Desktop/meshcore-open/lib/l10n/app_en.arb \
--l10n-dir /home/zjs81/Desktop/meshcore-open/lib/l10n \
--missing-only \
--model ministral-3:latest
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
import time
@@ -448,11 +465,48 @@ def fmt_duration(seconds: float) -> str:
return f"{h}h {m2}m"
def find_missing_keys(source_data: Dict[str, Any], target_data: Dict[str, Any]) -> List[str]:
"""Find keys that are in source but not in target (excluding metadata keys)."""
missing = []
for key in source_data:
if key == "@@locale":
continue
if key.startswith("@"):
continue
if key not in target_data:
missing.append(key)
return missing
def get_all_locale_files(l10n_dir: str, template_file: str) -> List[Tuple[str, str]]:
"""Find all locale .arb files in the directory, excluding the template.
Returns list of (locale_code, file_path) tuples.
"""
locales = []
template_basename = os.path.basename(template_file)
for filename in os.listdir(l10n_dir):
if not filename.endswith('.arb'):
continue
if filename == template_basename:
continue
# Extract locale from filename like app_es.arb -> es
if filename.startswith('app_') and filename.endswith('.arb'):
locale = filename[4:-4] # Remove 'app_' prefix and '.arb' suffix
filepath = os.path.join(l10n_dir, filename)
locales.append((locale, filepath))
return sorted(locales)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--in", dest="in_path", required=True, help="Input .arb/.json file path")
ap.add_argument("--out", dest="out_path", required=True, help="Output .arb/.json file path")
ap.add_argument("--to-locale", required=True, help="Target locale code, e.g. es, fr, de")
ap.add_argument("--in", dest="in_path", required=True, help="Input .arb/.json file path (source/template)")
ap.add_argument("--out", dest="out_path", default=None, help="Output .arb/.json file path (required unless using --l10n-dir)")
ap.add_argument("--to-locale", default=None, help="Target locale code, e.g. es, fr, de (required unless using --l10n-dir)")
ap.add_argument("--l10n-dir", default=None, help="Directory containing locale .arb files. When set, translates all locales.")
ap.add_argument("--missing-only", action="store_true", help="Only translate keys missing from target file")
ap.add_argument("--target-lang", default=None, help="Target language name for the model, e.g. Spanish (defaults from locale)")
ap.add_argument("--model", default="gemma3:4b", help="Ollama model name")
ap.add_argument("--fallback-model", default=None, help="Larger model to use for low-confidence translations")
@@ -504,19 +558,119 @@ def main() -> int:
"vi": "Vietnamese",
"id": "Indonesian",
}
target_lang = args.target_lang or locale_map.get(args.to_locale, args.to_locale)
# Read source/template file
try:
with open(args.in_path, "r", encoding="utf-8") as f:
data = json.load(f)
source_data = json.load(f)
except Exception as e:
print(f"Failed to read input: {e}", file=sys.stderr)
return 2
if not isinstance(data, dict):
if not isinstance(source_data, dict):
print("Input JSON must be an object at top-level.", file=sys.stderr)
return 2
# If --l10n-dir is provided, process all locale files
if args.l10n_dir:
locales = get_all_locale_files(args.l10n_dir, args.in_path)
if not locales:
print(f"No locale files found in {args.l10n_dir}", file=sys.stderr)
return 1
print(f"Found {len(locales)} locale file(s) to process")
total_translated = 0
for locale_code, locale_path in locales:
target_lang = locale_map.get(locale_code, locale_code)
# Read existing target file
try:
with open(locale_path, "r", encoding="utf-8") as f:
target_data = json.load(f)
except Exception as e:
print(f" [{locale_code}] Failed to read {locale_path}: {e}")
continue
if args.missing_only:
missing_keys = find_missing_keys(source_data, target_data)
if not missing_keys:
print(f" [{locale_code}] No missing keys")
continue
print(f" [{locale_code}] {len(missing_keys)} missing key(s): {', '.join(missing_keys[:5])}{'...' if len(missing_keys) > 5 else ''}")
else:
missing_keys = None
# Run translation for this locale
result = translate_locale(
source_data=source_data,
target_data=target_data,
target_locale=locale_code,
target_lang=target_lang,
out_path=locale_path,
args=args,
locale_map=locale_map,
missing_keys=missing_keys,
)
total_translated += result
print(f"\nTotal: {total_translated} string(s) translated across {len(locales)} locale(s)")
return 0
# Single locale mode - validate required args
if not args.out_path:
print("--out is required when not using --l10n-dir", file=sys.stderr)
return 1
if not args.to_locale:
print("--to-locale is required when not using --l10n-dir", file=sys.stderr)
return 1
target_lang = args.target_lang or locale_map.get(args.to_locale, args.to_locale)
# Read existing target file if --missing-only and file exists
target_data: Dict[str, Any] = {}
missing_keys: Optional[List[str]] = None
if args.missing_only:
if os.path.exists(args.out_path):
try:
with open(args.out_path, "r", encoding="utf-8") as f:
target_data = json.load(f)
missing_keys = find_missing_keys(source_data, target_data)
if not missing_keys:
print(f"No missing keys in {args.out_path}")
return 0
print(f"Found {len(missing_keys)} missing key(s) to translate")
except Exception as e:
print(f"Failed to read target file: {e}", file=sys.stderr)
return 2
else:
print(f"Target file {args.out_path} does not exist. Will translate all strings.")
result = translate_locale(
source_data=source_data,
target_data=target_data,
target_locale=args.to_locale,
target_lang=target_lang,
out_path=args.out_path,
args=args,
locale_map=locale_map,
missing_keys=missing_keys,
)
return 0 if result >= 0 else 1
def translate_locale(
source_data: Dict[str, Any],
target_data: Dict[str, Any],
target_locale: str,
target_lang: str,
out_path: str,
args,
locale_map: Dict[str, str],
missing_keys: Optional[List[str]] = None,
) -> int:
"""Translate a single locale. Returns number of strings translated."""
cfg = OllamaConfig(
host=args.host,
model=args.model,
@@ -540,17 +694,34 @@ def main() -> int:
top_p=args.top_p,
)
out_data: Dict[str, Any] = dict(data)
out_data["@@locale"] = args.to_locale
# Start with target data (preserves existing translations) or source data
if target_data:
out_data: Dict[str, Any] = dict(target_data)
else:
out_data: Dict[str, Any] = dict(source_data)
out_data["@@locale"] = target_locale
items: List[Tuple[str, str]] = [(k, v) for k, v in data.items() if is_translatable_entry(k, v)]
# Build list of items to translate
if missing_keys is not None:
# Only translate missing keys
items: List[Tuple[str, str]] = [
(k, source_data[k]) for k in missing_keys
if is_translatable_entry(k, source_data.get(k))
]
# Also copy over any metadata keys for missing items
for key in missing_keys:
meta_key = f"@{key}"
if meta_key in source_data:
out_data[meta_key] = source_data[meta_key]
else:
items: List[Tuple[str, str]] = [(k, v) for k, v in source_data.items() if is_translatable_entry(k, v)]
# Apply manual translations first
manual_count = 0
items_to_translate: List[Tuple[str, str]] = []
for k, v in items:
if k in MANUAL_TRANSLATIONS and args.to_locale in MANUAL_TRANSLATIONS[k]:
out_data[k] = MANUAL_TRANSLATIONS[k][args.to_locale]
if k in MANUAL_TRANSLATIONS and target_locale in MANUAL_TRANSLATIONS[k]:
out_data[k] = MANUAL_TRANSLATIONS[k][target_locale]
manual_count += 1
else:
items_to_translate.append((k, v))
@@ -560,8 +731,8 @@ def main() -> int:
total = len(items_to_translate)
if total == 0 and manual_count == 0:
print("No translatable string entries found (excluding @@locale and @metadata).", file=sys.stderr)
return 1
print("No translatable string entries found (excluding @@locale and @metadata).")
return 0
if total == 0:
print("All strings handled by manual translations.")
@@ -705,19 +876,19 @@ def main() -> int:
if args.dry_run:
print("Dry run: not writing output file.")
return 0
return translated_ok
try:
with open(args.out_path, "w", encoding="utf-8") as f:
with open(out_path, "w", encoding="utf-8") as f:
json.dump(out_data, f, ensure_ascii=False, indent=2)
f.write("\n")
except Exception as e:
print(f"Failed to write output: {e}", file=sys.stderr)
return 2
return -1
print(f"Wrote: {args.out_path}")
return 0
print(f"Wrote: {out_path}")
return translated_ok
if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
+121
View File
@@ -0,0 +1,121 @@
{
"bg": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"de": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"es": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"fr": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"it": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"nl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"pl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"pt": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"sk": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"sl": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"sv": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
],
"zh": [
"community_regenerateSecret",
"community_regenerateSecretConfirm",
"community_regenerate",
"community_secretRegenerated",
"community_updateSecret",
"community_secretUpdated",
"community_scanToUpdateSecret"
]
}