mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 02:29:55 +02:00
feat(network): add relay connection policies
This commit is contained in:
@@ -26,7 +26,7 @@ final class AppModelTests: XCTestCase {
|
||||
func testInitializesCoreWithSavedCustomRelayConfiguration() async {
|
||||
let core = FakeCoreGateway()
|
||||
let preferences = Fixtures.preferences()
|
||||
let configuration = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
||||
let configuration = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||
preferences.setRelayConfiguration(configuration)
|
||||
_ = makeModel(core, preferences: preferences)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
||||
repo.setNotificationsEnabled(true)
|
||||
repo.setReceiveFolder(ReceiveFolder(kind: .iosSecurityScopedUrl, value: "file:///x", displayName: "Custom"))
|
||||
repo.setRelayConfiguration(RelayConfiguration(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
|
||||
))
|
||||
|
||||
@@ -44,7 +44,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
||||
XCTAssertEqual(reloaded.preferences.receiveFolder.displayName, "Custom")
|
||||
XCTAssertEqual(reloaded.preferences.receiveFolder.kind, .iosSecurityScopedUrl)
|
||||
XCTAssertEqual(reloaded.preferences.relayConfiguration, RelayConfiguration(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayURLs: ["https://relay-one.example", "https://relay-two.example:443"]
|
||||
))
|
||||
XCTAssertNotNil(store.data(forKey: "relay_configuration"))
|
||||
@@ -60,7 +60,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(
|
||||
repo.preferences.relayConfiguration,
|
||||
RelayConfiguration(mode: .custom, relayURLs: [])
|
||||
RelayConfiguration(mode: .strictCustom, relayURLs: [])
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ final class AppPreferencesRepositoryTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(
|
||||
repo.preferences.relayConfiguration,
|
||||
RelayConfiguration(mode: .custom, relayURLs: [])
|
||||
RelayConfiguration(mode: .strictCustom, relayURLs: [])
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ final class CoreRepositoryLifecycleTests: XCTestCase {
|
||||
|
||||
let concurrentInitialization = await repository.initialize(
|
||||
appDataDir: "/tmp/second",
|
||||
networkConfiguration: RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
||||
networkConfiguration: RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||
)
|
||||
assertLifecycleFailure(concurrentInitialization, equals: .transitionInProgress)
|
||||
|
||||
|
||||
@@ -2,6 +2,32 @@ import XCTest
|
||||
@testable import VniDrop
|
||||
|
||||
final class RelayConfigurationTests: XCTestCase {
|
||||
func testCustomFallbackValidatesAndPreservesItsMode() throws {
|
||||
let result = try RelayConfigurationValidator.validate(
|
||||
mode: .customWithDirectFallback,
|
||||
relayURLs: ["https://relay.example/"]
|
||||
)
|
||||
|
||||
XCTAssertEqual(
|
||||
result,
|
||||
RelayConfiguration(
|
||||
mode: .customWithDirectFallback,
|
||||
relayURLs: ["https://relay.example"]
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testLocalOnlyRetainsPreviouslySavedRelayURLs() throws {
|
||||
let retained = ["https://relay.example"]
|
||||
let result = try RelayConfigurationValidator.validate(
|
||||
mode: .localOnly,
|
||||
relayURLs: ["not a URL"],
|
||||
retainedRelayURLs: retained
|
||||
)
|
||||
|
||||
XCTAssertEqual(result, RelayConfiguration(mode: .localOnly, relayURLs: retained))
|
||||
}
|
||||
|
||||
func testAutomaticModeIgnoresRelayDrafts() throws {
|
||||
let result = try RelayConfigurationValidator.validate(
|
||||
mode: .automatic,
|
||||
@@ -24,32 +50,32 @@ final class RelayConfigurationTests: XCTestCase {
|
||||
|
||||
func testCustomModeTrimsValidHTTPSRelayURLs() throws {
|
||||
let result = try RelayConfigurationValidator.validate(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayURLs: [" https://relay.example/ ", "https://backup.example:443"]
|
||||
)
|
||||
XCTAssertEqual(result, RelayConfiguration(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayURLs: ["https://relay.example", "https://backup.example"]
|
||||
))
|
||||
}
|
||||
|
||||
func testCustomModeIgnoresEmptyURLRows() throws {
|
||||
let result = try RelayConfigurationValidator.validate(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayURLs: ["", " ", "https://relay.example"]
|
||||
)
|
||||
XCTAssertEqual(result.relayURLs, ["https://relay.example"])
|
||||
}
|
||||
|
||||
func testCustomModeRequiresAtLeastOneRelay() {
|
||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .custom, relayURLs: [])) { error in
|
||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: [])) { error in
|
||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .missingURL)
|
||||
}
|
||||
}
|
||||
|
||||
func testCustomModeRequiresHTTPS() {
|
||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayURLs: ["http://relay.example"]
|
||||
)) { error in
|
||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .httpsRequired(index: 0))
|
||||
@@ -67,7 +93,7 @@ final class RelayConfigurationTests: XCTestCase {
|
||||
]
|
||||
for relayURL in invalidURLs {
|
||||
XCTAssertThrowsError(
|
||||
try RelayConfigurationValidator.validate(mode: .custom, relayURLs: [relayURL]),
|
||||
try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: [relayURL]),
|
||||
"Expected \(relayURL) to be rejected"
|
||||
) { error in
|
||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .invalidURL(index: 0))
|
||||
@@ -77,7 +103,7 @@ final class RelayConfigurationTests: XCTestCase {
|
||||
|
||||
func testCustomModeRejectsNormalizedDuplicate() {
|
||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayURLs: ["https://relay.example", "https://RELAY.example:443/"]
|
||||
)) { error in
|
||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .duplicateURL(index: 1))
|
||||
@@ -88,7 +114,7 @@ final class RelayConfigurationTests: XCTestCase {
|
||||
let relayURLs = (0...RelayConfigurationValidator.maximumRelayCount).map {
|
||||
"https://relay-\($0).example"
|
||||
}
|
||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .custom, relayURLs: relayURLs)) { error in
|
||||
XCTAssertThrowsError(try RelayConfigurationValidator.validate(mode: .strictCustom, relayURLs: relayURLs)) { error in
|
||||
XCTAssertEqual(error as? RelayConfigurationValidationError, .tooManyURLs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,13 +59,13 @@ final class SettingsModelTests: XCTestCase {
|
||||
let core = FakeCoreGateway()
|
||||
let preferences = Fixtures.preferences()
|
||||
let model = makeModel(core, preferences: preferences)
|
||||
model.setRelayMode(.custom)
|
||||
model.setRelayMode(.strictCustom)
|
||||
model.setRelayURL(" https://relay.example/ ", at: 0)
|
||||
|
||||
model.applyRelayConfiguration()
|
||||
|
||||
await waitUntil { preferences.preferences.relayConfiguration.mode == .custom }
|
||||
let expected = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
||||
await waitUntil { preferences.preferences.relayConfiguration.mode == .strictCustom }
|
||||
let expected = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||
XCTAssertEqual(preferences.preferences.relayConfiguration, expected)
|
||||
XCTAssertEqual(core.initializedNetworkConfigurations, [expected])
|
||||
XCTAssertFalse(model.state.relayConfigurationIsDirty)
|
||||
@@ -75,7 +75,7 @@ final class SettingsModelTests: XCTestCase {
|
||||
let core = FakeCoreGateway()
|
||||
let preferences = Fixtures.preferences()
|
||||
let relayURLs = ["https://relay.example", "https://backup.example"]
|
||||
preferences.setRelayConfiguration(RelayConfiguration(mode: .custom, relayURLs: relayURLs))
|
||||
preferences.setRelayConfiguration(RelayConfiguration(mode: .strictCustom, relayURLs: relayURLs))
|
||||
let model = makeModel(core, preferences: preferences)
|
||||
|
||||
model.setRelayMode(.automatic)
|
||||
@@ -86,7 +86,7 @@ final class SettingsModelTests: XCTestCase {
|
||||
XCTAssertEqual(core.initializedNetworkConfigurations, [
|
||||
RelayConfiguration(mode: .automatic, relayURLs: relayURLs),
|
||||
])
|
||||
model.setRelayMode(.custom)
|
||||
model.setRelayMode(.strictCustom)
|
||||
XCTAssertEqual(model.state.relayURLs, relayURLs)
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ final class SettingsModelTests: XCTestCase {
|
||||
isInitialized: true,
|
||||
status: CoreStatus(endpointId: "endpoint", activeTransfers: 0, activeShares: 1)
|
||||
))
|
||||
model.setRelayMode(.custom)
|
||||
model.setRelayMode(.strictCustom)
|
||||
model.setRelayURL("https://relay.example", at: 0)
|
||||
|
||||
model.applyRelayConfiguration()
|
||||
@@ -114,8 +114,8 @@ final class SettingsModelTests: XCTestCase {
|
||||
core.initializeResult = .failure(CoreNetworkLifecycleError.activeNetworkWork)
|
||||
let preferences = Fixtures.preferences()
|
||||
let model = makeModel(core, preferences: preferences)
|
||||
let attempted = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
||||
model.setRelayMode(.custom)
|
||||
let attempted = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||
model.setRelayMode(.strictCustom)
|
||||
model.setRelayURL(attempted.relayURLs[0], at: 0)
|
||||
|
||||
model.applyRelayConfiguration()
|
||||
@@ -134,8 +134,8 @@ final class SettingsModelTests: XCTestCase {
|
||||
core.initializeResults = [.failure(TestError.unimplemented), .success(())]
|
||||
let preferences = Fixtures.preferences()
|
||||
let model = makeModel(core, preferences: preferences)
|
||||
let attempted = RelayConfiguration(mode: .custom, relayURLs: ["https://relay.example"])
|
||||
model.setRelayMode(.custom)
|
||||
let attempted = RelayConfiguration(mode: .strictCustom, relayURLs: ["https://relay.example"])
|
||||
model.setRelayMode(.strictCustom)
|
||||
model.setRelayURL(attempted.relayURLs[0], at: 0)
|
||||
|
||||
model.applyRelayConfiguration()
|
||||
|
||||
@@ -21,7 +21,13 @@ enum FolderAccessStatus {
|
||||
|
||||
enum RelayPreferenceMode: String, Codable, CaseIterable, Sendable {
|
||||
case automatic
|
||||
case custom
|
||||
case strictCustom = "custom"
|
||||
case customWithDirectFallback = "custom-with-direct-fallback"
|
||||
case localOnly = "local-only"
|
||||
|
||||
var usesCustomRelayURLs: Bool {
|
||||
self == .strictCustom || self == .customWithDirectFallback
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayConfiguration: Equatable, Codable, Sendable {
|
||||
@@ -55,8 +61,8 @@ enum RelayConfigurationValidator {
|
||||
relayURLs: [String],
|
||||
retainedRelayURLs: [String] = []
|
||||
) throws -> RelayConfiguration {
|
||||
guard mode == .custom else {
|
||||
return RelayConfiguration(mode: .automatic, relayURLs: retainedRelayURLs)
|
||||
guard mode.usesCustomRelayURLs else {
|
||||
return RelayConfiguration(mode: mode, relayURLs: retainedRelayURLs)
|
||||
}
|
||||
|
||||
let relayEntries = relayURLs.enumerated().compactMap { index, value -> (Int, String)? in
|
||||
@@ -104,7 +110,7 @@ enum RelayConfigurationValidator {
|
||||
normalizedURLs.append(canonicalURL)
|
||||
}
|
||||
|
||||
return RelayConfiguration(mode: .custom, relayURLs: normalizedURLs)
|
||||
return RelayConfiguration(mode: mode, relayURLs: normalizedURLs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,10 +183,9 @@ final class AppPreferencesRepository: ObservableObject {
|
||||
let data = defaults.data(forKey: Key.relayConfiguration),
|
||||
let configuration = try? JSONDecoder().decode(RelayConfiguration.self, from: data)
|
||||
else {
|
||||
// A stored profile must never silently fall back to public relays. Keeping
|
||||
// Custom selected with no URLs makes startup fail closed and lets Settings
|
||||
// offer an explicit repair or reset to Automatic.
|
||||
return RelayConfiguration(mode: .custom, relayURLs: [])
|
||||
// A stored profile must never silently fall back to public relays. Strict
|
||||
// custom with no URLs makes startup fail closed until Settings repairs it.
|
||||
return RelayConfiguration(mode: .strictCustom, relayURLs: [])
|
||||
}
|
||||
return configuration
|
||||
}
|
||||
|
||||
@@ -40,11 +40,18 @@ struct NativeCoreBindingFactory: CoreBindingFactory {
|
||||
switch networkConfiguration.mode {
|
||||
case .automatic:
|
||||
nativeConfiguration = defaultCoreNetworkConfig()
|
||||
case .custom:
|
||||
case .strictCustom:
|
||||
nativeConfiguration = CoreNetworkConfig(
|
||||
mode: .custom,
|
||||
mode: .strictCustom,
|
||||
relayUrls: networkConfiguration.relayURLs
|
||||
)
|
||||
case .customWithDirectFallback:
|
||||
nativeConfiguration = CoreNetworkConfig(
|
||||
mode: .customWithDirectFallback,
|
||||
relayUrls: networkConfiguration.relayURLs
|
||||
)
|
||||
case .localOnly:
|
||||
nativeConfiguration = CoreNetworkConfig(mode: .localOnly, relayUrls: [])
|
||||
}
|
||||
return try VnidropCore.initializeWithNetworkConfig(
|
||||
appDataDir: appDataDir,
|
||||
|
||||
@@ -248,7 +248,7 @@ final class SettingsModel: ObservableObject {
|
||||
func setRelayMode(_ mode: RelayPreferenceMode) {
|
||||
hasRelayConfigurationDraft = true
|
||||
state.relayMode = mode
|
||||
if mode == .custom && state.relayURLs.isEmpty { state.relayURLs = [""] }
|
||||
if mode.usesCustomRelayURLs && state.relayURLs.isEmpty { state.relayURLs = [""] }
|
||||
updateRelayConfigurationDraft()
|
||||
}
|
||||
|
||||
@@ -351,7 +351,7 @@ final class SettingsModel: ObservableObject {
|
||||
state.relayValidationError = nil
|
||||
state.relayApplyErrorKey = nil
|
||||
let saved = preferences.preferences.relayConfiguration
|
||||
let draftURLs = state.relayMode == .automatic ? saved.relayURLs : state.relayURLs
|
||||
let draftURLs = state.relayMode.usesCustomRelayURLs ? state.relayURLs : saved.relayURLs
|
||||
state.relayConfigurationIsDirty = saved != RelayConfiguration(mode: state.relayMode, relayURLs: draftURLs)
|
||||
hasRelayConfigurationDraft = state.relayConfigurationIsDirty
|
||||
}
|
||||
|
||||
@@ -125,7 +125,18 @@ private struct SettingsSectionContent: View {
|
||||
func relayModeLabel(_ mode: RelayPreferenceMode) -> String {
|
||||
switch mode {
|
||||
case .automatic: return String(localized: "relay_mode_automatic")
|
||||
case .custom: return String(localized: "relay_mode_custom")
|
||||
case .strictCustom: return String(localized: "relay_mode_custom")
|
||||
case .customWithDirectFallback: return String(localized: "relay_mode_custom_direct_fallback")
|
||||
case .localOnly: return String(localized: "relay_mode_local_only")
|
||||
}
|
||||
}
|
||||
|
||||
func relayModeDescriptionKey(_ mode: RelayPreferenceMode) -> String {
|
||||
switch mode {
|
||||
case .automatic: return "relay_mode_automatic_description"
|
||||
case .strictCustom: return "relay_mode_custom_description"
|
||||
case .customWithDirectFallback: return "relay_mode_custom_direct_fallback_description"
|
||||
case .localOnly: return "relay_mode_local_only_description"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,15 +70,10 @@ struct NetworkSettings: View {
|
||||
Text(relayModeLabel(mode)).tag(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.labelsHidden()
|
||||
.pickerStyle(.inline)
|
||||
.disabled(model.state.isApplyingRelayConfiguration)
|
||||
} footer: {
|
||||
Text(LocalizedStringKey(
|
||||
model.state.relayMode == .automatic
|
||||
? "relay_mode_automatic_description"
|
||||
: "relay_mode_custom_description"
|
||||
))
|
||||
Text(LocalizedStringKey(relayModeDescriptionKey(model.state.relayMode)))
|
||||
}
|
||||
|
||||
Section {
|
||||
@@ -99,15 +94,17 @@ struct NetworkSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
if model.state.relayMode == .custom {
|
||||
if model.state.relayMode.usesCustomRelayURLs {
|
||||
Section {
|
||||
Label {
|
||||
Text(LocalizedStringKey("relay_strict_warning"))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} icon: {
|
||||
Image(systemName: "exclamationmark.shield.fill")
|
||||
if model.state.relayMode == .strictCustom {
|
||||
Label {
|
||||
Text(LocalizedStringKey("relay_strict_warning"))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} icon: {
|
||||
Image(systemName: "exclamationmark.shield.fill")
|
||||
}
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
.foregroundStyle(.orange)
|
||||
|
||||
ForEach(Array(model.state.relayURLs.indices), id: \.self) { index in
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
|
||||
@@ -10756,55 +10756,55 @@
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Automatisch"
|
||||
"value": "Automatisch (empfohlen)"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Automatic"
|
||||
"value": "Automatic (recommended)"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Automático"
|
||||
"value": "Automático (recomendado)"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Automatique"
|
||||
"value": "Automatique (recommandé)"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Automatica"
|
||||
"value": "Automatica (consigliata)"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Automatisch"
|
||||
"value": "Automatisch (aanbevolen)"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Automatyczny"
|
||||
"value": "Automatyczny (zalecany)"
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Automático"
|
||||
"value": "Automático (recomendado)"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Автоматически"
|
||||
"value": "Автоматически (рекомендуется)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10876,55 +10876,55 @@
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Benutzerdefiniert"
|
||||
"value": "Strikt benutzerdefiniert"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Custom"
|
||||
"value": "Strict custom"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personalizado"
|
||||
"value": "Personalizado estricto"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personnalisé"
|
||||
"value": "Personnalisé strict"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personalizzata"
|
||||
"value": "Personalizzata rigorosa"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Aangepast"
|
||||
"value": "Strikt aangepast"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Niestandardowy"
|
||||
"value": "Ścisły niestandardowy"
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personalizado"
|
||||
"value": "Personalizado estrito"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Пользовательский"
|
||||
"value": "Строго пользовательский"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10936,55 +10936,295 @@
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Verwendet ausschließlich die unten aufgeführten Relay-Server."
|
||||
"value": "Verwendet nur die konfigurierten eigenen Relays oder Direktverbindungen. Meldet einen Fehler, wenn kein eigenes Relay erreichbar ist."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Use only the relay servers below."
|
||||
"value": "Use only the configured custom relays or direct connections. Report an error if no custom relay can be established."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Usa únicamente los servidores de retransmisión indicados a continuación."
|
||||
"value": "Usa solo los relés personalizados configurados o conexiones directas. Informa de un error si no se puede establecer ningún relé personalizado."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Utiliser uniquement les serveurs relais ci-dessous."
|
||||
"value": "Utilise uniquement les relais personnalisés configurés ou les connexions directes. Signale une erreur si aucun relais personnalisé ne peut être établi."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Usa solo i server relay indicati di seguito."
|
||||
"value": "Usa solo i relay personalizzati configurati o connessioni dirette. Segnala un errore se non è possibile stabilire alcun relay personalizzato."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Gebruikt alleen de onderstaande relayservers."
|
||||
"value": "Gebruikt alleen de ingestelde aangepaste relays of rechtstreekse verbindingen. Meldt een fout als geen aangepaste relay bereikbaar is."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Używa wyłącznie poniższych serwerów przekaźnikowych."
|
||||
"value": "Używa tylko skonfigurowanych własnych przekaźników lub połączeń bezpośrednich. Zgłasza błąd, jeśli nie można połączyć się z żadnym własnym przekaźnikiem."
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Utiliza apenas os servidores de retransmissão abaixo."
|
||||
"value": "Utiliza apenas os retransmissores personalizados configurados ou ligações diretas. Apresenta um erro se não for possível estabelecer nenhum retransmissor personalizado."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Использовать только указанные ниже серверы-ретрансляторы."
|
||||
"value": "Использует только настроенные пользовательские ретрансляторы или прямые соединения. Сообщает об ошибке, если ни один пользовательский ретранслятор недоступен."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relay_mode_custom_direct_fallback": {
|
||||
"comment": "Network settings label for custom relays that allow direct-only startup fallback.",
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Benutzerdefiniert mit direktem Rückfall"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Custom with direct fallback"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personalizado con conexión directa de reserva"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personnalisé avec repli direct"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personalizzata con ripiego diretto"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Aangepast met directe terugval"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Niestandardowy z trybem bezpośrednim"
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Personalizado com alternativa direta"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Пользовательский с прямым резервом"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relay_mode_custom_direct_fallback_description": {
|
||||
"comment": "Network settings description of custom relays with direct-only startup fallback.",
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Bevorzugt die konfigurierten eigenen Relays. Sind sie nicht verfügbar, werden nur Direktverbindungen verwendet. Öffentliche Relays werden nie genutzt."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Prefer the configured custom relays. If unavailable, continue with direct connections only. Never use public relays."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Prefiere los relés personalizados configurados. Si no están disponibles, continúa solo con conexiones directas. Nunca usa relés públicos."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Préfère les relais personnalisés configurés. S’ils sont indisponibles, continue uniquement avec des connexions directes. N’utilise jamais les relais publics."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Preferisce i relay personalizzati configurati. Se non sono disponibili, continua solo con connessioni dirette. Non usa mai relay pubblici."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Geeft de voorkeur aan de ingestelde aangepaste relays. Als die niet beschikbaar zijn, worden alleen rechtstreekse verbindingen gebruikt. Openbare relays worden nooit gebruikt."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Preferuje skonfigurowane własne przekaźniki. Jeśli są niedostępne, kontynuuje tylko przez połączenia bezpośrednie. Nigdy nie używa publicznych przekaźników."
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Prefere os retransmissores personalizados configurados. Se não estiverem disponíveis, continua apenas com ligações diretas. Nunca utiliza retransmissores públicos."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Предпочитает настроенные пользовательские ретрансляторы. Если они недоступны, продолжает работу только через прямые соединения. Публичные ретрансляторы не используются."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relay_mode_local_only": {
|
||||
"comment": "Network settings label for direct connections without any relay.",
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Nur lokales Netzwerk"
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Local only"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Solo red local"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Réseau local uniquement"
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Solo rete locale"
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Alleen lokaal netwerk"
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Tylko sieć lokalna"
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Apenas rede local"
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Только локальная сеть"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relay_mode_local_only_description": {
|
||||
"comment": "Network settings description of direct-only local-network mode.",
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Deaktiviert alle Relays und erlaubt nur Direktverbindungen, hauptsächlich für Geräte im selben Netzwerk."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Disable all relays and allow only direct connections, primarily for devices on the same network."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Desactiva todos los relés y permite solo conexiones directas, principalmente para dispositivos de la misma red."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Désactive tous les relais et autorise uniquement les connexions directes, principalement pour les appareils sur le même réseau."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Disattiva tutti i relay e consente solo connessioni dirette, soprattutto per dispositivi sulla stessa rete."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Schakelt alle relays uit en staat alleen rechtstreekse verbindingen toe, vooral voor apparaten in hetzelfde netwerk."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Wyłącza wszystkie przekaźniki i zezwala tylko na połączenia bezpośrednie, głównie dla urządzeń w tej samej sieci."
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Desativa todos os retransmissores e permite apenas ligações diretas, principalmente para dispositivos na mesma rede."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Отключает все ретрансляторы и разрешает только прямые соединения, прежде всего для устройств в одной сети."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11236,55 +11476,55 @@
|
||||
"de": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Der benutzerdefinierte Modus ist strikt: VniDrop greift weder auf öffentliche Relays noch auf öffentliche Erkennung zurück. Andere Geräte müssen Ihre konfigurierten Relays erreichen können."
|
||||
"value": "Der strikt benutzerdefinierte Modus startet nur, wenn mindestens ein konfiguriertes Relay erreichbar ist. VniDrop verwendet in diesem Modus nie öffentliche Relays oder öffentliche Erkennung."
|
||||
}
|
||||
},
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Custom mode is strict: VniDrop will not fall back to public relays or public discovery. Other devices must be able to reach your configured relays."
|
||||
"value": "Strict custom mode will not start unless at least one configured relay is reachable. VniDrop never uses public relays or public discovery in this mode."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "El modo personalizado es estricto: VniDrop no recurrirá a relés públicos ni al descubrimiento público. Los demás dispositivos deben poder acceder a los relés configurados."
|
||||
"value": "El modo personalizado estricto no se inicia a menos que se pueda acceder al menos a un relé configurado. VniDrop nunca usa relés ni descubrimiento públicos en este modo."
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Le mode personnalisé est strict : VniDrop n’utilisera ni les relais publics ni la découverte publique comme solution de repli. Les autres appareils doivent pouvoir accéder aux relais configurés."
|
||||
"value": "Le mode personnalisé strict ne démarre que si au moins un relais configuré est accessible. VniDrop n’utilise jamais de relais ni de découverte publics dans ce mode."
|
||||
}
|
||||
},
|
||||
"it": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "La modalità personalizzata è rigorosa: VniDrop non userà relay pubblici o il rilevamento pubblico come ripiego. Gli altri dispositivi devono poter raggiungere i relay configurati."
|
||||
"value": "La modalità personalizzata rigorosa si avvia solo se almeno un relay configurato è raggiungibile. In questa modalità VniDrop non usa mai relay o rilevamento pubblici."
|
||||
}
|
||||
},
|
||||
"nl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "De aangepaste modus is strikt: VniDrop valt niet terug op openbare relays of openbare detectie. Andere apparaten moeten uw ingestelde relays kunnen bereiken."
|
||||
"value": "De strikt aangepaste modus start alleen als minstens één ingestelde relay bereikbaar is. VniDrop gebruikt in deze modus nooit openbare relays of openbare detectie."
|
||||
}
|
||||
},
|
||||
"pl": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Tryb niestandardowy jest rygorystyczny: VniDrop nie użyje awaryjnie publicznych przekaźników ani publicznego wykrywania. Inne urządzenia muszą mieć dostęp do skonfigurowanych przekaźników."
|
||||
"value": "Ścisły tryb niestandardowy uruchamia się tylko wtedy, gdy co najmniej jeden skonfigurowany przekaźnik jest dostępny. VniDrop nigdy nie używa w tym trybie publicznych przekaźników ani publicznego wykrywania."
|
||||
}
|
||||
},
|
||||
"pt": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "O modo personalizado é estrito: o VniDrop não recorrerá a retransmissores públicos nem à descoberta pública. Os outros dispositivos têm de conseguir aceder aos retransmissores configurados."
|
||||
"value": "O modo personalizado estrito só inicia se pelo menos um retransmissor configurado estiver acessível. Neste modo, o VniDrop nunca utiliza retransmissores nem descoberta públicos."
|
||||
}
|
||||
},
|
||||
"ru": {
|
||||
"stringUnit": {
|
||||
"state": "needs_review",
|
||||
"value": "Пользовательский режим работает строго: VniDrop не будет переключаться на публичные ретрансляторы или публичное обнаружение. Другие устройства должны иметь доступ к настроенным ретрансляторам."
|
||||
"value": "Строго пользовательский режим запускается, только если доступен хотя бы один настроенный ретранслятор. В этом режиме VniDrop никогда не использует публичные ретрансляторы или публичное обнаружение."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user