From ef42875ddbed341a0daac58d420452a7eb33f40f Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:12:43 +0200 Subject: [PATCH 01/36] feat(apple): generate type-safe L10n accessors and migrate all key literals Replaces every stringly-typed localization key in the Apple app with compile-time-checked accessors generated from localization/strings.json. A mistyped key is now a build error instead of a silent fallback to the raw key at runtime. The runtime path is unchanged: plain keys are String.LocalizationValue constants resolved with String(localized:) and Apple's String Catalog still does the lookup; keys with arguments become typed, named functions applying args through String(format:). Generator: new renderSwiftAccessors emits apple/VniDrop/Generated/L10n.swift, wired into generate. Renamed generic arg1/arg2 tokens on four keys to semantic names (receiver, transferName, deviceId) and updated their context notes; positional output is unchanged so .xcstrings (bar the 4 comments) and the Android XML regenerate identical. Migration: every key-carrying value flipped to String.LocalizationValue end to end, resolved only at the leaf. Zero key literals and zero LocalizedStringKey remain in app or test code. macOS build passes; iOS test run pending. --- apple/Tests/ProgressDerivationTests.swift | 10 +- apple/Tests/UiFeedbackTests.swift | 20 +- apple/VniDrop/App/RootView.swift | 4 +- apple/VniDrop/Core/TransferProgress.swift | 68 +- .../Approvals/ApprovalCoordinator.swift | 9 +- .../Features/Approvals/ApprovalModal.swift | 14 +- .../Receive/ReceiveInvitationActions.swift | 38 +- .../Features/Receive/ReceiveModel.swift | 14 +- .../Features/Receive/ReceiveScreen.swift | 32 +- apple/VniDrop/Features/Send/SendModel.swift | 12 +- apple/VniDrop/Features/Send/SendScreen.swift | 49 +- .../Features/Send/TransferComposer.swift | 48 +- .../Features/Send/TransferDetailsView.swift | 100 +- .../Features/Send/TransferShareActions.swift | 10 +- .../Features/Settings/SettingsModel.swift | 36 +- .../Features/Settings/SettingsScreen.swift | 22 +- .../Features/Settings/SettingsSections.swift | 122 +- apple/VniDrop/Generated/L10n.swift | 3923 +++++++++++++++++ apple/VniDrop/Resources/Localizable.xcstrings | 8 +- .../UI/Components/AdaptiveDrawer.swift | 2 +- apple/VniDrop/UI/Components/Components.swift | 4 +- apple/VniDrop/UI/Feedback/UiMessage.swift | 4 +- .../VniDrop/UI/Feedback/UserFacingError.swift | 60 +- .../UI/Navigation/AppDestination.swift | 8 +- localization/src/commands/generate.ts | 8 + localization/src/config.ts | 6 + localization/src/lib/swift-accessors.ts | 139 + localization/strings.json | 90 +- 28 files changed, 4465 insertions(+), 395 deletions(-) create mode 100644 apple/VniDrop/Generated/L10n.swift create mode 100644 localization/src/lib/swift-accessors.ts diff --git a/apple/Tests/ProgressDerivationTests.swift b/apple/Tests/ProgressDerivationTests.swift index 4a20940..6e6de33 100644 --- a/apple/Tests/ProgressDerivationTests.swift +++ b/apple/Tests/ProgressDerivationTests.swift @@ -40,7 +40,7 @@ final class ProgressDerivationTests: XCTestCase { event(phase: "import", kind: "started", json: "{}"), ] let progress = progressForTransfer(events: events, transferId: 1) - XCTAssertEqual(progress?.labelKey, "progress_preparing") + XCTAssertEqual(progress?.labelKey, L10n.Progress.preparing) XCTAssertEqual(progress?.progress, 0.3) } @@ -58,14 +58,14 @@ final class ProgressDerivationTests: XCTestCase { totalSizeHint: 100 ) XCTAssertEqual(progress?.kind, "completed") - XCTAssertEqual(progress?.labelKey, "progress_completed") + XCTAssertEqual(progress?.labelKey, L10n.Progress.completed) XCTAssertEqual(progress?.progress, 1) } func testStatusLabelKeys() { - XCTAssertEqual(statusLabelKey(.sharing), "status_available") - XCTAssertEqual(statusLabelKey(.receiving), "status_receiving") - XCTAssertEqual(statusLabelKey(.done), "status_completed") + XCTAssertEqual(statusLabelKey(.sharing), L10n.Status.available) + XCTAssertEqual(statusLabelKey(.receiving), L10n.Status.receiving) + XCTAssertEqual(statusLabelKey(.done), L10n.Status.completed) } private func event(phase: String, kind: String, json: String) -> CoreEventModel { diff --git a/apple/Tests/UiFeedbackTests.swift b/apple/Tests/UiFeedbackTests.swift index 7c1df1e..fdbbfbe 100644 --- a/apple/Tests/UiFeedbackTests.swift +++ b/apple/Tests/UiFeedbackTests.swift @@ -42,22 +42,22 @@ final class UserFacingErrorTests: XCTestCase { } func testToUiTextMapsKnownReasons() { - XCTAssertEqual(InvitationError.message("The transfer was refused").toUiText(), .resource("error_permission")) - XCTAssertEqual(InvitationError.message("invalid ticket").toUiText(), .resource("error_invalid_ticket")) - XCTAssertEqual(InvitationError.message("Select at least one file to share").toUiText(), .resource("error_share_empty")) - XCTAssertEqual(InvitationError.message("Camera access is required").toUiText(), .resource("error_camera")) + XCTAssertEqual(InvitationError.message("The transfer was refused").toUiText(), .resource(L10n.Error.permission)) + XCTAssertEqual(InvitationError.message("invalid ticket").toUiText(), .resource(L10n.Error.invalidTicket)) + XCTAssertEqual(InvitationError.message("Select at least one file to share").toUiText(), .resource(L10n.Error.shareEmpty)) + XCTAssertEqual(InvitationError.message("Camera access is required").toUiText(), .resource(L10n.Error.camera)) } func testToUiTextFallsBackToGeneric() { - XCTAssertEqual(InvitationError.message("something entirely unexpected").toUiText(), .resource("error_generic")) + XCTAssertEqual(InvitationError.message("something entirely unexpected").toUiText(), .resource(L10n.Error.generic)) } func testToUiTextMapsTypedTransferFailures() { - XCTAssertEqual(VnidropError.FilesystemPermission(reason: "read-only folder").toUiText(), .resource("error_filesystem")) - XCTAssertEqual(VnidropError.DestinationExists(reason: "target exists").toUiText(), .resource("error_destination_exists")) - XCTAssertEqual(VnidropError.StorageFull(reason: "disk full").toUiText(), .resource("error_storage_full")) - XCTAssertEqual(VnidropError.Network(reason: "offline").toUiText(), .resource("error_network")) - XCTAssertEqual(VnidropError.InvalidInput(reason: "bad path").toUiText(), .resource("error_invalid_input")) + XCTAssertEqual(VnidropError.FilesystemPermission(reason: "read-only folder").toUiText(), .resource(L10n.Error.filesystem)) + XCTAssertEqual(VnidropError.DestinationExists(reason: "target exists").toUiText(), .resource(L10n.Error.destinationExists)) + XCTAssertEqual(VnidropError.StorageFull(reason: "disk full").toUiText(), .resource(L10n.Error.storageFull)) + XCTAssertEqual(VnidropError.Network(reason: "offline").toUiText(), .resource(L10n.Error.network)) + XCTAssertEqual(VnidropError.InvalidInput(reason: "bad path").toUiText(), .resource(L10n.Error.invalidInput)) XCTAssertFalse(VnidropError.FilesystemPermission(reason: "read-only").canRetryWithoutChangingInput) XCTAssertFalse(VnidropError.DestinationExists(reason: "target exists").canRetryWithoutChangingInput) XCTAssertTrue(VnidropError.Network(reason: "offline").canRetryWithoutChangingInput) diff --git a/apple/VniDrop/App/RootView.swift b/apple/VniDrop/App/RootView.swift index 5a77d2d..eea5b8f 100644 --- a/apple/VniDrop/App/RootView.swift +++ b/apple/VniDrop/App/RootView.swift @@ -111,7 +111,7 @@ struct RootView: View { #if os(macOS) NavigationSplitView { List(AppDestination.allCases, selection: sidebarBinding) { destination in - Label(LocalizedStringKey(destination.labelKey), systemImage: destination.systemImage) + Label(String(localized: destination.labelKey), systemImage: destination.systemImage) .tag(destination) } .navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 260) @@ -123,7 +123,7 @@ struct RootView: View { ForEach(AppDestination.allCases) { destination in screen(for: destination, windowClass: windowClass) .tabItem { - Label(LocalizedStringKey(destination.labelKey), systemImage: destination.systemImage) + Label(String(localized: destination.labelKey), systemImage: destination.systemImage) } .tag(destination) } diff --git a/apple/VniDrop/Core/TransferProgress.swift b/apple/VniDrop/Core/TransferProgress.swift index 6c5dabd..227dbc4 100644 --- a/apple/VniDrop/Core/TransferProgress.swift +++ b/apple/VniDrop/Core/TransferProgress.swift @@ -20,7 +20,7 @@ struct TransferProgress: Equatable { let transferId: UInt64? let phase: String let kind: String - let labelKey: String + let labelKey: String.LocalizationValue let progress: Double? var detail: String? = nil /// Pre-resolved label that overrides `labelKey` when set (e.g. "Sending to 2", @@ -28,15 +28,15 @@ struct TransferProgress: Equatable { var label: String? = nil } -func statusLabelKey(_ status: TransferStatus) -> String { +func statusLabelKey(_ status: TransferStatus) -> String.LocalizationValue { switch status { - case .importing: return "status_preparing" - case .sharing: return "status_available" - case .receiving: return "status_receiving" - case .done: return "status_completed" - case .cancelled: return "status_cancelled" - case .stopped: return "status_stopped" - case .failed: return "status_failed" + case .importing: return L10n.Status.preparing + case .sharing: return L10n.Status.available + case .receiving: return L10n.Status.receiving + case .done: return L10n.Status.completed + case .cancelled: return L10n.Status.cancelled + case .stopped: return L10n.Status.stopped + case .failed: return L10n.Status.failed } } @@ -93,20 +93,20 @@ func progressForReceiver( if latest.kind == "aborted" { return TransferProgress( transferId: transferId, phase: "transfer", kind: "aborted", - labelKey: "progress_interrupted", progress: nil, detail: nil + labelKey: L10n.Progress.interrupted, progress: nil, detail: nil + ) + } + if latest.kind == "completed" && !transferEvents.contains(where: { $0.kind == "progress" || $0.kind == "started" }) { + return TransferProgress( + transferId: transferId, phase: "transfer", kind: "completed", + labelKey: L10n.Progress.completed, progress: 1, detail: nil ) } let progress = aggregateReceiverProgress(events: transferEvents, totalSizeHint: totalSizeHint) - if latest.kind == "completed" && (progress.map { $0 >= 0.999 } ?? true) { - return TransferProgress( - transferId: transferId, phase: "transfer", kind: "completed", - labelKey: "progress_completed", progress: 1, detail: nil - ) - } return TransferProgress( transferId: transferId, phase: "transfer", kind: latest.kind, - labelKey: "progress_sending", progress: progress, detail: progressDetail(latest) + labelKey: L10n.Progress.sending, progress: progress, detail: progressDetail(latest) ) } @@ -127,26 +127,26 @@ func formatBytes(_ size: UInt64) -> String { // MARK: - Internals (ported literally from AppUiModels.kt) -private func humanProgressLabel(_ event: CoreEventModel) -> String { +private func humanProgressLabel(_ event: CoreEventModel) -> String.LocalizationValue { switch (event.phase, event.kind) { case ("import", "copy-progress"), ("import", "outboard-progress"), ("import", "started"): - return "progress_preparing" - case ("import", "done"): return "progress_ready" - case ("ticket", "created"): return "progress_share_ready" - case ("network", "connecting"): return "progress_connecting" - case ("network", "connected"): return "progress_connected" - case ("download", "found-collection"): return "progress_getting_ready" - case ("download", "progress"): return "progress_downloading" - case ("export", "progress"): return "progress_saving" - case ("transfer", "progress"): return "progress_sending" - case ("transfer", "started"): return "progress_connected" - case ("transfer", "completed"): return "progress_completed" - case ("lifecycle", "done"): return "progress_completed" - case ("lifecycle", "cancelled"): return "progress_cancelled" + return L10n.Progress.preparing + case ("import", "done"): return L10n.Progress.ready + case ("ticket", "created"): return L10n.Progress.shareReady + case ("network", "connecting"): return L10n.Progress.connecting + case ("network", "connected"): return L10n.Progress.connected + case ("download", "found-collection"): return L10n.Progress.gettingReady + case ("download", "progress"): return L10n.Progress.downloading + case ("export", "progress"): return L10n.Progress.saving + case ("transfer", "progress"): return L10n.Progress.sending + case ("transfer", "started"): return L10n.Progress.connected + case ("transfer", "completed"): return L10n.Progress.completed + case ("lifecycle", "done"): return L10n.Progress.completed + case ("lifecycle", "cancelled"): return L10n.Progress.cancelled default: - if event.phase == "handshake" { return "progress_requesting_access" } - if event.kind == "failed" { return "progress_failed" } - return "progress_working" + if event.phase == "handshake" { return L10n.Progress.requestingAccess } + if event.kind == "failed" { return L10n.Progress.failed } + return L10n.Progress.working } } diff --git a/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift b/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift index 0417987..6b9a844 100644 --- a/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift +++ b/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift @@ -149,12 +149,9 @@ final class ApprovalCoordinator: ObservableObject { for request in pending where !publishedNotificationIds.contains(request.id) { let receiver = request.receiverName ?? request.receiverDeviceName - ?? String(localized: "approval_nearby_device") - let title = String(localized: "approval_connection_request") - let body = String( - format: String(localized: "approval_request_body"), - receiver, request.transferName - ) + ?? String(localized: L10n.Approval.nearbyDevice) + let title = String(localized: L10n.Approval.connectionRequest) + let body = L10n.Approval.requestBody(receiver: receiver, transferName: request.transferName) let result = await notifications.publish( LocalNotification(id: Self.notificationId(request.id), title: title, body: body) ) diff --git a/apple/VniDrop/Features/Approvals/ApprovalModal.swift b/apple/VniDrop/Features/Approvals/ApprovalModal.swift index e317edf..c731ef8 100644 --- a/apple/VniDrop/Features/Approvals/ApprovalModal.swift +++ b/apple/VniDrop/Features/Approvals/ApprovalModal.swift @@ -38,32 +38,32 @@ private struct ApprovalSheet: View { var body: some View { let busy = state.respondingIds.contains(request.id) - let receiver = request.receiverName ?? request.receiverDeviceName ?? String(localized: "approval_nearby_device") + let receiver = request.receiverName ?? request.receiverDeviceName ?? String(localized: L10n.Approval.nearbyDevice) VStack(spacing: 16) { Image(systemName: "checkmark.shield.fill") .font(.system(size: 44)) .foregroundStyle(.tint) .padding(.top, 12) - Text(LocalizedStringKey("approval_connection_request")) + Text(String(localized: L10n.Approval.connectionRequest)) .font(.title2).fontWeight(.semibold) - Text(String(format: String(localized: "approval_request_body"), receiver, request.transferName)) + Text(L10n.Approval.requestBody(receiver: receiver, transferName: request.transferName)) .multilineTextAlignment(.center) - Text(String(format: String(localized: "approval_endpoint_id"), request.remoteEndpointId)) + Text(L10n.Approval.endpointId(deviceId: request.remoteEndpointId)) .font(.caption).foregroundStyle(.secondary) .multilineTextAlignment(.center) if state.pending.count > 1 { - Text(String(format: String(localized: "approval_pending_count"), state.pending.count)) + Text(L10n.Approval.pendingCount(count: state.pending.count)) .font(.caption).foregroundStyle(.secondary) } Spacer(minLength: 0) if busy { ProgressView() } VStack(spacing: 10) { Button(action: { onAccept(request.id) }) { - Text(LocalizedStringKey("button_approve")).frame(maxWidth: .infinity) + Text(String(localized: L10n.Button.approve)).frame(maxWidth: .infinity) } .buttonStyle(.borderedProminent).controlSize(.large).disabled(busy) Button(role: .destructive, action: { onRefuse(request.id) }) { - Text(LocalizedStringKey("button_refuse")).frame(maxWidth: .infinity) + Text(String(localized: L10n.Button.refuse)).frame(maxWidth: .infinity) } .buttonStyle(.bordered).controlSize(.large).disabled(busy) } diff --git a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift index 9fb38b7..f0b7a21 100644 --- a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift +++ b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift @@ -2,7 +2,7 @@ import SwiftUI enum ReceiveMethodAvailability { case available, unavailable, hidden } -/// Invitation acquisition actions shared by the native Apple feature models. +/// Invitation acquisition actions, ported from `ReceiveInvitationActions` (iosMain). @MainActor protocol ReceiveInvitationActions: AnyObject { var fileAvailability: ReceiveMethodAvailability { get } @@ -23,25 +23,25 @@ struct ReceiveMethodPanel: View { var body: some View { VStack(alignment: .leading, spacing: 12) { - Text(LocalizedStringKey("receive_choose_method_title")).font(VniType.titleLarge) - Text(LocalizedStringKey("receive_choose_method_body")).foregroundStyle(colors.foregroundLighter) + Text(String(localized: L10n.Receive.chooseMethodTitle)).font(VniType.titleLarge) + Text(String(localized: L10n.Receive.chooseMethodBody)).foregroundStyle(colors.foregroundLighter) MethodRow( - icon: "doc", titleKey: "receive_method_file", descKey: "receive_method_file_description", + icon: "doc", titleKey: L10n.Receive.methodFile, descKey: L10n.Receive.methodFileDescription, availability: actions.fileAvailability ) { actions.pickInvitation { model.onInvitationResult(.invitationFile, $0) } } if actions.qrAvailability != .hidden { MethodRow( - icon: "qrcode.viewfinder", titleKey: "receive_method_scan", descKey: "receive_method_scan_description", + icon: "qrcode.viewfinder", titleKey: L10n.Receive.methodScan, descKey: L10n.Receive.methodScanDescription, availability: actions.qrAvailability ) { actions.scanQrCode { model.onInvitationResult(.qrCode, $0) } } } if actions.nfcAvailability != .hidden { MethodRow( icon: "wave.3.right", - titleOverride: model.state.isWaitingForNfc ? String(localized: "receive_nfc_waiting") : nil, - titleKey: "receive_method_nfc", descKey: "receive_method_nfc_description", + titleOverride: model.state.isWaitingForNfc ? String(localized: L10n.Receive.nfcWaiting) : nil, + titleKey: L10n.Receive.methodNfc, descKey: L10n.Receive.methodNfcDescription, availability: model.state.isWaitingForNfc ? .unavailable : actions.nfcAvailability ) { model.setWaitingForNfc(true) @@ -58,8 +58,8 @@ private struct MethodRow: View { @Environment(\.vniColors) private var colors let icon: String var titleOverride: String? = nil - let titleKey: String - let descKey: String + let titleKey: String.LocalizationValue + let descKey: String.LocalizationValue let availability: ReceiveMethodAvailability let onTap: () -> Void @@ -74,13 +74,13 @@ private struct MethodRow: View { if let titleOverride { Text(titleOverride).font(VniType.bodyLarge) } else { - Text(LocalizedStringKey(titleKey)).font(VniType.bodyLarge) + Text(String(localized: titleKey)).font(VniType.bodyLarge) } - Text(LocalizedStringKey(descKey)).font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter) + Text(String(localized: descKey)).font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter) } Spacer() if availability == .unavailable { - Text(LocalizedStringKey("value_unavailable")).font(VniType.labelSmall).foregroundStyle(colors.foregroundLighter) + Text(String(localized: L10n.Value.unavailable)).font(VniType.labelSmall).foregroundStyle(colors.foregroundLighter) } } .padding(16) @@ -102,7 +102,7 @@ struct InvitationReviewPanel: View { var body: some View { VStack(alignment: .leading, spacing: 14) { - Text(LocalizedStringKey("receive_review_title")).font(VniType.titleLarge) + Text(String(localized: L10n.Receive.reviewTitle)).font(VniType.titleLarge) if state.isInspecting { ProgressView().frame(maxWidth: .infinity).padding(40) } @@ -110,16 +110,16 @@ struct InvitationReviewPanel: View { let metadata = inspection.metadata VStack(alignment: .leading, spacing: 8) { Text(metadata.transferName).font(VniType.bodyLarge).lineLimit(2) - Text("\(metadata.fileCount) \(String(localized: "metadata_files").lowercased()) · \(formatBytes(metadata.totalSize))") + Text("\(metadata.fileCount) \(String(localized: L10n.Metadata.files).lowercased()) · \(formatBytes(metadata.totalSize))") .foregroundStyle(colors.foregroundLighter) } .padding(16) .frame(maxWidth: .infinity, alignment: .leading) .background(colors.backgroundSurface200, in: RoundedRectangle(cornerRadius: 14)) - Field(label: String(localized: "field_receiver_name"), + Field(label: String(localized: L10n.Field.receiverName), value: Binding(get: { state.receiverName }, set: { model.setReceiverName($0) })) - Text(state.receiveFolder?.displayName ?? String(localized: "value_unavailable")) + Text(state.receiveFolder?.displayName ?? String(localized: L10n.Value.unavailable)) .font(VniType.bodySmall) .foregroundStyle(state.folderAccessStatus == .writable ? colors.foregroundLight : colors.destructiveDefault) @@ -127,11 +127,11 @@ struct InvitationReviewPanel: View { let progressId = state.activeReceiveTransferId ?? model.coreState.events.first { $0.direction == "receive" && $0.transferId != nil }?.transferId let progress = progressId.flatMap { progressForTransfer(events: model.coreState.events, transferId: $0) } - ProgressRow(labelKey: progress?.labelKey ?? "progress_receiving", progress: progress?.progress, detail: progress?.detail) - SecondaryButton(title: String(localized: "button_cancel_receive"), action: model.cancelActiveReceive) + ProgressRow(labelKey: progress?.labelKey ?? L10n.Progress.receiving, progress: progress?.progress, detail: progress?.detail) + SecondaryButton(title: String(localized: L10n.Button.cancelReceive), action: model.cancelActiveReceive) } else { PrimaryButton( - title: String(localized: "button_receive"), action: model.receive, + title: String(localized: L10n.Button.receive), action: model.receive, enabled: state.canReceive(coreInitialized: model.coreState.isInitialized) ) } diff --git a/apple/VniDrop/Features/Receive/ReceiveModel.swift b/apple/VniDrop/Features/Receive/ReceiveModel.swift index 919f287..aa7f82f 100644 --- a/apple/VniDrop/Features/Receive/ReceiveModel.swift +++ b/apple/VniDrop/Features/Receive/ReceiveModel.swift @@ -133,7 +133,7 @@ final class ReceiveModel: ObservableObject { case .success: state.historyDeleteTarget = nil state.isDeletingHistory = false - let key = target == .all ? "receive_history_cleared" : "transfer_deleted" + let key = target == .all ? L10n.Receive.historyCleared : L10n.Transfer.deleted messages.tryShow(UiMessage(text: .resource(key), tone: .success)) case .failure(let error): state.isDeletingHistory = false @@ -173,9 +173,9 @@ final class ReceiveModel: ObservableObject { resetAcquisition() let canReveal = fileSystemService.canRevealReceiveFolder(folder) messages.tryShow(UiMessage( - text: .resource("receive_completed"), + text: .resource(L10n.Receive.completed), tone: .success, - actionLabel: canReveal ? .resource("button_show_in_files") : nil, + actionLabel: canReveal ? .resource(L10n.Button.showInFiles) : nil, onAction: canReveal ? { self.revealReceiveFolder(folder) } : nil )) case .failure(let error): @@ -192,8 +192,8 @@ final class ReceiveModel: ObservableObject { messages.tryShow(UiMessage( text: uiText, tone: .error, - actionLabel: error.canRetryWithoutChangingInput ? .resource("button_retry") : nil, - onAction: error.canRetryWithoutChangingInput ? { self.receive() } : nil + actionLabel: .resource(L10n.Button.retry), + onAction: { self.receive() } )) } } @@ -222,14 +222,14 @@ final class ReceiveModel: ObservableObject { Task { let result = await fileSystemService.revealReceiveFolder(folder) if case .failure = result { - messages.show(UiMessage(text: .resource("receive_open_files_failed"), tone: .error)) + messages.show(UiMessage(text: .resource(L10n.Receive.openFilesFailed), tone: .error)) } } } private func inspectInvitation(_ method: ReceiveMethod, _ raw: String) { let ticket = raw.trimmingCharacters(in: .whitespacesAndNewlines) - if ticket.isEmpty { return messages.error(.resource("error_invitation_empty")) } + if ticket.isEmpty { return messages.error(.resource(L10n.Error.invitationEmpty)) } state.isAcquisitionOpen = true state.ticket = ticket state.method = method diff --git a/apple/VniDrop/Features/Receive/ReceiveScreen.swift b/apple/VniDrop/Features/Receive/ReceiveScreen.swift index 9bd50e8..d82dc24 100644 --- a/apple/VniDrop/Features/Receive/ReceiveScreen.swift +++ b/apple/VniDrop/Features/Receive/ReceiveScreen.swift @@ -22,17 +22,17 @@ struct ReceiveScreen: View { history } } - .navigationTitle(Text(LocalizedStringKey("receive_title"))) + .navigationTitle(Text(String(localized: L10n.Receive.title))) .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: model.openAcquisition) { - Label(String(localized: "button_receive_files"), systemImage: "plus") + Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus") } } if !deletable.isEmpty { ToolbarItem(placement: .primaryAction) { Button(role: .destructive, action: model.requestClearHistory) { - Label(String(localized: "receive_clear_history"), systemImage: "trash") + Label(String(localized: L10n.Receive.clearHistory), systemImage: "trash") } } } @@ -50,11 +50,11 @@ struct ReceiveScreen: View { } } .alert( - Text(LocalizedStringKey(clearAllPending ? "receive_clear_history_title" : "receive_delete_history_title")), + Text(String(localized: clearAllPending ? L10n.Receive.clearHistoryTitle : L10n.Receive.deleteHistoryTitle)), isPresented: Binding(get: { model.state.historyDeleteTarget != nil }, set: { if !$0 { Task { @MainActor in model.dismissHistoryDelete() } } }) ) { - Button(String(localized: "button_cancel"), role: .cancel, action: model.dismissHistoryDelete) - Button(String(localized: clearAllPending ? "receive_clear_history" : "button_delete_transfer"), + Button(String(localized: L10n.Button.cancel), role: .cancel, action: model.dismissHistoryDelete) + Button(String(localized: clearAllPending ? L10n.Receive.clearHistory : L10n.Button.deleteTransfer), role: .destructive, action: model.confirmHistoryDelete) } message: { historyDeleteMessage @@ -74,27 +74,27 @@ struct ReceiveScreen: View { Button(role: .destructive) { model.requestDeleteHistoryItem(transfer.transferId) } label: { - Label(String(localized: "button_delete_transfer"), systemImage: "trash") + Label(String(localized: L10n.Button.deleteTransfer), systemImage: "trash") } } } } } header: { - Text(LocalizedStringKey("receive_history_title")) + Text(String(localized: L10n.Receive.historyTitle)) } footer: { - Text(LocalizedStringKey("receive_new_subtitle")) + Text(String(localized: L10n.Receive.newSubtitle)) } } } private var emptyState: some View { ContentUnavailableView { - Label(String(localized: "receive_empty_title"), systemImage: "tray.and.arrow.down") + Label(String(localized: L10n.Receive.emptyTitle), systemImage: "tray.and.arrow.down") } description: { - Text(LocalizedStringKey("receive_empty_body")) + Text(String(localized: L10n.Receive.emptyBody)) } actions: { Button(action: model.openAcquisition) { - Label(String(localized: "button_receive_files"), systemImage: "plus") + Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus") } .buttonStyle(.borderedProminent) .controlSize(.large) @@ -107,10 +107,10 @@ struct ReceiveScreen: View { private var historyDeleteMessage: some View { if let target = model.state.historyDeleteTarget { if target == .all { - Text(LocalizedStringKey("receive_clear_history_description")) + Text(String(localized: L10n.Receive.clearHistoryDescription)) } else { - Text(String(format: String(localized: "receive_delete_history_description"), - transferName(for: target) ?? String(localized: "receive_unknown_transfer"))) + Text(L10n.Receive.deleteHistoryDescription( + transferName: transferName(for: target) ?? String(localized: L10n.Receive.unknownTransfer))) } } } @@ -134,7 +134,7 @@ private struct ReceiveTransferRow: View { .frame(width: 40, height: 40) .background(.quaternary, in: RoundedRectangle(cornerRadius: 9)) VStack(alignment: .leading, spacing: 3) { - Text(transfer.transferName ?? String(localized: "receive_unknown_transfer")) + Text(transfer.transferName ?? String(localized: L10n.Receive.unknownTransfer)) .font(.body).lineLimit(1) Text("\(formatBytes(transfer.totalSize)) · \(statusLabel(transfer.status))") .font(.caption).foregroundStyle(.secondary) diff --git a/apple/VniDrop/Features/Send/SendModel.swift b/apple/VniDrop/Features/Send/SendModel.swift index 53aa931..b9d74ed 100644 --- a/apple/VniDrop/Features/Send/SendModel.swift +++ b/apple/VniDrop/Features/Send/SendModel.swift @@ -217,7 +217,7 @@ final class SendModel: ObservableObject { state.receiverHistory = [] state.isDeleteConfirmationOpen = false state.isDeleting = false - messages.tryShow(UiMessage(text: .resource("transfer_deleted"), tone: .success)) + messages.tryShow(UiMessage(text: .resource(L10n.Transfer.deleted), tone: .success)) case .failure(let error): state.isDeleting = false messages.error(error) @@ -249,7 +249,7 @@ final class SendModel: ObservableObject { switch result { case .success: _ = await repository.refresh() - messages.tryShow(UiMessage(text: .resource("transfer_event_stopped"), tone: .info)) + messages.tryShow(UiMessage(text: .resource(L10n.Transfer.eventStopped), tone: .info)) case .failure(let error): messages.error(error) } @@ -261,10 +261,10 @@ final class SendModel: ObservableObject { func onInvitationResult(_ action: InvitationAction, _ result: Result) { switch result { case .success: - let key: String? + let key: String.LocalizationValue? switch action { - case .export: key = "transfer_invitation_saved" - case .nfc: key = "transfer_nfc_written" + case .export: key = L10n.Transfer.invitationSaved + case .nfc: key = L10n.Transfer.nfcWritten case .share: key = nil // system share sheet already confirms } if let key { messages.tryShow(UiMessage(text: .resource(key), tone: .success)) } @@ -297,7 +297,7 @@ final class SendModel: ObservableObject { state.transferName = "" state.accessPolicy = .requireApproval state.isSharing = false - messages.show(UiMessage(text: .resource("send_transfer_created"), tone: .success)) + messages.show(UiMessage(text: .resource(L10n.Send.transferCreated), tone: .success)) case .failure(let error): state.isSharing = false messages.error(error) diff --git a/apple/VniDrop/Features/Send/SendScreen.swift b/apple/VniDrop/Features/Send/SendScreen.swift index 5b56ef7..93ddc21 100644 --- a/apple/VniDrop/Features/Send/SendScreen.swift +++ b/apple/VniDrop/Features/Send/SendScreen.swift @@ -27,11 +27,11 @@ struct SendScreen: View { catalog } } - .navigationTitle(Text(LocalizedStringKey("send_title"))) + .navigationTitle(Text(String(localized: L10n.Send.title))) .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: model.openComposer) { - Label(String(localized: "button_create_new_transfer"), systemImage: "plus") + Label(String(localized: L10n.Button.createNewTransfer), systemImage: "plus") } } } @@ -65,14 +65,14 @@ struct SendScreen: View { } } .alert( - Text(LocalizedStringKey("transfer_delete_title")), + Text(String(localized: L10n.Transfer.deleteTitle)), isPresented: Binding(get: { model.state.isDeleteConfirmationOpen }, set: { if !$0 { Task { @MainActor in model.dismissDeleteTransfer() } } }) ) { - Button(String(localized: "button_cancel"), role: .cancel, action: model.dismissDeleteTransfer) - Button(String(localized: "button_delete_transfer"), role: .destructive, action: model.confirmDeleteTransfer) + Button(String(localized: L10n.Button.cancel), role: .cancel, action: model.dismissDeleteTransfer) + Button(String(localized: L10n.Button.deleteTransfer), role: .destructive, action: model.confirmDeleteTransfer) } message: { - Text(String(format: String(localized: "transfer_delete_description"), - transfer.transferName ?? String(localized: "send_new_transfer_title"))) + Text(L10n.Transfer.deleteDescription( + transferName: transfer.transferName ?? String(localized: L10n.Send.newTransferTitle))) } } @@ -92,21 +92,21 @@ struct SendScreen: View { .buttonStyle(.plain) } } header: { - Text(LocalizedStringKey("send_transfers_title")) + Text(String(localized: L10n.Send.transfersTitle)) } footer: { - Text(LocalizedStringKey("send_subtitle")) + Text(String(localized: L10n.Send.subtitle)) } } } private var emptyState: some View { ContentUnavailableView { - Label(String(localized: "send_empty_title"), systemImage: "paperplane") + Label(String(localized: L10n.Send.emptyTitle), systemImage: "paperplane") } description: { - Text(LocalizedStringKey("send_empty_body")) + Text(String(localized: L10n.Send.emptyBody)) } actions: { Button(action: model.openComposer) { - Label(String(localized: "button_create_new_transfer"), systemImage: "plus") + Label(String(localized: L10n.Button.createNewTransfer), systemImage: "plus") } .buttonStyle(.borderedProminent) .controlSize(.large) @@ -127,21 +127,18 @@ struct SendScreen: View { private func sharingProgress(for transfer: Transfer) -> TransferProgress? { let active = (model.receiversByTransfer[transfer.transferId] ?? []).filter { $0.status == .accepted } if active.isEmpty { return nil } - let fractions: [Double] = active.compactMap { receiver -> Double? in - let progress = progressForReceiver(events: model.coreState.events, transferId: transfer.transferId, - remoteEndpointId: receiver.remoteEndpointId, totalSizeHint: transfer.totalSize) - guard progress?.kind == "started" || progress?.kind == "progress" else { return nil } - return progress?.progress + let fractions = active.compactMap { + progressForReceiver(events: model.coreState.events, transferId: transfer.transferId, + remoteEndpointId: $0.remoteEndpointId, totalSizeHint: transfer.totalSize)?.progress } - guard !fractions.isEmpty else { return nil } - let combined = fractions.reduce(0, +) / Double(fractions.count) + let combined = fractions.isEmpty ? nil : fractions.reduce(0, +) / Double(fractions.count) if active.count == 1 { return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress", - labelKey: "progress_sending", progress: combined) + labelKey: L10n.Progress.sending, progress: combined) } return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress", - labelKey: "progress_sending", progress: combined, - label: String(format: String(localized: "progress_sending_to_count"), active.count)) + labelKey: L10n.Progress.sending, progress: combined, + label: L10n.Progress.sendingToCount(count: active.count)) } } @@ -157,7 +154,7 @@ private struct TransferListItem: View { .background(.quaternary, in: RoundedRectangle(cornerRadius: 9)) VStack(alignment: .leading, spacing: 3) { HStack { - Text(transfer.transferName ?? String(localized: "send_new_transfer_title")) + Text(transfer.transferName ?? String(localized: L10n.Send.newTransferTitle)) .font(.body).lineLimit(1) Spacer() StatusPill(label: statusLabel(transfer.status), tone: transfer.status.pillTone) @@ -192,13 +189,13 @@ struct FileArtwork: View { } func statusLabel(_ status: TransferStatus) -> String { - String(localized: String.LocalizationValue(statusLabelKey(status))) + String(localized: statusLabelKey(status)) } func accessPolicyLabel(_ policy: ShareAccessPolicy) -> String { switch policy { - case .requireApproval: return String(localized: "send_access_approval") - case .anyoneWithTransfer: return String(localized: "send_access_anyone") + case .requireApproval: return String(localized: L10n.Send.accessApproval) + case .anyoneWithTransfer: return String(localized: L10n.Send.accessAnyone) } } diff --git a/apple/VniDrop/Features/Send/TransferComposer.swift b/apple/VniDrop/Features/Send/TransferComposer.swift index 6404bf8..4dbd74f 100644 --- a/apple/VniDrop/Features/Send/TransferComposer.swift +++ b/apple/VniDrop/Features/Send/TransferComposer.swift @@ -23,13 +23,13 @@ struct TransferComposer: View { private var chooseStep: some View { VStack(alignment: .leading, spacing: 16) { - Text(LocalizedStringKey("send_choose_file_title")).font(.title2).fontWeight(.semibold) - Text(LocalizedStringKey("send_choose_file_body")) + Text(String(localized: L10n.Send.chooseFileTitle)).font(.title2).fontWeight(.semibold) + Text(String(localized: L10n.Send.chooseFileBody)) .font(.subheadline).foregroundStyle(.secondary) VStack(spacing: 14) { Image(systemName: "doc").font(.system(size: 30)).foregroundStyle(.tint) - PrimaryButton(title: String(localized: "button_choose_files"), action: model.selectFile).fixedSize() - QuietButton(title: String(localized: "button_choose_folder"), action: model.selectFolder) + PrimaryButton(title: String(localized: L10n.Button.chooseFiles), action: model.selectFile).fixedSize() + QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder) } .frame(maxWidth: .infinity) .padding(28) @@ -39,9 +39,9 @@ struct TransferComposer: View { private var reviewStep: some View { VStack(alignment: .leading, spacing: 16) { - Text(LocalizedStringKey("send_review_title")).font(.title2).fontWeight(.semibold) + Text(String(localized: L10n.Send.reviewTitle)).font(.title2).fontWeight(.semibold) if state.selectedFiles.count > 1 { - Text(String(format: String(localized: "send_selected_files_count"), state.selectedFiles.count)) + Text(L10n.Send.selectedFilesCount(count: state.selectedFiles.count)) .font(.subheadline).foregroundStyle(.secondary) } ForEach(state.selectedFiles) { file in @@ -51,23 +51,23 @@ struct TransferComposer: View { onRemove: { model.removeSelectedFile(file.value) } ) } - Field(label: String(localized: "field_transfer_name"), + Field(label: String(localized: L10n.Field.transferName), value: Binding(get: { state.transferName }, set: { model.setTransferName($0) })) - Field(label: String(localized: "field_sender_name"), + Field(label: String(localized: L10n.Field.senderName), value: Binding(get: { state.senderName }, set: { model.setSenderName($0) })) - Text(LocalizedStringKey("send_access_title")).font(.headline) + Text(String(localized: L10n.Send.accessTitle)).font(.headline) PolicyOption( - icon: "checkmark.shield", titleKey: "send_access_approval", descKey: "send_access_approval_description", + icon: "checkmark.shield", titleKey: L10n.Send.accessApproval, descKey: L10n.Send.accessApprovalDescription, selected: state.accessPolicy == .requireApproval, onTap: { model.setAccessPolicy(.requireApproval) } ) PolicyOption( - icon: "globe", titleKey: "send_access_anyone", descKey: "send_access_anyone_description", + icon: "globe", titleKey: L10n.Send.accessAnyone, descKey: L10n.Send.accessAnyoneDescription, selected: state.accessPolicy == .anyoneWithTransfer, onTap: { model.setAccessPolicy(.anyoneWithTransfer) } ) if state.accessPolicy == .anyoneWithTransfer { - Label(String(localized: "send_access_anyone_warning"), systemImage: "exclamationmark.triangle.fill") + Label(String(localized: L10n.Send.accessAnyoneWarning), systemImage: "exclamationmark.triangle.fill") .font(.caption).foregroundStyle(.orange) } actions @@ -77,7 +77,7 @@ struct TransferComposer: View { @ViewBuilder private var actions: some View { let shareTitle = state.isSharing - ? String(localized: "button_sharing_file") : String(localized: "button_share_file") + ? String(localized: L10n.Button.sharingFile) : String(localized: L10n.Button.shareFile) let shareButton = PrimaryButton( title: shareTitle, action: model.createShare, enabled: state.canCreateShare(coreInitialized: model.coreState.isInitialized) @@ -85,15 +85,15 @@ struct TransferComposer: View { if windowClass == .phone { VStack(spacing: 8) { shareButton - QuietButton(title: String(localized: "button_change_files"), action: model.selectFile, enabled: !state.isSharing) - QuietButton(title: String(localized: "button_choose_folder"), action: model.selectFolder, enabled: !state.isSharing) + QuietButton(title: String(localized: L10n.Button.changeFiles), action: model.selectFile, enabled: !state.isSharing) + QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder, enabled: !state.isSharing) } } else { HStack(spacing: 8) { shareButton.fixedSize() - QuietButton(title: String(localized: "button_change_files"), action: model.selectFile, enabled: !state.isSharing) - QuietButton(title: String(localized: "button_choose_folder"), action: model.selectFolder, enabled: !state.isSharing) - QuietButton(title: String(localized: "button_clear"), action: model.clearSelectedSource, enabled: !state.isSharing) + QuietButton(title: String(localized: L10n.Button.changeFiles), action: model.selectFile, enabled: !state.isSharing) + QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder, enabled: !state.isSharing) + QuietButton(title: String(localized: L10n.Button.clear), action: model.clearSelectedSource, enabled: !state.isSharing) } } } @@ -128,16 +128,16 @@ private struct SelectedFileCard: View { } private var subtitle: String { - if file.isDirectory { return String(localized: "send_folder_label") } + if file.isDirectory { return String(localized: L10n.Send.folderLabel) } if let size = file.sizeBytes { return formatBytes(size) } - return String(localized: "send_file_size_unknown") + return String(localized: L10n.Send.fileSizeUnknown) } } private struct PolicyOption: View { let icon: String - let titleKey: String - let descKey: String + let titleKey: String.LocalizationValue + let descKey: String.LocalizationValue let selected: Bool let onTap: () -> Void @@ -149,8 +149,8 @@ private struct PolicyOption: View { .foregroundStyle(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary)) .frame(width: 22) VStack(alignment: .leading, spacing: 3) { - Text(LocalizedStringKey(titleKey)) - Text(LocalizedStringKey(descKey)).font(.caption).foregroundStyle(.secondary) + Text(String(localized: titleKey)) + Text(String(localized: descKey)).font(.caption).foregroundStyle(.secondary) } Spacer() Image(systemName: selected ? "checkmark.circle.fill" : "circle") diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index 652bb72..643d0f6 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -23,29 +23,29 @@ struct TransferDetailsView: View { var body: some View { Form { Section { - LabeledContent(String(localized: "metadata_status"), value: statusLabel(transfer.status)) - LabeledContent(String(localized: "metadata_size"), value: formatBytes(transfer.totalSize)) - LabeledContent(String(localized: "send_access_title"), value: accessPolicyLabel(transfer.accessPolicy)) + LabeledContent(String(localized: L10n.Metadata.status), value: statusLabel(transfer.status)) + LabeledContent(String(localized: L10n.Metadata.size), value: formatBytes(transfer.totalSize)) + LabeledContent(String(localized: L10n.Send.accessTitle), value: accessPolicyLabel(transfer.accessPolicy)) } header: { - Text(transfer.transferName ?? String(localized: "send_new_transfer_title")) + Text(transfer.transferName ?? String(localized: L10n.Send.newTransferTitle)) } Section { DetailDestination( - title: String(localized: "transfer_activity_title"), - description: String(localized: "transfer_activity_description"), + title: String(localized: L10n.Transfer.activityTitle), + description: String(localized: L10n.Transfer.activityDescription), count: events.filter { $0.transferId == transfer.transferId && $0.isMeaningfulActivity }.count, onTap: model.openActivity ) DetailDestination( - title: String(localized: "transfer_receivers_title"), + title: String(localized: L10n.Transfer.receiversTitle), description: receiversDescription(pendingReceivers, completedReceivers), count: pendingReceivers + completedReceivers, onTap: model.openReceivers ) DetailDestination( - title: String(localized: "transfer_share_title"), - description: String(localized: "transfer_share_description"), + title: String(localized: L10n.Transfer.shareTitle), + description: String(localized: L10n.Transfer.shareDescription), count: 0, onTap: model.openShare ) @@ -56,13 +56,13 @@ struct TransferDetailsView: View { Button(role: .destructive) { showStopConfirmation = true } label: { - Label(String(localized: "send_stop_sharing"), systemImage: "stop.circle") + Label(String(localized: L10n.Send.stopSharing), systemImage: "stop.circle") } } } } .formStyle(.grouped) - .navigationTitle(Text(LocalizedStringKey("send_transfer_details_title"))) + .navigationTitle(Text(String(localized: L10n.Send.transferDetailsTitle))) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif @@ -74,27 +74,27 @@ struct TransferDetailsView: View { } } .confirmationDialog( - Text(LocalizedStringKey("send_stop_sharing")), + Text(String(localized: L10n.Send.stopSharing)), isPresented: $showStopConfirmation, titleVisibility: .visible ) { - Button(String(localized: "send_stop_sharing"), role: .destructive) { + Button(String(localized: L10n.Send.stopSharing), role: .destructive) { model.stopSharing(transferId: transfer.transferId) } - Button(String(localized: "button_cancel"), role: .cancel) {} + Button(String(localized: L10n.Button.cancel), role: .cancel) {} } message: { - Text(LocalizedStringKey("send_stop_sharing_description")) + Text(String(localized: L10n.Send.stopSharingDescription)) } } } private func receiversDescription(_ pending: Int, _ completed: Int) -> String { if pending > 0 && completed > 0 { - return "\(String(format: String(localized: "transfer_receivers_pending"), pending)) · \(String(format: String(localized: "transfer_receivers_completed_count"), completed))" + return "\(L10n.Transfer.receiversPending(count: pending)) · \(L10n.Transfer.receiversCompletedCount(count: completed))" } - if pending > 0 { return String(format: String(localized: "transfer_receivers_pending"), pending) } - if completed > 0 { return String(format: String(localized: "transfer_receivers_completed_count"), completed) } - return String(localized: "transfer_receivers_description") + if pending > 0 { return L10n.Transfer.receiversPending(count: pending) } + if completed > 0 { return L10n.Transfer.receiversCompletedCount(count: completed) } + return String(localized: L10n.Transfer.receiversDescription) } private struct DetailDestination: View { @@ -171,13 +171,13 @@ struct TransferActivityPanel: View { let visible = events .filter { $0.transferId == transferId && $0.isMeaningfulActivity } .sorted { $0.timestamp > $1.timestamp } - PanelContainer(title: String(localized: "transfer_activity_title")) { + PanelContainer(title: String(localized: L10n.Transfer.activityTitle)) { if visible.isEmpty { - Text(LocalizedStringKey("transfer_no_activity")).foregroundStyle(colors.foregroundLighter) + Text(String(localized: L10n.Transfer.noActivity)).foregroundStyle(colors.foregroundLighter) } else { ForEach(Array(visible.enumerated()), id: \.offset) { index, event in if index > 0 { Divider().overlay(colors.borderDefault) } - Text(LocalizedStringKey(event.activityTitleKey)) + Text(String(localized: event.activityTitleKey)) .fontWeight(.medium).padding(.vertical, 14) } } @@ -194,11 +194,11 @@ struct ReceiverHistoryPanel: View { let onCancel: (String) -> Void var body: some View { - PanelContainer(title: String(localized: "transfer_receivers_title")) { + PanelContainer(title: String(localized: L10n.Transfer.receiversTitle)) { if loading { ProgressView().frame(maxWidth: .infinity).padding(40) } else if receivers.isEmpty { - Text(LocalizedStringKey("transfer_no_receivers")).foregroundStyle(colors.foregroundLighter) + Text(String(localized: L10n.Transfer.noReceivers)).foregroundStyle(colors.foregroundLighter) } else { ForEach(Array(receivers.enumerated()), id: \.element.id) { index, receiver in if index > 0 { Divider().overlay(colors.borderDefault) } @@ -232,7 +232,7 @@ private struct ReceiverRow: View { } var body: some View { - let name = receiver.receiverName ?? receiver.receiverDeviceName ?? String(localized: "transfer_nearby_device") + let name = receiver.receiverName ?? receiver.receiverDeviceName ?? String(localized: L10n.Transfer.nearbyDevice) let showLive = sendProgress != nil && receiver.status != .completed && receiver.status != .refused && receiver.status != .expired HStack(alignment: .top, spacing: 12) { @@ -244,7 +244,7 @@ private struct ReceiverRow: View { if showLive, let sendProgress { ProgressRow(labelKey: sendProgress.labelKey, progress: sendProgress.progress, detail: sendProgress.detail, labelText: sendProgress.label) } else { - Text(LocalizedStringKey(receiver.status.statusTextKey)) + Text(String(localized: receiver.status.statusTextKey)) .font(VniType.bodySmall).fontWeight(.medium) .foregroundStyle(receiver.status.statusColor(colors)) } @@ -257,7 +257,7 @@ private struct ReceiverRow: View { Button(role: .destructive) { onCancel(receiver.id) } label: { - Text(LocalizedStringKey("button_refuse")) + Text(String(localized: L10n.Button.refuse)) .font(VniType.bodySmall) } .buttonStyle(.borderless) @@ -275,15 +275,15 @@ struct TransferSharePanel: View { let transfer: Transfer var body: some View { - PanelContainer(title: String(localized: "transfer_share_title")) { + PanelContainer(title: String(localized: L10n.Transfer.shareTitle)) { if let ticket = transfer.ticket { qrCard(ticket: ticket) - Text(LocalizedStringKey("transfer_scan_qr")) + Text(String(localized: L10n.Transfer.scanQr)) .font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter) .frame(maxWidth: .infinity) ShareActionsView(model: model, transfer: transfer, ticket: ticket) } else { - Text(LocalizedStringKey("transfer_event_preparing")).foregroundStyle(colors.foregroundLighter) + Text(String(localized: L10n.Transfer.eventPreparing)).foregroundStyle(colors.foregroundLighter) } } } @@ -334,31 +334,31 @@ extension CoreEventModel { "receiver-refused", "receiver-completed", "share-stopped", "failed"].contains(kind) } - var activityTitleKey: String { - if phase == "import" && kind == "started" { return "transfer_event_preparing" } - if phase == "ticket" && kind == "created" { return "transfer_event_ready" } - if phase == "network" { return "transfer_event_connecting" } - if phase == "download" { return "transfer_event_downloading" } - if phase == "export" { return "transfer_event_saving" } - if kind == "receiver-requested" { return "transfer_event_requested" } - if kind == "receiver-accepted" || kind == "receiver-auto-approved" { return "transfer_event_approved" } - if kind == "receiver-refused" { return "transfer_event_refused" } - if kind == "receiver-completed" { return "transfer_event_completed" } - if kind == "share-stopped" || (phase == "lifecycle" && kind == "cancelled") { return "transfer_event_stopped" } - if kind == "failed" { return "transfer_event_failed" } - return "transfer_event_updated" + var activityTitleKey: String.LocalizationValue { + if phase == "import" && kind == "started" { return L10n.Transfer.eventPreparing } + if phase == "ticket" && kind == "created" { return L10n.Transfer.eventReady } + if phase == "network" { return L10n.Transfer.eventConnecting } + if phase == "download" { return L10n.Transfer.eventDownloading } + if phase == "export" { return L10n.Transfer.eventSaving } + if kind == "receiver-requested" { return L10n.Transfer.eventRequested } + if kind == "receiver-accepted" || kind == "receiver-auto-approved" { return L10n.Transfer.eventApproved } + if kind == "receiver-refused" { return L10n.Transfer.eventRefused } + if kind == "receiver-completed" { return L10n.Transfer.eventCompleted } + if kind == "share-stopped" || (phase == "lifecycle" && kind == "cancelled") { return L10n.Transfer.eventStopped } + if kind == "failed" { return L10n.Transfer.eventFailed } + return L10n.Transfer.eventUpdated } } extension ReceiverDeliveryStatus { - var statusTextKey: String { + var statusTextKey: String.LocalizationValue { switch self { - case .requested: return "transfer_receiver_requested" - case .accepted: return "transfer_receiver_accepted" - case .refused: return "transfer_receiver_refused" - case .expired: return "transfer_receiver_expired" - case .completed: return "transfer_receiver_completed" - case .unknown: return "transfer_receiver_unknown" + case .requested: return L10n.Transfer.receiverRequested + case .accepted: return L10n.Transfer.receiverAccepted + case .refused: return L10n.Transfer.receiverRefused + case .expired: return L10n.Transfer.receiverExpired + case .completed: return L10n.Transfer.receiverCompleted + case .unknown: return L10n.Transfer.receiverUnknown } } diff --git a/apple/VniDrop/Features/Send/TransferShareActions.swift b/apple/VniDrop/Features/Send/TransferShareActions.swift index 73ca4df..325f342 100644 --- a/apple/VniDrop/Features/Send/TransferShareActions.swift +++ b/apple/VniDrop/Features/Send/TransferShareActions.swift @@ -2,7 +2,7 @@ import SwiftUI enum NfcShareAvailability { case available, unavailable, hidden } -/// Invitation delivery actions shared by the native Apple feature models. +/// Invitation delivery actions, ported from `TransferShareActions` (iosMain). /// Platform implementations perform export, native share, and NFC write. @MainActor protocol TransferShareActions: AnyObject { @@ -30,7 +30,7 @@ struct ShareActionsView: View { VStack(spacing: 12) { if actions.nfcAvailability != .hidden { SecondaryButton( - title: writingNfc ? String(localized: "transfer_nfc_waiting") : String(localized: "button_write_nfc"), + title: writingNfc ? String(localized: L10n.Transfer.nfcWaiting) : String(localized: L10n.Button.writeNfc), action: { writingNfc = true actions.writeInvitationToNfc(ticket: ticket) { result in @@ -41,16 +41,16 @@ struct ShareActionsView: View { enabled: actions.nfcAvailability == .available && !writingNfc ) if actions.nfcAvailability == .unavailable { - Text(LocalizedStringKey("transfer_nfc_unavailable")) + Text(String(localized: L10n.Transfer.nfcUnavailable)) .font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter) } } - SecondaryButton(title: String(localized: "button_download_invitation"), action: { + SecondaryButton(title: String(localized: L10n.Button.downloadInvitation), action: { actions.exportInvitation(ticket: ticket, transferName: transfer.transferName ?? "") { model.onInvitationResult(.export, $0) } }) - PrimaryButton(title: String(localized: "button_native_share"), action: { + PrimaryButton(title: String(localized: L10n.Button.nativeShare), action: { actions.shareInvitation(ticket: ticket, transferName: transfer.transferName ?? "") { model.onInvitationResult(.share, $0) } diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift index 7f36840..ceef5b8 100644 --- a/apple/VniDrop/Features/Settings/SettingsModel.swift +++ b/apple/VniDrop/Features/Settings/SettingsModel.swift @@ -11,15 +11,15 @@ enum SettingsSection: Hashable { case about case bugReport - var titleKey: String { + var titleKey: String.LocalizationValue { switch self { - case .overview: return "settings_title" - case .preferences: return "preferences_title" - case .appearance: return "appearance_title" - case .notifications: return "notifications_title" - case .storage: return "storage_title" - case .about: return "about_title" - case .bugReport: return "about_bug_report" + case .overview: return L10n.Settings.title + case .preferences: return L10n.Preferences.title + case .appearance: return L10n.Appearance.title + case .notifications: return L10n.Notifications.title + case .storage: return L10n.Storage.title + case .about: return L10n.About.title + case .bugReport: return L10n.About.bugReport } } } @@ -184,11 +184,11 @@ final class SettingsModel: ObservableObject { await enableNotifications() } else { preferences.setNotificationsEnabled(false) - let key = permission == .unsupported ? "notifications_unsupported" : "notifications_permission_denied" + let key = permission == .unsupported ? L10n.Notifications.unsupported : L10n.Notifications.permissionDenied messages.show(UiMessage( text: .resource(key), tone: .warning, - actionLabel: permission == .denied ? .resource("button_open_settings") : nil, + actionLabel: permission == .denied ? .resource(L10n.Button.openSettings) : nil, onAction: permission == .denied ? { self.openNotificationSettings() } : nil )) } @@ -200,7 +200,7 @@ final class SettingsModel: ObservableObject { Task { preferences.setDiagnosticsEnabled(enabled) messages.show(UiMessage( - text: .resource(enabled ? "diagnostics_enabled_message" : "diagnostics_disabled_message"), + text: .resource(enabled ? L10n.Diagnostics.enabledMessage : L10n.Diagnostics.disabledMessage), tone: .success )) } @@ -219,11 +219,11 @@ final class SettingsModel: ObservableObject { let what = snapshot.bugWhatHappened.trimmingCharacters(in: .whitespacesAndNewlines) let expected = snapshot.bugExpected.trimmingCharacters(in: .whitespacesAndNewlines) if what.isEmpty { - messages.show(UiMessage(text: .resource("bug_report_missing_what"), tone: .warning)) + messages.show(UiMessage(text: .resource(L10n.Bug.reportMissingWhat), tone: .warning)) return } if expected.isEmpty { - messages.show(UiMessage(text: .resource("bug_report_missing_expected"), tone: .warning)) + messages.show(UiMessage(text: .resource(L10n.Bug.reportMissingExpected), tone: .warning)) return } state.isSubmittingBugReport = true @@ -242,11 +242,11 @@ final class SettingsModel: ObservableObject { state.bugSteps = "" state.bugContact = "" state.bugIncludeLogs = true - messages.show(UiMessage(text: .resource("bug_report_submitted"), tone: .success)) + messages.show(UiMessage(text: .resource(L10n.Bug.reportSubmitted), tone: .success)) onSuccess() case .failure: state.isSubmittingBugReport = false - messages.show(UiMessage(text: .resource("bug_report_submit_failed"), tone: .error)) + messages.show(UiMessage(text: .resource(L10n.Bug.reportSubmitFailed), tone: .error)) } } } @@ -257,7 +257,7 @@ final class SettingsModel: ObservableObject { let result = await notifications.openSettings() if case .failure = result { enableNotificationsAfterSettings = false - messages.show(UiMessage(text: .resource("notifications_settings_open_failed"), tone: .error)) + messages.show(UiMessage(text: .resource(L10n.Notifications.settingsOpenFailed), tone: .error)) } } } @@ -278,7 +278,7 @@ final class SettingsModel: ObservableObject { private func enableNotifications() async { preferences.setNotificationsEnabled(true) - messages.show(UiMessage(text: .resource("notifications_enabled_message"), tone: .success)) + messages.show(UiMessage(text: .resource(L10n.Notifications.enabledMessage), tone: .success)) } // MARK: - Storage @@ -328,7 +328,7 @@ final class SettingsModel: ObservableObject { state.isDeletingTransfers = false if failures == 0 { loadStorageUsage() - messages.show(UiMessage(text: .resource("storage_transfers_deleted"), tone: .success)) + messages.show(UiMessage(text: .resource(L10n.Storage.transfersDeleted), tone: .success)) } else { messages.error(InvitationError.message("Could not delete \(failures) transfer records")) } diff --git a/apple/VniDrop/Features/Settings/SettingsScreen.swift b/apple/VniDrop/Features/Settings/SettingsScreen.swift index a83d210..099a7a0 100644 --- a/apple/VniDrop/Features/Settings/SettingsScreen.swift +++ b/apple/VniDrop/Features/Settings/SettingsScreen.swift @@ -25,26 +25,26 @@ struct SettingsScreen: View { Form { Section { NavigationLink(value: SettingsSection.preferences) { - SettingsRow(icon: "person.crop.circle", title: String(localized: "preferences_title"), value: model.state.username) + SettingsRow(icon: "person.crop.circle", title: String(localized: L10n.Preferences.title), value: model.state.username) } NavigationLink(value: SettingsSection.appearance) { - SettingsRow(icon: "sun.max", title: String(localized: "appearance_title"), value: themeModeLabel(model.state.themeMode)) + SettingsRow(icon: "sun.max", title: String(localized: L10n.Appearance.title), value: themeModeLabel(model.state.themeMode)) } } Section { NavigationLink(value: SettingsSection.notifications) { - SettingsRow(icon: "bell", title: String(localized: "notifications_title"), value: nil) + SettingsRow(icon: "bell", title: String(localized: L10n.Notifications.title), value: nil) } NavigationLink(value: SettingsSection.storage) { - SettingsRow(icon: "internaldrive", title: String(localized: "storage_title"), value: nil) + SettingsRow(icon: "internaldrive", title: String(localized: L10n.Storage.title), value: nil) } NavigationLink(value: SettingsSection.about) { - SettingsRow(icon: "info.circle", title: String(localized: "about_title"), value: nil) + SettingsRow(icon: "info.circle", title: String(localized: L10n.About.title), value: nil) } } } .formStyle(.grouped) - .navigationTitle(Text(LocalizedStringKey("settings_title"))) + .navigationTitle(Text(String(localized: L10n.Settings.title))) .navigationDestination(for: SettingsSection.self) { section in sectionForm(section) } @@ -57,7 +57,7 @@ struct SettingsScreen: View { SettingsSectionContent(model: model, section: section) } .formStyle(.grouped) - .navigationTitle(Text(LocalizedStringKey(section.titleKey))) + .navigationTitle(Text(String(localized: section.titleKey))) if section == .about { content @@ -69,7 +69,7 @@ struct SettingsScreen: View { Button { showBugReport = true } label: { - Label(String(localized: "about_bug_report"), systemImage: "ladybug") + Label(String(localized: L10n.About.bugReport), systemImage: "ladybug") } } } @@ -130,8 +130,8 @@ struct SettingsRow: View { func themeModeLabel(_ mode: ThemeMode) -> String { switch mode { - case .system: return String(localized: "appearance_system_mode") - case .light: return String(localized: "appearance_light_mode") - case .dark: return String(localized: "appearance_dark_mode") + case .system: return String(localized: L10n.Appearance.systemMode) + case .light: return String(localized: L10n.Appearance.lightMode) + case .dark: return String(localized: L10n.Appearance.darkMode) } } diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index b055699..1cbc173 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -7,16 +7,16 @@ struct PreferencesSettings: View { @ObservedObject var model: SettingsModel var body: some View { - Section(String(localized: "field_username")) { - TextField(String(localized: "field_username"), + Section(String(localized: L10n.Field.username)) { + TextField(String(localized: L10n.Field.username), text: Binding(get: { model.state.username }, set: { model.setUsername($0) })) } if model.state.supportsCustomReceiveFolders { - Section(String(localized: "preferences_receive_folder_title")) { - Text(model.state.receiveFolder?.displayName ?? String(localized: "value_unavailable")) + Section(String(localized: L10n.Preferences.receiveFolderTitle)) { + Text(model.state.receiveFolder?.displayName ?? String(localized: L10n.Value.unavailable)) .foregroundStyle(.secondary) - Button(String(localized: "button_choose_folder"), action: model.chooseReceiveFolder) - Button(String(localized: "button_reset_default"), action: model.resetReceiveFolder) + Button(String(localized: L10n.Button.chooseFolder), action: model.chooseReceiveFolder) + Button(String(localized: L10n.Button.resetDefault), action: model.resetReceiveFolder) } } } @@ -27,7 +27,7 @@ struct AppearanceSettings: View { var body: some View { Section { - Picker(String(localized: "appearance_title"), + Picker(String(localized: L10n.Appearance.title), selection: Binding(get: { model.state.themeMode }, set: { model.setThemeMode($0) })) { ForEach(ThemeMode.allCases, id: \.self) { mode in Text(themeModeLabel(mode)).tag(mode) @@ -48,10 +48,10 @@ struct NotificationSettings: View { get: { model.state.notificationsEnabled }, set: { model.setNotificationsEnabled($0) } )) { - Text(LocalizedStringKey("notifications_local_title")) + Text(String(localized: L10n.Notifications.localTitle)) } if model.state.notificationPermission == .denied { - Button(String(localized: "button_open_settings"), action: model.openNotificationSettings) + Button(String(localized: L10n.Button.openSettings), action: model.openNotificationSettings) } } } @@ -64,22 +64,22 @@ struct StorageSettings: View { var body: some View { Section { if let storage = model.state.storage { - LabeledContent(String(localized: "storage_received_files"), value: formatBytes(storage.receivedFiles)) - LabeledContent(String(localized: "storage_transfer_data"), value: formatBytes(storage.transferCache)) - LabeledContent(String(localized: "storage_app_data"), value: formatBytes(storage.appData)) - LabeledContent(String(localized: "storage_temporary"), value: formatBytes(storage.temporary)) - LabeledContent(String(localized: "storage_total")) { + LabeledContent(String(localized: L10n.Storage.receivedFiles), value: formatBytes(storage.receivedFiles)) + LabeledContent(String(localized: L10n.Storage.transferData), value: formatBytes(storage.transferCache)) + LabeledContent(String(localized: L10n.Storage.appData), value: formatBytes(storage.appData)) + LabeledContent(String(localized: L10n.Storage.temporary), value: formatBytes(storage.temporary)) + LabeledContent(String(localized: L10n.Storage.total)) { Text(formatBytes(storage.total)).fontWeight(.semibold) } } else { HStack { - Text(LocalizedStringKey("storage_calculating")).foregroundStyle(.secondary) + Text(String(localized: L10n.Storage.calculating)).foregroundStyle(.secondary) Spacer() ProgressView() } } } footer: { - Text(LocalizedStringKey("storage_footer")) + Text(String(localized: L10n.Storage.footer)) } Section { @@ -88,8 +88,8 @@ struct StorageSettings: View { } label: { HStack { Text(model.state.isDeletingTransfers - ? String(localized: "storage_deleting") - : String(localized: "storage_delete_transfers")) + ? String(localized: L10n.Storage.deleting) + : String(localized: L10n.Storage.deleteTransfers)) if model.state.isDeletingTransfers { Spacer() ProgressView() @@ -100,16 +100,16 @@ struct StorageSettings: View { } .onAppear { model.loadStorageUsage() } .confirmationDialog( - Text(LocalizedStringKey("storage_delete_transfers")), + Text(String(localized: L10n.Storage.deleteTransfers)), isPresented: $showDeleteConfirmation, titleVisibility: .visible ) { - Button(String(localized: "storage_delete_transfers"), role: .destructive) { + Button(String(localized: L10n.Storage.deleteTransfers), role: .destructive) { model.deleteAllTransfers() } - Button(String(localized: "button_cancel"), role: .cancel) {} + Button(String(localized: L10n.Button.cancel), role: .cancel) {} } message: { - Text(LocalizedStringKey("storage_delete_transfers_description")) + Text(String(localized: L10n.Storage.deleteTransfersDescription)) } } } @@ -121,40 +121,40 @@ struct AboutSettings: View { var body: some View { Section { - Text(LocalizedStringKey("about_tagline")).font(.headline) - Text(LocalizedStringKey("about_description")).foregroundStyle(.secondary) + Text(String(localized: L10n.About.tagline)).font(.headline) + Text(String(localized: L10n.About.description)).foregroundStyle(.secondary) } - Section(String(localized: "about_is_title")) { - AboutPoint("about_is_direct", "paperplane") - AboutPoint("about_is_no_account", "person.crop.circle.badge.xmark") - AboutPoint("about_is_in_control", "checkmark.shield") - AboutPoint("about_is_encrypted", "lock") - AboutPoint("about_is_open", "chevron.left.forwardslash.chevron.right") + Section(String(localized: L10n.About.isTitle)) { + AboutPoint(L10n.About.isDirect, "paperplane") + AboutPoint(L10n.About.isNoAccount, "person.crop.circle.badge.xmark") + AboutPoint(L10n.About.isInControl, "checkmark.shield") + AboutPoint(L10n.About.isEncrypted, "lock") + AboutPoint(L10n.About.isOpen, "chevron.left.forwardslash.chevron.right") } - Section(String(localized: "about_isnt_title")) { - AboutPoint("about_isnt_cloud", "icloud.slash") - AboutPoint("about_isnt_sync", "arrow.triangle.2.circlepath") - AboutPoint("about_isnt_public", "megaphone") + Section(String(localized: L10n.About.isntTitle)) { + AboutPoint(L10n.About.isntCloud, "icloud.slash") + AboutPoint(L10n.About.isntSync, "arrow.triangle.2.circlepath") + AboutPoint(L10n.About.isntPublic, "megaphone") } - Section(String(localized: "about_privacy_title")) { - AboutPoint("about_privacy_capability", "qrcode") - AboutPoint("about_privacy_deny", "hand.raised") - AboutPoint("about_privacy_relay", "antenna.radiowaves.left.and.right") - AboutPoint("about_privacy_local", "internaldrive") + Section(String(localized: L10n.About.privacyTitle)) { + AboutPoint(L10n.About.privacyCapability, "qrcode") + AboutPoint(L10n.About.privacyDeny, "hand.raised") + AboutPoint(L10n.About.privacyRelay, "antenna.radiowaves.left.and.right") + AboutPoint(L10n.About.privacyLocal, "internaldrive") } - Section(String(localized: "about_title")) { - LabeledContent(String(localized: "version_title"), value: model.state.appVersion) + Section(String(localized: L10n.About.title)) { + LabeledContent(String(localized: L10n.Version.title), value: model.state.appVersion) if let device = model.state.deviceInfo { - LabeledContent(String(localized: "device_model_title"), value: device.deviceModel ?? "—") - LabeledContent(String(localized: "os_version_title"), value: device.operatingSystem) + LabeledContent(String(localized: L10n.Device.modelTitle), value: device.deviceModel ?? "—") + LabeledContent(String(localized: L10n.Os.versionTitle), value: device.operatingSystem) } - LabeledContent(String(localized: "about_license_label"), value: "Apache 2.0") + LabeledContent(String(localized: L10n.About.licenseLabel), value: "Apache 2.0") Link(destination: Self.privacyPolicyURL) { - Label(String(localized: "about_privacy_policy_label"), systemImage: "hand.raised") + Label(String(localized: L10n.About.privacyPolicyLabel), systemImage: "hand.raised") } } @@ -164,7 +164,7 @@ struct AboutSettings: View { get: { model.state.diagnosticsEnabled }, set: { model.setDiagnosticsEnabled($0) } )) { - Text(LocalizedStringKey("diagnostics_title")) + Text(String(localized: L10n.Diagnostics.title)) } } } @@ -188,13 +188,13 @@ struct BugReportSheet: View { BugReportSettings(model: model, onSubmitted: { dismiss() }) } .formStyle(.grouped) - .navigationTitle(Text(LocalizedStringKey("about_bug_report"))) + .navigationTitle(Text(String(localized: L10n.About.bugReport))) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { - Button(String(localized: "button_cancel")) { dismiss() } + Button(String(localized: L10n.Button.cancel)) { dismiss() } } } } @@ -204,17 +204,17 @@ struct BugReportSheet: View { /// A bullet-style informational row with an SF Symbol and wrapping localized text. private struct AboutPoint: View { - let key: String + let key: String.LocalizationValue let symbol: String - init(_ key: String, _ symbol: String) { + init(_ key: String.LocalizationValue, _ symbol: String) { self.key = key self.symbol = symbol } var body: some View { Label { - Text(LocalizedStringKey(key)) + Text(String(localized: key)) .font(.subheadline) .fixedSize(horizontal: false, vertical: true) } icon: { @@ -228,36 +228,36 @@ struct BugReportSettings: View { var onSubmitted: () -> Void = {} var body: some View { - Section(String(localized: "bug_report_what_label")) { + Section(String(localized: L10n.Bug.reportWhatLabel)) { TextField("", text: Binding(get: { model.state.bugWhatHappened }, set: { model.setBugWhatHappened($0) }), - prompt: Text(LocalizedStringKey("bug_report_what_hint")), axis: .vertical) + prompt: Text(String(localized: L10n.Bug.reportWhatHint)), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } - Section(String(localized: "bug_report_expected_label")) { + Section(String(localized: L10n.Bug.reportExpectedLabel)) { TextField("", text: Binding(get: { model.state.bugExpected }, set: { model.setBugExpected($0) }), - prompt: Text(LocalizedStringKey("bug_report_expected_hint")), axis: .vertical) + prompt: Text(String(localized: L10n.Bug.reportExpectedHint)), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } - Section(String(localized: "bug_report_steps_label")) { + Section(String(localized: L10n.Bug.reportStepsLabel)) { TextField("", text: Binding(get: { model.state.bugSteps }, set: { model.setBugSteps($0) }), - prompt: Text(LocalizedStringKey("bug_report_steps_hint")), axis: .vertical) + prompt: Text(String(localized: L10n.Bug.reportStepsHint)), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } - Section(String(localized: "bug_report_contact_label")) { + Section(String(localized: L10n.Bug.reportContactLabel)) { TextField("", text: Binding(get: { model.state.bugContact }, set: { model.setBugContact($0) }), - prompt: Text(LocalizedStringKey("bug_report_contact_hint"))) + prompt: Text(String(localized: L10n.Bug.reportContactHint))) .labelsHidden() } Section { Toggle(isOn: Binding(get: { model.state.bugIncludeLogs }, set: { model.setBugIncludeLogs($0) })) { - Text(LocalizedStringKey("bug_report_include_logs")) + Text(String(localized: L10n.Bug.reportIncludeLogs)) } Button(action: { model.submitBugReport(onSuccess: onSubmitted) }) { Text(model.state.isSubmittingBugReport - ? String(localized: "bug_report_submitting") : String(localized: "bug_report_submit")) + ? String(localized: L10n.Bug.reportSubmitting) : String(localized: L10n.Bug.reportSubmit)) } .disabled(model.state.isSubmittingBugReport) } diff --git a/apple/VniDrop/Generated/L10n.swift b/apple/VniDrop/Generated/L10n.swift new file mode 100644 index 0000000..de09ca8 --- /dev/null +++ b/apple/VniDrop/Generated/L10n.swift @@ -0,0 +1,3923 @@ +// Generated by localization/ (bun run src/cli.ts generate). Do not edit. +// Keys resolve through Apple's String Catalog exactly as a literal would; these +// accessors only make the key compile-time-checked. If a runtime language +// switcher (live `.environment(\.locale)`) is ever added, switch the plain +// `static let` constants to computed `static var` so the locale is not frozen. +import Foundation + +enum L10n { + enum About { + /// Report a bug + /// + /// Key: `about_bug_report` + /// Context: About screen: button/link that opens the bug report form. + /// + /// - en: Report a bug + /// - fr: Signaler un bug + /// - es: Informar de un error + /// - it: Segnala un bug + /// - de: Fehler melden + /// - pt: Comunicar um erro + /// - pl: Zgłoś błąd + /// - nl: Een fout melden + /// - ru: Сообщить об ошибке + static let bugReport: String.LocalizationValue = "about_bug_report" + /// VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done. + /// + /// Key: `about_description` + /// Context: About screen: intro paragraph describing what VniDrop does. + /// + /// - en: VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done. + /// - fr: VniDrop transfère fichiers et dossiers directement d’un appareil à un autre sur le réseau, sans les téléverser vers un service d’hébergement. Aucun compte à créer, et aucune copie de votre transfert ne reste dans le cloud une fois terminé. + /// - es: VniDrop transfiere archivos y carpetas directamente de un dispositivo a otro a través de la red, sin subirlos a ningún servicio de alojamiento. No hay que crear ninguna cuenta y no queda ninguna copia de su transferencia en la nube una vez que termina. + /// - it: VniDrop trasferisce file e cartelle direttamente da un dispositivo a un altro sulla rete, senza caricarli su alcun servizio di hosting. Non c’è alcun account da creare e nessuna copia del suo trasferimento rimane nel cloud una volta terminato. + /// - de: VniDrop überträgt Dateien und Ordner direkt von einem Gerät auf ein anderes über das Netzwerk, ohne sie auf einen Hosting-Dienst hochzuladen. Es muss kein Konto erstellt werden, und nach Abschluss verbleibt keine Kopie Ihrer Übertragung in der Cloud. + /// - pt: O VniDrop transfere ficheiros e pastas diretamente de um dispositivo para outro através da rede, sem os carregar para qualquer serviço de alojamento. Não é necessário criar qualquer conta e não fica nenhuma cópia da sua transferência na nuvem depois de terminar. + /// - pl: VniDrop przesyła pliki i foldery bezpośrednio z jednego urządzenia na drugie przez sieć, bez przesyłania ich do jakiejkolwiek usługi hostingowej. Nie trzeba zakładać konta, a po zakończeniu żadna kopia transferu nie pozostaje w chmurze. + /// - nl: VniDrop verplaatst bestanden en mappen rechtstreeks van het ene apparaat naar het andere via het netwerk, zonder ze naar een hostingdienst te uploaden. U hoeft geen account aan te maken en er blijft geen kopie van uw overdracht in de cloud achter zodra u klaar bent. + /// - ru: VniDrop передаёт файлы и папки напрямую с одного устройства на другое по сети, не загружая их в какой-либо хостинг-сервис. Не нужно создавать учётную запись, и после завершения ни одна копия вашей передачи не остаётся в облаке. + static let description: String.LocalizationValue = "about_description" + /// A direct device-to-device transfer — your files go straight to the receiver. + /// + /// Key: `about_is_direct` + /// Context: About screen, 'What VniDrop is' list: point about direct device-to-device transfer. + /// + /// - en: A direct device-to-device transfer — your files go straight to the receiver. + /// - fr: Un transfert direct d’appareil à appareil — vos fichiers vont droit au destinataire. + /// - es: Una transferencia directa entre dispositivos: sus archivos van directamente al destinatario. + /// - it: Un trasferimento diretto da dispositivo a dispositivo: i suoi file vanno direttamente al destinatario. + /// - de: Eine direkte Gerät-zu-Gerät-Übertragung – Ihre Dateien gehen direkt an den Empfänger. + /// - pt: Uma transferência direta entre dispositivos — os seus ficheiros vão diretamente para o destinatário. + /// - pl: Bezpośredni transfer między urządzeniami — pliki trafiają prosto do odbiorcy. + /// - nl: Een directe overdracht van apparaat naar apparaat — uw bestanden gaan rechtstreeks naar de ontvanger. + /// - ru: Прямая передача с устройства на устройство — ваши файлы попадают напрямую к получателю. + static let isDirect: String.LocalizationValue = "about_is_direct" + /// Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash. + /// + /// Key: `about_is_encrypted` + /// Context: About screen, 'What VniDrop is' list: point about encrypted, verified connections. + /// + /// - en: Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash. + /// - fr: Les connexions sont authentifiées et chiffrées de bout en bout (via Iroh), et les fichiers entrants sont vérifiés par leur empreinte de contenu. + /// - es: Las conexiones están autenticadas y cifradas de extremo a extremo (mediante Iroh), y los archivos entrantes se verifican por su huella de contenido. + /// - it: Le connessioni sono autenticate e cifrate end-to-end (tramite Iroh) e i file in arrivo vengono verificati tramite l’impronta del loro contenuto. + /// - de: Verbindungen sind authentifiziert und Ende-zu-Ende-verschlüsselt (über Iroh), und eingehende Dateien werden anhand ihres Inhalts-Hashs überprüft. + /// - pt: As ligações são autenticadas e cifradas ponto a ponto (através do Iroh), e os ficheiros recebidos são verificados pela impressão digital do seu conteúdo. + /// - pl: Połączenia są uwierzytelniane i szyfrowane od końca do końca (przez Iroh), a przychodzące pliki są weryfikowane na podstawie skrótu ich zawartości. + /// - nl: Verbindingen zijn geverifieerd en end-to-end versleuteld (via Iroh), en binnenkomende bestanden worden gecontroleerd aan de hand van hun inhouds-hash. + /// - ru: Соединения аутентифицированы и зашифрованы сквозным шифрованием (через Iroh), а входящие файлы проверяются по хешу их содержимого. + static let isEncrypted: String.LocalizationValue = "about_is_encrypted" + /// You decide who receives: approve each request, or open a transfer to anyone holding the invitation. + /// + /// Key: `about_is_in_control` + /// Context: About screen, 'What VniDrop is' list: point about controlling who receives. + /// + /// - en: You decide who receives: approve each request, or open a transfer to anyone holding the invitation. + /// - fr: Vous décidez qui reçoit : approuvez chaque demande, ou ouvrez un transfert à toute personne disposant de l’invitation. + /// - es: Usted decide quién recibe: apruebe cada solicitud o abra una transferencia a cualquiera que tenga la invitación. + /// - it: Lei decide chi riceve: approvi ogni richiesta oppure apra un trasferimento a chiunque disponga dell’invito. + /// - de: Sie entscheiden, wer empfängt: Genehmigen Sie jede Anfrage oder öffnen Sie eine Übertragung für alle, die die Einladung besitzen. + /// - pt: É você quem decide quem recebe: aprove cada pedido ou abra uma transferência a qualquer pessoa que tenha o convite. + /// - pl: To Ty decydujesz, kto otrzymuje: zatwierdź każde żądanie lub udostępnij transfer każdemu, kto ma zaproszenie. + /// - nl: U bepaalt wie ontvangt: keur elk verzoek goed, of stel een overdracht open voor iedereen met de uitnodiging. + /// - ru: Вы решаете, кто получает: одобряйте каждый запрос или откройте передачу всем, у кого есть приглашение. + static let isInControl: String.LocalizationValue = "about_is_in_control" + /// Account-free — there’s nothing to sign up for. + /// + /// Key: `about_is_no_account` + /// Context: About screen, 'What VniDrop is' list: point about being account-free. + /// + /// - en: Account-free — there’s nothing to sign up for. + /// - fr: Sans compte — il n’y a rien à créer. + /// - es: Sin cuenta: no hay nada que registrar. + /// - it: Senza account: non c’è nulla da registrare. + /// - de: Ohne Konto – es gibt nichts zu registrieren. + /// - pt: Sem conta — não há nada para registar. + /// - pl: Bez konta — nie trzeba się rejestrować. + /// - nl: Zonder account — er is niets om aan te melden. + /// - ru: Без учётной записи — регистрироваться не нужно. + static let isNoAccount: String.LocalizationValue = "about_is_no_account" + /// Open source, released under the Apache 2.0 license. + /// + /// Key: `about_is_open` + /// Context: About screen, 'What VniDrop is' list: point about being open source. + /// + /// - en: Open source, released under the Apache 2.0 license. + /// - fr: Open source, publié sous licence Apache 2.0. + /// - es: Código abierto, publicado bajo la licencia Apache 2.0. + /// - it: Open source, rilasciato con licenza Apache 2.0. + /// - de: Open Source, veröffentlicht unter der Apache-2.0-Lizenz. + /// - pt: Código aberto, publicado sob a licença Apache 2.0. + /// - pl: Otwarte oprogramowanie, wydane na licencji Apache 2.0. + /// - nl: Opensource, uitgebracht onder de Apache 2.0-licentie. + /// - ru: Открытый исходный код, распространяется по лицензии Apache 2.0. + static let isOpen: String.LocalizationValue = "about_is_open" + /// What VniDrop is + /// + /// Key: `about_is_title` + /// Context: About screen: section heading for the 'What VniDrop is' list. + /// + /// - en: What VniDrop is + /// - fr: Ce qu’est VniDrop + /// - es: Qué es VniDrop + /// - it: Cos’è VniDrop + /// - de: Was VniDrop ist + /// - pt: O que o VniDrop é + /// - pl: Czym jest VniDrop + /// - nl: Wat VniDrop is + /// - ru: Что такое VniDrop + static let isTitle: String.LocalizationValue = "about_is_title" + /// Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer. + /// + /// Key: `about_isnt_cloud` + /// Context: About screen, 'What VniDrop isn't' list: point about not being cloud storage. + /// + /// - en: Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer. + /// - fr: Pas un stockage cloud — aucun serveur ne détient vos fichiers, et rien n’attend dans le cloud après un transfert. + /// - es: No es almacenamiento en la nube: ningún servidor guarda sus archivos y nada queda en la nube después de una transferencia. + /// - it: Non è un’archiviazione cloud: nessun server conserva i suoi file e nulla resta nel cloud dopo un trasferimento. + /// - de: Kein Cloud-Speicher – kein Server hält Ihre Dateien, und nach einer Übertragung wartet nichts in der Cloud. + /// - pt: Não é armazenamento na nuvem — nenhum servidor guarda os seus ficheiros e nada fica na nuvem após uma transferência. + /// - pl: To nie magazyn w chmurze — żaden serwer nie przechowuje Twoich plików i nic nie pozostaje w chmurze po transferze. + /// - nl: Geen cloudopslag — geen enkele server bewaart uw bestanden en er wacht niets in de cloud na een overdracht. + /// - ru: Это не облачное хранилище — ни один сервер не хранит ваши файлы, и после передачи в облаке ничего не остаётся. + static let isntCloud: String.LocalizationValue = "about_isnt_cloud" + /// Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby. + /// + /// Key: `about_isnt_public` + /// Context: About screen, 'What VniDrop isn't' list: point about invitations not being public broadcasts. + /// + /// - en: Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby. + /// - fr: Pas une diffusion publique — une invitation est un lien d’accès privé, pas une annonce à tout le voisinage. + /// - es: No es difusión pública: una invitación es un enlace de acceso privado, no un anuncio para todos los que estén cerca. + /// - it: Non è una diffusione pubblica: un invito è un link di accesso privato, non un annuncio a tutti nelle vicinanze. + /// - de: Keine öffentliche Übertragung – eine Einladung ist ein privater Zugangslink, keine Ankündigung an alle in der Nähe. + /// - pt: Não é difusão pública — um convite é uma ligação de acesso privado, não um anúncio a toda a gente por perto. + /// - pl: To nie publiczne rozgłaszanie — zaproszenie to prywatny link dostępu, a nie ogłoszenie dla wszystkich w pobliżu. + /// - nl: Geen openbare uitzending — een uitnodiging is een privétoegangslink, geen aankondiging aan iedereen in de buurt. + /// - ru: Это не публичная рассылка — приглашение является частной ссылкой доступа, а не объявлением для всех поблизости. + static let isntPublic: String.LocalizationValue = "about_isnt_public" + /// Not a sync or backup service. + /// + /// Key: `about_isnt_sync` + /// Context: About screen, 'What VniDrop isn't' list: point about not being sync/backup. + /// + /// - en: Not a sync or backup service. + /// - fr: Pas un service de synchronisation ou de sauvegarde. + /// - es: No es un servicio de sincronización ni de copia de seguridad. + /// - it: Non è un servizio di sincronizzazione o di backup. + /// - de: Kein Synchronisierungs- oder Backup-Dienst. + /// - pt: Não é um serviço de sincronização ou de cópia de segurança. + /// - pl: To nie usługa synchronizacji ani kopii zapasowej. + /// - nl: Geen synchronisatie- of back-updienst. + /// - ru: Это не служба синхронизации или резервного копирования. + static let isntSync: String.LocalizationValue = "about_isnt_sync" + /// What VniDrop isn’t + /// + /// Key: `about_isnt_title` + /// Context: About screen: section heading for the 'What VniDrop isn't' list. + /// + /// - en: What VniDrop isn’t + /// - fr: Ce que VniDrop n’est pas + /// - es: Qué no es VniDrop + /// - it: Cosa non è VniDrop + /// - de: Was VniDrop nicht ist + /// - pt: O que o VniDrop não é + /// - pl: Czym VniDrop nie jest + /// - nl: Wat VniDrop niet is + /// - ru: Чем VniDrop не является + static let isntTitle: String.LocalizationValue = "about_isnt_title" + /// License + /// + /// Key: `about_license_label` + /// Context: About screen: label for the license row. + /// + /// - en: License + /// - fr: Licence + /// - es: Licencia + /// - it: Licenza + /// - de: Lizenz + /// - pt: Licença + /// - pl: Licencja + /// - nl: Licentie + /// - ru: Лицензия + static let licenseLabel: String.LocalizationValue = "about_license_label" + /// Privacy policy + /// + /// Key: `about_privacy` + /// Context: About screen: link to the privacy policy. + /// + /// - en: Privacy policy + /// - fr: Politique de confidentialité + /// - es: Política de privacidad + /// - it: Informativa sulla privacy + /// - de: Datenschutzrichtlinie + /// - pt: Política de privacidade + /// - pl: Polityka prywatności + /// - nl: Privacybeleid + /// - ru: Политика конфиденциальности + static let privacy: String.LocalizationValue = "about_privacy" + /// Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.” + /// + /// Key: `about_privacy_capability` + /// Context: About screen, Privacy & security list: point that invitations are capabilities to share carefully. + /// + /// - en: Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.” + /// - fr: Les invitations sont des clés d’accès. Traitez un QR code, un tag NFC ou un fichier .vnd comme un lien d’accès privé et ne le partagez qu’avec les personnes visées — en particulier avec « Toute personne disposant de ce transfert ». + /// - es: Las invitaciones son claves de acceso. Trate un código QR, una etiqueta NFC o un archivo .vnd como un enlace de acceso privado y compártalo solo con las personas previstas, especialmente con «Cualquiera que tenga esta transferencia». + /// - it: Gli inviti sono chiavi di accesso. Tratti un codice QR, un tag NFC o un file .vnd come un link di accesso privato e lo condivida solo con le persone previste, soprattutto con «Chiunque abbia questo trasferimento». + /// - de: Einladungen sind Zugangsschlüssel. Behandeln Sie einen QR-Code, ein NFC-Tag oder eine .vnd-Datei wie einen privaten Zugangslink und teilen Sie ihn nur mit den vorgesehenen Personen – insbesondere bei „Jeder mit dieser Übertragung“. + /// - pt: Os convites são chaves de acesso. Trate um código QR, uma etiqueta NFC ou um ficheiro .vnd como uma ligação de acesso privado e partilhe-o apenas com as pessoas pretendidas — sobretudo com «Qualquer pessoa com esta transferência». + /// - pl: Zaproszenia są kluczami dostępu. Traktuj kod QR, tag NFC lub plik .vnd jak prywatny link dostępu i udostępniaj go tylko zamierzonym osobom — zwłaszcza przy opcji „Każdy, kto ma ten transfer”. + /// - nl: Uitnodigingen zijn toegangssleutels. Behandel een QR-code, NFC-tag of .vnd-bestand als een privétoegangslink en deel deze alleen met de bedoelde personen — vooral bij ‘Iedereen met deze overdracht’. + /// - ru: Приглашения — это ключи доступа. Относитесь к QR-коду, NFC-метке или файлу .vnd как к частной ссылке доступа и делитесь ими только с теми, для кого они предназначены, особенно при варианте «Любой, у кого есть эта передача». + static let privacyCapability: String.LocalizationValue = "about_privacy_capability" + /// Deny by default — VniDrop serves only the content of an active share and rejects unknown requests. + /// + /// Key: `about_privacy_deny` + /// Context: About screen, Privacy & security list: point about denying unknown requests by default. + /// + /// - en: Deny by default — VniDrop serves only the content of an active share and rejects unknown requests. + /// - fr: Refus par défaut — VniDrop ne sert que le contenu d’un partage actif et rejette les demandes inconnues. + /// - es: Denegación por defecto: VniDrop solo sirve el contenido de un recurso compartido activo y rechaza las solicitudes desconocidas. + /// - it: Rifiuto predefinito: VniDrop serve solo il contenuto di una condivisione attiva e rifiuta le richieste sconosciute. + /// - de: Standardmäßig ablehnen – VniDrop stellt nur den Inhalt einer aktiven Freigabe bereit und weist unbekannte Anfragen ab. + /// - pt: Recusa por predefinição — o VniDrop apenas fornece o conteúdo de uma partilha ativa e rejeita pedidos desconhecidos. + /// - pl: Domyślnie odmowa — VniDrop udostępnia tylko zawartość aktywnego udostępnienia i odrzuca nieznane żądania. + /// - nl: Standaard weigeren — VniDrop levert alleen de inhoud van een actieve deling en wijst onbekende verzoeken af. + /// - ru: Отказ по умолчанию — VniDrop предоставляет только содержимое активной передачи и отклоняет неизвестные запросы. + static let privacyDeny: String.LocalizationValue = "about_privacy_deny" + /// Received files are saved on your device and never silently overwrite an existing file. + /// + /// Key: `about_privacy_local` + /// Context: About screen, Privacy & security list: point about received files staying local and never overwriting. + /// + /// - en: Received files are saved on your device and never silently overwrite an existing file. + /// - fr: Les fichiers reçus sont enregistrés sur votre appareil et n’écrasent jamais un fichier existant. + /// - es: Los archivos recibidos se guardan en su dispositivo y nunca sobrescriben un archivo existente. + /// - it: I file ricevuti vengono salvati sul suo dispositivo e non sovrascrivono mai un file esistente. + /// - de: Empfangene Dateien werden auf Ihrem Gerät gespeichert und überschreiben niemals eine vorhandene Datei. + /// - pt: Os ficheiros recebidos são guardados no seu dispositivo e nunca substituem um ficheiro existente. + /// - pl: Odebrane pliki są zapisywane na Twoim urządzeniu i nigdy nie nadpisują istniejącego pliku. + /// - nl: Ontvangen bestanden worden op uw apparaat bewaard en overschrijven nooit een bestaand bestand. + /// - ru: Полученные файлы сохраняются на вашем устройстве и никогда не перезаписывают существующий файл. + static let privacyLocal: String.LocalizationValue = "about_privacy_local" + /// Privacy policy + /// + /// Key: `about_privacy_policy_label` + /// Context: About screen: label for the privacy policy row. + /// + /// - en: Privacy policy + /// - fr: Politique de confidentialité + /// - es: Política de privacidad + /// - it: Informativa sulla privacy + /// - de: Datenschutzrichtlinie + /// - pt: Política de privacidade + /// - pl: Polityka prywatności + /// - nl: Privacybeleid + /// - ru: Политика конфиденциальности + static let privacyPolicyLabel: String.LocalizationValue = "about_privacy_policy_label" + /// If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files. + /// + /// Key: `about_privacy_relay` + /// Context: About screen, Privacy & security list: point explaining encrypted relays. + /// + /// - en: If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files. + /// - fr: Si deux appareils ne peuvent pas se connecter directement, la connexion chiffrée peut être relayée. Les relais ne transmettent que des paquets chiffrés ; ils ne stockent jamais vos fichiers. + /// - es: Si dos dispositivos no pueden conectarse directamente, la conexión cifrada puede retransmitirse. Los relés solo reenvían paquetes cifrados; nunca almacenan sus archivos. + /// - it: Se due dispositivi non riescono a connettersi direttamente, la connessione cifrata può essere instradata tramite relay. I relay inoltrano solo pacchetti cifrati; non memorizzano mai i suoi file. + /// - de: Wenn zwei Geräte sich nicht direkt verbinden können, kann die verschlüsselte Verbindung weitergeleitet werden. Relays leiten nur verschlüsselte Pakete weiter; sie speichern niemals Ihre Dateien. + /// - pt: Se dois dispositivos não conseguirem ligar-se diretamente, a ligação cifrada pode ser retransmitida. Os retransmissores apenas encaminham pacotes cifrados; nunca guardam os seus ficheiros. + /// - pl: Jeśli dwa urządzenia nie mogą połączyć się bezpośrednio, szyfrowane połączenie może być przekazywane przez przekaźnik. Przekaźniki przekazują tylko zaszyfrowane pakiety; nigdy nie przechowują Twoich plików. + /// - nl: Als twee apparaten niet rechtstreeks verbinding kunnen maken, kan de versleutelde verbinding via een relay worden doorgestuurd. Relays sturen alleen versleutelde pakketten door; ze slaan uw bestanden nooit op. + /// - ru: Если два устройства не могут соединиться напрямую, зашифрованное соединение может передаваться через ретранслятор. Ретрансляторы пересылают только зашифрованные пакеты; они никогда не хранят ваши файлы. + static let privacyRelay: String.LocalizationValue = "about_privacy_relay" + /// Privacy & security + /// + /// Key: `about_privacy_title` + /// Context: About screen: section heading for the Privacy & security list. + /// + /// - en: Privacy & security + /// - fr: Confidentialité et sécurité + /// - es: Privacidad y seguridad + /// - it: Privacy e sicurezza + /// - de: Datenschutz & Sicherheit + /// - pt: Privacidade e segurança + /// - pl: Prywatność i bezpieczeństwo + /// - nl: Privacy en beveiliging + /// - ru: Конфиденциальность и безопасность + static let privacyTitle: String.LocalizationValue = "about_privacy_title" + /// Send files directly. Stay in control of who receives them. + /// + /// Key: `about_tagline` + /// Context: About screen: short tagline under the app name. + /// + /// - en: Send files directly. Stay in control of who receives them. + /// - fr: Envoyez des fichiers directement. Gardez le contrôle de qui les reçoit. + /// - es: Envíe archivos directamente. Mantenga el control de quién los recibe. + /// - it: Invii file direttamente. Mantenga il controllo su chi li riceve. + /// - de: Senden Sie Dateien direkt. Behalten Sie die Kontrolle darüber, wer sie empfängt. + /// - pt: Envie ficheiros diretamente. Mantenha o controlo sobre quem os recebe. + /// - pl: Wysyłaj pliki bezpośrednio. Zachowaj kontrolę nad tym, kto je otrzymuje. + /// - nl: Verstuur bestanden rechtstreeks. Houd controle over wie ze ontvangt. + /// - ru: Отправляйте файлы напрямую. Сохраняйте контроль над тем, кто их получает. + static let tagline: String.LocalizationValue = "about_tagline" + /// About + /// + /// Key: `about_title` + /// Context: About screen: navigation/screen title. + /// + /// - en: About + /// - fr: À propos + /// - es: Acerca de + /// - it: Informazioni + /// - de: Über + /// - pt: Acerca de + /// - pl: Informacje + /// - nl: Over + /// - ru: О приложении + static let title: String.LocalizationValue = "about_title" + } + enum Appearance { + /// Match this device’s light or dark appearance. + /// + /// Key: `appearance_auto_description` + /// Context: Settings > Appearance: description for the System/auto option. + /// + /// - en: Match this device’s light or dark appearance. + /// - fr: Suivre l’apparence claire ou sombre de cet appareil. + /// - es: Seguir la apariencia clara u oscura de este dispositivo. + /// - it: Segue l’aspetto chiaro o scuro di questo dispositivo. + /// - de: Der hellen oder dunklen Darstellung dieses Geräts folgen. + /// - pt: Acompanhar o aspeto claro ou escuro deste dispositivo. + /// - pl: Dopasuj do jasnego lub ciemnego wyglądu tego urządzenia. + /// - nl: De lichte of donkere weergave van dit apparaat volgen. + /// - ru: Следовать светлому или тёмному оформлению этого устройства. + static let autoDescription: String.LocalizationValue = "appearance_auto_description" + /// Dark mode + /// + /// Key: `appearance_dark_mode` + /// Context: Settings > Appearance: label for the dark theme option. + /// + /// - en: Dark mode + /// - fr: Mode sombre + /// - es: Modo oscuro + /// - it: Modalità scura + /// - de: Dunkelmodus + /// - pt: Modo escuro + /// - pl: Tryb ciemny + /// - nl: Donkere modus + /// - ru: Тёмный режим + static let darkMode: String.LocalizationValue = "appearance_dark_mode" + /// Light mode + /// + /// Key: `appearance_light_mode` + /// Context: Settings > Appearance: label for the light theme option. + /// + /// - en: Light mode + /// - fr: Mode clair + /// - es: Modo claro + /// - it: Modalità chiara + /// - de: Hellmodus + /// - pt: Modo claro + /// - pl: Tryb jasny + /// - nl: Lichte modus + /// - ru: Светлый режим + static let lightMode: String.LocalizationValue = "appearance_light_mode" + /// System + /// + /// Key: `appearance_system_mode` + /// Context: Settings > Appearance: label for the follow-system option. + /// + /// - en: System + /// - fr: Système + /// - es: Sistema + /// - it: Sistema + /// - de: System + /// - pt: Sistema + /// - pl: System + /// - nl: Systeem + /// - ru: Системный + static let systemMode: String.LocalizationValue = "appearance_system_mode" + /// Appearance + /// + /// Key: `appearance_title` + /// Context: Settings > Appearance: section title. + /// + /// - en: Appearance + /// - fr: Apparence + /// - es: Apariencia + /// - it: Aspetto + /// - de: Darstellung + /// - pt: Aspeto + /// - pl: Wygląd + /// - nl: Weergave + /// - ru: Оформление + static let title: String.LocalizationValue = "appearance_title" + } + enum Approval { + /// Receive request + /// + /// Key: `approval_connection_request` + /// Context: Approval prompt: title when a receiver requests to download a transfer. + /// + /// - en: Receive request + /// - fr: Demande de réception + /// - es: Solicitud de recepción + /// - it: Richiesta di ricezione + /// - de: Empfangsanfrage + /// - pt: Pedido de receção + /// - pl: Prośba o odbiór + /// - nl: Ontvangstverzoek + /// - ru: Запрос на получение + static let connectionRequest: String.LocalizationValue = "approval_connection_request" + /// Device ID: {deviceId} + /// + /// Key: `approval_endpoint_id` + /// Context: Approval prompt: shows the requesting device's ID. {deviceId} = device/endpoint identifier. + /// + /// - en: Device ID: {deviceId} + /// - fr: Identifiant de l’appareil : {deviceId} + /// - es: ID del dispositivo: {deviceId} + /// - it: ID dispositivo: {deviceId} + /// - de: Geräte-ID: {deviceId} + /// - pt: ID do dispositivo: {deviceId} + /// - pl: Identyfikator urządzenia: {deviceId} + /// - nl: Apparaat-ID: {deviceId} + /// - ru: Идентификатор устройства: {deviceId} + static func endpointId(deviceId: String) -> String { + String(format: String(localized: "approval_endpoint_id"), deviceId) + } + /// A nearby device + /// + /// Key: `approval_nearby_device` + /// Context: Approval prompt: fallback label for a requester with no display name. + /// + /// - en: A nearby device + /// - fr: Un appareil à proximité + /// - es: Un dispositivo cercano + /// - it: Un dispositivo nelle vicinanze + /// - de: Ein Gerät in der Nähe + /// - pt: Um dispositivo próximo + /// - pl: Urządzenie w pobliżu + /// - nl: Een apparaat in de buurt + /// - ru: Устройство поблизости + static let nearbyDevice: String.LocalizationValue = "approval_nearby_device" + /// {count} requests waiting + /// + /// Key: `approval_pending_count` + /// Context: Send/transfer list: badge showing how many receive requests are awaiting approval. {count} = pending requests. + /// + /// - en: {count} requests waiting + /// - fr: {count} demandes en attente + /// - es: {count} solicitudes en espera + /// - it: {count} richieste in attesa + /// - de: {count} Anfragen warten + /// - pt: {count} pedidos em espera + /// - pl: Oczekujące prośby: {count} + /// - nl: {count} verzoeken in behandeling + /// - ru: Ожидающих запросов: {count} + static func pendingCount(count: Int) -> String { + String(format: String(localized: "approval_pending_count"), count) + } + /// {receiver} wants to receive “{transferName}”. + /// + /// Key: `approval_request_body` + /// Context: Approval prompt body: '{receiver} wants to receive "{transferName}".' {receiver} = requester name, {transferName} = transfer name. + /// + /// - en: {receiver} wants to receive “{transferName}”. + /// - fr: {receiver} souhaite recevoir « {transferName} ». + /// - es: {receiver} quiere recibir «{transferName}». + /// - it: {receiver} vuole ricevere «{transferName}». + /// - de: {receiver} möchte „{transferName}“ empfangen. + /// - pt: {receiver} quer receber «{transferName}». + /// - pl: {receiver} chce odebrać „{transferName}”. + /// - nl: {receiver} wil ‘{transferName}’ ontvangen. + /// - ru: {receiver} хочет получить «{transferName}». + static func requestBody(receiver: String, transferName: String) -> String { + String(format: String(localized: "approval_request_body"), receiver, transferName) + } + } + enum Battery { + /// Battery level + /// + /// Key: `battery_level_title` + /// Context: Device information row: battery level label. + /// + /// - en: Battery level + /// - fr: Niveau de batterie + /// - es: Nivel de batería + /// - it: Livello batteria + /// - de: Batteriestand + /// - pt: Nível da bateria + /// - pl: Poziom baterii + /// - nl: Batterijniveau + /// - ru: Уровень заряда + static let levelTitle: String.LocalizationValue = "battery_level_title" + } + enum Bug { + /// name@example.com + /// + /// Key: `bug_report_contact_hint` + /// Context: Bug report form: placeholder text in the contact email field. + /// + /// - en: name@example.com + /// - fr: nom@exemple.com + /// - es: nombre@ejemplo.com + /// - it: nome@esempio.com + /// - de: name@beispiel.com + /// - pt: nome@exemplo.com + /// - pl: nazwa@przyklad.com + /// - nl: naam@voorbeeld.com + /// - ru: имя@пример.com + static let reportContactHint: String.LocalizationValue = "bug_report_contact_hint" + /// Contact email (optional) + /// + /// Key: `bug_report_contact_label` + /// Context: Bug report form: label for the optional contact email field. + /// + /// - en: Contact email (optional) + /// - fr: E-mail de contact (facultatif) + /// - es: Correo de contacto (opcional) + /// - it: Email di contatto (facoltativa) + /// - de: Kontakt-E-Mail (optional) + /// - pt: E-mail de contacto (opcional) + /// - pl: E-mail kontaktowy (opcjonalnie) + /// - nl: Contact-e-mail (optioneel) + /// - ru: Контактный e-mail (необязательно) + static let reportContactLabel: String.LocalizationValue = "bug_report_contact_label" + /// Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted). + /// + /// Key: `bug_report_description` + /// Context: Bug report form: intro text explaining what gets attached. + /// + /// - en: Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted). + /// - fr: Dites-nous ce qui s’est passé. Nous joignons les infos de l’appareil et, en option, les journaux récents (valeurs sensibles masquées). + /// - es: Cuéntenos qué salió mal. Adjuntamos información del dispositivo y, opcionalmente, registros recientes (con los valores sensibles ocultos). + /// - it: Ci dica cosa è andato storto. Alleghiamo le informazioni sul dispositivo e, facoltativamente, i log recenti (con i valori sensibili oscurati). + /// - de: Sagen Sie uns, was schiefgelaufen ist. Wir hängen Geräteinformationen und optional aktuelle Protokolle an (sensible Werte werden geschwärzt). + /// - pt: Diga-nos o que correu mal. Anexamos informações do dispositivo e, opcionalmente, registos recentes (com os valores sensíveis ocultados). + /// - pl: Napisz, co poszło nie tak. Dołączamy informacje o urządzeniu i opcjonalnie ostatnie dzienniki (z ukrytymi wrażliwymi wartościami). + /// - nl: Vertel ons wat er misging. We voegen apparaatgegevens toe en optioneel recente logbestanden (met gevoelige waarden onleesbaar gemaakt). + /// - ru: Расскажите, что пошло не так. Мы прикладываем сведения об устройстве и, при желании, недавние журналы (конфиденциальные значения скрыты). + static let reportDescription: String.LocalizationValue = "bug_report_description" + /// Device information + /// + /// Key: `bug_report_device_section` + /// Context: Bug report form: heading for the attached device information section. + /// + /// - en: Device information + /// - fr: Informations sur l’appareil + /// - es: Información del dispositivo + /// - it: Informazioni sul dispositivo + /// - de: Geräteinformationen + /// - pt: Informações do dispositivo + /// - pl: Informacje o urządzeniu + /// - nl: Apparaatgegevens + /// - ru: Сведения об устройстве + static let reportDeviceSection: String.LocalizationValue = "bug_report_device_section" + /// Describe what you expected to happen + /// + /// Key: `bug_report_expected_hint` + /// Context: Bug report form: placeholder in the 'what did you expect' field. + /// + /// - en: Describe what you expected to happen + /// - fr: Décrivez ce à quoi vous vous attendiez + /// - es: Describa lo que esperaba que ocurriera + /// - it: Descriva cosa si aspettava che accadesse + /// - de: Beschreiben Sie, was Sie erwartet haben + /// - pt: Descreva o que esperava que acontecesse + /// - pl: Opisz oczekiwane zachowanie + /// - nl: Beschrijf wat u verwachtte + /// - ru: Опишите, что вы ожидали + static let reportExpectedHint: String.LocalizationValue = "bug_report_expected_hint" + /// What did you expect? + /// + /// Key: `bug_report_expected_label` + /// Context: Bug report form: label for the expected-behavior field. + /// + /// - en: What did you expect? + /// - fr: À quoi vous attendiez-vous ? + /// - es: ¿Qué esperaba? + /// - it: Cosa si aspettava? + /// - de: Was haben Sie erwartet? + /// - pt: O que esperava? + /// - pl: Czego oczekiwano? + /// - nl: Wat verwachtte u? + /// - ru: Что вы ожидали? + static let reportExpectedLabel: String.LocalizationValue = "bug_report_expected_label" + /// Include recent logs + /// + /// Key: `bug_report_include_logs` + /// Context: Bug report form: toggle label to attach recent logs. + /// + /// - en: Include recent logs + /// - fr: Inclure les journaux récents + /// - es: Incluir registros recientes + /// - it: Includi i log recenti + /// - de: Aktuelle Protokolle einschließen + /// - pt: Incluir registos recentes + /// - pl: Dołącz ostatnie dzienniki + /// - nl: Recente logbestanden meesturen + /// - ru: Включить недавние журналы + static let reportIncludeLogs: String.LocalizationValue = "bug_report_include_logs" + /// Helps us diagnose the issue. Sensitive values are redacted before sending. + /// + /// Key: `bug_report_include_logs_description` + /// Context: Bug report form: explanation under the include-logs toggle. + /// + /// - en: Helps us diagnose the issue. Sensitive values are redacted before sending. + /// - fr: Nous aide à diagnostiquer le problème. Les valeurs sensibles sont masquées avant l’envoi. + /// - es: Nos ayuda a diagnosticar el problema. Los valores sensibles se ocultan antes de enviarlos. + /// - it: Ci aiuta a diagnosticare il problema. I valori sensibili vengono oscurati prima dell’invio. + /// - de: Hilft uns, das Problem zu diagnostizieren. Sensible Werte werden vor dem Senden geschwärzt. + /// - pt: Ajuda-nos a diagnosticar o problema. Os valores sensíveis são ocultados antes do envio. + /// - pl: Pomaga nam zdiagnozować problem. Wrażliwe wartości są ukrywane przed wysłaniem. + /// - nl: Helpt ons het probleem te diagnosticeren. Gevoelige waarden worden vóór verzending onleesbaar gemaakt. + /// - ru: Помогает нам диагностировать проблему. Конфиденциальные значения скрываются перед отправкой. + static let reportIncludeLogsDescription: String.LocalizationValue = "bug_report_include_logs_description" + /// Log attachment size + /// + /// Key: `bug_report_logs_size` + /// Context: Bug report form: label showing the size of the log attachment. + /// + /// - en: Log attachment size + /// - fr: Taille de la pièce jointe des journaux + /// - es: Tamaño del archivo de registros + /// - it: Dimensione dell’allegato dei log + /// - de: Größe des Protokollanhangs + /// - pt: Tamanho do anexo de registos + /// - pl: Rozmiar załącznika z dziennikami + /// - nl: Grootte van logbijlage + /// - ru: Размер вложения с журналами + static let reportLogsSize: String.LocalizationValue = "bug_report_logs_size" + /// Please describe what you expected. + /// + /// Key: `bug_report_missing_expected` + /// Context: Bug report form: validation error when the expected-behavior field is empty. + /// + /// - en: Please describe what you expected. + /// - fr: Veuillez décrire ce à quoi vous vous attendiez. + /// - es: Describa lo que esperaba. + /// - it: Descriva cosa si aspettava. + /// - de: Bitte beschreiben Sie, was Sie erwartet haben. + /// - pt: Descreva o que esperava. + /// - pl: Opisz oczekiwane zachowanie. + /// - nl: Beschrijf wat u verwachtte. + /// - ru: Опишите, что вы ожидали. + static let reportMissingExpected: String.LocalizationValue = "bug_report_missing_expected" + /// Please describe what happened. + /// + /// Key: `bug_report_missing_what` + /// Context: Bug report form: validation error when the what-happened field is empty. + /// + /// - en: Please describe what happened. + /// - fr: Veuillez décrire ce qui s’est passé. + /// - es: Describa lo que ocurrió. + /// - it: Descriva cosa è accaduto. + /// - de: Bitte beschreiben Sie, was passiert ist. + /// - pt: Descreva o que aconteceu. + /// - pl: Opisz, co się stało. + /// - nl: Beschrijf wat er gebeurde. + /// - ru: Опишите, что произошло. + static let reportMissingWhat: String.LocalizationValue = "bug_report_missing_what" + /// List the steps to reproduce the issue + /// + /// Key: `bug_report_steps_hint` + /// Context: Bug report form: placeholder in the steps-to-reproduce field. + /// + /// - en: List the steps to reproduce the issue + /// - fr: Listez les étapes pour reproduire le problème + /// - es: Enumere los pasos para reproducir el problema + /// - it: Elenchi i passaggi per riprodurre il problema + /// - de: Listen Sie die Schritte zum Reproduzieren des Problems auf + /// - pt: Enumere os passos para reproduzir o problema + /// - pl: Wymień kroki umożliwiające odtworzenie problemu + /// - nl: Noem de stappen om het probleem te reproduceren + /// - ru: Перечислите шаги для воспроизведения проблемы + static let reportStepsHint: String.LocalizationValue = "bug_report_steps_hint" + /// Steps to reproduce (optional) + /// + /// Key: `bug_report_steps_label` + /// Context: Bug report form: label for the optional steps-to-reproduce field. + /// + /// - en: Steps to reproduce (optional) + /// - fr: Étapes pour reproduire (facultatif) + /// - es: Pasos para reproducir (opcional) + /// - it: Passaggi per riprodurre (facoltativi) + /// - de: Schritte zum Reproduzieren (optional) + /// - pt: Passos para reproduzir (opcional) + /// - pl: Kroki do odtworzenia (opcjonalnie) + /// - nl: Stappen om te reproduceren (optioneel) + /// - ru: Шаги для воспроизведения (необязательно) + static let reportStepsLabel: String.LocalizationValue = "bug_report_steps_label" + /// Submit report + /// + /// Key: `bug_report_submit` + /// Context: Bug report form: submit button. + /// + /// - en: Submit report + /// - fr: Envoyer le rapport + /// - es: Enviar informe + /// - it: Invia segnalazione + /// - de: Bericht senden + /// - pt: Enviar relatório + /// - pl: Wyślij zgłoszenie + /// - nl: Rapport versturen + /// - ru: Отправить отчёт + static let reportSubmit: String.LocalizationValue = "bug_report_submit" + /// Could not submit the bug report. Try again later. + /// + /// Key: `bug_report_submit_failed` + /// Context: Bug report form: error message when submission fails. + /// + /// - en: Could not submit the bug report. Try again later. + /// - fr: Impossible d’envoyer le rapport de bug. Réessayez plus tard. + /// - es: No se pudo enviar el informe de error. Inténtelo de nuevo más tarde. + /// - it: Impossibile inviare la segnalazione. Riprovi più tardi. + /// - de: Der Fehlerbericht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut. + /// - pt: Não foi possível enviar o relatório de erro. Tente novamente mais tarde. + /// - pl: Nie udało się wysłać zgłoszenia błędu. Spróbuj ponownie później. + /// - nl: Het foutrapport kon niet worden verstuurd. Probeer het later opnieuw. + /// - ru: Не удалось отправить отчёт об ошибке. Повторите попытку позже. + static let reportSubmitFailed: String.LocalizationValue = "bug_report_submit_failed" + /// Thanks — your bug report was recorded. + /// + /// Key: `bug_report_submitted` + /// Context: Bug report form: confirmation message after a successful submission. + /// + /// - en: Thanks — your bug report was recorded. + /// - fr: Merci — votre rapport de bug a été enregistré. + /// - es: Gracias: su informe de error se ha registrado. + /// - it: Grazie: la sua segnalazione è stata registrata. + /// - de: Danke – Ihr Fehlerbericht wurde erfasst. + /// - pt: Obrigado — o seu relatório de erro foi registado. + /// - pl: Dziękujemy — Twoje zgłoszenie błędu zostało zapisane. + /// - nl: Bedankt — uw foutrapport is vastgelegd. + /// - ru: Спасибо — ваш отчёт об ошибке записан. + static let reportSubmitted: String.LocalizationValue = "bug_report_submitted" + /// Submitting… + /// + /// Key: `bug_report_submitting` + /// Context: Bug report form: progress label while the report is being sent. + /// + /// - en: Submitting… + /// - fr: Envoi… + /// - es: Enviando… + /// - it: Invio in corso… + /// - de: Wird gesendet… + /// - pt: A enviar… + /// - pl: Wysyłanie… + /// - nl: Bezig met versturen… + /// - ru: Отправка… + static let reportSubmitting: String.LocalizationValue = "bug_report_submitting" + /// Describe what happened + /// + /// Key: `bug_report_what_hint` + /// Context: Bug report form: placeholder in the what-happened field. + /// + /// - en: Describe what happened + /// - fr: Décrivez ce qui s’est passé + /// - es: Describa lo que ocurrió + /// - it: Descriva cosa è accaduto + /// - de: Beschreiben Sie, was passiert ist + /// - pt: Descreva o que aconteceu + /// - pl: Opisz, co się stało + /// - nl: Beschrijf wat er gebeurde + /// - ru: Опишите, что произошло + static let reportWhatHint: String.LocalizationValue = "bug_report_what_hint" + /// What happened? + /// + /// Key: `bug_report_what_label` + /// Context: Bug report form: label for the what-happened field. + /// + /// - en: What happened? + /// - fr: Que s’est-il passé ? + /// - es: ¿Qué ocurrió? + /// - it: Cosa è accaduto? + /// - de: Was ist passiert? + /// - pt: O que aconteceu? + /// - pl: Co się stało? + /// - nl: Wat is er gebeurd? + /// - ru: Что произошло? + static let reportWhatLabel: String.LocalizationValue = "bug_report_what_label" + } + enum Button { + /// Approve + /// + /// Key: `button_approve` + /// Context: Button: approve a receiver's request. + /// + /// - en: Approve + /// - fr: Approuver + /// - es: Aprobar + /// - it: Approva + /// - de: Genehmigen + /// - pt: Aprovar + /// - pl: Zatwierdź + /// - nl: Goedkeuren + /// - ru: Одобрить + static let approve: String.LocalizationValue = "button_approve" + /// Back + /// + /// Key: `button_back` + /// Context: Button: go back to the previous step/screen. + /// + /// - en: Back + /// - fr: Retour + /// - es: Atrás + /// - it: Indietro + /// - de: Zurück + /// - pt: Voltar + /// - pl: Wstecz + /// - nl: Terug + /// - ru: Назад + static let back: String.LocalizationValue = "button_back" + /// Cancel + /// + /// Key: `button_cancel` + /// Context: Button: cancel the current action or dialog. + /// + /// - en: Cancel + /// - fr: Annuler + /// - es: Cancelar + /// - it: Annulla + /// - de: Abbrechen + /// - pt: Cancelar + /// - pl: Anuluj + /// - nl: Annuleren + /// - ru: Отмена + static let cancel: String.LocalizationValue = "button_cancel" + /// Cancel + /// + /// Key: `button_cancel_receive` + /// Context: Button: cancel an in-progress receive. + /// + /// - en: Cancel + /// - fr: Annuler + /// - es: Cancelar + /// - it: Annulla + /// - de: Abbrechen + /// - pt: Cancelar + /// - pl: Anuluj + /// - nl: Annuleren + /// - ru: Отмена + static let cancelReceive: String.LocalizationValue = "button_cancel_receive" + /// Change files + /// + /// Key: `button_change_files` + /// Context: Button: change the selected files in the send flow. + /// + /// - en: Change files + /// - fr: Modifier les fichiers + /// - es: Cambiar archivos + /// - it: Cambia file + /// - de: Dateien ändern + /// - pt: Alterar ficheiros + /// - pl: Zmień pliki + /// - nl: Bestanden wijzigen + /// - ru: Изменить файлы + static let changeFiles: String.LocalizationValue = "button_change_files" + /// Choose files + /// + /// Key: `button_choose_files` + /// Context: Button: open the file picker to choose files to send. + /// + /// - en: Choose files + /// - fr: Choisir des fichiers + /// - es: Elegir archivos + /// - it: Scegli file + /// - de: Dateien auswählen + /// - pt: Escolher ficheiros + /// - pl: Wybierz pliki + /// - nl: Bestanden kiezen + /// - ru: Выбрать файлы + static let chooseFiles: String.LocalizationValue = "button_choose_files" + /// Choose folder + /// + /// Key: `button_choose_folder` + /// Context: Button: open the folder picker (send selection or receive folder). + /// + /// - en: Choose folder + /// - fr: Choisir un dossier + /// - es: Elegir carpeta + /// - it: Scegli cartella + /// - de: Ordner auswählen + /// - pt: Escolher pasta + /// - pl: Wybierz folder + /// - nl: Map kiezen + /// - ru: Выбрать папку + static let chooseFolder: String.LocalizationValue = "button_choose_folder" + /// Clear + /// + /// Key: `button_clear` + /// Context: Button: clear the current input or selection. + /// + /// - en: Clear + /// - fr: Effacer + /// - es: Borrar + /// - it: Cancella + /// - de: Löschen + /// - pt: Limpar + /// - pl: Wyczyść + /// - nl: Wissen + /// - ru: Очистить + static let clear: String.LocalizationValue = "button_clear" + /// Close + /// + /// Key: `button_close` + /// Context: Button: close the current sheet/dialog. + /// + /// - en: Close + /// - fr: Fermer + /// - es: Cerrar + /// - it: Chiudi + /// - de: Schließen + /// - pt: Fechar + /// - pl: Zamknij + /// - nl: Sluiten + /// - ru: Закрыть + static let close: String.LocalizationValue = "button_close" + /// New transfer + /// + /// Key: `button_create_new_transfer` + /// Context: Button: start creating a new transfer (Send tab). + /// + /// - en: New transfer + /// - fr: Nouveau transfert + /// - es: Nueva transferencia + /// - it: Nuovo trasferimento + /// - de: Neue Übertragung + /// - pt: Nova transferência + /// - pl: Nowy transfer + /// - nl: Nieuwe overdracht + /// - ru: Новая передача + static let createNewTransfer: String.LocalizationValue = "button_create_new_transfer" + /// Delete transfer + /// + /// Key: `button_delete_transfer` + /// Context: Button: delete a transfer. + /// + /// - en: Delete transfer + /// - fr: Supprimer le transfert + /// - es: Eliminar transferencia + /// - it: Elimina trasferimento + /// - de: Übertragung löschen + /// - pt: Eliminar transferência + /// - pl: Usuń transfer + /// - nl: Overdracht verwijderen + /// - ru: Удалить передачу + static let deleteTransfer: String.LocalizationValue = "button_delete_transfer" + /// Save .vnd file + /// + /// Key: `button_download_invitation` + /// Context: Button: save the invitation as a .vnd file. + /// + /// - en: Save .vnd file + /// - fr: Enregistrer le fichier .vnd + /// - es: Guardar archivo .vnd + /// - it: Salva file .vnd + /// - de: .vnd-Datei sichern + /// - pt: Guardar ficheiro .vnd + /// - pl: Zapisz plik .vnd + /// - nl: .vnd-bestand bewaren + /// - ru: Сохранить файл .vnd + static let downloadInvitation: String.LocalizationValue = "button_download_invitation" + /// Share invitation + /// + /// Key: `button_native_share` + /// Context: Button: open the OS share sheet to share the invitation. + /// + /// - en: Share invitation + /// - fr: Partager l’invitation + /// - es: Compartir invitación + /// - it: Condividi invito + /// - de: Einladung teilen + /// - pt: Partilhar convite + /// - pl: Udostępnij zaproszenie + /// - nl: Uitnodiging delen + /// - ru: Поделиться приглашением + static let nativeShare: String.LocalizationValue = "button_native_share" + /// Open Settings + /// + /// Key: `button_open_settings` + /// Context: Button: open the OS Settings app (e.g. for permissions). + /// + /// - en: Open Settings + /// - fr: Ouvrir les Réglages + /// - es: Abrir Ajustes + /// - it: Apri Impostazioni + /// - de: Einstellungen öffnen + /// - pt: Abrir Definições + /// - pl: Otwórz Ustawienia + /// - nl: Instellingen openen + /// - ru: Открыть Настройки + static let openSettings: String.LocalizationValue = "button_open_settings" + /// Receive + /// + /// Key: `button_receive` + /// Context: Button: receive label (Receive action). + /// + /// - en: Receive + /// - fr: Recevoir + /// - es: Recibir + /// - it: Ricevi + /// - de: Empfangen + /// - pt: Receber + /// - pl: Odbierz + /// - nl: Ontvangen + /// - ru: Получить + static let receive: String.LocalizationValue = "button_receive" + /// Start receiving + /// + /// Key: `button_receive_files` + /// Context: Button: start receiving a transfer. + /// + /// - en: Start receiving + /// - fr: Commencer à recevoir + /// - es: Empezar a recibir + /// - it: Inizia a ricevere + /// - de: Empfang starten + /// - pt: Começar a receber + /// - pl: Rozpocznij odbieranie + /// - nl: Ontvangen starten + /// - ru: Начать получение + static let receiveFiles: String.LocalizationValue = "button_receive_files" + /// Refuse + /// + /// Key: `button_refuse` + /// Context: Button: refuse a receiver's request. + /// + /// - en: Refuse + /// - fr: Refuser + /// - es: Rechazar + /// - it: Rifiuta + /// - de: Ablehnen + /// - pt: Recusar + /// - pl: Odrzuć + /// - nl: Weigeren + /// - ru: Отклонить + static let refuse: String.LocalizationValue = "button_refuse" + /// Remove file + /// + /// Key: `button_remove_file` + /// Context: Button: remove a single file from the send selection. + /// + /// - en: Remove file + /// - fr: Retirer le fichier + /// - es: Quitar archivo + /// - it: Rimuovi file + /// - de: Datei entfernen + /// - pt: Remover ficheiro + /// - pl: Usuń plik + /// - nl: Bestand verwijderen + /// - ru: Удалить файл + static let removeFile: String.LocalizationValue = "button_remove_file" + /// Use default + /// + /// Key: `button_reset_default` + /// Context: Button: reset a setting to its default value. + /// + /// - en: Use default + /// - fr: Valeur par défaut + /// - es: Usar predeterminado + /// - it: Usa predefinito + /// - de: Standard verwenden + /// - pt: Usar predefinição + /// - pl: Użyj domyślnego + /// - nl: Standaard gebruiken + /// - ru: По умолчанию + static let resetDefault: String.LocalizationValue = "button_reset_default" + /// Retry + /// + /// Key: `button_retry` + /// Context: Button: retry after a failed operation. + /// + /// - en: Retry + /// - fr: Réessayer + /// - es: Reintentar + /// - it: Riprova + /// - de: Wiederholen + /// - pt: Tentar novamente + /// - pl: Spróbuj ponownie + /// - nl: Opnieuw proberen + /// - ru: Повторить + static let retry: String.LocalizationValue = "button_retry" + /// Start sharing + /// + /// Key: `button_share_file` + /// Context: Button: start sharing the transfer (make it available). + /// + /// - en: Start sharing + /// - fr: Commencer le partage + /// - es: Empezar a compartir + /// - it: Inizia a condividere + /// - de: Freigabe starten + /// - pt: Começar a partilhar + /// - pl: Rozpocznij udostępnianie + /// - nl: Delen starten + /// - ru: Начать общий доступ + static let shareFile: String.LocalizationValue = "button_share_file" + /// Preparing transfer… + /// + /// Key: `button_sharing_file` + /// Context: Button: disabled/loading state while the transfer is being prepared. + /// + /// - en: Preparing transfer… + /// - fr: Préparation du transfert… + /// - es: Preparando la transferencia… + /// - it: Preparazione del trasferimento… + /// - de: Übertragung wird vorbereitet… + /// - pt: A preparar a transferência… + /// - pl: Przygotowywanie transferu… + /// - nl: Overdracht voorbereiden… + /// - ru: Подготовка передачи… + static let sharingFile: String.LocalizationValue = "button_sharing_file" + /// Show in Files + /// + /// Key: `button_show_in_files` + /// Context: Button: reveal a received file in the Files app. + /// + /// - en: Show in Files + /// - fr: Afficher dans Fichiers + /// - es: Mostrar en Archivos + /// - it: Mostra in File + /// - de: In „Dateien“ anzeigen + /// - pt: Mostrar em Ficheiros + /// - pl: Pokaż w Plikach + /// - nl: Toon in Bestanden + /// - ru: Показать в Файлах + static let showInFiles: String.LocalizationValue = "button_show_in_files" + /// Write to NFC tag + /// + /// Key: `button_write_nfc` + /// Context: Button: write the invitation to an NFC tag. + /// + /// - en: Write to NFC tag + /// - fr: Écrire sur un tag NFC + /// - es: Escribir en etiqueta NFC + /// - it: Scrivi su tag NFC + /// - de: Auf NFC-Tag schreiben + /// - pt: Escrever em etiqueta NFC + /// - pl: Zapisz na tagu NFC + /// - nl: Naar NFC-tag schrijven + /// - ru: Записать на NFC-метку + static let writeNfc: String.LocalizationValue = "button_write_nfc" + } + enum Device { + /// Device model + /// + /// Key: `device_model_title` + /// Context: Device information row: device model label. + /// + /// - en: Device model + /// - fr: Modèle de l’appareil + /// - es: Modelo del dispositivo + /// - it: Modello del dispositivo + /// - de: Gerätemodell + /// - pt: Modelo do dispositivo + /// - pl: Model urządzenia + /// - nl: Apparaatmodel + /// - ru: Модель устройства + static let modelTitle: String.LocalizationValue = "device_model_title" + /// Device name + /// + /// Key: `device_name_title` + /// Context: Device information row: device name label. + /// + /// - en: Device name + /// - fr: Nom de l’appareil + /// - es: Nombre del dispositivo + /// - it: Nome del dispositivo + /// - de: Gerätename + /// - pt: Nome do dispositivo + /// - pl: Nazwa urządzenia + /// - nl: Apparaatnaam + /// - ru: Имя устройства + static let nameTitle: String.LocalizationValue = "device_name_title" + } + enum Diagnostics { + /// Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included. + /// + /// Key: `diagnostics_description` + /// Context: Settings > Diagnostics: explanation of what anonymous diagnostics collect. + /// + /// - en: Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included. + /// - fr: Envoyer des rapports de plantage et des événements d’utilisation anonymes pour nous aider à améliorer VniDrop. Vous pouvez désactiver cela à tout moment. Les invitations, chemins de fichiers et contenus de transfert ne sont jamais inclus. + /// - es: Enviar informes de fallos y eventos de uso anónimos para ayudarnos a mejorar VniDrop. Puede desactivarlo en cualquier momento. Las invitaciones, las rutas de archivos y el contenido de las transferencias nunca se incluyen. + /// - it: Invia report di arresto anomalo ed eventi d’uso anonimi per aiutarci a migliorare VniDrop. Può disattivarlo in qualsiasi momento. Inviti, percorsi dei file e contenuti dei trasferimenti non vengono mai inclusi. + /// - de: Anonyme Absturzberichte und Nutzungsereignisse senden, damit wir VniDrop verbessern können. Sie können dies jederzeit deaktivieren. Einladungen, Dateipfade und Übertragungsinhalte werden niemals einbezogen. + /// - pt: Enviar relatórios de falhas e eventos de utilização anónimos para nos ajudar a melhorar o VniDrop. Pode desativar isto a qualquer momento. Convites, caminhos de ficheiros e conteúdos das transferências nunca são incluídos. + /// - pl: Wysyłaj anonimowe raporty o awariach i zdarzenia użytkowania, aby pomóc nam ulepszać VniDrop. Możesz to wyłączyć w dowolnej chwili. Zaproszenia, ścieżki plików i zawartość transferów nigdy nie są dołączane. + /// - nl: Verstuur anonieme crashrapporten en gebruiksgebeurtenissen zodat we VniDrop kunnen verbeteren. U kunt dit op elk moment uitschakelen. Uitnodigingen, bestandspaden en overdrachtsinhoud worden nooit meegestuurd. + /// - ru: Отправлять анонимные отчёты о сбоях и события использования, чтобы помочь нам улучшать VniDrop. Вы можете отключить это в любой момент. Приглашения, пути к файлам и содержимое передач никогда не включаются. + static let description: String.LocalizationValue = "diagnostics_description" + /// Diagnostics sharing is off. + /// + /// Key: `diagnostics_disabled_message` + /// Context: Settings > Diagnostics: confirmation shown when diagnostics are turned off. + /// + /// - en: Diagnostics sharing is off. + /// - fr: Le partage des diagnostics est désactivé. + /// - es: El uso compartido de diagnósticos está desactivado. + /// - it: La condivisione dei dati diagnostici è disattivata. + /// - de: Die Freigabe von Diagnosedaten ist deaktiviert. + /// - pt: A partilha de diagnósticos está desativada. + /// - pl: Udostępnianie diagnostyki jest wyłączone. + /// - nl: Het delen van diagnostische gegevens is uitgeschakeld. + /// - ru: Передача диагностики отключена. + static let disabledMessage: String.LocalizationValue = "diagnostics_disabled_message" + /// Diagnostics sharing is on. + /// + /// Key: `diagnostics_enabled_message` + /// Context: Settings > Diagnostics: confirmation shown when diagnostics are turned on. + /// + /// - en: Diagnostics sharing is on. + /// - fr: Le partage des diagnostics est activé. + /// - es: El uso compartido de diagnósticos está activado. + /// - it: La condivisione dei dati diagnostici è attivata. + /// - de: Die Freigabe von Diagnosedaten ist aktiviert. + /// - pt: A partilha de diagnósticos está ativada. + /// - pl: Udostępnianie diagnostyki jest włączone. + /// - nl: Het delen van diagnostische gegevens is ingeschakeld. + /// - ru: Передача диагностики включена. + static let enabledMessage: String.LocalizationValue = "diagnostics_enabled_message" + /// Share diagnostics + /// + /// Key: `diagnostics_title` + /// Context: Settings > Diagnostics: toggle title. + /// + /// - en: Share diagnostics + /// - fr: Partager les diagnostics + /// - es: Compartir diagnósticos + /// - it: Condividi dati diagnostici + /// - de: Diagnosedaten teilen + /// - pt: Partilhar diagnósticos + /// - pl: Udostępniaj diagnostykę + /// - nl: Diagnostische gegevens delen + /// - ru: Делиться диагностикой + static let title: String.LocalizationValue = "diagnostics_title" + } + enum Error { + /// Camera access is required to scan a QR code. + /// + /// Key: `error_camera` + /// Context: Error: camera permission is needed to scan a QR code. + /// + /// - en: Camera access is required to scan a QR code. + /// - fr: L’accès à la caméra est nécessaire pour scanner un QR code. + /// - es: Se necesita acceso a la cámara para escanear un código QR. + /// - it: Per scansionare un codice QR è necessario l’accesso alla fotocamera. + /// - de: Für das Scannen eines QR-Codes ist Kamerazugriff erforderlich. + /// - pt: É necessário acesso à câmara para ler um código QR. + /// - pl: Do zeskanowania kodu QR wymagany jest dostęp do aparatu. + /// - nl: Voor het scannen van een QR-code is toegang tot de camera vereist. + /// - ru: Для сканирования QR-кода требуется доступ к камере. + static let camera: String.LocalizationValue = "error_camera" + /// Could not load device information. + /// + /// Key: `error_device_info` + /// Context: Error: device information could not be loaded. + /// + /// - en: Could not load device information. + /// - fr: Impossible de charger les informations de l’appareil. + /// - es: No se pudo cargar la información del dispositivo. + /// - it: Impossibile caricare le informazioni sul dispositivo. + /// - de: Geräteinformationen konnten nicht geladen werden. + /// - pt: Não foi possível carregar as informações do dispositivo. + /// - pl: Nie udało się wczytać informacji o urządzeniu. + /// - nl: Apparaatgegevens konden niet worden geladen. + /// - ru: Не удалось загрузить сведения об устройстве. + static let deviceInfo: String.LocalizationValue = "error_device_info" + /// A file with the same name already exists in the destination. Choose another folder or remove the existing file. + /// + /// Key: `error_destination_exists` + /// Context: Error: a received file would overwrite an existing destination file. + /// + /// - en: A file with the same name already exists in the destination. Choose another folder or remove the existing file. + /// - fr: Un fichier portant le même nom existe déjà dans la destination. Choisissez un autre dossier ou supprimez le fichier existant. + /// - es: Ya existe un archivo con el mismo nombre en el destino. Elija otra carpeta o elimine el archivo existente. + /// - it: Nella destinazione esiste già un file con lo stesso nome. Scelga un’altra cartella o rimuova il file esistente. + /// - de: Am Ziel ist bereits eine Datei mit demselben Namen vorhanden. Wählen Sie einen anderen Ordner oder entfernen Sie die vorhandene Datei. + /// - pt: Já existe um ficheiro com o mesmo nome no destino. Escolha outra pasta ou remova o ficheiro existente. + /// - pl: W miejscu docelowym istnieje już plik o tej samej nazwie. Wybierz inny folder lub usuń istniejący plik. + /// - nl: Er staat al een bestand met dezelfde naam in de doelmap. Kies een andere map of verwijder het bestaande bestand. + /// - ru: В папке назначения уже есть файл с таким именем. Выберите другую папку или удалите существующий файл. + static let destinationExists: String.LocalizationValue = "error_destination_exists" + /// VniDrop could not access the selected files or folder. Check permissions and try again. + /// + /// Key: `error_filesystem` + /// Context: Error: the selected files/folder could not be accessed. + /// + /// - en: VniDrop could not access the selected files or folder. Check permissions and try again. + /// - fr: VniDrop n’a pas pu accéder aux fichiers ou au dossier sélectionnés. Vérifiez les autorisations et réessayez. + /// - es: VniDrop no pudo acceder a los archivos o la carpeta seleccionados. Compruebe los permisos e inténtelo de nuevo. + /// - it: VniDrop non ha potuto accedere ai file o alla cartella selezionati. Controlli le autorizzazioni e riprovi. + /// - de: VniDrop konnte nicht auf die ausgewählten Dateien oder den Ordner zugreifen. Überprüfen Sie die Berechtigungen und versuchen Sie es erneut. + /// - pt: O VniDrop não conseguiu aceder aos ficheiros ou à pasta selecionados. Verifique as permissões e tente novamente. + /// - pl: VniDrop nie mógł uzyskać dostępu do wybranych plików lub folderu. Sprawdź uprawnienia i spróbuj ponownie. + /// - nl: VniDrop kon geen toegang krijgen tot de geselecteerde bestanden of map. Controleer de machtigingen en probeer het opnieuw. + /// - ru: VniDrop не удалось получить доступ к выбранным файлам или папке. Проверьте разрешения и повторите попытку. + static let filesystem: String.LocalizationValue = "error_filesystem" + /// Something went wrong. Try again. + /// + /// Key: `error_generic` + /// Context: Error: generic fallback message. + /// + /// - en: Something went wrong. Try again. + /// - fr: Une erreur est survenue. Réessayez. + /// - es: Algo salió mal. Inténtelo de nuevo. + /// - it: Qualcosa è andato storto. Riprovi. + /// - de: Etwas ist schiefgelaufen. Versuchen Sie es erneut. + /// - pt: Algo correu mal. Tente novamente. + /// - pl: Coś poszło nie tak. Spróbuj ponownie. + /// - nl: Er is iets misgegaan. Probeer het opnieuw. + /// - ru: Что-то пошло не так. Повторите попытку. + static let generic: String.LocalizationValue = "error_generic" + /// VniDrop could not finish starting up. Close the app and try again. + /// + /// Key: `error_initialization` + /// Context: Error: the app failed to finish starting up. + /// + /// - en: VniDrop could not finish starting up. Close the app and try again. + /// - fr: VniDrop n’a pas pu terminer son démarrage. Fermez l’app et réessayez. + /// - es: VniDrop no pudo terminar de iniciarse. Cierre la app e inténtelo de nuevo. + /// - it: VniDrop non ha potuto completare l’avvio. Chiuda l’app e riprovi. + /// - de: VniDrop konnte den Start nicht abschließen. Schließen Sie die App und versuchen Sie es erneut. + /// - pt: O VniDrop não conseguiu concluir o arranque. Feche a app e tente novamente. + /// - pl: VniDrop nie mógł dokończyć uruchamiania. Zamknij aplikację i spróbuj ponownie. + /// - nl: VniDrop kon het opstarten niet voltooien. Sluit de app en probeer het opnieuw. + /// - ru: VniDrop не удалось завершить запуск. Закройте приложение и повторите попытку. + static let initialization: String.LocalizationValue = "error_initialization" + /// Some transfer information is invalid. Review your selection or ask the sender to share again. + /// + /// Key: `error_invalid_input` + /// Context: Error: transfer input or metadata is invalid. + /// + /// - en: Some transfer information is invalid. Review your selection or ask the sender to share again. + /// - fr: Certaines informations du transfert ne sont pas valides. Vérifiez votre sélection ou demandez à l’expéditeur de partager à nouveau. + /// - es: Parte de la información de la transferencia no es válida. Revise la selección o pida al remitente que vuelva a compartirla. + /// - it: Alcune informazioni del trasferimento non sono valide. Controlli la selezione o chieda al mittente di condividere di nuovo. + /// - de: Einige Übertragungsinformationen sind ungültig. Prüfen Sie Ihre Auswahl oder bitten Sie den Absender, erneut zu teilen. + /// - pt: Algumas informações da transferência são inválidas. Reveja a seleção ou peça ao remetente para partilhar novamente. + /// - pl: Niektóre informacje o transferze są nieprawidłowe. Sprawdź wybór lub poproś nadawcę o ponowne udostępnienie. + /// - nl: Sommige overdrachtsgegevens zijn ongeldig. Controleer uw selectie of vraag de afzender opnieuw te delen. + /// - ru: Некоторые данные передачи недействительны. Проверьте выбор или попросите отправителя поделиться снова. + static let invalidInput: String.LocalizationValue = "error_invalid_input" + /// This invitation could not be read. Ask the sender for a new one. + /// + /// Key: `error_invalid_ticket` + /// Context: Error: the invitation/ticket could not be parsed. + /// + /// - en: This invitation could not be read. Ask the sender for a new one. + /// - fr: Cette invitation n’a pas pu être lue. Demandez-en une nouvelle à l’expéditeur. + /// - es: No se pudo leer esta invitación. Pida al remitente una nueva. + /// - it: Impossibile leggere questo invito. Ne chieda uno nuovo al mittente. + /// - de: Diese Einladung konnte nicht gelesen werden. Bitten Sie den Absender um eine neue. + /// - pt: Não foi possível ler este convite. Peça um novo ao remetente. + /// - pl: Nie udało się odczytać tego zaproszenia. Poproś nadawcę o nowe. + /// - nl: Deze uitnodiging kon niet worden gelezen. Vraag de afzender om een nieuwe. + /// - ru: Не удалось прочитать это приглашение. Попросите отправителя прислать новое. + static let invalidTicket: String.LocalizationValue = "error_invalid_ticket" + /// That invitation is empty. Try opening it again. + /// + /// Key: `error_invitation_empty` + /// Context: Error: the opened invitation contained no data. + /// + /// - en: That invitation is empty. Try opening it again. + /// - fr: Cette invitation est vide. Essayez de l’ouvrir à nouveau. + /// - es: Esa invitación está vacía. Intente abrirla de nuevo. + /// - it: Questo invito è vuoto. Provi ad aprirlo di nuovo. + /// - de: Diese Einladung ist leer. Versuchen Sie, sie erneut zu öffnen. + /// - pt: Esse convite está vazio. Tente abri-lo novamente. + /// - pl: To zaproszenie jest puste. Spróbuj otworzyć je ponownie. + /// - nl: Die uitnodiging is leeg. Probeer deze opnieuw te openen. + /// - ru: Это приглашение пустое. Попробуйте открыть его снова. + static let invitationEmpty: String.LocalizationValue = "error_invitation_empty" + /// The native VniDrop library is missing from this build. + /// + /// Key: `error_missing_native_library` + /// Context: Error: the native library is missing from the build. + /// + /// - en: The native VniDrop library is missing from this build. + /// - fr: La bibliothèque native de VniDrop est absente de cette version. + /// - es: Falta la biblioteca nativa de VniDrop en esta versión. + /// - it: La libreria nativa di VniDrop non è presente in questa build. + /// - de: Die native VniDrop-Bibliothek fehlt in diesem Build. + /// - pt: A biblioteca nativa do VniDrop está em falta nesta compilação. + /// - pl: W tej kompilacji brakuje natywnej biblioteki VniDrop. + /// - nl: De native VniDrop-bibliotheek ontbreekt in deze build. + /// - ru: В этой сборке отсутствует нативная библиотека VniDrop. + static let missingNativeLibrary: String.LocalizationValue = "error_missing_native_library" + /// VniDrop could not reach the sender. Check the connection on both devices and try again. + /// + /// Key: `error_network` + /// Context: Error: the sender could not be reached over the local network. + /// + /// - en: VniDrop could not reach the sender. Check the connection on both devices and try again. + /// - fr: VniDrop n’a pas pu joindre l’expéditeur. Vérifiez la connexion sur les deux appareils et réessayez. + /// - es: VniDrop no pudo contactar con el remitente. Compruebe la conexión en ambos dispositivos e inténtelo de nuevo. + /// - it: VniDrop non è riuscito a raggiungere il mittente. Controlli la connessione su entrambi i dispositivi e riprovi. + /// - de: VniDrop konnte den Absender nicht erreichen. Prüfen Sie die Verbindung auf beiden Geräten und versuchen Sie es erneut. + /// - pt: O VniDrop não conseguiu contactar o remetente. Verifique a ligação nos dois dispositivos e tente novamente. + /// - pl: VniDrop nie mógł połączyć się z nadawcą. Sprawdź połączenie na obu urządzeniach i spróbuj ponownie. + /// - nl: VniDrop kon de afzender niet bereiken. Controleer de verbinding op beide apparaten en probeer het opnieuw. + /// - ru: VniDrop не удалось связаться с отправителем. Проверьте подключение на обоих устройствах и повторите попытку. + static let network: String.LocalizationValue = "error_network" + /// This NFC tag could not be used. Try another tag. + /// + /// Key: `error_nfc` + /// Context: Error: the NFC tag could not be read/used. + /// + /// - en: This NFC tag could not be used. Try another tag. + /// - fr: Ce tag NFC n’a pas pu être utilisé. Essayez-en un autre. + /// - es: No se pudo usar esta etiqueta NFC. Pruebe con otra. + /// - it: Impossibile usare questo tag NFC. Ne provi un altro. + /// - de: Dieses NFC-Tag konnte nicht verwendet werden. Versuchen Sie ein anderes. + /// - pt: Não foi possível utilizar esta etiqueta NFC. Tente outra. + /// - pl: Nie udało się użyć tego tagu NFC. Spróbuj innego. + /// - nl: Deze NFC-tag kon niet worden gebruikt. Probeer een andere. + /// - ru: Не удалось использовать эту NFC-метку. Попробуйте другую. + static let nfc: String.LocalizationValue = "error_nfc" + /// The sender has not approved this transfer, or it was refused. + /// + /// Key: `error_permission` + /// Context: Error: the transfer was not approved or was refused by the sender. + /// + /// - en: The sender has not approved this transfer, or it was refused. + /// - fr: L’expéditeur n’a pas approuvé ce transfert, ou il a été refusé. + /// - es: El remitente no ha aprobado esta transferencia, o fue rechazada. + /// - it: Il mittente non ha approvato questo trasferimento, oppure è stato rifiutato. + /// - de: Der Absender hat diese Übertragung nicht genehmigt oder sie wurde abgelehnt. + /// - pt: O remetente não aprovou esta transferência, ou foi recusada. + /// - pl: Nadawca nie zatwierdził tego transferu lub został on odrzucony. + /// - nl: De afzender heeft deze overdracht niet goedgekeurd, of deze is geweigerd. + /// - ru: Отправитель не одобрил эту передачу, или она была отклонена. + static let permission: String.LocalizationValue = "error_permission" + /// VniDrop could not save transfer data on this device. + /// + /// Key: `error_repository` + /// Context: Error: transfer data could not be saved locally. + /// + /// - en: VniDrop could not save transfer data on this device. + /// - fr: VniDrop n’a pas pu enregistrer les données de transfert sur cet appareil. + /// - es: VniDrop no pudo guardar los datos de la transferencia en este dispositivo. + /// - it: VniDrop non ha potuto salvare i dati del trasferimento su questo dispositivo. + /// - de: VniDrop konnte die Übertragungsdaten auf diesem Gerät nicht speichern. + /// - pt: O VniDrop não conseguiu guardar os dados da transferência neste dispositivo. + /// - pl: VniDrop nie mógł zapisać danych transferu na tym urządzeniu. + /// - nl: VniDrop kon de overdrachtsgegevens niet op dit apparaat bewaren. + /// - ru: VniDrop не удалось сохранить данные передачи на этом устройстве. + static let repository: String.LocalizationValue = "error_repository" + /// Could not open the selected item. Try choosing it again. + /// + /// Key: `error_selection_failed` + /// Context: Error: the selected item could not be opened. + /// + /// - en: Could not open the selected item. Try choosing it again. + /// - fr: Impossible d’ouvrir l’élément sélectionné. Essayez de le choisir à nouveau. + /// - es: No se pudo abrir el elemento seleccionado. Intente elegirlo de nuevo. + /// - it: Impossibile aprire l’elemento selezionato. Provi a sceglierlo di nuovo. + /// - de: Das ausgewählte Objekt konnte nicht geöffnet werden. Versuchen Sie, es erneut auszuwählen. + /// - pt: Não foi possível abrir o item selecionado. Tente escolhê-lo novamente. + /// - pl: Nie udało się otworzyć wybranego elementu. Spróbuj wybrać go ponownie. + /// - nl: Het geselecteerde item kon niet worden geopend. Probeer het opnieuw te kiezen. + /// - ru: Не удалось открыть выбранный объект. Попробуйте выбрать его снова. + static let selectionFailed: String.LocalizationValue = "error_selection_failed" + /// Select at least one item to share. + /// + /// Key: `error_share_empty` + /// Context: Error: attempted to share with nothing selected. + /// + /// - en: Select at least one item to share. + /// - fr: Sélectionnez au moins un élément à partager. + /// - es: Seleccione al menos un elemento para compartir. + /// - it: Selezioni almeno un elemento da condividere. + /// - de: Wählen Sie mindestens ein Objekt zum Teilen aus. + /// - pt: Selecione pelo menos um item para partilhar. + /// - pl: Wybierz co najmniej jeden element do udostępnienia. + /// - nl: Selecteer minstens één item om te delen. + /// - ru: Выберите хотя бы один объект для отправки. + static let shareEmpty: String.LocalizationValue = "error_share_empty" + /// VniDrop could not open its network sockets on this device. + /// + /// Key: `error_socket_bind` + /// Context: Error: the app could not open its network sockets. + /// + /// - en: VniDrop could not open its network sockets on this device. + /// - fr: VniDrop n’a pas pu ouvrir ses sockets réseau sur cet appareil. + /// - es: VniDrop no pudo abrir sus sockets de red en este dispositivo. + /// - it: VniDrop non ha potuto aprire i suoi socket di rete su questo dispositivo. + /// - de: VniDrop konnte seine Netzwerk-Sockets auf diesem Gerät nicht öffnen. + /// - pt: O VniDrop não conseguiu abrir os seus sockets de rede neste dispositivo. + /// - pl: VniDrop nie mógł otworzyć gniazd sieciowych na tym urządzeniu. + /// - nl: VniDrop kon zijn netwerksockets niet openen op dit apparaat. + /// - ru: VniDrop не удалось открыть сетевые сокеты на этом устройстве. + static let socketBind: String.LocalizationValue = "error_socket_bind" + /// VniDrop is still starting. Open the invitation again in a moment. + /// + /// Key: `error_starting_up` + /// Context: Error: an invitation was opened before startup finished. + /// + /// - en: VniDrop is still starting. Open the invitation again in a moment. + /// - fr: VniDrop démarre encore. Rouvrez l’invitation dans un instant. + /// - es: VniDrop todavía se está iniciando. Vuelva a abrir la invitación en un momento. + /// - it: VniDrop è ancora in fase di avvio. Riapra l’invito tra un momento. + /// - de: VniDrop startet noch. Öffnen Sie die Einladung gleich erneut. + /// - pt: O VniDrop ainda está a iniciar. Abra o convite novamente dentro de momentos. + /// - pl: VniDrop jeszcze się uruchamia. Otwórz zaproszenie ponownie za chwilę. + /// - nl: VniDrop is nog aan het opstarten. Open de uitnodiging zo meteen opnieuw. + /// - ru: VniDrop ещё запускается. Откройте приглашение снова через мгновение. + static let startingUp: String.LocalizationValue = "error_starting_up" + /// There is not enough storage space to save this transfer. Free up space and try again. + /// + /// Key: `error_storage_full` + /// Context: Error: the destination does not have enough free storage. + /// + /// - en: There is not enough storage space to save this transfer. Free up space and try again. + /// - fr: L’espace de stockage est insuffisant pour enregistrer ce transfert. Libérez de l’espace et réessayez. + /// - es: No hay suficiente espacio de almacenamiento para guardar esta transferencia. Libere espacio e inténtelo de nuevo. + /// - it: Lo spazio di archiviazione non è sufficiente per salvare il trasferimento. Liberi spazio e riprovi. + /// - de: Zum Speichern dieser Übertragung ist nicht genügend Speicherplatz vorhanden. Geben Sie Speicherplatz frei und versuchen Sie es erneut. + /// - pt: Não existe espaço de armazenamento suficiente para guardar esta transferência. Liberte espaço e tente novamente. + /// - pl: Brakuje miejsca na zapisanie tego transferu. Zwolnij miejsce i spróbuj ponownie. + /// - nl: Er is onvoldoende opslagruimte om deze overdracht op te slaan. Maak ruimte vrij en probeer het opnieuw. + /// - ru: Недостаточно места для сохранения этой передачи. Освободите место и повторите попытку. + static let storageFull: String.LocalizationValue = "error_storage_full" + /// The transfer data could not be processed. Ask the sender to share it again. + /// + /// Key: `error_transfer` + /// Context: Error: transfer data could not be processed; network failures use error_network. + /// + /// - en: The transfer data could not be processed. Ask the sender to share it again. + /// - fr: Les données du transfert n’ont pas pu être traitées. Demandez à l’expéditeur de les partager à nouveau. + /// - es: No se pudieron procesar los datos de la transferencia. Pida al remitente que vuelva a compartirlos. + /// - it: Non è stato possibile elaborare i dati del trasferimento. Chieda al mittente di condividerli di nuovo. + /// - de: Die Übertragungsdaten konnten nicht verarbeitet werden. Bitten Sie den Absender, sie erneut zu teilen. + /// - pt: Não foi possível processar os dados da transferência. Peça ao remetente para os partilhar novamente. + /// - pl: Nie udało się przetworzyć danych transferu. Poproś nadawcę o ponowne udostępnienie. + /// - nl: De overdrachtsgegevens konden niet worden verwerkt. Vraag de afzender ze opnieuw te delen. + /// - ru: Не удалось обработать данные передачи. Попросите отправителя поделиться ими снова. + static let transfer: String.LocalizationValue = "error_transfer" + } + enum Field { + /// Receiver name + /// + /// Key: `field_receiver_name` + /// Context: Text field label: the receiver's display name. + /// + /// - en: Receiver name + /// - fr: Nom du destinataire + /// - es: Nombre del destinatario + /// - it: Nome del destinatario + /// - de: Empfängername + /// - pt: Nome do destinatário + /// - pl: Nazwa odbiorcy + /// - nl: Naam van ontvanger + /// - ru: Имя получателя + static let receiverName: String.LocalizationValue = "field_receiver_name" + /// Sender name + /// + /// Key: `field_sender_name` + /// Context: Text field label: the sender's display name. + /// + /// - en: Sender name + /// - fr: Nom de l’expéditeur + /// - es: Nombre del remitente + /// - it: Nome del mittente + /// - de: Absendername + /// - pt: Nome do remetente + /// - pl: Nazwa nadawcy + /// - nl: Naam van afzender + /// - ru: Имя отправителя + static let senderName: String.LocalizationValue = "field_sender_name" + /// Transfer name + /// + /// Key: `field_transfer_name` + /// Context: Text field label: the name given to a transfer. + /// + /// - en: Transfer name + /// - fr: Nom du transfert + /// - es: Nombre de la transferencia + /// - it: Nome del trasferimento + /// - de: Übertragungsname + /// - pt: Nome da transferência + /// - pl: Nazwa transferu + /// - nl: Naam van overdracht + /// - ru: Название передачи + static let transferName: String.LocalizationValue = "field_transfer_name" + /// Display name + /// + /// Key: `field_username` + /// Context: Settings text field label: this device's display name. + /// + /// - en: Display name + /// - fr: Nom d’affichage + /// - es: Nombre visible + /// - it: Nome visualizzato + /// - de: Anzeigename + /// - pt: Nome a apresentar + /// - pl: Nazwa wyświetlana + /// - nl: Weergavenaam + /// - ru: Отображаемое имя + static let username: String.LocalizationValue = "field_username" + } + enum Folder { + /// Permission needed + /// + /// Key: `folder_status_permission_required` + /// Context: Receive-folder status: permission is required to write to the chosen folder. + /// + /// - en: Permission needed + /// - fr: Autorisation requise + /// - es: Permiso necesario + /// - it: Autorizzazione necessaria + /// - de: Berechtigung erforderlich + /// - pt: Permissão necessária + /// - pl: Wymagane uprawnienie + /// - nl: Machtiging vereist + /// - ru: Требуется разрешение + static let statusPermissionRequired: String.LocalizationValue = "folder_status_permission_required" + /// Unavailable + /// + /// Key: `folder_status_unavailable` + /// Context: Receive-folder status: the chosen folder is unavailable. + /// + /// - en: Unavailable + /// - fr: Indisponible + /// - es: No disponible + /// - it: Non disponibile + /// - de: Nicht verfügbar + /// - pt: Indisponível + /// - pl: Niedostępny + /// - nl: Niet beschikbaar + /// - ru: Недоступно + static let statusUnavailable: String.LocalizationValue = "folder_status_unavailable" + /// Checking folder… + /// + /// Key: `folder_status_validating` + /// Context: Receive-folder status: the folder is being checked. + /// + /// - en: Checking folder… + /// - fr: Vérification du dossier… + /// - es: Comprobando carpeta… + /// - it: Controllo della cartella… + /// - de: Ordner wird geprüft… + /// - pt: A verificar a pasta… + /// - pl: Sprawdzanie folderu… + /// - nl: Map controleren… + /// - ru: Проверка папки… + static let statusValidating: String.LocalizationValue = "folder_status_validating" + /// Ready + /// + /// Key: `folder_status_writable` + /// Context: Receive-folder status: the folder is valid and writable. + /// + /// - en: Ready + /// - fr: Prêt + /// - es: Listo + /// - it: Pronta + /// - de: Bereit + /// - pt: Pronto + /// - pl: Gotowy + /// - nl: Gereed + /// - ru: Готово + static let statusWritable: String.LocalizationValue = "folder_status_writable" + } + enum Metadata { + /// Files + /// + /// Key: `metadata_files` + /// Context: Transfer metadata label: number of files. + /// + /// - en: Files + /// - fr: Fichiers + /// - es: Archivos + /// - it: File + /// - de: Dateien + /// - pt: Ficheiros + /// - pl: Pliki + /// - nl: Bestanden + /// - ru: Файлы + static let files: String.LocalizationValue = "metadata_files" + /// Size + /// + /// Key: `metadata_size` + /// Context: Transfer metadata label: total size. + /// + /// - en: Size + /// - fr: Taille + /// - es: Tamaño + /// - it: Dimensione + /// - de: Größe + /// - pt: Tamanho + /// - pl: Rozmiar + /// - nl: Grootte + /// - ru: Размер + static let size: String.LocalizationValue = "metadata_size" + /// Status + /// + /// Key: `metadata_status` + /// Context: Transfer metadata label: current status. + /// + /// - en: Status + /// - fr: Statut + /// - es: Estado + /// - it: Stato + /// - de: Status + /// - pt: Estado + /// - pl: Status + /// - nl: Status + /// - ru: Статус + static let status: String.LocalizationValue = "metadata_status" + } + enum Nav { + /// Receive + /// + /// Key: `nav_receive` + /// Context: Bottom navigation: Receive tab label. + /// + /// - en: Receive + /// - fr: Recevoir + /// - es: Recibir + /// - it: Ricevi + /// - de: Empfangen + /// - pt: Receber + /// - pl: Odbierz + /// - nl: Ontvangen + /// - ru: Получить + static let receive: String.LocalizationValue = "nav_receive" + /// Send + /// + /// Key: `nav_send` + /// Context: Bottom navigation: Send tab label. + /// + /// - en: Send + /// - fr: Envoyer + /// - es: Enviar + /// - it: Invia + /// - de: Senden + /// - pt: Enviar + /// - pl: Wyślij + /// - nl: Versturen + /// - ru: Отправить + static let send: String.LocalizationValue = "nav_send" + /// Settings + /// + /// Key: `nav_settings` + /// Context: Bottom navigation: Settings tab label. + /// + /// - en: Settings + /// - fr: Réglages + /// - es: Ajustes + /// - it: Impostazioni + /// - de: Einstellungen + /// - pt: Definições + /// - pl: Ustawienia + /// - nl: Instellingen + /// - ru: Настройки + static let settings: String.LocalizationValue = "nav_settings" + } + enum Network { + /// Network + /// + /// Key: `network_title` + /// Context: Device information row: network label. + /// + /// - en: Network + /// - fr: Réseau + /// - es: Red + /// - it: Rete + /// - de: Netzwerk + /// - pt: Rede + /// - pl: Sieć + /// - nl: Netwerk + /// - ru: Сеть + static let title: String.LocalizationValue = "network_title" + } + enum Notifications { + /// Get notified about new receive requests while VniDrop is in the background. + /// + /// Key: `notifications_description` + /// Context: Settings > Notifications: explanation of what notifications are used for. + /// + /// - en: Get notified about new receive requests while VniDrop is in the background. + /// - fr: Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan. + /// - es: Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano. + /// - it: Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background. + /// - de: Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft. + /// - pt: Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano. + /// - pl: Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle. + /// - nl: Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait. + /// - ru: Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне. + static let description: String.LocalizationValue = "notifications_description" + /// Notifications enabled. + /// + /// Key: `notifications_enabled_message` + /// Context: Settings > Notifications: confirmation when notifications are enabled. + /// + /// - en: Notifications enabled. + /// - fr: Notifications activées. + /// - es: Notificaciones activadas. + /// - it: Notifiche attivate. + /// - de: Mitteilungen aktiviert. + /// - pt: Notificações ativadas. + /// - pl: Powiadomienia włączone. + /// - nl: Meldingen ingeschakeld. + /// - ru: Уведомления включены. + static let enabledMessage: String.LocalizationValue = "notifications_enabled_message" + /// Allow notifications + /// + /// Key: `notifications_local_title` + /// Context: Settings > Notifications: label for the allow-notifications action. + /// + /// - en: Allow notifications + /// - fr: Autoriser les notifications + /// - es: Permitir notificaciones + /// - it: Consenti le notifiche + /// - de: Mitteilungen erlauben + /// - pt: Permitir notificações + /// - pl: Zezwól na powiadomienia + /// - nl: Meldingen toestaan + /// - ru: Разрешить уведомления + static let localTitle: String.LocalizationValue = "notifications_local_title" + /// Notifications are turned off for VniDrop. You can enable them in Settings. + /// + /// Key: `notifications_permission_denied` + /// Context: Settings > Notifications: message when the OS permission is denied. + /// + /// - en: Notifications are turned off for VniDrop. You can enable them in Settings. + /// - fr: Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages. + /// - es: Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes. + /// - it: Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni. + /// - de: Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren. + /// - pt: As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições. + /// - pl: Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach. + /// - nl: Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen. + /// - ru: Уведомления отключены для VniDrop. Вы можете включить их в Настройках. + static let permissionDenied: String.LocalizationValue = "notifications_permission_denied" + /// Could not open notification settings. + /// + /// Key: `notifications_settings_open_failed` + /// Context: Settings > Notifications: error when the OS notification settings can't be opened. + /// + /// - en: Could not open notification settings. + /// - fr: Impossible d’ouvrir les réglages de notifications. + /// - es: No se pudieron abrir los ajustes de notificaciones. + /// - it: Impossibile aprire le impostazioni delle notifiche. + /// - de: Die Mitteilungseinstellungen konnten nicht geöffnet werden. + /// - pt: Não foi possível abrir as definições de notificações. + /// - pl: Nie udało się otworzyć ustawień powiadomień. + /// - nl: De meldingsinstellingen konden niet worden geopend. + /// - ru: Не удалось открыть настройки уведомлений. + static let settingsOpenFailed: String.LocalizationValue = "notifications_settings_open_failed" + /// Notifications + /// + /// Key: `notifications_title` + /// Context: Settings > Notifications: section title. + /// + /// - en: Notifications + /// - fr: Notifications + /// - es: Notificaciones + /// - it: Notifiche + /// - de: Mitteilungen + /// - pt: Notificações + /// - pl: Powiadomienia + /// - nl: Meldingen + /// - ru: Уведомления + static let title: String.LocalizationValue = "notifications_title" + /// Notifications are not available on this device. + /// + /// Key: `notifications_unsupported` + /// Context: Settings > Notifications: message when notifications aren't supported on the device. + /// + /// - en: Notifications are not available on this device. + /// - fr: Les notifications ne sont pas disponibles sur cet appareil. + /// - es: Las notificaciones no están disponibles en este dispositivo. + /// - it: Le notifiche non sono disponibili su questo dispositivo. + /// - de: Mitteilungen sind auf diesem Gerät nicht verfügbar. + /// - pt: As notificações não estão disponíveis neste dispositivo. + /// - pl: Powiadomienia nie są dostępne na tym urządzeniu. + /// - nl: Meldingen zijn niet beschikbaar op dit apparaat. + /// - ru: Уведомления недоступны на этом устройстве. + static let unsupported: String.LocalizationValue = "notifications_unsupported" + } + enum Os { + /// Operating system + /// + /// Key: `os_version_title` + /// Context: Device information row: operating system version label. + /// + /// - en: Operating system + /// - fr: Système d’exploitation + /// - es: Sistema operativo + /// - it: Sistema operativo + /// - de: Betriebssystem + /// - pt: Sistema operativo + /// - pl: System operacyjny + /// - nl: Besturingssysteem + /// - ru: Операционная система + static let versionTitle: String.LocalizationValue = "os_version_title" + } + enum Preferences { + /// Save received transfers to + /// + /// Key: `preferences_receive_folder_title` + /// Context: Settings > Preferences: label for the received-files destination folder. + /// + /// - en: Save received transfers to + /// - fr: Enregistrer les transferts reçus dans + /// - es: Guardar las transferencias recibidas en + /// - it: Salva i trasferimenti ricevuti in + /// - de: Empfangene Übertragungen sichern in + /// - pt: Guardar as transferências recebidas em + /// - pl: Zapisuj odebrane transfery w + /// - nl: Ontvangen overdrachten bewaren in + /// - ru: Сохранять полученные передачи в + static let receiveFolderTitle: String.LocalizationValue = "preferences_receive_folder_title" + /// Preferences + /// + /// Key: `preferences_title` + /// Context: Settings > Preferences: section title. + /// + /// - en: Preferences + /// - fr: Préférences + /// - es: Preferencias + /// - it: Preferenze + /// - de: Voreinstellungen + /// - pt: Preferências + /// - pl: Preferencje + /// - nl: Voorkeuren + /// - ru: Параметры + static let title: String.LocalizationValue = "preferences_title" + } + enum Progress { + /// Cancelled + /// + /// Key: `progress_cancelled` + /// Context: Transfer progress label: cancelled. + /// + /// - en: Cancelled + /// - fr: Annulé + /// - es: Cancelado + /// - it: Annullato + /// - de: Abgebrochen + /// - pt: Cancelado + /// - pl: Anulowano + /// - nl: Geannuleerd + /// - ru: Отменено + static let cancelled: String.LocalizationValue = "progress_cancelled" + /// Completed + /// + /// Key: `progress_completed` + /// Context: Transfer progress label: completed. + /// + /// - en: Completed + /// - fr: Terminé + /// - es: Completado + /// - it: Completato + /// - de: Abgeschlossen + /// - pt: Concluído + /// - pl: Ukończono + /// - nl: Voltooid + /// - ru: Завершено + static let completed: String.LocalizationValue = "progress_completed" + /// Connected + /// + /// Key: `progress_connected` + /// Context: Transfer progress label: connected. + /// + /// - en: Connected + /// - fr: Connecté + /// - es: Conectado + /// - it: Connesso + /// - de: Verbunden + /// - pt: Ligado + /// - pl: Połączono + /// - nl: Verbonden + /// - ru: Подключено + static let connected: String.LocalizationValue = "progress_connected" + /// Connecting + /// + /// Key: `progress_connecting` + /// Context: Transfer progress label: connecting. + /// + /// - en: Connecting + /// - fr: Connexion + /// - es: Conectando + /// - it: Connessione + /// - de: Verbinden + /// - pt: A ligar + /// - pl: Łączenie + /// - nl: Verbinden + /// - ru: Подключение + static let connecting: String.LocalizationValue = "progress_connecting" + /// Downloading + /// + /// Key: `progress_downloading` + /// Context: Transfer progress label: downloading. + /// + /// - en: Downloading + /// - fr: Téléchargement + /// - es: Descargando + /// - it: Download + /// - de: Wird geladen + /// - pt: A descarregar + /// - pl: Pobieranie + /// - nl: Downloaden + /// - ru: Загрузка + static let downloading: String.LocalizationValue = "progress_downloading" + /// Failed + /// + /// Key: `progress_failed` + /// Context: Transfer progress label: failed. + /// + /// - en: Failed + /// - fr: Échec + /// - es: Fallido + /// - it: Non riuscito + /// - de: Fehlgeschlagen + /// - pt: Falhou + /// - pl: Niepowodzenie + /// - nl: Mislukt + /// - ru: Ошибка + static let failed: String.LocalizationValue = "progress_failed" + /// Getting ready + /// + /// Key: `progress_getting_ready` + /// Context: Transfer progress label: getting ready. + /// + /// - en: Getting ready + /// - fr: Préparation + /// - es: Preparándose + /// - it: Preparazione + /// - de: Wird bereitgemacht + /// - pt: A preparar-se + /// - pl: Przygotowywanie + /// - nl: Klaarmaken + /// - ru: Подготовка + static let gettingReady: String.LocalizationValue = "progress_getting_ready" + /// Transfer interrupted + /// + /// Key: `progress_interrupted` + /// Context: Transfer progress label: the transfer was interrupted. + /// + /// - en: Transfer interrupted + /// - fr: Transfert interrompu + /// - es: Transferencia interrumpida + /// - it: Trasferimento interrotto + /// - de: Übertragung unterbrochen + /// - pt: Transferência interrompida + /// - pl: Transfer przerwany + /// - nl: Overdracht onderbroken + /// - ru: Передача прервана + static let interrupted: String.LocalizationValue = "progress_interrupted" + /// Preparing + /// + /// Key: `progress_preparing` + /// Context: Transfer progress label: preparing. + /// + /// - en: Preparing + /// - fr: Préparation + /// - es: Preparando + /// - it: Preparazione + /// - de: Wird vorbereitet + /// - pt: A preparar + /// - pl: Przygotowywanie + /// - nl: Voorbereiden + /// - ru: Подготовка + static let preparing: String.LocalizationValue = "progress_preparing" + /// Ready + /// + /// Key: `progress_ready` + /// Context: Transfer progress label: ready. + /// + /// - en: Ready + /// - fr: Prêt + /// - es: Listo + /// - it: Pronto + /// - de: Bereit + /// - pt: Pronto + /// - pl: Gotowe + /// - nl: Gereed + /// - ru: Готово + static let ready: String.LocalizationValue = "progress_ready" + /// Receiving + /// + /// Key: `progress_receiving` + /// Context: Transfer progress label: receiving. + /// + /// - en: Receiving + /// - fr: Réception + /// - es: Recibiendo + /// - it: Ricezione + /// - de: Wird empfangen + /// - pt: A receber + /// - pl: Odbieranie + /// - nl: Ontvangen + /// - ru: Получение + static let receiving: String.LocalizationValue = "progress_receiving" + /// Requesting access + /// + /// Key: `progress_requesting_access` + /// Context: Transfer progress label: requesting access from the sender. + /// + /// - en: Requesting access + /// - fr: Demande d’accès + /// - es: Solicitando acceso + /// - it: Richiesta di accesso + /// - de: Zugriff wird angefragt + /// - pt: A pedir acesso + /// - pl: Prośba o dostęp + /// - nl: Toegang aanvragen + /// - ru: Запрос доступа + static let requestingAccess: String.LocalizationValue = "progress_requesting_access" + /// Saving + /// + /// Key: `progress_saving` + /// Context: Transfer progress label: saving received files. + /// + /// - en: Saving + /// - fr: Enregistrement + /// - es: Guardando + /// - it: Salvataggio + /// - de: Wird gesichert + /// - pt: A guardar + /// - pl: Zapisywanie + /// - nl: Bewaren + /// - ru: Сохранение + static let saving: String.LocalizationValue = "progress_saving" + /// Sending + /// + /// Key: `progress_sending` + /// Context: Transfer progress label: sending. + /// + /// - en: Sending + /// - fr: Envoi + /// - es: Enviando + /// - it: Invio + /// - de: Wird gesendet + /// - pt: A enviar + /// - pl: Wysyłanie + /// - nl: Versturen + /// - ru: Отправка + static let sending: String.LocalizationValue = "progress_sending" + /// Sending to {count} + /// + /// Key: `progress_sending_to_count` + /// Context: Transfer progress label: sending to N receivers. %lld = receiver count. + /// + /// - en: Sending to {count} + /// - fr: Envoi à {count} + /// - es: Enviando a {count} + /// - it: Invio a {count} + /// - de: Senden an {count} + /// - pt: A enviar para {count} + /// - pl: Wysyłanie do {count} + /// - nl: Versturen naar {count} + /// - ru: Отправка получателям: {count} + static func sendingToCount(count: Int) -> String { + String(format: String(localized: "progress_sending_to_count"), count) + } + /// Ready to share + /// + /// Key: `progress_share_ready` + /// Context: Transfer progress label: the share is ready. + /// + /// - en: Ready to share + /// - fr: Prêt à partager + /// - es: Listo para compartir + /// - it: Pronto per la condivisione + /// - de: Bereit zum Teilen + /// - pt: Pronto para partilhar + /// - pl: Gotowe do udostępnienia + /// - nl: Klaar om te delen + /// - ru: Готово к отправке + static let shareReady: String.LocalizationValue = "progress_share_ready" + /// Working… + /// + /// Key: `progress_working` + /// Context: Transfer progress label: generic working/in-progress state. + /// + /// - en: Working… + /// - fr: En cours… + /// - es: Trabajando… + /// - it: Elaborazione… + /// - de: Wird bearbeitet… + /// - pt: A processar… + /// - pl: Przetwarzanie… + /// - nl: Bezig… + /// - ru: Обработка… + static let working: String.LocalizationValue = "progress_working" + } + enum Receive { + /// Choose the invitation method available to you. + /// + /// Key: `receive_choose_method_body` + /// Context: Receive flow: body text prompting the user to pick an invitation method. + /// + /// - en: Choose the invitation method available to you. + /// - fr: Choisissez la méthode d’invitation à votre disposition. + /// - es: Elija el método de invitación de que disponga. + /// - it: Scelga il metodo di invito a sua disposizione. + /// - de: Wählen Sie die Ihnen zur Verfügung stehende Einladungsmethode. + /// - pt: Escolha o método de convite ao seu dispor. + /// - pl: Wybierz dostępną metodę zaproszenia. + /// - nl: Kies de uitnodigingsmethode die u ter beschikking staat. + /// - ru: Выберите доступный вам способ приглашения. + static let chooseMethodBody: String.LocalizationValue = "receive_choose_method_body" + /// How would you like to connect? + /// + /// Key: `receive_choose_method_title` + /// Context: Receive flow: title of the connect-method chooser. + /// + /// - en: How would you like to connect? + /// - fr: Comment souhaitez-vous vous connecter ? + /// - es: ¿Cómo quiere conectarse? + /// - it: Come vuole connettersi? + /// - de: Wie möchten Sie sich verbinden? + /// - pt: Como pretende ligar-se? + /// - pl: Jak chcesz się połączyć? + /// - nl: Hoe wilt u verbinding maken? + /// - ru: Как вы хотите подключиться? + static let chooseMethodTitle: String.LocalizationValue = "receive_choose_method_title" + /// Clear history + /// + /// Key: `receive_clear_history` + /// Context: Receive history: action to clear all history. + /// + /// - en: Clear history + /// - fr: Effacer l’historique + /// - es: Borrar historial + /// - it: Cancella cronologia + /// - de: Verlauf löschen + /// - pt: Limpar histórico + /// - pl: Wyczyść historię + /// - nl: Geschiedenis wissen + /// - ru: Очистить историю + static let clearHistory: String.LocalizationValue = "receive_clear_history" + /// All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device. + /// + /// Key: `receive_clear_history_description` + /// Context: Receive history: confirmation dialog body for clearing all history. + /// + /// - en: All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device. + /// - fr: Toutes les réceptions terminées, échouées et annulées seront retirées de l’historique de VniDrop. Les fichiers téléchargés resteront sur cet appareil. + /// - es: Todas las recepciones completadas, fallidas y canceladas se eliminarán del historial de VniDrop. Los archivos descargados permanecerán en este dispositivo. + /// - it: Tutte le ricezioni completate, non riuscite e annullate verranno rimosse dalla cronologia di VniDrop. I file scaricati rimarranno su questo dispositivo. + /// - de: Alle abgeschlossenen, fehlgeschlagenen und abgebrochenen Empfänge werden aus dem Verlauf von VniDrop entfernt. Heruntergeladene Dateien verbleiben auf diesem Gerät. + /// - pt: Todas as receções concluídas, falhadas e canceladas serão removidas do histórico do VniDrop. Os ficheiros descarregados permanecerão neste dispositivo. + /// - pl: Wszystkie ukończone, nieudane i anulowane odbiory zostaną usunięte z historii VniDrop. Pobrane pliki pozostaną na tym urządzeniu. + /// - nl: Alle voltooide, mislukte en geannuleerde ontvangsten worden uit de geschiedenis van VniDrop verwijderd. Gedownloade bestanden blijven op dit apparaat. + /// - ru: Все завершённые, неудачные и отменённые получения будут удалены из истории VniDrop. Загруженные файлы останутся на этом устройстве. + static let clearHistoryDescription: String.LocalizationValue = "receive_clear_history_description" + /// Clear receive history? + /// + /// Key: `receive_clear_history_title` + /// Context: Receive history: confirmation dialog title for clearing all history. + /// + /// - en: Clear receive history? + /// - fr: Effacer l’historique de réception ? + /// - es: ¿Borrar el historial de recepción? + /// - it: Cancellare la cronologia di ricezione? + /// - de: Empfangsverlauf löschen? + /// - pt: Limpar o histórico de receção? + /// - pl: Wyczyścić historię odbioru? + /// - nl: Ontvangstgeschiedenis wissen? + /// - ru: Очистить историю получения? + static let clearHistoryTitle: String.LocalizationValue = "receive_clear_history_title" + /// Transfer received. + /// + /// Key: `receive_completed` + /// Context: Receive flow: toast/message when a transfer finishes downloading. + /// + /// - en: Transfer received. + /// - fr: Transfert reçu. + /// - es: Transferencia recibida. + /// - it: Trasferimento ricevuto. + /// - de: Übertragung empfangen. + /// - pt: Transferência recebida. + /// - pl: Transfer odebrany. + /// - nl: Overdracht ontvangen. + /// - ru: Передача получена. + static let completed: String.LocalizationValue = "receive_completed" + /// “{transferName}” will be removed from VniDrop’s history. The downloaded file will remain on this device. + /// + /// Key: `receive_delete_history_description` + /// Context: Receive history: confirmation body for removing one item. {transferName} = transfer name. + /// + /// - en: “{transferName}” will be removed from VniDrop’s history. The downloaded file will remain on this device. + /// - fr: « {transferName} » sera retiré de l’historique de VniDrop. Le fichier téléchargé restera sur cet appareil. + /// - es: «{transferName}» se eliminará del historial de VniDrop. El archivo descargado permanecerá en este dispositivo. + /// - it: «{transferName}» verrà rimosso dalla cronologia di VniDrop. Il file scaricato rimarrà su questo dispositivo. + /// - de: „{transferName}“ wird aus dem Verlauf von VniDrop entfernt. Die heruntergeladene Datei verbleibt auf diesem Gerät. + /// - pt: «{transferName}» será removido do histórico do VniDrop. O ficheiro descarregado permanecerá neste dispositivo. + /// - pl: „{transferName}” zostanie usunięty z historii VniDrop. Pobrany plik pozostanie na tym urządzeniu. + /// - nl: ‘{transferName}’ wordt uit de geschiedenis van VniDrop verwijderd. Het gedownloade bestand blijft op dit apparaat. + /// - ru: «{transferName}» будет удалён из истории VniDrop. Загруженный файл останется на этом устройстве. + static func deleteHistoryDescription(transferName: String) -> String { + String(format: String(localized: "receive_delete_history_description"), transferName) + } + /// Delete from receive history + /// + /// Key: `receive_delete_history_item` + /// Context: Receive history: swipe/menu action to delete a single history item. + /// + /// - en: Delete from receive history + /// - fr: Supprimer de l’historique de réception + /// - es: Eliminar del historial de recepción + /// - it: Elimina dalla cronologia di ricezione + /// - de: Aus Empfangsverlauf löschen + /// - pt: Eliminar do histórico de receção + /// - pl: Usuń z historii odbioru + /// - nl: Uit ontvangstgeschiedenis verwijderen + /// - ru: Удалить из истории получения + static let deleteHistoryItem: String.LocalizationValue = "receive_delete_history_item" + /// Remove from history? + /// + /// Key: `receive_delete_history_title` + /// Context: Receive history: confirmation title for removing one item. + /// + /// - en: Remove from history? + /// - fr: Retirer de l’historique ? + /// - es: ¿Quitar del historial? + /// - it: Rimuovere dalla cronologia? + /// - de: Aus dem Verlauf entfernen? + /// - pt: Remover do histórico? + /// - pl: Usunąć z historii? + /// - nl: Uit geschiedenis verwijderen? + /// - ru: Удалить из истории? + static let deleteHistoryTitle: String.LocalizationValue = "receive_delete_history_title" + /// Open a VniDrop invitation, scan a QR code, or hold near an NFC tag. + /// + /// Key: `receive_empty_body` + /// Context: Receive tab empty state: instructions on how to receive. + /// + /// - en: Open a VniDrop invitation, scan a QR code, or hold near an NFC tag. + /// - fr: Ouvrez une invitation VniDrop, scannez un QR code ou approchez un tag NFC. + /// - es: Abra una invitación de VniDrop, escanee un código QR o acerque una etiqueta NFC. + /// - it: Apra un invito VniDrop, scansioni un codice QR o avvicini un tag NFC. + /// - de: Öffnen Sie eine VniDrop-Einladung, scannen Sie einen QR-Code oder halten Sie das Gerät an ein NFC-Tag. + /// - pt: Abra um convite do VniDrop, leia um código QR ou aproxime uma etiqueta NFC. + /// - pl: Otwórz zaproszenie VniDrop, zeskanuj kod QR lub zbliż tag NFC. + /// - nl: Open een VniDrop-uitnodiging, scan een QR-code of houd het apparaat bij een NFC-tag. + /// - ru: Откройте приглашение VniDrop, отсканируйте QR-код или поднесите NFC-метку. + static let emptyBody: String.LocalizationValue = "receive_empty_body" + /// Nothing received yet + /// + /// Key: `receive_empty_title` + /// Context: Receive tab empty state: title when nothing has been received. + /// + /// - en: Nothing received yet + /// - fr: Rien reçu pour l’instant + /// - es: Aún no se ha recibido nada + /// - it: Ancora nulla di ricevuto + /// - de: Noch nichts empfangen + /// - pt: Ainda não recebeu nada + /// - pl: Nic jeszcze nie odebrano + /// - nl: Nog niets ontvangen + /// - ru: Пока ничего не получено + static let emptyTitle: String.LocalizationValue = "receive_empty_title" + /// Receive history cleared. + /// + /// Key: `receive_history_cleared` + /// Context: Receive history: toast confirming history was cleared. + /// + /// - en: Receive history cleared. + /// - fr: Historique de réception effacé. + /// - es: Historial de recepción borrado. + /// - it: Cronologia di ricezione cancellata. + /// - de: Empfangsverlauf gelöscht. + /// - pt: Histórico de receção limpo. + /// - pl: Historia odbioru wyczyszczona. + /// - nl: Ontvangstgeschiedenis gewist. + /// - ru: История получения очищена. + static let historyCleared: String.LocalizationValue = "receive_history_cleared" + /// History + /// + /// Key: `receive_history_title` + /// Context: Receive tab: History section title. + /// + /// - en: History + /// - fr: Historique + /// - es: Historial + /// - it: Cronologia + /// - de: Verlauf + /// - pt: Histórico + /// - pl: Historia + /// - nl: Geschiedenis + /// - ru: История + static let historyTitle: String.LocalizationValue = "receive_history_title" + /// Open a .vnd invitation + /// + /// Key: `receive_method_file` + /// Context: Receive method: open a .vnd invitation file. + /// + /// - en: Open a .vnd invitation + /// - fr: Ouvrir une invitation .vnd + /// - es: Abrir una invitación .vnd + /// - it: Apri un invito .vnd + /// - de: Eine .vnd-Einladung öffnen + /// - pt: Abrir um convite .vnd + /// - pl: Otwórz zaproszenie .vnd + /// - nl: Een .vnd-uitnodiging openen + /// - ru: Открыть приглашение .vnd + static let methodFile: String.LocalizationValue = "receive_method_file" + /// Choose an invitation saved or shared to this device. + /// + /// Key: `receive_method_file_description` + /// Context: Receive method description: open a saved/shared invitation file. + /// + /// - en: Choose an invitation saved or shared to this device. + /// - fr: Choisissez une invitation enregistrée ou partagée sur cet appareil. + /// - es: Elija una invitación guardada o compartida en este dispositivo. + /// - it: Scelga un invito salvato o condiviso su questo dispositivo. + /// - de: Wählen Sie eine auf diesem Gerät gespeicherte oder geteilte Einladung. + /// - pt: Escolha um convite guardado ou partilhado neste dispositivo. + /// - pl: Wybierz zaproszenie zapisane lub udostępnione na tym urządzeniu. + /// - nl: Kies een uitnodiging die op dit apparaat is bewaard of gedeeld. + /// - ru: Выберите приглашение, сохранённое или отправленное на это устройство. + static let methodFileDescription: String.LocalizationValue = "receive_method_file_description" + /// Read NFC tag + /// + /// Key: `receive_method_nfc` + /// Context: Receive method: read an NFC tag. + /// + /// - en: Read NFC tag + /// - fr: Lire un tag NFC + /// - es: Leer etiqueta NFC + /// - it: Leggi tag NFC + /// - de: NFC-Tag lesen + /// - pt: Ler etiqueta NFC + /// - pl: Odczytaj tag NFC + /// - nl: NFC-tag lezen + /// - ru: Прочитать NFC-метку + static let methodNfc: String.LocalizationValue = "receive_method_nfc" + /// Hold this device near the sender’s invitation tag. + /// + /// Key: `receive_method_nfc_description` + /// Context: Receive method description: hold near the sender's NFC tag. + /// + /// - en: Hold this device near the sender’s invitation tag. + /// - fr: Approchez cet appareil du tag d’invitation de l’expéditeur. + /// - es: Acerque este dispositivo a la etiqueta de invitación del remitente. + /// - it: Avvicini questo dispositivo al tag di invito del mittente. + /// - de: Halten Sie dieses Gerät an das Einladungs-Tag des Absenders. + /// - pt: Aproxime este dispositivo da etiqueta de convite do remetente. + /// - pl: Zbliż to urządzenie do tagu zaproszenia nadawcy. + /// - nl: Houd dit apparaat bij de uitnodigingstag van de afzender. + /// - ru: Поднесите это устройство к метке приглашения отправителя. + static let methodNfcDescription: String.LocalizationValue = "receive_method_nfc_description" + /// Scan QR code + /// + /// Key: `receive_method_scan` + /// Context: Receive method: scan a QR code. + /// + /// - en: Scan QR code + /// - fr: Scanner un QR code + /// - es: Escanear código QR + /// - it: Scansiona codice QR + /// - de: QR-Code scannen + /// - pt: Ler código QR + /// - pl: Zeskanuj kod QR + /// - nl: QR-code scannen + /// - ru: Сканировать QR-код + static let methodScan: String.LocalizationValue = "receive_method_scan" + /// Use the camera to scan the sender’s VniDrop code. + /// + /// Key: `receive_method_scan_description` + /// Context: Receive method description: use the camera to scan the sender's code. + /// + /// - en: Use the camera to scan the sender’s VniDrop code. + /// - fr: Utilisez la caméra pour scanner le code VniDrop de l’expéditeur. + /// - es: Use la cámara para escanear el código de VniDrop del remitente. + /// - it: Usi la fotocamera per scansionare il codice VniDrop del mittente. + /// - de: Verwenden Sie die Kamera, um den VniDrop-Code des Absenders zu scannen. + /// - pt: Use a câmara para ler o código VniDrop do remetente. + /// - pl: Użyj aparatu, aby zeskanować kod VniDrop nadawcy. + /// - nl: Gebruik de camera om de VniDrop-code van de afzender te scannen. + /// - ru: Используйте камеру, чтобы отсканировать код VniDrop отправителя. + static let methodScanDescription: String.LocalizationValue = "receive_method_scan_description" + /// Transfers you’ve received on this device. + /// + /// Key: `receive_new_subtitle` + /// Context: Receive tab: subtitle under the title. + /// + /// - en: Transfers you’ve received on this device. + /// - fr: Transferts que vous avez reçus sur cet appareil. + /// - es: Transferencias que ha recibido en este dispositivo. + /// - it: Trasferimenti che ha ricevuto su questo dispositivo. + /// - de: Übertragungen, die Sie auf diesem Gerät empfangen haben. + /// - pt: Transferências que recebeu neste dispositivo. + /// - pl: Transfery odebrane na tym urządzeniu. + /// - nl: Overdrachten die u op dit apparaat hebt ontvangen. + /// - ru: Передачи, полученные на этом устройстве. + static let newSubtitle: String.LocalizationValue = "receive_new_subtitle" + /// Hold near the NFC tag… + /// + /// Key: `receive_nfc_waiting` + /// Context: Receive flow: prompt while waiting to read an NFC tag. + /// + /// - en: Hold near the NFC tag… + /// - fr: Approchez du tag NFC… + /// - es: Acerque a la etiqueta NFC… + /// - it: Avvicini al tag NFC… + /// - de: Halten Sie das Gerät an das NFC-Tag… + /// - pt: Aproxime da etiqueta NFC… + /// - pl: Zbliż do tagu NFC… + /// - nl: Houd bij de NFC-tag… + /// - ru: Поднесите к NFC-метке… + static let nfcWaiting: String.LocalizationValue = "receive_nfc_waiting" + /// Couldn’t open VniDrop in Files. + /// + /// Key: `receive_open_files_failed` + /// Context: Receive flow: error when opening VniDrop's folder in Files fails. + /// + /// - en: Couldn’t open VniDrop in Files. + /// - fr: Impossible d’ouvrir VniDrop dans Fichiers. + /// - es: No se pudo abrir VniDrop en Archivos. + /// - it: Impossibile aprire VniDrop in File. + /// - de: VniDrop konnte in „Dateien“ nicht geöffnet werden. + /// - pt: Não foi possível abrir o VniDrop em Ficheiros. + /// - pl: Nie udało się otworzyć VniDrop w Plikach. + /// - nl: VniDrop kon niet worden geopend in Bestanden. + /// - ru: Не удалось открыть VniDrop в Файлах. + static let openFilesFailed: String.LocalizationValue = "receive_open_files_failed" + /// Review transfer + /// + /// Key: `receive_review_title` + /// Context: Receive flow: title of the review screen before accepting a transfer. + /// + /// - en: Review transfer + /// - fr: Vérifier le transfert + /// - es: Revisar transferencia + /// - it: Rivedi trasferimento + /// - de: Übertragung prüfen + /// - pt: Rever transferência + /// - pl: Przejrzyj transfer + /// - nl: Overdracht controleren + /// - ru: Проверить передачу + static let reviewTitle: String.LocalizationValue = "receive_review_title" + /// Receive + /// + /// Key: `receive_title` + /// Context: Receive tab: screen title. + /// + /// - en: Receive + /// - fr: Recevoir + /// - es: Recibir + /// - it: Ricevi + /// - de: Empfangen + /// - pt: Receber + /// - pl: Odbierz + /// - nl: Ontvangen + /// - ru: Получить + static let title: String.LocalizationValue = "receive_title" + /// VniDrop transfer + /// + /// Key: `receive_unknown_transfer` + /// Context: Receive flow: fallback name for a transfer with no title. + /// + /// - en: VniDrop transfer + /// - fr: Transfert VniDrop + /// - es: Transferencia de VniDrop + /// - it: Trasferimento VniDrop + /// - de: VniDrop-Übertragung + /// - pt: Transferência VniDrop + /// - pl: Transfer VniDrop + /// - nl: VniDrop-overdracht + /// - ru: Передача VniDrop + static let unknownTransfer: String.LocalizationValue = "receive_unknown_transfer" + } + enum Send { + /// Anyone with this transfer + /// + /// Key: `send_access_anyone` + /// Context: Send access option: anyone with the invitation can receive. + /// + /// - en: Anyone with this transfer + /// - fr: Toute personne disposant de ce transfert + /// - es: Cualquiera que tenga esta transferencia + /// - it: Chiunque abbia questo trasferimento + /// - de: Jeder mit dieser Übertragung + /// - pt: Qualquer pessoa com esta transferência + /// - pl: Każdy, kto ma ten transfer + /// - nl: Iedereen met deze overdracht + /// - ru: Любой, у кого есть эта передача + static let accessAnyone: String.LocalizationValue = "send_access_anyone" + /// No approval is required. Only use this for items you are comfortable sharing. + /// + /// Key: `send_access_anyone_description` + /// Context: Send access option description: no approval required. + /// + /// - en: No approval is required. Only use this for items you are comfortable sharing. + /// - fr: Aucune approbation requise. À n’utiliser que pour des éléments que vous êtes à l’aise de partager. + /// - es: No se requiere aprobación. Úselo solo para elementos que no le importe compartir. + /// - it: Nessuna approvazione richiesta. Da usare solo per elementi che non ha problemi a condividere. + /// - de: Keine Genehmigung erforderlich. Verwenden Sie dies nur für Objekte, die Sie unbedenklich teilen können. + /// - pt: Não é necessária aprovação. Utilize apenas para itens que não se importe de partilhar. + /// - pl: Nie jest wymagane zatwierdzenie. Używaj tylko dla elementów, które możesz swobodnie udostępniać. + /// - nl: Geen goedkeuring vereist. Gebruik dit alleen voor items die u gerust kunt delen. + /// - ru: Одобрение не требуется. Используйте только для объектов, которыми вы готовы поделиться. + static let accessAnyoneDescription: String.LocalizationValue = "send_access_anyone_description" + /// Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items. + /// + /// Key: `send_access_anyone_warning` + /// Context: Send access option warning: caution about open access. + /// + /// - en: Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items. + /// - fr: Toute personne disposant de l’invitation peut télécharger jusqu’à ce que vous arrêtiez le partage. Ne l’utilisez pas pour des éléments privés ou sensibles. + /// - es: Cualquiera que tenga la invitación puede descargar hasta que deje de compartir. No lo use para elementos privados o sensibles. + /// - it: Chiunque abbia l’invito può scaricare finché non interrompe la condivisione. Non lo usi per elementi privati o sensibili. + /// - de: Jeder mit der Einladung kann herunterladen, bis Sie die Freigabe beenden. Verwenden Sie dies nicht für private oder sensible Objekte. + /// - pt: Qualquer pessoa com o convite pode descarregar até parar de partilhar. Não utilize para itens privados ou sensíveis. + /// - pl: Każdy, kto ma zaproszenie, może pobierać, dopóki nie zatrzymasz udostępniania. Nie używaj tego dla prywatnych ani wrażliwych elementów. + /// - nl: Iedereen met de uitnodiging kan downloaden totdat u stopt met delen. Gebruik dit niet voor privé- of gevoelige items. + /// - ru: Любой, у кого есть приглашение, может загружать, пока вы не остановите общий доступ. Не используйте это для личных или конфиденциальных объектов. + static let accessAnyoneWarning: String.LocalizationValue = "send_access_anyone_warning" + /// Ask before each download + /// + /// Key: `send_access_approval` + /// Context: Send access option: approve each receiver. + /// + /// - en: Ask before each download + /// - fr: Demander avant chaque téléchargement + /// - es: Preguntar antes de cada descarga + /// - it: Chiedi prima di ogni download + /// - de: Vor jedem Download fragen + /// - pt: Perguntar antes de cada descarga + /// - pl: Pytaj przed każdym pobraniem + /// - nl: Vragen vóór elke download + /// - ru: Спрашивать перед каждой загрузкой + static let accessApproval: String.LocalizationValue = "send_access_approval" + /// You approve or refuse every new receiver. + /// + /// Key: `send_access_approval_description` + /// Context: Send access option description: you approve/refuse each receiver. + /// + /// - en: You approve or refuse every new receiver. + /// - fr: Vous approuvez ou refusez chaque nouveau destinataire. + /// - es: Usted aprueba o rechaza a cada nuevo destinatario. + /// - it: Approva o rifiuta ogni nuovo destinatario. + /// - de: Sie genehmigen oder lehnen jeden neuen Empfänger ab. + /// - pt: Aprova ou recusa cada novo destinatário. + /// - pl: Zatwierdzasz lub odrzucasz każdego nowego odbiorcę. + /// - nl: U keurt elke nieuwe ontvanger goed of weigert deze. + /// - ru: Вы одобряете или отклоняете каждого нового получателя. + static let accessApprovalDescription: String.LocalizationValue = "send_access_approval_description" + /// Who can receive it? + /// + /// Key: `send_access_title` + /// Context: Send flow: heading for the who-can-receive access chooser. + /// + /// - en: Who can receive it? + /// - fr: Qui peut le recevoir ? + /// - es: ¿Quién puede recibirla? + /// - it: Chi può riceverlo? + /// - de: Wer kann sie empfangen? + /// - pt: Quem a pode receber? + /// - pl: Kto może to odebrać? + /// - nl: Wie kan het ontvangen? + /// - ru: Кто может это получить? + static let accessTitle: String.LocalizationValue = "send_access_title" + /// Select files or a folder from this device. You can review the selection before creating the transfer. + /// + /// Key: `send_choose_file_body` + /// Context: Send flow: body text for the choose-what-to-share step. + /// + /// - en: Select files or a folder from this device. You can review the selection before creating the transfer. + /// - fr: Sélectionnez des fichiers ou un dossier sur cet appareil. Vous pourrez vérifier la sélection avant de créer le transfert. + /// - es: Seleccione archivos o una carpeta de este dispositivo. Podrá revisar la selección antes de crear la transferencia. + /// - it: Selezioni file o una cartella da questo dispositivo. Potrà rivedere la selezione prima di creare il trasferimento. + /// - de: Wählen Sie Dateien oder einen Ordner auf diesem Gerät aus. Sie können die Auswahl überprüfen, bevor Sie die Übertragung erstellen. + /// - pt: Selecione ficheiros ou uma pasta deste dispositivo. Poderá rever a seleção antes de criar a transferência. + /// - pl: Wybierz pliki lub folder z tego urządzenia. Przed utworzeniem transferu możesz przejrzeć wybór. + /// - nl: Selecteer bestanden of een map op dit apparaat. U kunt de selectie controleren voordat u de overdracht aanmaakt. + /// - ru: Выберите файлы или папку на этом устройстве. Вы сможете проверить выбор перед созданием передачи. + static let chooseFileBody: String.LocalizationValue = "send_choose_file_body" + /// Choose what to share + /// + /// Key: `send_choose_file_title` + /// Context: Send flow: title of the choose-what-to-share step. + /// + /// - en: Choose what to share + /// - fr: Choisissez quoi partager + /// - es: Elija qué compartir + /// - it: Scelga cosa condividere + /// - de: Wählen Sie, was Sie teilen möchten + /// - pt: Escolha o que partilhar + /// - pl: Wybierz, co udostępnić + /// - nl: Kies wat u wilt delen + /// - ru: Выберите, чем поделиться + static let chooseFileTitle: String.LocalizationValue = "send_choose_file_title" + /// Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file. + /// + /// Key: `send_empty_body` + /// Context: Send tab empty state: instructions on how to create a transfer. + /// + /// - en: Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file. + /// - fr: Créez un transfert, décidez qui peut le recevoir, puis invitez-les avec un QR code, un tag NFC ou un fichier d’invitation. + /// - es: Cree una transferencia, decida quién puede recibirla y luego invite a esas personas con un código QR, una etiqueta NFC o un archivo de invitación. + /// - it: Crei un trasferimento, decida chi può riceverlo, poi inviti queste persone con un codice QR, un tag NFC o un file di invito. + /// - de: Erstellen Sie eine Übertragung, entscheiden Sie, wer sie empfangen darf, und laden Sie diese Personen dann mit einem QR-Code, einem NFC-Tag oder einer Einladungsdatei ein. + /// - pt: Crie uma transferência, decida quem a pode receber e depois convide essas pessoas com um código QR, uma etiqueta NFC ou um ficheiro de convite. + /// - pl: Utwórz transfer, zdecyduj, kto może go odebrać, a następnie zaproś te osoby za pomocą kodu QR, tagu NFC lub pliku zaproszenia. + /// - nl: Maak een overdracht aan, bepaal wie deze kan ontvangen en nodig die personen vervolgens uit met een QR-code, NFC-tag of uitnodigingsbestand. + /// - ru: Создайте передачу, решите, кто может её получить, затем пригласите этих людей с помощью QR-кода, NFC-метки или файла приглашения. + static let emptyBody: String.LocalizationValue = "send_empty_body" + /// Nothing shared yet + /// + /// Key: `send_empty_title` + /// Context: Send tab empty state: title when nothing has been shared. + /// + /// - en: Nothing shared yet + /// - fr: Rien partagé pour l’instant + /// - es: Aún no se ha compartido nada + /// - it: Ancora nulla di condiviso + /// - de: Noch nichts geteilt + /// - pt: Ainda não partilhou nada + /// - pl: Nic jeszcze nie udostępniono + /// - nl: Nog niets gedeeld + /// - ru: Пока ничем не поделились + static let emptyTitle: String.LocalizationValue = "send_empty_title" + /// Size unavailable + /// + /// Key: `send_file_size_unknown` + /// Context: Send flow: shown when a selected file's size can't be determined. + /// + /// - en: Size unavailable + /// - fr: Taille indisponible + /// - es: Tamaño no disponible + /// - it: Dimensione non disponibile + /// - de: Größe nicht verfügbar + /// - pt: Tamanho indisponível + /// - pl: Rozmiar niedostępny + /// - nl: Grootte niet beschikbaar + /// - ru: Размер недоступен + static let fileSizeUnknown: String.LocalizationValue = "send_file_size_unknown" + /// Folder + /// + /// Key: `send_folder_label` + /// Context: Send flow: label indicating a selected item is a folder. + /// + /// - en: Folder + /// - fr: Dossier + /// - es: Carpeta + /// - it: Cartella + /// - de: Ordner + /// - pt: Pasta + /// - pl: Folder + /// - nl: Map + /// - ru: Папка + static let folderLabel: String.LocalizationValue = "send_folder_label" + /// Create a new transfer + /// + /// Key: `send_new_transfer_description` + /// Context: Send flow: description for the create-new-transfer entry. + /// + /// - en: Create a new transfer + /// - fr: Créer un nouveau transfert + /// - es: Crear una nueva transferencia + /// - it: Crea un nuovo trasferimento + /// - de: Eine neue Übertragung erstellen + /// - pt: Criar uma nova transferência + /// - pl: Utwórz nowy transfer + /// - nl: Een nieuwe overdracht aanmaken + /// - ru: Создать новую передачу + static let newTransferDescription: String.LocalizationValue = "send_new_transfer_description" + /// New transfer + /// + /// Key: `send_new_transfer_title` + /// Context: Send flow: title for the new-transfer step. + /// + /// - en: New transfer + /// - fr: Nouveau transfert + /// - es: Nueva transferencia + /// - it: Nuovo trasferimento + /// - de: Neue Übertragung + /// - pt: Nova transferência + /// - pl: Nowy transfer + /// - nl: Nieuwe overdracht + /// - ru: Новая передача + static let newTransferTitle: String.LocalizationValue = "send_new_transfer_title" + /// Review transfer + /// + /// Key: `send_review_title` + /// Context: Send flow: title of the review-transfer step before creating it. + /// + /// - en: Review transfer + /// - fr: Vérifier le transfert + /// - es: Revisar transferencia + /// - it: Rivedi trasferimento + /// - de: Übertragung prüfen + /// - pt: Rever transferência + /// - pl: Przejrzyj transfer + /// - nl: Overdracht controleren + /// - ru: Проверить передачу + static let reviewTitle: String.LocalizationValue = "send_review_title" + /// {count} files selected + /// + /// Key: `send_selected_files_count` + /// Context: Send flow: count of files chosen. {count} = selected files. NOTE: singular case reads '1 files' — should become a plural. + /// + /// - en: {count} files selected + /// - fr: {count} fichiers sélectionnés + /// - es: {count} archivos seleccionados + /// - it: {count} file selezionati + /// - de: {count} Dateien ausgewählt + /// - pt: {count} ficheiros selecionados + /// - pl: Wybrane pliki: {count} + /// - nl: {count} bestanden geselecteerd + /// - ru: Выбрано файлов: {count} + static func selectedFilesCount(count: Int) -> String { + String(format: String(localized: "send_selected_files_count"), count) + } + /// Stop sharing + /// + /// Key: `send_stop_sharing` + /// Context: Transfer details: action to stop sharing a transfer. + /// + /// - en: Stop sharing + /// - fr: Arrêter le partage + /// - es: Dejar de compartir + /// - it: Interrompi condivisione + /// - de: Freigabe beenden + /// - pt: Parar de partilhar + /// - pl: Zatrzymaj udostępnianie + /// - nl: Stoppen met delen + /// - ru: Остановить общий доступ + static let stopSharing: String.LocalizationValue = "send_stop_sharing" + /// This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped. + /// + /// Key: `send_stop_sharing_description` + /// Context: Transfer details: confirmation body for stopping sharing. + /// + /// - en: This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped. + /// - fr: Cela arrête le transfert et interrompt toute personne en train de le télécharger. Il reste dans votre historique en tant qu’Arrêté. + /// - es: Esto detiene la transferencia e interrumpe a quien esté descargándola. Permanece en su historial como Detenida. + /// - it: Questo interrompe il trasferimento e blocca chiunque lo stia scaricando. Rimane nella sua cronologia come Interrotto. + /// - de: Dies beendet die Übertragung und unterbricht alle, die sie gerade herunterladen. Sie verbleibt in Ihrem Verlauf als „Beendet“. + /// - pt: Isto para a transferência e interrompe quem estiver a descarregá-la. Permanece no seu histórico como Parada. + /// - pl: To zatrzymuje transfer i przerywa każdego, kto go właśnie pobiera. Pozostaje w historii jako Zatrzymany. + /// - nl: Hiermee stopt de overdracht en wordt iedereen die deze op dit moment downloadt onderbroken. De overdracht blijft in uw geschiedenis staan als Gestopt. + /// - ru: Это остановит передачу и прервёт всех, кто сейчас её загружает. Она останется в вашей истории со статусом «Остановлена». + static let stopSharingDescription: String.LocalizationValue = "send_stop_sharing_description" + /// Transfers you’re sharing from this device. + /// + /// Key: `send_subtitle` + /// Context: Send tab: subtitle under the title. + /// + /// - en: Transfers you’re sharing from this device. + /// - fr: Transferts que vous partagez depuis cet appareil. + /// - es: Transferencias que está compartiendo desde este dispositivo. + /// - it: Trasferimenti che sta condividendo da questo dispositivo. + /// - de: Übertragungen, die Sie von diesem Gerät aus teilen. + /// - pt: Transferências que está a partilhar a partir deste dispositivo. + /// - pl: Transfery udostępniane z tego urządzenia. + /// - nl: Overdrachten die u vanaf dit apparaat deelt. + /// - ru: Передачи, которыми вы делитесь с этого устройства. + static let subtitle: String.LocalizationValue = "send_subtitle" + /// Send + /// + /// Key: `send_title` + /// Context: Send tab: screen title. + /// + /// - en: Send + /// - fr: Envoyer + /// - es: Enviar + /// - it: Invia + /// - de: Senden + /// - pt: Enviar + /// - pl: Wyślij + /// - nl: Versturen + /// - ru: Отправить + static let title: String.LocalizationValue = "send_title" + /// Transfer created. + /// + /// Key: `send_transfer_created` + /// Context: Send flow: toast confirming a transfer was created. + /// + /// - en: Transfer created. + /// - fr: Transfert créé. + /// - es: Transferencia creada. + /// - it: Trasferimento creato. + /// - de: Übertragung erstellt. + /// - pt: Transferência criada. + /// - pl: Transfer utworzony. + /// - nl: Overdracht aangemaakt. + /// - ru: Передача создана. + static let transferCreated: String.LocalizationValue = "send_transfer_created" + /// Transfer details + /// + /// Key: `send_transfer_details_title` + /// Context: Send flow: title of the transfer details screen. + /// + /// - en: Transfer details + /// - fr: Détails du transfert + /// - es: Detalles de la transferencia + /// - it: Dettagli del trasferimento + /// - de: Übertragungsdetails + /// - pt: Detalhes da transferência + /// - pl: Szczegóły transferu + /// - nl: Overdrachtsdetails + /// - ru: Сведения о передаче + static let transferDetailsTitle: String.LocalizationValue = "send_transfer_details_title" + /// Your transfers + /// + /// Key: `send_transfers_title` + /// Context: Send tab: 'Your transfers' list heading. + /// + /// - en: Your transfers + /// - fr: Vos transferts + /// - es: Sus transferencias + /// - it: I suoi trasferimenti + /// - de: Ihre Übertragungen + /// - pt: As suas transferências + /// - pl: Twoje transfery + /// - nl: Uw overdrachten + /// - ru: Ваши передачи + static let transfersTitle: String.LocalizationValue = "send_transfers_title" + } + enum Settings { + /// Your name, where transfers are saved, appearance, and notifications. + /// + /// Key: `settings_subtitle` + /// Context: Settings screen: subtitle summarizing what's configurable. + /// + /// - en: Your name, where transfers are saved, appearance, and notifications. + /// - fr: Votre nom, l’emplacement d’enregistrement des transferts, l’apparence et les notifications. + /// - es: Su nombre, dónde se guardan las transferencias, la apariencia y las notificaciones. + /// - it: Il suo nome, dove vengono salvati i trasferimenti, l’aspetto e le notifiche. + /// - de: Ihr Name, wo Übertragungen gesichert werden, Darstellung und Mitteilungen. + /// - pt: O seu nome, onde as transferências são guardadas, o aspeto e as notificações. + /// - pl: Twoja nazwa, miejsce zapisu transferów, wygląd i powiadomienia. + /// - nl: Uw naam, waar overdrachten worden bewaard, weergave en meldingen. + /// - ru: Ваше имя, место сохранения передач, оформление и уведомления. + static let subtitle: String.LocalizationValue = "settings_subtitle" + /// Settings + /// + /// Key: `settings_title` + /// Context: Settings screen: title. + /// + /// - en: Settings + /// - fr: Réglages + /// - es: Ajustes + /// - it: Impostazioni + /// - de: Einstellungen + /// - pt: Definições + /// - pl: Ustawienia + /// - nl: Instellingen + /// - ru: Настройки + static let title: String.LocalizationValue = "settings_title" + } + enum Snackbar { + /// Dismiss + /// + /// Key: `snackbar_dismiss` + /// Context: Snackbar: accessibility label / action to dismiss the snackbar. + /// + /// - en: Dismiss + /// - fr: Ignorer + /// - es: Descartar + /// - it: Ignora + /// - de: Ausblenden + /// - pt: Ignorar + /// - pl: Zamknij + /// - nl: Sluiten + /// - ru: Закрыть + static let dismiss: String.LocalizationValue = "snackbar_dismiss" + } + enum Status { + /// Available + /// + /// Key: `status_available` + /// Context: Transfer status: available (actively shared). + /// + /// - en: Available + /// - fr: Disponible + /// - es: Disponible + /// - it: Disponibile + /// - de: Verfügbar + /// - pt: Disponível + /// - pl: Dostępny + /// - nl: Beschikbaar + /// - ru: Доступно + static let available: String.LocalizationValue = "status_available" + /// Cancelled + /// + /// Key: `status_cancelled` + /// Context: Transfer status: cancelled. + /// + /// - en: Cancelled + /// - fr: Annulé + /// - es: Cancelado + /// - it: Annullato + /// - de: Abgebrochen + /// - pt: Cancelado + /// - pl: Anulowany + /// - nl: Geannuleerd + /// - ru: Отменено + static let cancelled: String.LocalizationValue = "status_cancelled" + /// Completed + /// + /// Key: `status_completed` + /// Context: Transfer status: completed. + /// + /// - en: Completed + /// - fr: Terminé + /// - es: Completado + /// - it: Completato + /// - de: Abgeschlossen + /// - pt: Concluído + /// - pl: Ukończony + /// - nl: Voltooid + /// - ru: Завершено + static let completed: String.LocalizationValue = "status_completed" + /// Failed + /// + /// Key: `status_failed` + /// Context: Transfer status: failed. + /// + /// - en: Failed + /// - fr: Échec + /// - es: Fallido + /// - it: Non riuscito + /// - de: Fehlgeschlagen + /// - pt: Falhou + /// - pl: Niepowodzenie + /// - nl: Mislukt + /// - ru: Ошибка + static let failed: String.LocalizationValue = "status_failed" + /// Preparing + /// + /// Key: `status_preparing` + /// Context: Transfer status: preparing. + /// + /// - en: Preparing + /// - fr: Préparation + /// - es: Preparando + /// - it: Preparazione + /// - de: Wird vorbereitet + /// - pt: A preparar + /// - pl: Przygotowywanie + /// - nl: Voorbereiden + /// - ru: Подготовка + static let preparing: String.LocalizationValue = "status_preparing" + /// Receiving + /// + /// Key: `status_receiving` + /// Context: Transfer status: receiving. + /// + /// - en: Receiving + /// - fr: Réception + /// - es: Recibiendo + /// - it: Ricezione + /// - de: Wird empfangen + /// - pt: A receber + /// - pl: Odbieranie + /// - nl: Ontvangen + /// - ru: Получение + static let receiving: String.LocalizationValue = "status_receiving" + /// Stopped + /// + /// Key: `status_stopped` + /// Context: Transfer status: stopped. + /// + /// - en: Stopped + /// - fr: Arrêté + /// - es: Detenido + /// - it: Interrotto + /// - de: Beendet + /// - pt: Parada + /// - pl: Zatrzymany + /// - nl: Gestopt + /// - ru: Остановлено + static let stopped: String.LocalizationValue = "status_stopped" + } + enum Storage { + /// Calculating… + /// + /// Key: `storage_calculating` + /// Context: Settings > Storage: placeholder while a size is being calculated. + /// + /// - en: Calculating… + /// - fr: Calcul… + /// - es: Calculando… + /// - it: Calcolo… + /// - de: Wird berechnet… + /// - pt: A calcular… + /// - pl: Obliczanie… + /// - nl: Berekenen… + /// - ru: Вычисление… + static let calculating: String.LocalizationValue = "storage_calculating" + /// Delete all transfers + /// + /// Key: `storage_delete_transfers` + /// Context: Settings > Storage: button to delete all transfer records. + /// + /// - en: Delete all transfers + /// - fr: Supprimer tous les transferts + /// - es: Eliminar todas las transferencias + /// - it: Elimina tutti i trasferimenti + /// - de: Alle Übertragungen löschen + /// - pt: Eliminar todas as transferências + /// - pl: Usuń wszystkie transfery + /// - nl: Alle overdrachten verwijderen + /// - ru: Удалить все передачи + static let deleteTransfers: String.LocalizationValue = "storage_delete_transfers" + /// This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone. + /// + /// Key: `storage_delete_transfers_description` + /// Context: Settings > Storage: confirmation body for deleting all transfer records. + /// + /// - en: This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone. + /// - fr: Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible. + /// - es: Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer. + /// - it: Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata. + /// - de: Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden. + /// - pt: Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada. + /// - pl: Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć. + /// - nl: Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt. + /// - ru: Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить. + static let deleteTransfersDescription: String.LocalizationValue = "storage_delete_transfers_description" + /// App data + /// + /// Key: `storage_app_data` + /// Context: Settings > Storage: label for non-transfer application data. + /// + /// - en: App data + /// - fr: Données de l’app + /// - es: Datos de la aplicación + /// - it: Dati dell’app + /// - de: App-Daten + /// - pt: Dados da aplicação + /// - pl: Dane aplikacji + /// - nl: Appgegevens + /// - ru: Данные приложения + static let appData: String.LocalizationValue = "storage_app_data" + /// Deleting… + /// + /// Key: `storage_deleting` + /// Context: Settings > Storage: progress label while transfers are being deleted. + /// + /// - en: Deleting… + /// - fr: Suppression… + /// - es: Eliminando… + /// - it: Eliminazione… + /// - de: Wird gelöscht… + /// - pt: A eliminar… + /// - pl: Usuwanie… + /// - nl: Verwijderen… + /// - ru: Удаление… + static let deleting: String.LocalizationValue = "storage_deleting" + /// Transfer data includes your history and cached content for active shares. Unneeded cache is reclaimed automatically after transfer records are removed, which may take a little time. Received files are tracked for this summary but are never deleted here. + /// + /// Key: `storage_footer` + /// Context: Settings > Storage: footer explaining how storage is managed. + /// + /// - en: Transfer data includes your history and cached content for active shares. Unneeded cache is reclaimed automatically after transfer records are removed, which may take a little time. Received files are tracked for this summary but are never deleted here. + /// - fr: Les données de transfert comprennent votre historique et le contenu mis en cache pour les partages actifs. Le cache inutile est récupéré automatiquement après la suppression des enregistrements, ce qui peut prendre un peu de temps. Les fichiers reçus sont suivis pour ce récapitulatif, mais ne sont jamais supprimés ici. + /// - es: Los datos de transferencia incluyen su historial y el contenido en caché de los recursos compartidos activos. La caché innecesaria se recupera automáticamente tras eliminar los registros, lo que puede tardar un poco. Los archivos recibidos se registran para este resumen, pero nunca se eliminan aquí. + /// - it: I dati di trasferimento includono la cronologia e il contenuto nella cache per le condivisioni attive. La cache non necessaria viene recuperata automaticamente dopo la rimozione dei record, operazione che può richiedere un po’ di tempo. I file ricevuti vengono monitorati per questo riepilogo, ma non sono mai eliminati qui. + /// - de: Übertragungsdaten umfassen Ihren Verlauf und zwischengespeicherte Inhalte aktiver Freigaben. Nicht mehr benötigter Cache wird nach dem Löschen der Datensätze automatisch bereinigt; dies kann etwas dauern. Empfangene Dateien werden für diese Übersicht erfasst, aber hier niemals gelöscht. + /// - pt: Os dados de transferência incluem o histórico e o conteúdo em cache das partilhas ativas. A cache desnecessária é recuperada automaticamente após a remoção dos registos, o que pode demorar algum tempo. Os ficheiros recebidos são acompanhados para este resumo, mas nunca são eliminados aqui. + /// - pl: Dane transferu obejmują historię oraz zawartość w pamięci podręcznej dla aktywnych udostępnień. Niepotrzebna pamięć podręczna jest odzyskiwana automatycznie po usunięciu rekordów, co może chwilę potrwać. Odebrane pliki są śledzone na potrzeby tego podsumowania, ale nigdy nie są tu usuwane. + /// - nl: Overdrachtsgegevens omvatten uw geschiedenis en inhoud in de cache voor actieve shares. Onnodige cache wordt automatisch opgeruimd nadat overdrachtsrecords zijn verwijderd; dit kan enige tijd duren. Ontvangen bestanden worden voor dit overzicht bijgehouden, maar hier nooit verwijderd. + /// - ru: Данные передачи включают историю и кэшированное содержимое активных раздач. Ненужный кэш освобождается автоматически после удаления записей; это может занять некоторое время. Полученные файлы учитываются в этой сводке, но никогда не удаляются здесь. + static let footer: String.LocalizationValue = "storage_footer" + /// Received files + /// + /// Key: `storage_received_files` + /// Context: Settings > Storage: label for the received-files size row. + /// + /// - en: Received files + /// - fr: Fichiers reçus + /// - es: Archivos recibidos + /// - it: File ricevuti + /// - de: Empfangene Dateien + /// - pt: Ficheiros recebidos + /// - pl: Odebrane pliki + /// - nl: Ontvangen bestanden + /// - ru: Полученные файлы + static let receivedFiles: String.LocalizationValue = "storage_received_files" + /// Temporary files + /// + /// Key: `storage_temporary` + /// Context: Settings > Storage: label for the temporary-files size row. + /// + /// - en: Temporary files + /// - fr: Fichiers temporaires + /// - es: Archivos temporales + /// - it: File temporanei + /// - de: Temporäre Dateien + /// - pt: Ficheiros temporários + /// - pl: Pliki tymczasowe + /// - nl: Tijdelijke bestanden + /// - ru: Временные файлы + static let temporary: String.LocalizationValue = "storage_temporary" + /// Storage + /// + /// Key: `storage_title` + /// Context: Settings > Storage: section title. + /// + /// - en: Storage + /// - fr: Stockage + /// - es: Almacenamiento + /// - it: Archiviazione + /// - de: Speicher + /// - pt: Armazenamento + /// - pl: Pamięć + /// - nl: Opslag + /// - ru: Хранилище + static let title: String.LocalizationValue = "storage_title" + /// Total + /// + /// Key: `storage_total` + /// Context: Settings > Storage: label for the total-size row. + /// + /// - en: Total + /// - fr: Total + /// - es: Total + /// - it: Totale + /// - de: Gesamt + /// - pt: Total + /// - pl: Łącznie + /// - nl: Totaal + /// - ru: Всего + static let total: String.LocalizationValue = "storage_total" + /// Transfer data + /// + /// Key: `storage_transfer_data` + /// Context: Settings > Storage: label for the transfer-engine data size row. + /// + /// - en: Transfer data + /// - fr: Données de transfert + /// - es: Datos de transferencia + /// - it: Dati di trasferimento + /// - de: Übertragungsdaten + /// - pt: Dados de transferência + /// - pl: Dane transferu + /// - nl: Overdrachtsgegevens + /// - ru: Данные передачи + static let transferData: String.LocalizationValue = "storage_transfer_data" + /// All transfers deleted + /// + /// Key: `storage_transfers_deleted` + /// Context: Settings > Storage: toast confirming all transfers were deleted. + /// + /// - en: All transfers deleted + /// - fr: Tous les transferts supprimés + /// - es: Todas las transferencias eliminadas + /// - it: Tutti i trasferimenti eliminati + /// - de: Alle Übertragungen gelöscht + /// - pt: Todas as transferências eliminadas + /// - pl: Usunięto wszystkie transfery + /// - nl: Alle overdrachten verwijderd + /// - ru: Все передачи удалены + static let transfersDeleted: String.LocalizationValue = "storage_transfers_deleted" + } + enum Transfer { + /// See important updates for this transfer + /// + /// Key: `transfer_activity_description` + /// Context: Transfer details: description under the Activity section. + /// + /// - en: See important updates for this transfer + /// - fr: Consultez les mises à jour importantes de ce transfert + /// - es: Vea las actualizaciones importantes de esta transferencia + /// - it: Veda gli aggiornamenti importanti di questo trasferimento + /// - de: Wichtige Aktualisierungen zu dieser Übertragung ansehen + /// - pt: Ver as atualizações importantes desta transferência + /// - pl: Zobacz ważne aktualizacje tego transferu + /// - nl: Bekijk belangrijke updates voor deze overdracht + /// - ru: Просматривайте важные обновления этой передачи + static let activityDescription: String.LocalizationValue = "transfer_activity_description" + /// Activity + /// + /// Key: `transfer_activity_title` + /// Context: Transfer details: Activity section title. + /// + /// - en: Activity + /// - fr: Activité + /// - es: Actividad + /// - it: Attività + /// - de: Aktivität + /// - pt: Atividade + /// - pl: Aktywność + /// - nl: Activiteit + /// - ru: Активность + static let activityTitle: String.LocalizationValue = "transfer_activity_title" + /// “{transferName}” will stop being shared and its transfer history will be removed from this device. + /// + /// Key: `transfer_delete_description` + /// Context: Transfer details: confirmation body for deleting a transfer. {transferName} = transfer name. + /// + /// - en: “{transferName}” will stop being shared and its transfer history will be removed from this device. + /// - fr: « {transferName} » cessera d’être partagé et son historique de transfert sera retiré de cet appareil. + /// - es: «{transferName}» dejará de compartirse y su historial de transferencia se eliminará de este dispositivo. + /// - it: «{transferName}» non verrà più condiviso e la sua cronologia di trasferimento verrà rimossa da questo dispositivo. + /// - de: „{transferName}“ wird nicht mehr geteilt und sein Übertragungsverlauf wird von diesem Gerät entfernt. + /// - pt: «{transferName}» deixará de ser partilhado e o seu histórico de transferência será removido deste dispositivo. + /// - pl: „{transferName}” przestanie być udostępniany, a jego historia transferu zostanie usunięta z tego urządzenia. + /// - nl: ‘{transferName}’ wordt niet meer gedeeld en de overdrachtsgeschiedenis wordt van dit apparaat verwijderd. + /// - ru: Общий доступ к «{transferName}» будет остановлен, а история передачи будет удалена с этого устройства. + static func deleteDescription(transferName: String) -> String { + String(format: String(localized: "transfer_delete_description"), transferName) + } + /// Delete transfer? + /// + /// Key: `transfer_delete_title` + /// Context: Transfer details: confirmation title for deleting a transfer. + /// + /// - en: Delete transfer? + /// - fr: Supprimer le transfert ? + /// - es: ¿Eliminar la transferencia? + /// - it: Eliminare il trasferimento? + /// - de: Übertragung löschen? + /// - pt: Eliminar a transferência? + /// - pl: Usunąć transfer? + /// - nl: Overdracht verwijderen? + /// - ru: Удалить передачу? + static let deleteTitle: String.LocalizationValue = "transfer_delete_title" + /// Transfer deleted. + /// + /// Key: `transfer_deleted` + /// Context: Transfer details: toast confirming a transfer was deleted. + /// + /// - en: Transfer deleted. + /// - fr: Transfert supprimé. + /// - es: Transferencia eliminada. + /// - it: Trasferimento eliminato. + /// - de: Übertragung gelöscht. + /// - pt: Transferência eliminada. + /// - pl: Transfer usunięty. + /// - nl: Overdracht verwijderd. + /// - ru: Передача удалена. + static let deleted: String.LocalizationValue = "transfer_deleted" + /// Deleting… + /// + /// Key: `transfer_deleting` + /// Context: Transfer details: progress label while a transfer is being deleted. + /// + /// - en: Deleting… + /// - fr: Suppression… + /// - es: Eliminando… + /// - it: Eliminazione… + /// - de: Wird gelöscht… + /// - pt: A eliminar… + /// - pl: Usuwanie… + /// - nl: Verwijderen… + /// - ru: Удаление… + static let deleting: String.LocalizationValue = "transfer_deleting" + /// Transfer details + /// + /// Key: `transfer_details_title` + /// Context: Transfer details: screen title. + /// + /// - en: Transfer details + /// - fr: Détails du transfert + /// - es: Detalles de la transferencia + /// - it: Dettagli del trasferimento + /// - de: Übertragungsdetails + /// - pt: Detalhes da transferência + /// - pl: Szczegóły transferu + /// - nl: Overdrachtsdetails + /// - ru: Сведения о передаче + static let detailsTitle: String.LocalizationValue = "transfer_details_title" + /// Receiver access approved + /// + /// Key: `transfer_event_approved` + /// Context: Transfer activity event: a receiver's access was approved. + /// + /// - en: Receiver access approved + /// - fr: Accès du destinataire approuvé + /// - es: Acceso del destinatario aprobado + /// - it: Accesso del destinatario approvato + /// - de: Empfängerzugriff genehmigt + /// - pt: Acesso do destinatário aprovado + /// - pl: Zatwierdzono dostęp odbiorcy + /// - nl: Toegang ontvanger goedgekeurd + /// - ru: Доступ получателя одобрен + static let eventApproved: String.LocalizationValue = "transfer_event_approved" + /// A receiver completed the transfer + /// + /// Key: `transfer_event_completed` + /// Context: Transfer activity event: a receiver completed the transfer. + /// + /// - en: A receiver completed the transfer + /// - fr: Un destinataire a terminé le transfert + /// - es: Un destinatario completó la transferencia + /// - it: Un destinatario ha completato il trasferimento + /// - de: Ein Empfänger hat die Übertragung abgeschlossen + /// - pt: Um destinatário concluiu a transferência + /// - pl: Odbiorca ukończył transfer + /// - nl: Een ontvanger heeft de overdracht voltooid + /// - ru: Получатель завершил передачу + static let eventCompleted: String.LocalizationValue = "transfer_event_completed" + /// Connecting to sender + /// + /// Key: `transfer_event_connecting` + /// Context: Transfer activity event: connecting to the sender. + /// + /// - en: Connecting to sender + /// - fr: Connexion à l’expéditeur + /// - es: Conectando con el remitente + /// - it: Connessione al mittente + /// - de: Verbindung zum Absender + /// - pt: A ligar ao remetente + /// - pl: Łączenie z nadawcą + /// - nl: Verbinden met afzender + /// - ru: Подключение к отправителю + static let eventConnecting: String.LocalizationValue = "transfer_event_connecting" + /// Downloading + /// + /// Key: `transfer_event_downloading` + /// Context: Transfer activity event: downloading. + /// + /// - en: Downloading + /// - fr: Téléchargement + /// - es: Descargando + /// - it: Download + /// - de: Wird geladen + /// - pt: A descarregar + /// - pl: Pobieranie + /// - nl: Downloaden + /// - ru: Загрузка + static let eventDownloading: String.LocalizationValue = "transfer_event_downloading" + /// The transfer encountered a problem + /// + /// Key: `transfer_event_failed` + /// Context: Transfer activity event: the transfer hit a problem. + /// + /// - en: The transfer encountered a problem + /// - fr: Le transfert a rencontré un problème + /// - es: La transferencia tuvo un problema + /// - it: Il trasferimento ha riscontrato un problema + /// - de: Bei der Übertragung ist ein Problem aufgetreten + /// - pt: A transferência teve um problema + /// - pl: Podczas transferu wystąpił problem + /// - nl: Er is een probleem opgetreden bij de overdracht + /// - ru: При передаче возникла проблема + static let eventFailed: String.LocalizationValue = "transfer_event_failed" + /// Preparing your transfer + /// + /// Key: `transfer_event_preparing` + /// Context: Transfer activity event: preparing the transfer. + /// + /// - en: Preparing your transfer + /// - fr: Préparation de votre transfert + /// - es: Preparando su transferencia + /// - it: Preparazione del trasferimento + /// - de: Ihre Übertragung wird vorbereitet + /// - pt: A preparar a sua transferência + /// - pl: Przygotowywanie transferu + /// - nl: Uw overdracht voorbereiden + /// - ru: Подготовка вашей передачи + static let eventPreparing: String.LocalizationValue = "transfer_event_preparing" + /// Ready to share + /// + /// Key: `transfer_event_ready` + /// Context: Transfer activity event: ready to share. + /// + /// - en: Ready to share + /// - fr: Prêt à partager + /// - es: Listo para compartir + /// - it: Pronto per la condivisione + /// - de: Bereit zum Teilen + /// - pt: Pronto para partilhar + /// - pl: Gotowe do udostępnienia + /// - nl: Klaar om te delen + /// - ru: Готово к отправке + static let eventReady: String.LocalizationValue = "transfer_event_ready" + /// Receiver access refused + /// + /// Key: `transfer_event_refused` + /// Context: Transfer activity event: a receiver's access was refused. + /// + /// - en: Receiver access refused + /// - fr: Accès du destinataire refusé + /// - es: Acceso del destinatario rechazado + /// - it: Accesso del destinatario rifiutato + /// - de: Empfängerzugriff abgelehnt + /// - pt: Acesso do destinatário recusado + /// - pl: Odrzucono dostęp odbiorcy + /// - nl: Toegang ontvanger geweigerd + /// - ru: Доступ получателя отклонён + static let eventRefused: String.LocalizationValue = "transfer_event_refused" + /// A receiver requested access + /// + /// Key: `transfer_event_requested` + /// Context: Transfer activity event: a receiver requested access. + /// + /// - en: A receiver requested access + /// - fr: Un destinataire a demandé l’accès + /// - es: Un destinatario solicitó acceso + /// - it: Un destinatario ha richiesto l’accesso + /// - de: Ein Empfänger hat Zugriff angefragt + /// - pt: Um destinatário pediu acesso + /// - pl: Odbiorca poprosił o dostęp + /// - nl: Een ontvanger heeft toegang aangevraagd + /// - ru: Получатель запросил доступ + static let eventRequested: String.LocalizationValue = "transfer_event_requested" + /// Saving + /// + /// Key: `transfer_event_saving` + /// Context: Transfer activity event: saving received files. + /// + /// - en: Saving + /// - fr: Enregistrement + /// - es: Guardando + /// - it: Salvataggio + /// - de: Wird gesichert + /// - pt: A guardar + /// - pl: Zapisywanie + /// - nl: Bewaren + /// - ru: Сохранение + static let eventSaving: String.LocalizationValue = "transfer_event_saving" + /// Sharing stopped + /// + /// Key: `transfer_event_stopped` + /// Context: Transfer activity event: sharing was stopped. + /// + /// - en: Sharing stopped + /// - fr: Partage arrêté + /// - es: Se dejó de compartir + /// - it: Condivisione interrotta + /// - de: Freigabe beendet + /// - pt: Partilha parada + /// - pl: Zatrzymano udostępnianie + /// - nl: Delen gestopt + /// - ru: Раздача остановлена + static let eventStopped: String.LocalizationValue = "transfer_event_stopped" + /// Transfer updated + /// + /// Key: `transfer_event_updated` + /// Context: Transfer activity event: the transfer was updated. + /// + /// - en: Transfer updated + /// - fr: Transfert mis à jour + /// - es: Transferencia actualizada + /// - it: Trasferimento aggiornato + /// - de: Übertragung aktualisiert + /// - pt: Transferência atualizada + /// - pl: Transfer zaktualizowany + /// - nl: Overdracht bijgewerkt + /// - ru: Передача обновлена + static let eventUpdated: String.LocalizationValue = "transfer_event_updated" + /// {count} files + /// + /// Key: `transfer_file_count` + /// Context: Transfer subtitle: file count with pluralization. {count} = number of files. + /// + /// - en: {count} files + /// - fr: {count} fichiers + /// - es: {count} archivos + /// - it: {count} file + /// - de: {count} Dateien + /// - pt: {count} ficheiros + /// - pl: {count} pliku + /// - nl: {count} bestanden + /// - ru: {count} файла + static func fileCount(count: Int) -> String { + String(format: String(localized: "transfer_file_count"), count) + } + /// Invitation saved. + /// + /// Key: `transfer_invitation_saved` + /// Context: Transfer share: toast confirming the invitation file was saved. + /// + /// - en: Invitation saved. + /// - fr: Invitation enregistrée. + /// - es: Invitación guardada. + /// - it: Invito salvato. + /// - de: Einladung gesichert. + /// - pt: Convite guardado. + /// - pl: Zaproszenie zapisane. + /// - nl: Uitnodiging bewaard. + /// - ru: Приглашение сохранено. + static let invitationSaved: String.LocalizationValue = "transfer_invitation_saved" + /// Nearby device + /// + /// Key: `transfer_nearby_device` + /// Context: Transfer share/receivers: label for a nearby device. + /// + /// - en: Nearby device + /// - fr: Appareil à proximité + /// - es: Dispositivo cercano + /// - it: Dispositivo nelle vicinanze + /// - de: Gerät in der Nähe + /// - pt: Dispositivo próximo + /// - pl: Urządzenie w pobliżu + /// - nl: Apparaat in de buurt + /// - ru: Устройство поблизости + static let nearbyDevice: String.LocalizationValue = "transfer_nearby_device" + /// NFC tag writing is not available on this device. + /// + /// Key: `transfer_nfc_unavailable` + /// Context: Transfer share: message when NFC writing isn't available on the device. + /// + /// - en: NFC tag writing is not available on this device. + /// - fr: L’écriture de tag NFC n’est pas disponible sur cet appareil. + /// - es: La escritura de etiquetas NFC no está disponible en este dispositivo. + /// - it: La scrittura dei tag NFC non è disponibile su questo dispositivo. + /// - de: Das Beschreiben von NFC-Tags ist auf diesem Gerät nicht verfügbar. + /// - pt: A escrita de etiquetas NFC não está disponível neste dispositivo. + /// - pl: Zapis tagów NFC nie jest dostępny na tym urządzeniu. + /// - nl: Het schrijven van NFC-tags is niet beschikbaar op dit apparaat. + /// - ru: Запись NFC-меток недоступна на этом устройстве. + static let nfcUnavailable: String.LocalizationValue = "transfer_nfc_unavailable" + /// Hold your device near a writable NFC tag. + /// + /// Key: `transfer_nfc_waiting` + /// Context: Transfer share: prompt while waiting to write an NFC tag. + /// + /// - en: Hold your device near a writable NFC tag. + /// - fr: Approchez votre appareil d’un tag NFC inscriptible. + /// - es: Acerque su dispositivo a una etiqueta NFC grabable. + /// - it: Avvicini il dispositivo a un tag NFC scrivibile. + /// - de: Halten Sie Ihr Gerät an ein beschreibbares NFC-Tag. + /// - pt: Aproxime o seu dispositivo de uma etiqueta NFC gravável. + /// - pl: Zbliż urządzenie do zapisywalnego tagu NFC. + /// - nl: Houd uw apparaat bij een beschrijfbare NFC-tag. + /// - ru: Поднесите устройство к записываемой NFC-метке. + static let nfcWaiting: String.LocalizationValue = "transfer_nfc_waiting" + /// Invitation written to the NFC tag. + /// + /// Key: `transfer_nfc_written` + /// Context: Transfer share: confirmation the invitation was written to the NFC tag. + /// + /// - en: Invitation written to the NFC tag. + /// - fr: Invitation écrite sur le tag NFC. + /// - es: Invitación escrita en la etiqueta NFC. + /// - it: Invito scritto sul tag NFC. + /// - de: Einladung auf das NFC-Tag geschrieben. + /// - pt: Convite escrito na etiqueta NFC. + /// - pl: Zaproszenie zapisane na tagu NFC. + /// - nl: Uitnodiging naar de NFC-tag geschreven. + /// - ru: Приглашение записано на NFC-метку. + static let nfcWritten: String.LocalizationValue = "transfer_nfc_written" + /// There is no activity to show yet. + /// + /// Key: `transfer_no_activity` + /// Context: Transfer details: empty state for the Activity section. + /// + /// - en: There is no activity to show yet. + /// - fr: Aucune activité à afficher pour l’instant. + /// - es: Todavía no hay actividad que mostrar. + /// - it: Non c’è ancora alcuna attività da mostrare. + /// - de: Es gibt noch keine Aktivität anzuzeigen. + /// - pt: Ainda não há atividade para mostrar. + /// - pl: Nie ma jeszcze aktywności do wyświetlenia. + /// - nl: Er is nog geen activiteit om weer te geven. + /// - ru: Пока нет активности для отображения. + static let noActivity: String.LocalizationValue = "transfer_no_activity" + /// Nobody has requested this transfer yet. + /// + /// Key: `transfer_no_receivers` + /// Context: Transfer details: empty state for the Receivers section. + /// + /// - en: Nobody has requested this transfer yet. + /// - fr: Personne n’a encore demandé ce transfert. + /// - es: Nadie ha solicitado aún esta transferencia. + /// - it: Nessuno ha ancora richiesto questo trasferimento. + /// - de: Noch niemand hat diese Übertragung angefragt. + /// - pt: Ainda ninguém pediu esta transferência. + /// - pl: Nikt jeszcze nie poprosił o ten transfer. + /// - nl: Nog niemand heeft deze overdracht aangevraagd. + /// - ru: Никто ещё не запросил эту передачу. + static let noReceivers: String.LocalizationValue = "transfer_no_receivers" + /// Approved — waiting for completion + /// + /// Key: `transfer_receiver_accepted` + /// Context: Receiver status: approved, waiting for the download to complete. + /// + /// - en: Approved — waiting for completion + /// - fr: Approuvé — en attente de la fin + /// - es: Aprobado: esperando a que se complete + /// - it: Approvato: in attesa del completamento + /// - de: Genehmigt – wartet auf Abschluss + /// - pt: Aprovado — a aguardar conclusão + /// - pl: Zatwierdzono — oczekiwanie na ukończenie + /// - nl: Goedgekeurd — wachten op voltooiing + /// - ru: Одобрено — ожидание завершения + static let receiverAccepted: String.LocalizationValue = "transfer_receiver_accepted" + /// Received successfully + /// + /// Key: `transfer_receiver_completed` + /// Context: Receiver status: received successfully. + /// + /// - en: Received successfully + /// - fr: Reçu avec succès + /// - es: Recibido correctamente + /// - it: Ricevuto correttamente + /// - de: Erfolgreich empfangen + /// - pt: Recebido com sucesso + /// - pl: Odebrano pomyślnie + /// - nl: Succesvol ontvangen + /// - ru: Успешно получено + static let receiverCompleted: String.LocalizationValue = "transfer_receiver_completed" + /// Request expired + /// + /// Key: `transfer_receiver_expired` + /// Context: Receiver status: the request expired. + /// + /// - en: Request expired + /// - fr: Demande expirée + /// - es: Solicitud caducada + /// - it: Richiesta scaduta + /// - de: Anfrage abgelaufen + /// - pt: Pedido expirado + /// - pl: Prośba wygasła + /// - nl: Verzoek verlopen + /// - ru: Запрос истёк + static let receiverExpired: String.LocalizationValue = "transfer_receiver_expired" + /// Request refused + /// + /// Key: `transfer_receiver_refused` + /// Context: Receiver status: the request was refused. + /// + /// - en: Request refused + /// - fr: Demande refusée + /// - es: Solicitud rechazada + /// - it: Richiesta rifiutata + /// - de: Anfrage abgelehnt + /// - pt: Pedido recusado + /// - pl: Prośba odrzucona + /// - nl: Verzoek geweigerd + /// - ru: Запрос отклонён + static let receiverRefused: String.LocalizationValue = "transfer_receiver_refused" + /// Waiting for your approval + /// + /// Key: `transfer_receiver_requested` + /// Context: Receiver status: waiting for the sender's approval. + /// + /// - en: Waiting for your approval + /// - fr: En attente de votre approbation + /// - es: Esperando su aprobación + /// - it: In attesa della sua approvazione + /// - de: Wartet auf Ihre Genehmigung + /// - pt: A aguardar a sua aprovação + /// - pl: Oczekiwanie na Twoje zatwierdzenie + /// - nl: Wachten op uw goedkeuring + /// - ru: Ожидание вашего одобрения + static let receiverRequested: String.LocalizationValue = "transfer_receiver_requested" + /// Status unavailable + /// + /// Key: `transfer_receiver_unknown` + /// Context: Receiver status: status unavailable/unknown. + /// + /// - en: Status unavailable + /// - fr: Statut indisponible + /// - es: Estado no disponible + /// - it: Stato non disponibile + /// - de: Status nicht verfügbar + /// - pt: Estado indisponível + /// - pl: Status niedostępny + /// - nl: Status niet beschikbaar + /// - ru: Статус недоступен + static let receiverUnknown: String.LocalizationValue = "transfer_receiver_unknown" + /// {count} completed + /// + /// Key: `transfer_receivers_completed_count` + /// Context: Receivers summary: how many receivers completed. {count} = completed receivers. + /// + /// - en: {count} completed + /// - fr: {count} terminés + /// - es: {count} completados + /// - it: {count} completati + /// - de: {count} abgeschlossen + /// - pt: {count} concluídos + /// - pl: Ukończone: {count} + /// - nl: {count} voltooid + /// - ru: Завершено: {count} + static func receiversCompletedCount(count: Int) -> String { + String(format: String(localized: "transfer_receivers_completed_count"), count) + } + /// Requests, approvals, and completed deliveries + /// + /// Key: `transfer_receivers_description` + /// Context: Transfer details: description under the Receivers section. + /// + /// - en: Requests, approvals, and completed deliveries + /// - fr: Demandes, approbations et livraisons terminées + /// - es: Solicitudes, aprobaciones y entregas completadas + /// - it: Richieste, approvazioni e consegne completate + /// - de: Anfragen, Genehmigungen und abgeschlossene Zustellungen + /// - pt: Pedidos, aprovações e entregas concluídas + /// - pl: Prośby, zatwierdzenia i ukończone dostawy + /// - nl: Verzoeken, goedkeuringen en voltooide leveringen + /// - ru: Запросы, одобрения и завершённые доставки + static let receiversDescription: String.LocalizationValue = "transfer_receivers_description" + /// {count} waiting + /// + /// Key: `transfer_receivers_pending` + /// Context: Receivers summary: how many requests are waiting. {count} = pending receivers. + /// + /// - en: {count} waiting + /// - fr: {count} en attente + /// - es: {count} en espera + /// - it: {count} in attesa + /// - de: {count} warten + /// - pt: {count} em espera + /// - pl: Oczekujące: {count} + /// - nl: {count} in behandeling + /// - ru: Ожидают: {count} + static func receiversPending(count: Int) -> String { + String(format: String(localized: "transfer_receivers_pending"), count) + } + /// Receivers + /// + /// Key: `transfer_receivers_title` + /// Context: Transfer details: Receivers section title. + /// + /// - en: Receivers + /// - fr: Destinataires + /// - es: Destinatarios + /// - it: Destinatari + /// - de: Empfänger + /// - pt: Destinatários + /// - pl: Odbiorcy + /// - nl: Ontvangers + /// - ru: Получатели + static let receiversTitle: String.LocalizationValue = "transfer_receivers_title" + /// Scan with VniDrop to receive this transfer + /// + /// Key: `transfer_scan_qr` + /// Context: Transfer share: caption under the QR code. + /// + /// - en: Scan with VniDrop to receive this transfer + /// - fr: Scannez avec VniDrop pour recevoir ce transfert + /// - es: Escanee con VniDrop para recibir esta transferencia + /// - it: Scansioni con VniDrop per ricevere questo trasferimento + /// - de: Mit VniDrop scannen, um diese Übertragung zu empfangen + /// - pt: Leia com o VniDrop para receber esta transferência + /// - pl: Zeskanuj za pomocą VniDrop, aby odebrać ten transfer + /// - nl: Scan met VniDrop om deze overdracht te ontvangen + /// - ru: Отсканируйте с помощью VniDrop, чтобы получить эту передачу + static let scanQr: String.LocalizationValue = "transfer_scan_qr" + /// QR code, invitation file, and nearby options + /// + /// Key: `transfer_share_description` + /// Context: Transfer details: description under the Share section. + /// + /// - en: QR code, invitation file, and nearby options + /// - fr: QR code, fichier d’invitation et options à proximité + /// - es: Código QR, archivo de invitación y opciones cercanas + /// - it: Codice QR, file di invito e opzioni nelle vicinanze + /// - de: QR-Code, Einladungsdatei und Optionen in der Nähe + /// - pt: Código QR, ficheiro de convite e opções por perto + /// - pl: Kod QR, plik zaproszenia i opcje w pobliżu + /// - nl: QR-code, uitnodigingsbestand en opties in de buurt + /// - ru: QR-код, файл приглашения и варианты поблизости + static let shareDescription: String.LocalizationValue = "transfer_share_description" + /// Share + /// + /// Key: `transfer_share_title` + /// Context: Transfer details: Share section title. + /// + /// - en: Share + /// - fr: Partager + /// - es: Compartir + /// - it: Condividi + /// - de: Teilen + /// - pt: Partilhar + /// - pl: Udostępnij + /// - nl: Delen + /// - ru: Поделиться + static let shareTitle: String.LocalizationValue = "transfer_share_title" + } + enum Value { + /// Not available + /// + /// Key: `value_unavailable` + /// Context: Placeholder shown when a device-info or metadata value can't be read. + /// + /// - en: Not available + /// - fr: Non disponible + /// - es: No disponible + /// - it: Non disponibile + /// - de: Nicht verfügbar + /// - pt: Indisponível + /// - pl: Niedostępne + /// - nl: Niet beschikbaar + /// - ru: Недоступно + static let unavailable: String.LocalizationValue = "value_unavailable" + } + enum Version { + /// App version + /// + /// Key: `version_title` + /// Context: Device information / Settings row: app version label. + /// + /// - en: App version + /// - fr: Version de l’app + /// - es: Versión de la app + /// - it: Versione dell’app + /// - de: App-Version + /// - pt: Versão da app + /// - pl: Wersja aplikacji + /// - nl: App-versie + /// - ru: Версия приложения + static let title: String.LocalizationValue = "version_title" + } +} diff --git a/apple/VniDrop/Resources/Localizable.xcstrings b/apple/VniDrop/Resources/Localizable.xcstrings index c40866d..17af5c5 100644 --- a/apple/VniDrop/Resources/Localizable.xcstrings +++ b/apple/VniDrop/Resources/Localizable.xcstrings @@ -1810,7 +1810,7 @@ } }, "approval_endpoint_id": { - "comment": "Approval prompt: shows the requesting device's ID. {arg1} = device/endpoint identifier.", + "comment": "Approval prompt: shows the requesting device's ID. {deviceId} = device/endpoint identifier.", "extractionState": "manual", "localizations": { "de": { @@ -1990,7 +1990,7 @@ } }, "approval_request_body": { - "comment": "Approval prompt body: '{arg1} wants to receive \"{arg2}\".' {arg1} = requester name, {arg2} = transfer name.", + "comment": "Approval prompt body: '{receiver} wants to receive \"{transferName}\".' {receiver} = requester name, {transferName} = transfer name.", "extractionState": "manual", "localizations": { "de": { @@ -9130,7 +9130,7 @@ } }, "receive_delete_history_description": { - "comment": "Receive history: confirmation body for removing one item. {arg1} = transfer name.", + "comment": "Receive history: confirmation body for removing one item. {transferName} = transfer name.", "extractionState": "manual", "localizations": { "de": { @@ -13090,7 +13090,7 @@ } }, "transfer_delete_description": { - "comment": "Transfer details: confirmation body for deleting a transfer. {arg1} = transfer name.", + "comment": "Transfer details: confirmation body for deleting a transfer. {transferName} = transfer name.", "extractionState": "manual", "localizations": { "de": { diff --git a/apple/VniDrop/UI/Components/AdaptiveDrawer.swift b/apple/VniDrop/UI/Components/AdaptiveDrawer.swift index 2d19e20..7b75752 100644 --- a/apple/VniDrop/UI/Components/AdaptiveDrawer.swift +++ b/apple/VniDrop/UI/Components/AdaptiveDrawer.swift @@ -41,7 +41,7 @@ private struct SheetChrome: View { ScrollView { content().padding(.top, 4) } .toolbar { ToolbarItem(placement: .cancellationAction) { - Button(String(localized: "button_close"), action: onClose) + Button(String(localized: L10n.Button.close), action: onClose) } } #if os(iOS) diff --git a/apple/VniDrop/UI/Components/Components.swift b/apple/VniDrop/UI/Components/Components.swift index 7d2b504..eb0dc4e 100644 --- a/apple/VniDrop/UI/Components/Components.swift +++ b/apple/VniDrop/UI/Components/Components.swift @@ -30,7 +30,7 @@ struct StatusPill: View { // MARK: - ProgressRow struct ProgressRow: View { - let labelKey: String + let labelKey: String.LocalizationValue let progress: Double? var detail: String? = nil /// Pre-resolved label; when set it overrides `labelKey`. @@ -62,7 +62,7 @@ struct ProgressRow: View { if let labelText { Text(labelText) } else { - Text(LocalizedStringKey(labelKey)) + Text(String(localized: labelKey)) } } } diff --git a/apple/VniDrop/UI/Feedback/UiMessage.swift b/apple/VniDrop/UI/Feedback/UiMessage.swift index 2a71c8c..2ab6c77 100644 --- a/apple/VniDrop/UI/Feedback/UiMessage.swift +++ b/apple/VniDrop/UI/Feedback/UiMessage.swift @@ -4,14 +4,14 @@ import Combine /// A localizable UI string: either a catalog key or dynamic text, ported from /// `UiText` in `ui/feedback/UiMessageController.kt`. enum UiText: Equatable { - case resource(String) // Localizable.xcstrings key + case resource(String.LocalizationValue) // Localizable.xcstrings key (use L10n.*) case dynamic(String) /// Resolves to display text. Keys go through the string catalog. func resolved() -> String { switch self { case .dynamic(let value): return value - case .resource(let key): return String(localized: String.LocalizationValue(key)) + case .resource(let value): return String(localized: value) } } } diff --git a/apple/VniDrop/UI/Feedback/UserFacingError.swift b/apple/VniDrop/UI/Feedback/UserFacingError.swift index bac00c4..2bc2e9d 100644 --- a/apple/VniDrop/UI/Feedback/UserFacingError.swift +++ b/apple/VniDrop/UI/Feedback/UserFacingError.swift @@ -8,34 +8,34 @@ extension Error { if let vni = self as? VnidropError { switch vni { case .Ticket: - return .resource("error_invalid_ticket") + return .resource(L10n.Error.invalidTicket) case .Permission: - return .resource("error_permission") + return .resource(L10n.Error.permission) case .Filesystem: - return .resource("error_filesystem") + return .resource(L10n.Error.filesystem) case .FilesystemPermission: - return .resource("error_filesystem") + return .resource(L10n.Error.filesystem) case .DestinationExists: - return .resource("error_destination_exists") + return .resource(L10n.Error.destinationExists) case .StorageFull: - return .resource("error_storage_full") + return .resource(L10n.Error.storageFull) case .Network: - return .resource("error_network") + return .resource(L10n.Error.network) case .Transfer(let reason): return transferUiText(reason) case .Repository: - return .resource("error_repository") + return .resource(L10n.Error.repository) case .Cancelled: - return .resource("error_generic") + return .resource(L10n.Error.generic) case .InvalidInput: - return .resource("error_invalid_input") + return .resource(L10n.Error.invalidInput) case .Initialization(let reason): return initializationUiText(reason) case .Internal(let reason): - return reasonHints(reason) ?? .resource("error_generic") + return reasonHints(reason) ?? .resource(L10n.Error.generic) } } - return reasonHints(technicalDetail) ?? .resource("error_generic") + return reasonHints(technicalDetail) ?? .resource(L10n.Error.generic) } /// True when the user intentionally backed out of a flow. @@ -79,20 +79,20 @@ extension Error { private func transferUiText(_ reason: String) -> UiText { let detail = reason.lowercased() if detail.contains("refused") || detail.contains("denied") || detail.contains("not approved") { - return .resource("error_permission") + return .resource(L10n.Error.permission) } - return .resource("error_transfer") + return .resource(L10n.Error.transfer) } private func initializationUiText(_ reason: String) -> UiText { let detail = reason.lowercased() if detail.contains("native") && detail.contains("library") { - return .resource("error_missing_native_library") + return .resource(L10n.Error.missingNativeLibrary) } if detail.contains("socket") || detail.contains("bind") { - return .resource("error_socket_bind") + return .resource(L10n.Error.socketBind) } - return .resource("error_initialization") + return .resource(L10n.Error.initialization) } private func reasonHints(_ detailRaw: String) -> UiText? { @@ -100,50 +100,50 @@ private func reasonHints(_ detailRaw: String) -> UiText? { if detail.isEmpty { return nil } if detail.contains("still starting") || detail.contains("starting up") { - return .resource("error_starting_up") + return .resource(L10n.Error.startingUp) } if detail.contains("empty") && (detail.contains("invitation") || detail.contains("ticket") || detail.contains("qr")) { - return .resource("error_invitation_empty") + return .resource(L10n.Error.invitationEmpty) } if detail.contains("select at least one") || detail.contains("no files found") { - return .resource("error_share_empty") + return .resource(L10n.Error.shareEmpty) } if detail.contains("camera") { - return .resource("error_camera") + return .resource(L10n.Error.camera) } if detail.contains("nfc") || detail.contains("ndef") || (detail.contains("read-only") && detail.contains("tag")) || detail.contains("tag is too small") || detail.contains("no nfc tag") { - return .resource("error_nfc") + return .resource(L10n.Error.nfc) } if detail.contains("native") && detail.contains("library") { - return .resource("error_missing_native_library") + return .resource(L10n.Error.missingNativeLibrary) } if detail.contains("socket") || detail.contains("bind") { - return .resource("error_socket_bind") + return .resource(L10n.Error.socketBind) } if detail.contains("device information") || detail.contains("device info") { - return .resource("error_device_info") + return .resource(L10n.Error.deviceInfo) } if detail.contains("refused") || detail.contains("denied") || detail.contains("permission") || detail.contains("not approved") || detail.contains("waiting for approval") { - return .resource("error_permission") + return .resource(L10n.Error.permission) } if detail.contains("invalid ticket") || detail.contains("ticket error") || detail.contains("could not be read") || detail.contains("malformed") || detail.contains("invitation could not be opened") { - return .resource("error_invalid_ticket") + return .resource(L10n.Error.invalidTicket) } if detail.contains("selected") && (detail.contains("file") || detail.contains("folder") || detail.contains("document") || detail.contains("open")) { - return .resource("error_selection_failed") + return .resource(L10n.Error.selectionFailed) } if detail.contains("could not open the selected") || detail.contains("could not open selected") { - return .resource("error_selection_failed") + return .resource(L10n.Error.selectionFailed) } if detail.contains("document picker") || detail.contains("folder picker") || detail.contains("file descriptor") || detail.contains("view controller") { - return .resource("error_selection_failed") + return .resource(L10n.Error.selectionFailed) } return nil } diff --git a/apple/VniDrop/UI/Navigation/AppDestination.swift b/apple/VniDrop/UI/Navigation/AppDestination.swift index 1a755bd..991ec47 100644 --- a/apple/VniDrop/UI/Navigation/AppDestination.swift +++ b/apple/VniDrop/UI/Navigation/AppDestination.swift @@ -8,11 +8,11 @@ enum AppDestination: String, CaseIterable, Identifiable { var id: String { rawValue } - var labelKey: String { + var labelKey: String.LocalizationValue { switch self { - case .send: return "nav_send" - case .receive: return "nav_receive" - case .settings: return "nav_settings" + case .send: return L10n.Nav.send + case .receive: return L10n.Nav.receive + case .settings: return L10n.Nav.settings } } diff --git a/localization/src/commands/generate.ts b/localization/src/commands/generate.ts index b51fb90..222d496 100644 --- a/localization/src/commands/generate.ts +++ b/localization/src/commands/generate.ts @@ -5,8 +5,11 @@ */ import { mkdirSync } from "node:fs"; import { join } from "node:path"; +import { mkdir } from "node:fs/promises"; +import { dirname } from "node:path"; import { APPLE_INFO_PLIST, + APPLE_L10N_SWIFT, APPLE_XCSTRINGS, kmpValuesDir, KMP_RESOURCES, @@ -14,6 +17,7 @@ import { } from "../config"; import { renderAndroid, type ParsedAndroid } from "../lib/android-xml"; import { fromCanonical, type Flavor } from "../lib/placeholders"; +import { renderSwiftAccessors } from "../lib/swift-accessors"; import { renderXcstrings, type XcCatalog, type XcEntry } from "../lib/xcstrings"; import { targetsOf, type StringEntry, type StringsFile } from "../types"; @@ -111,6 +115,10 @@ export async function generate() { await Bun.write(APPLE_XCSTRINGS, renderXcstrings(buildXcstrings(doc))); console.log(`Wrote ${APPLE_XCSTRINGS}`); + await mkdir(dirname(APPLE_L10N_SWIFT), { recursive: true }); + await Bun.write(APPLE_L10N_SWIFT, renderSwiftAccessors(doc)); + console.log(`Wrote ${APPLE_L10N_SWIFT}`); + await syncInfoPlistLocalizations(doc.supportedLanguages); for (const lang of doc.supportedLanguages) { diff --git a/localization/src/config.ts b/localization/src/config.ts index ace576f..a1a35c7 100644 --- a/localization/src/config.ts +++ b/localization/src/config.ts @@ -18,6 +18,12 @@ export const APPLE_INFO_PLIST = join( "apple/VniDrop/Resources/Info.plist", ); +/** Generated Swift accessors (`L10n`) for compile-time-checked catalog keys. */ +export const APPLE_L10N_SWIFT = join( + REPO_ROOT, + "apple/VniDrop/Generated/L10n.swift", +); + /** KMP / Compose Multiplatform resources root; one values[-lang]/strings.xml per language. */ export const KMP_RESOURCES = join( REPO_ROOT, diff --git a/localization/src/lib/swift-accessors.ts b/localization/src/lib/swift-accessors.ts new file mode 100644 index 0000000..534662d --- /dev/null +++ b/localization/src/lib/swift-accessors.ts @@ -0,0 +1,139 @@ +/** + * Swift accessor generator. + * + * Emits a compile-time-checked stand-in for each localization key so Apple call + * sites stop passing raw string literals. The runtime path is unchanged: plain + * keys become `String.LocalizationValue` constants used with `String(localized:)` + * exactly as before, and Apple's catalog lookup still does 100% of the work. + * + * Keys group by their first `_`-delimited segment (`button_…` -> `enum Button`); + * the remainder becomes a camelCase member. Plain keys are `static let` + * constants; keys with args become `static func` with typed, named parameters + * derived from the entry's `args` metadata. + */ +import { targetsOf, type Arg, type StringEntry, type StringsFile } from "../types"; + +const HEADER = `// Generated by localization/ (bun run src/cli.ts generate). Do not edit. +// Keys resolve through Apple's String Catalog exactly as a literal would; these +// accessors only make the key compile-time-checked. If a runtime language +// switcher (live \`.environment(\\.locale)\`) is ever added, switch the plain +// \`static let\` constants to computed \`static var\` so the locale is not frozen. +import Foundation +`; + +/** Swift type for a localization arg. */ +function swiftType(arg: Arg): string { + switch (arg.type) { + case "int": + return "Int"; + case "double": + return "Double"; + case "string": + return "String"; + } +} + +/** `create_new_transfer` -> `createNewTransfer`. */ +function camel(segment: string): string { + const parts = segment.split("_").filter(Boolean); + return parts + .map((p, i) => (i === 0 ? p : p.charAt(0).toUpperCase() + p.slice(1))) + .join(""); +} + +/** `button` -> `Button`. */ +function pascal(segment: string): string { + const c = camel(segment); + return c.charAt(0).toUpperCase() + c.slice(1); +} + +/** A key is groupable only when it starts with an identifier-safe segment. */ +function isGroupableKey(key: string): boolean { + return /^[A-Za-z][A-Za-z0-9_]*$/.test(key); +} + +/** Collapse whitespace/newlines so a value fits on one doc-comment line. */ +function oneLine(text: string): string { + return text.replace(/\s*\n\s*/g, " ").trim(); +} + +/** The displayable text for a language (plural shows its `other` form). */ +function translationText(entry: StringEntry, lang: string): string | undefined { + return entry.translations?.[lang] ?? entry.plural?.[lang]?.other; +} + +/** + * A rich Quick Help block: source-language text as the abstract, then the raw + * key, the context note, and every translation. Quick Help renders the Markdown. + */ +function docComment(key: string, entry: StringEntry, doc: StringsFile): string { + const lines: string[] = []; + const source = translationText(entry, doc.sourceLanguage); + if (source) lines.push(oneLine(source), ""); + + lines.push(`Key: \`${key}\``); + if (entry.context) lines.push(`Context: ${oneLine(entry.context)}`); + lines.push(""); + + for (const lang of doc.supportedLanguages) { + const value = translationText(entry, lang); + if (value !== undefined) lines.push(`- ${lang}: ${oneLine(value)}`); + } + + return lines + .map((line) => (line ? ` /// ${line}` : " ///")) + .join("\n"); +} + +function memberFor( + key: string, + member: string, + entry: StringEntry, + doc: StringsFile, +): string { + const comment = docComment(key, entry, doc); + const args = entry.args ?? []; + + // Plain key: a #define-style constant. Apple resolves it via String(localized:). + if (args.length === 0 && !entry.plural) { + return `${comment}\n static let ${member}: String.LocalizationValue = "${key}"`; + } + + // Arg'd (or plural) key: a typed, named function that applies the arguments + // through the same String(format: String(localized:)) path used before. + const params = args.map((a) => `${a.name}: ${swiftType(a)}`).join(", "); + const callArgs = args.map((a) => a.name).join(", "); + // The positional format string lives in the catalog; look it up by key and + // apply the args exactly as the hand-written call sites did. + return `${comment}\n static func ${member}(${params}) -> String {\n String(format: String(localized: "${key}"), ${callArgs})\n }`; +} + +export function renderSwiftAccessors(doc: StringsFile): string { + // group segment -> rendered members + const groups = new Map(); + + for (const [key, entry] of Object.entries(doc.strings)) { + if (!targetsOf(entry).includes("apple")) continue; + if (!isGroupableKey(key)) continue; // skips legacy `%@` literal keys + + const underscore = key.indexOf("_"); + const groupSeg = underscore === -1 ? key : key.slice(0, underscore); + const memberSeg = underscore === -1 ? key : key.slice(underscore + 1); + const group = pascal(groupSeg); + const member = camel(memberSeg) || camel(groupSeg); + + const list = groups.get(group) ?? []; + list.push(memberFor(key, member, entry, doc)); + groups.set(group, list); + } + + const body = [...groups.keys()] + .sort() + .map((group) => { + const members = groups.get(group)!.join("\n"); + return ` enum ${group} {\n${members}\n }`; + }) + .join("\n"); + + return `${HEADER}\nenum L10n {\n${body}\n}\n`; +} diff --git a/localization/strings.json b/localization/strings.json index 0ad438b..6b46ecb 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -476,23 +476,23 @@ } }, "approval_endpoint_id": { - "context": "Approval prompt: shows the requesting device's ID. {arg1} = device/endpoint identifier.", + "context": "Approval prompt: shows the requesting device's ID. {deviceId} = device/endpoint identifier.", "args": [ { - "name": "arg1", + "name": "deviceId", "type": "string" } ], "translations": { - "en": "Device ID: {arg1}", - "fr": "Identifiant de l’appareil : {arg1}", - "es": "ID del dispositivo: {arg1}", - "it": "ID dispositivo: {arg1}", - "de": "Geräte-ID: {arg1}", - "pt": "ID do dispositivo: {arg1}", - "pl": "Identyfikator urządzenia: {arg1}", - "nl": "Apparaat-ID: {arg1}", - "ru": "Идентификатор устройства: {arg1}" + "en": "Device ID: {deviceId}", + "fr": "Identifiant de l’appareil : {deviceId}", + "es": "ID del dispositivo: {deviceId}", + "it": "ID dispositivo: {deviceId}", + "de": "Geräte-ID: {deviceId}", + "pt": "ID do dispositivo: {deviceId}", + "pl": "Identyfikator urządzenia: {deviceId}", + "nl": "Apparaat-ID: {deviceId}", + "ru": "Идентификатор устройства: {deviceId}" } }, "approval_nearby_device": { @@ -530,27 +530,27 @@ } }, "approval_request_body": { - "context": "Approval prompt body: '{arg1} wants to receive \"{arg2}\".' {arg1} = requester name, {arg2} = transfer name.", + "context": "Approval prompt body: '{receiver} wants to receive \"{transferName}\".' {receiver} = requester name, {transferName} = transfer name.", "args": [ { - "name": "arg1", + "name": "receiver", "type": "string" }, { - "name": "arg2", + "name": "transferName", "type": "string" } ], "translations": { - "en": "{arg1} wants to receive “{arg2}”.", - "fr": "{arg1} souhaite recevoir « {arg2} ».", - "es": "{arg1} quiere recibir «{arg2}».", - "it": "{arg1} vuole ricevere «{arg2}».", - "de": "{arg1} möchte „{arg2}“ empfangen.", - "pt": "{arg1} quer receber «{arg2}».", - "pl": "{arg1} chce odebrać „{arg2}”.", - "nl": "{arg1} wil ‘{arg2}’ ontvangen.", - "ru": "{arg1} хочет получить «{arg2}»." + "en": "{receiver} wants to receive “{transferName}”.", + "fr": "{receiver} souhaite recevoir « {transferName} ».", + "es": "{receiver} quiere recibir «{transferName}».", + "it": "{receiver} vuole ricevere «{transferName}».", + "de": "{receiver} möchte „{transferName}“ empfangen.", + "pt": "{receiver} quer receber «{transferName}».", + "pl": "{receiver} chce odebrać „{transferName}”.", + "nl": "{receiver} wil ‘{transferName}’ ontvangen.", + "ru": "{receiver} хочет получить «{transferName}»." } }, "battery_level_title": { @@ -2212,23 +2212,23 @@ } }, "receive_delete_history_description": { - "context": "Receive history: confirmation body for removing one item. {arg1} = transfer name.", + "context": "Receive history: confirmation body for removing one item. {transferName} = transfer name.", "args": [ { - "name": "arg1", + "name": "transferName", "type": "string" } ], "translations": { - "en": "“{arg1}” will be removed from VniDrop’s history. The downloaded file will remain on this device.", - "fr": "« {arg1} » sera retiré de l’historique de VniDrop. Le fichier téléchargé restera sur cet appareil.", - "es": "«{arg1}» se eliminará del historial de VniDrop. El archivo descargado permanecerá en este dispositivo.", - "it": "«{arg1}» verrà rimosso dalla cronologia di VniDrop. Il file scaricato rimarrà su questo dispositivo.", - "de": "„{arg1}“ wird aus dem Verlauf von VniDrop entfernt. Die heruntergeladene Datei verbleibt auf diesem Gerät.", - "pt": "«{arg1}» será removido do histórico do VniDrop. O ficheiro descarregado permanecerá neste dispositivo.", - "pl": "„{arg1}” zostanie usunięty z historii VniDrop. Pobrany plik pozostanie na tym urządzeniu.", - "nl": "‘{arg1}’ wordt uit de geschiedenis van VniDrop verwijderd. Het gedownloade bestand blijft op dit apparaat.", - "ru": "«{arg1}» будет удалён из истории VniDrop. Загруженный файл останется на этом устройстве." + "en": "“{transferName}” will be removed from VniDrop’s history. The downloaded file will remain on this device.", + "fr": "« {transferName} » sera retiré de l’historique de VniDrop. Le fichier téléchargé restera sur cet appareil.", + "es": "«{transferName}» se eliminará del historial de VniDrop. El archivo descargado permanecerá en este dispositivo.", + "it": "«{transferName}» verrà rimosso dalla cronologia di VniDrop. Il file scaricato rimarrà su questo dispositivo.", + "de": "„{transferName}“ wird aus dem Verlauf von VniDrop entfernt. Die heruntergeladene Datei verbleibt auf diesem Gerät.", + "pt": "«{transferName}» será removido do histórico do VniDrop. O ficheiro descarregado permanecerá neste dispositivo.", + "pl": "„{transferName}” zostanie usunięty z historii VniDrop. Pobrany plik pozostanie na tym urządzeniu.", + "nl": "‘{transferName}’ wordt uit de geschiedenis van VniDrop verwijderd. Het gedownloade bestand blijft op dit apparaat.", + "ru": "«{transferName}» будет удалён из истории VniDrop. Загруженный файл останется на этом устройстве." } }, "receive_delete_history_item": { @@ -3148,23 +3148,23 @@ } }, "transfer_delete_description": { - "context": "Transfer details: confirmation body for deleting a transfer. {arg1} = transfer name.", + "context": "Transfer details: confirmation body for deleting a transfer. {transferName} = transfer name.", "args": [ { - "name": "arg1", + "name": "transferName", "type": "string" } ], "translations": { - "en": "“{arg1}” will stop being shared and its transfer history will be removed from this device.", - "fr": "« {arg1} » cessera d’être partagé et son historique de transfert sera retiré de cet appareil.", - "es": "«{arg1}» dejará de compartirse y su historial de transferencia se eliminará de este dispositivo.", - "it": "«{arg1}» non verrà più condiviso e la sua cronologia di trasferimento verrà rimossa da questo dispositivo.", - "de": "„{arg1}“ wird nicht mehr geteilt und sein Übertragungsverlauf wird von diesem Gerät entfernt.", - "pt": "«{arg1}» deixará de ser partilhado e o seu histórico de transferência será removido deste dispositivo.", - "pl": "„{arg1}” przestanie być udostępniany, a jego historia transferu zostanie usunięta z tego urządzenia.", - "nl": "‘{arg1}’ wordt niet meer gedeeld en de overdrachtsgeschiedenis wordt van dit apparaat verwijderd.", - "ru": "Общий доступ к «{arg1}» будет остановлен, а история передачи будет удалена с этого устройства." + "en": "“{transferName}” will stop being shared and its transfer history will be removed from this device.", + "fr": "« {transferName} » cessera d’être partagé et son historique de transfert sera retiré de cet appareil.", + "es": "«{transferName}» dejará de compartirse y su historial de transferencia se eliminará de este dispositivo.", + "it": "«{transferName}» non verrà più condiviso e la sua cronologia di trasferimento verrà rimossa da questo dispositivo.", + "de": "„{transferName}“ wird nicht mehr geteilt und sein Übertragungsverlauf wird von diesem Gerät entfernt.", + "pt": "«{transferName}» deixará de ser partilhado e o seu histórico de transferência será removido deste dispositivo.", + "pl": "„{transferName}” przestanie być udostępniany, a jego historia transferu zostanie usunięta z tego urządzenia.", + "nl": "‘{transferName}’ wordt niet meer gedeeld en de overdrachtsgeschiedenis wordt van dit apparaat verwijderd.", + "ru": "Общий доступ к «{transferName}» будет остановлен, а история передачи будет удалена с этого устройства." } }, "transfer_delete_title": { From 1563bf80d67e8e7b3e2f8869ad2109da1e3b5647 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:40:07 +0200 Subject: [PATCH 02/36] feat(apple): type-safe SF Symbols via SFSafeSymbols Replaces every stringly-typed SF Symbol name with a compile-time-checked SFSymbol case, mirroring the L10n accessor approach. A mistyped or OS-unavailable symbol is now a build error instead of a silently blank glyph at runtime. Adds the SFSafeSymbols SPM package (project.yml) and migrates all call sites: Image(systemName:)/Label(systemImage:) -> systemSymbol, and the five symbol-carrying view properties (AppDestination.systemSymbol, SettingsRow.icon, AboutPoint.symbol, MethodRow.icon, PolicyOption.icon) flipped from String to SFSymbol end to end. --- apple/VniDrop/App/RootView.swift | 5 +-- .../Features/Approvals/ApprovalModal.swift | 3 +- .../Receive/ReceiveInvitationActions.swift | 11 ++++--- .../Features/Receive/ReceiveScreen.swift | 13 ++++---- apple/VniDrop/Features/Send/SendScreen.swift | 11 ++++--- .../Features/Send/TransferComposer.swift | 17 +++++----- .../Features/Send/TransferDetailsView.swift | 7 ++-- .../Features/Settings/SettingsScreen.swift | 17 +++++----- .../Features/Settings/SettingsSections.swift | 33 ++++++++++--------- apple/VniDrop/UI/Feedback/SnackbarHost.swift | 3 +- .../UI/Navigation/AppDestination.swift | 9 ++--- apple/project.yml | 4 +++ 12 files changed, 74 insertions(+), 59 deletions(-) diff --git a/apple/VniDrop/App/RootView.swift b/apple/VniDrop/App/RootView.swift index eea5b8f..7abbc96 100644 --- a/apple/VniDrop/App/RootView.swift +++ b/apple/VniDrop/App/RootView.swift @@ -1,3 +1,4 @@ +import SFSafeSymbols import SwiftUI /// App root, ported from `App.kt`. Owns the object graph and feature models, wires @@ -111,7 +112,7 @@ struct RootView: View { #if os(macOS) NavigationSplitView { List(AppDestination.allCases, selection: sidebarBinding) { destination in - Label(String(localized: destination.labelKey), systemImage: destination.systemImage) + Label(String(localized: destination.labelKey), systemSymbol: destination.systemSymbol) .tag(destination) } .navigationSplitViewColumnWidth(min: 180, ideal: 200, max: 260) @@ -123,7 +124,7 @@ struct RootView: View { ForEach(AppDestination.allCases) { destination in screen(for: destination, windowClass: windowClass) .tabItem { - Label(String(localized: destination.labelKey), systemImage: destination.systemImage) + Label(String(localized: destination.labelKey), systemSymbol: destination.systemSymbol) } .tag(destination) } diff --git a/apple/VniDrop/Features/Approvals/ApprovalModal.swift b/apple/VniDrop/Features/Approvals/ApprovalModal.swift index c731ef8..53a6e01 100644 --- a/apple/VniDrop/Features/Approvals/ApprovalModal.swift +++ b/apple/VniDrop/Features/Approvals/ApprovalModal.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols /// Non-dismissable receiver-approval modal, presented as a native sheet that can't /// be swiped away. The endpoint id is the trusted identity; display names are @@ -40,7 +41,7 @@ private struct ApprovalSheet: View { let busy = state.respondingIds.contains(request.id) let receiver = request.receiverName ?? request.receiverDeviceName ?? String(localized: L10n.Approval.nearbyDevice) VStack(spacing: 16) { - Image(systemName: "checkmark.shield.fill") + Image(systemSymbol: .checkmarkShieldFill) .font(.system(size: 44)) .foregroundStyle(.tint) .padding(.top, 12) diff --git a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift index f0b7a21..65f1646 100644 --- a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift +++ b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift @@ -1,3 +1,4 @@ +import SFSafeSymbols import SwiftUI enum ReceiveMethodAvailability { case available, unavailable, hidden } @@ -27,19 +28,19 @@ struct ReceiveMethodPanel: View { Text(String(localized: L10n.Receive.chooseMethodBody)).foregroundStyle(colors.foregroundLighter) MethodRow( - icon: "doc", titleKey: L10n.Receive.methodFile, descKey: L10n.Receive.methodFileDescription, + icon: .doc, titleKey: L10n.Receive.methodFile, descKey: L10n.Receive.methodFileDescription, availability: actions.fileAvailability ) { actions.pickInvitation { model.onInvitationResult(.invitationFile, $0) } } if actions.qrAvailability != .hidden { MethodRow( - icon: "qrcode.viewfinder", titleKey: L10n.Receive.methodScan, descKey: L10n.Receive.methodScanDescription, + icon: .qrcodeViewfinder, titleKey: L10n.Receive.methodScan, descKey: L10n.Receive.methodScanDescription, availability: actions.qrAvailability ) { actions.scanQrCode { model.onInvitationResult(.qrCode, $0) } } } if actions.nfcAvailability != .hidden { MethodRow( - icon: "wave.3.right", + icon: .wave3Right, titleOverride: model.state.isWaitingForNfc ? String(localized: L10n.Receive.nfcWaiting) : nil, titleKey: L10n.Receive.methodNfc, descKey: L10n.Receive.methodNfcDescription, availability: model.state.isWaitingForNfc ? .unavailable : actions.nfcAvailability @@ -56,7 +57,7 @@ struct ReceiveMethodPanel: View { private struct MethodRow: View { @Environment(\.vniColors) private var colors - let icon: String + let icon: SFSymbol var titleOverride: String? = nil let titleKey: String.LocalizationValue let descKey: String.LocalizationValue @@ -67,7 +68,7 @@ private struct MethodRow: View { let enabled = availability == .available Button(action: onTap) { HStack(spacing: 14) { - Image(systemName: icon).font(.system(size: 22)) + Image(systemSymbol: icon).font(.system(size: 22)) .foregroundStyle(enabled ? colors.brandLink : colors.foregroundLighter) .frame(width: 24) VStack(alignment: .leading, spacing: 3) { diff --git a/apple/VniDrop/Features/Receive/ReceiveScreen.swift b/apple/VniDrop/Features/Receive/ReceiveScreen.swift index d82dc24..1f2c705 100644 --- a/apple/VniDrop/Features/Receive/ReceiveScreen.swift +++ b/apple/VniDrop/Features/Receive/ReceiveScreen.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols /// Receive screen, rebuilt on native SwiftUI. A grouped `List` of received /// transfers with swipe-to-delete, and the acquisition flow as a native sheet. @@ -26,13 +27,13 @@ struct ReceiveScreen: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: model.openAcquisition) { - Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus") + Label(String(localized: L10n.Button.receiveFiles), systemSymbol: .plus) } } if !deletable.isEmpty { ToolbarItem(placement: .primaryAction) { Button(role: .destructive, action: model.requestClearHistory) { - Label(String(localized: L10n.Receive.clearHistory), systemImage: "trash") + Label(String(localized: L10n.Receive.clearHistory), systemSymbol: .trash) } } } @@ -74,7 +75,7 @@ struct ReceiveScreen: View { Button(role: .destructive) { model.requestDeleteHistoryItem(transfer.transferId) } label: { - Label(String(localized: L10n.Button.deleteTransfer), systemImage: "trash") + Label(String(localized: L10n.Button.deleteTransfer), systemSymbol: .trash) } } } @@ -89,12 +90,12 @@ struct ReceiveScreen: View { private var emptyState: some View { ContentUnavailableView { - Label(String(localized: L10n.Receive.emptyTitle), systemImage: "tray.and.arrow.down") + Label(String(localized: L10n.Receive.emptyTitle), systemSymbol: .trayAndArrowDown) } description: { Text(String(localized: L10n.Receive.emptyBody)) } actions: { Button(action: model.openAcquisition) { - Label(String(localized: L10n.Button.receiveFiles), systemImage: "plus") + Label(String(localized: L10n.Button.receiveFiles), systemSymbol: .plus) } .buttonStyle(.borderedProminent) .controlSize(.large) @@ -129,7 +130,7 @@ private struct ReceiveTransferRow: View { var body: some View { HStack(spacing: 12) { - Image(systemName: "doc") + Image(systemSymbol: .doc) .foregroundStyle(.secondary) .frame(width: 40, height: 40) .background(.quaternary, in: RoundedRectangle(cornerRadius: 9)) diff --git a/apple/VniDrop/Features/Send/SendScreen.swift b/apple/VniDrop/Features/Send/SendScreen.swift index 93ddc21..66e0c4d 100644 --- a/apple/VniDrop/Features/Send/SendScreen.swift +++ b/apple/VniDrop/Features/Send/SendScreen.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols /// Send screen, rebuilt on native SwiftUI. A grouped `List` of outgoing transfers, /// with the composer and detail panels as native sheets and delete as an alert. @@ -31,7 +32,7 @@ struct SendScreen: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button(action: model.openComposer) { - Label(String(localized: L10n.Button.createNewTransfer), systemImage: "plus") + Label(String(localized: L10n.Button.createNewTransfer), systemSymbol: .plus) } } } @@ -101,12 +102,12 @@ struct SendScreen: View { private var emptyState: some View { ContentUnavailableView { - Label(String(localized: L10n.Send.emptyTitle), systemImage: "paperplane") + Label(String(localized: L10n.Send.emptyTitle), systemSymbol: .paperplane) } description: { Text(String(localized: L10n.Send.emptyBody)) } actions: { Button(action: model.openComposer) { - Label(String(localized: L10n.Button.createNewTransfer), systemImage: "plus") + Label(String(localized: L10n.Button.createNewTransfer), systemSymbol: .plus) } .buttonStyle(.borderedProminent) .controlSize(.large) @@ -166,7 +167,7 @@ private struct TransferListItem: View { .padding(.top, 2) } } - Image(systemName: "chevron.forward") + Image(systemSymbol: .chevronForward) .font(.footnote.weight(.semibold)).foregroundStyle(.tertiary) } .contentShape(Rectangle()) @@ -181,7 +182,7 @@ struct FileArtwork: View { image.resizable().aspectRatio(contentMode: .fill) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { - Image(systemName: "doc") + Image(systemSymbol: .doc) .font(.system(size: 18)) .foregroundStyle(.secondary) } diff --git a/apple/VniDrop/Features/Send/TransferComposer.swift b/apple/VniDrop/Features/Send/TransferComposer.swift index 4dbd74f..1cbfff7 100644 --- a/apple/VniDrop/Features/Send/TransferComposer.swift +++ b/apple/VniDrop/Features/Send/TransferComposer.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols /// Transfer composer drawer, ported from `feature/send/TransferComposer.kt`. /// Two steps: choose files/folder, then review + name + access policy + share. @@ -27,7 +28,7 @@ struct TransferComposer: View { Text(String(localized: L10n.Send.chooseFileBody)) .font(.subheadline).foregroundStyle(.secondary) VStack(spacing: 14) { - Image(systemName: "doc").font(.system(size: 30)).foregroundStyle(.tint) + Image(systemSymbol: .doc).font(.system(size: 30)).foregroundStyle(.tint) PrimaryButton(title: String(localized: L10n.Button.chooseFiles), action: model.selectFile).fixedSize() QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder) } @@ -57,17 +58,17 @@ struct TransferComposer: View { value: Binding(get: { state.senderName }, set: { model.setSenderName($0) })) Text(String(localized: L10n.Send.accessTitle)).font(.headline) PolicyOption( - icon: "checkmark.shield", titleKey: L10n.Send.accessApproval, descKey: L10n.Send.accessApprovalDescription, + icon: .checkmarkShield, titleKey: L10n.Send.accessApproval, descKey: L10n.Send.accessApprovalDescription, selected: state.accessPolicy == .requireApproval, onTap: { model.setAccessPolicy(.requireApproval) } ) PolicyOption( - icon: "globe", titleKey: L10n.Send.accessAnyone, descKey: L10n.Send.accessAnyoneDescription, + icon: .globe, titleKey: L10n.Send.accessAnyone, descKey: L10n.Send.accessAnyoneDescription, selected: state.accessPolicy == .anyoneWithTransfer, onTap: { model.setAccessPolicy(.anyoneWithTransfer) } ) if state.accessPolicy == .anyoneWithTransfer { - Label(String(localized: L10n.Send.accessAnyoneWarning), systemImage: "exclamationmark.triangle.fill") + Label(String(localized: L10n.Send.accessAnyoneWarning), systemSymbol: .exclamationmarkTriangleFill) .font(.caption).foregroundStyle(.orange) } actions @@ -116,7 +117,7 @@ private struct SelectedFileCard: View { Spacer() if canRemove { Button(role: .destructive, action: onRemove) { - Image(systemName: "trash") + Image(systemSymbol: .trash) } .buttonStyle(.borderless) .tint(.red) @@ -135,7 +136,7 @@ private struct SelectedFileCard: View { } private struct PolicyOption: View { - let icon: String + let icon: SFSymbol let titleKey: String.LocalizationValue let descKey: String.LocalizationValue let selected: Bool @@ -144,7 +145,7 @@ private struct PolicyOption: View { var body: some View { Button(action: onTap) { HStack(spacing: 12) { - Image(systemName: icon) + Image(systemSymbol: icon) .font(.system(size: 20)) .foregroundStyle(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.secondary)) .frame(width: 22) @@ -153,7 +154,7 @@ private struct PolicyOption: View { Text(String(localized: descKey)).font(.caption).foregroundStyle(.secondary) } Spacer() - Image(systemName: selected ? "checkmark.circle.fill" : "circle") + Image(systemSymbol: selected ? .checkmarkCircleFill : .circle) .foregroundStyle(selected ? AnyShapeStyle(.tint) : AnyShapeStyle(.tertiary)) } .padding(14) diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index 643d0f6..90d0077 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols import CoreImage.CIFilterBuiltins /// Transfer details + drawer panels, ported from `feature/send/TransferDetails.kt`. @@ -56,7 +57,7 @@ struct TransferDetailsView: View { Button(role: .destructive) { showStopConfirmation = true } label: { - Label(String(localized: L10n.Send.stopSharing), systemImage: "stop.circle") + Label(String(localized: L10n.Send.stopSharing), systemSymbol: .stopCircle) } } } @@ -69,7 +70,7 @@ struct TransferDetailsView: View { .toolbar { ToolbarItem(placement: .primaryAction) { Button(role: .destructive, action: model.requestDeleteTransfer) { - Image(systemName: "trash") + Image(systemSymbol: .trash) } } } @@ -115,7 +116,7 @@ private struct DetailDestination: View { .font(.footnote) .foregroundStyle(.secondary) } - Image(systemName: "chevron.forward") + Image(systemSymbol: .chevronForward) .font(.footnote.weight(.semibold)).foregroundStyle(.tertiary) } .contentShape(Rectangle()) diff --git a/apple/VniDrop/Features/Settings/SettingsScreen.swift b/apple/VniDrop/Features/Settings/SettingsScreen.swift index 099a7a0..ae0723f 100644 --- a/apple/VniDrop/Features/Settings/SettingsScreen.swift +++ b/apple/VniDrop/Features/Settings/SettingsScreen.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols /// Settings screen, rebuilt on a native `Form` with `NavigationStack` push /// navigation. The model stays the source of truth via a derived path binding. @@ -25,21 +26,21 @@ struct SettingsScreen: View { Form { Section { NavigationLink(value: SettingsSection.preferences) { - SettingsRow(icon: "person.crop.circle", title: String(localized: L10n.Preferences.title), value: model.state.username) + SettingsRow(icon: .personCropCircle, title: String(localized: L10n.Preferences.title), value: model.state.username) } NavigationLink(value: SettingsSection.appearance) { - SettingsRow(icon: "sun.max", title: String(localized: L10n.Appearance.title), value: themeModeLabel(model.state.themeMode)) + SettingsRow(icon: .sunMax, title: String(localized: L10n.Appearance.title), value: themeModeLabel(model.state.themeMode)) } } Section { NavigationLink(value: SettingsSection.notifications) { - SettingsRow(icon: "bell", title: String(localized: L10n.Notifications.title), value: nil) + SettingsRow(icon: .bell, title: String(localized: L10n.Notifications.title), value: nil) } NavigationLink(value: SettingsSection.storage) { - SettingsRow(icon: "internaldrive", title: String(localized: L10n.Storage.title), value: nil) + SettingsRow(icon: .internaldrive, title: String(localized: L10n.Storage.title), value: nil) } NavigationLink(value: SettingsSection.about) { - SettingsRow(icon: "info.circle", title: String(localized: L10n.About.title), value: nil) + SettingsRow(icon: .infoCircle, title: String(localized: L10n.About.title), value: nil) } } } @@ -69,7 +70,7 @@ struct SettingsScreen: View { Button { showBugReport = true } label: { - Label(String(localized: L10n.About.bugReport), systemImage: "ladybug") + Label(String(localized: L10n.About.bugReport), systemSymbol: .ladybug) } } } @@ -110,13 +111,13 @@ private struct SettingsSectionContent: View { } struct SettingsRow: View { - let icon: String + let icon: SFSymbol let title: String let value: String? var body: some View { HStack(spacing: 12) { - Image(systemName: icon) + Image(systemSymbol: icon) .foregroundStyle(.tint) .frame(width: 26) Text(title).foregroundStyle(.primary) diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index 1cbc173..665bcb9 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols /// Settings section detail views, rebuilt as native `Form` content. Each view is /// placed inside a parent `Form`, so it returns `Section`s / rows directly. @@ -126,24 +127,24 @@ struct AboutSettings: View { } Section(String(localized: L10n.About.isTitle)) { - AboutPoint(L10n.About.isDirect, "paperplane") - AboutPoint(L10n.About.isNoAccount, "person.crop.circle.badge.xmark") - AboutPoint(L10n.About.isInControl, "checkmark.shield") - AboutPoint(L10n.About.isEncrypted, "lock") - AboutPoint(L10n.About.isOpen, "chevron.left.forwardslash.chevron.right") + AboutPoint(L10n.About.isDirect, .paperplane) + AboutPoint(L10n.About.isNoAccount, .personCropCircleBadgeXmark) + AboutPoint(L10n.About.isInControl, .checkmarkShield) + AboutPoint(L10n.About.isEncrypted, .lock) + AboutPoint(L10n.About.isOpen, .chevronLeftForwardslashChevronRight) } Section(String(localized: L10n.About.isntTitle)) { - AboutPoint(L10n.About.isntCloud, "icloud.slash") - AboutPoint(L10n.About.isntSync, "arrow.triangle.2.circlepath") - AboutPoint(L10n.About.isntPublic, "megaphone") + AboutPoint(L10n.About.isntCloud, .icloudSlash) + AboutPoint(L10n.About.isntSync, .arrowTriangle2Circlepath) + AboutPoint(L10n.About.isntPublic, .megaphone) } Section(String(localized: L10n.About.privacyTitle)) { - AboutPoint(L10n.About.privacyCapability, "qrcode") - AboutPoint(L10n.About.privacyDeny, "hand.raised") - AboutPoint(L10n.About.privacyRelay, "antenna.radiowaves.left.and.right") - AboutPoint(L10n.About.privacyLocal, "internaldrive") + AboutPoint(L10n.About.privacyCapability, .qrcode) + AboutPoint(L10n.About.privacyDeny, .handRaised) + AboutPoint(L10n.About.privacyRelay, .antennaRadiowavesLeftAndRight) + AboutPoint(L10n.About.privacyLocal, .internaldrive) } Section(String(localized: L10n.About.title)) { @@ -154,7 +155,7 @@ struct AboutSettings: View { } LabeledContent(String(localized: L10n.About.licenseLabel), value: "Apache 2.0") Link(destination: Self.privacyPolicyURL) { - Label(String(localized: L10n.About.privacyPolicyLabel), systemImage: "hand.raised") + Label(String(localized: L10n.About.privacyPolicyLabel), systemSymbol: .handRaised) } } @@ -205,9 +206,9 @@ struct BugReportSheet: View { /// A bullet-style informational row with an SF Symbol and wrapping localized text. private struct AboutPoint: View { let key: String.LocalizationValue - let symbol: String + let symbol: SFSymbol - init(_ key: String.LocalizationValue, _ symbol: String) { + init(_ key: String.LocalizationValue, _ symbol: SFSymbol) { self.key = key self.symbol = symbol } @@ -218,7 +219,7 @@ private struct AboutPoint: View { .font(.subheadline) .fixedSize(horizontal: false, vertical: true) } icon: { - Image(systemName: symbol).foregroundStyle(.tint) + Image(systemSymbol: symbol).foregroundStyle(.tint) } } } diff --git a/apple/VniDrop/UI/Feedback/SnackbarHost.swift b/apple/VniDrop/UI/Feedback/SnackbarHost.swift index c49ec51..b60a414 100644 --- a/apple/VniDrop/UI/Feedback/SnackbarHost.swift +++ b/apple/VniDrop/UI/Feedback/SnackbarHost.swift @@ -1,4 +1,5 @@ import SwiftUI +import SFSafeSymbols /// Bottom toast host driven by `UiMessageController`, ported from /// `ui/feedback/VniDropSnackbarHost.kt`. Tone drives the accent color; errors get @@ -52,7 +53,7 @@ struct SnackbarHost: View { .buttonStyle(.borderless) } Button(action: dismiss) { - Image(systemName: "xmark") + Image(systemSymbol: .xmark) .font(.footnote.weight(.semibold)) .foregroundStyle(.secondary) .frame(width: 36, height: 36) diff --git a/apple/VniDrop/UI/Navigation/AppDestination.swift b/apple/VniDrop/UI/Navigation/AppDestination.swift index 991ec47..de8804d 100644 --- a/apple/VniDrop/UI/Navigation/AppDestination.swift +++ b/apple/VniDrop/UI/Navigation/AppDestination.swift @@ -1,4 +1,5 @@ import Foundation +import SFSafeSymbols /// Top-level destinations, ported from `ui/navigation/AppDestination.kt`. enum AppDestination: String, CaseIterable, Identifiable { @@ -17,11 +18,11 @@ enum AppDestination: String, CaseIterable, Identifiable { } /// SF Symbol approximating the Compose line icon. - var systemImage: String { + var systemSymbol: SFSymbol { switch self { - case .send: return "paperplane" - case .receive: return "tray.and.arrow.down" - case .settings: return "gearshape" + case .send: return .paperplane + case .receive: return .trayAndArrowDown + case .settings: return .gearshape } } } diff --git a/apple/project.yml b/apple/project.yml index ab0d4c5..ff0b7fb 100644 --- a/apple/project.yml +++ b/apple/project.yml @@ -12,6 +12,9 @@ options: packages: VnidropCore: path: VnidropCore + SFSafeSymbols: + url: https://github.com/SFSafeSymbols/SFSafeSymbols + from: "5.3.0" targets: VniDrop: @@ -48,6 +51,7 @@ targets: CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements dependencies: - package: VnidropCore + - package: SFSafeSymbols - sdk: SystemConfiguration.framework - sdk: Security.framework - sdk: libresolv.tbd From 08e61c57af7774ef7595b5d8fcb9ad07b33b5b98 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 17:50:57 +0200 Subject: [PATCH 03/36] feat(l10n): replace legacy %@ format keys with semantic template keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Apple catalog carried four stringly-named passthrough keys ("%@", "%@ · %@", "%@ %@ · %@", "%@%%") left over from the KMP port. They were never referenced as keys — the composite strings were built inline with hardcoded separators, so the middot/percent formatting wasn't localizable. Renames them to proper semantic keys in strings.json: - format_separated_pair(first:second:) "{first} · {second}" - format_separated_triple(first:second:third:) - battery_level_value(level:) "{level}%" and drops the pure-identity "%@". Wires the inline compositions (size · status, count files · size, receivers pending · completed, battery level) to the generated typed accessors. Catalog now validates with zero warnings. --- .../Receive/ReceiveInvitationActions.swift | 5 +- .../Features/Receive/ReceiveScreen.swift | 2 +- apple/VniDrop/Features/Send/SendScreen.swift | 2 +- .../Features/Send/TransferDetailsView.swift | 4 +- apple/VniDrop/Generated/L10n.swift | 53 +++ .../Platform/AppDependencies+iOS.swift | 2 +- apple/VniDrop/Resources/Localizable.xcstrings | 308 ++++++++++-------- localization/strings.json | 151 +++++---- 8 files changed, 324 insertions(+), 203 deletions(-) diff --git a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift index 65f1646..ee80794 100644 --- a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift +++ b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift @@ -111,7 +111,10 @@ struct InvitationReviewPanel: View { let metadata = inspection.metadata VStack(alignment: .leading, spacing: 8) { Text(metadata.transferName).font(VniType.bodyLarge).lineLimit(2) - Text("\(metadata.fileCount) \(String(localized: L10n.Metadata.files).lowercased()) · \(formatBytes(metadata.totalSize))") + Text(L10n.Format.separatedTriple( + first: "\(metadata.fileCount)", + second: String(localized: L10n.Metadata.files).lowercased(), + third: formatBytes(metadata.totalSize))) .foregroundStyle(colors.foregroundLighter) } .padding(16) diff --git a/apple/VniDrop/Features/Receive/ReceiveScreen.swift b/apple/VniDrop/Features/Receive/ReceiveScreen.swift index 1f2c705..a49012f 100644 --- a/apple/VniDrop/Features/Receive/ReceiveScreen.swift +++ b/apple/VniDrop/Features/Receive/ReceiveScreen.swift @@ -137,7 +137,7 @@ private struct ReceiveTransferRow: View { VStack(alignment: .leading, spacing: 3) { Text(transfer.transferName ?? String(localized: L10n.Receive.unknownTransfer)) .font(.body).lineLimit(1) - Text("\(formatBytes(transfer.totalSize)) · \(statusLabel(transfer.status))") + Text(L10n.Format.separatedPair(first: formatBytes(transfer.totalSize), second: statusLabel(transfer.status))) .font(.caption).foregroundStyle(.secondary) if transfer.status == .receiving, let progress { ProgressRow(labelKey: progress.labelKey, progress: progress.progress, detail: progress.detail) diff --git a/apple/VniDrop/Features/Send/SendScreen.swift b/apple/VniDrop/Features/Send/SendScreen.swift index 66e0c4d..ab9e3a4 100644 --- a/apple/VniDrop/Features/Send/SendScreen.swift +++ b/apple/VniDrop/Features/Send/SendScreen.swift @@ -160,7 +160,7 @@ private struct TransferListItem: View { Spacer() StatusPill(label: statusLabel(transfer.status), tone: transfer.status.pillTone) } - Text("\(formatBytes(transfer.totalSize)) · \(accessPolicyLabel(transfer.accessPolicy))") + Text(L10n.Format.separatedPair(first: formatBytes(transfer.totalSize), second: accessPolicyLabel(transfer.accessPolicy))) .font(.caption).foregroundStyle(.secondary).lineLimit(1) if let progress, transfer.status == .importing || transfer.status == .sharing { ProgressRow(labelKey: progress.labelKey, progress: progress.progress, detail: progress.detail, labelText: progress.label) diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index 90d0077..71e57ad 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -91,7 +91,9 @@ struct TransferDetailsView: View { private func receiversDescription(_ pending: Int, _ completed: Int) -> String { if pending > 0 && completed > 0 { - return "\(L10n.Transfer.receiversPending(count: pending)) · \(L10n.Transfer.receiversCompletedCount(count: completed))" + return L10n.Format.separatedPair( + first: L10n.Transfer.receiversPending(count: pending), + second: L10n.Transfer.receiversCompletedCount(count: completed)) } if pending > 0 { return L10n.Transfer.receiversPending(count: pending) } if completed > 0 { return L10n.Transfer.receiversCompletedCount(count: completed) } diff --git a/apple/VniDrop/Generated/L10n.swift b/apple/VniDrop/Generated/L10n.swift index de09ca8..e1532c4 100644 --- a/apple/VniDrop/Generated/L10n.swift +++ b/apple/VniDrop/Generated/L10n.swift @@ -514,6 +514,23 @@ enum L10n { /// - nl: Batterijniveau /// - ru: Уровень заряда static let levelTitle: String.LocalizationValue = "battery_level_title" + /// {level}% + /// + /// Key: `battery_level_value` + /// Context: Device information: battery charge formatted as a percentage. {level} = integer percent value. + /// + /// - en: {level}% + /// - fr: {level} % + /// - es: {level} % + /// - it: {level}% + /// - de: {level} % + /// - pt: {level}% + /// - pl: {level}% + /// - nl: {level}% + /// - ru: {level} % + static func levelValue(level: String) -> String { + String(format: String(localized: "battery_level_value"), level) + } } enum Bug { /// name@example.com @@ -1684,6 +1701,42 @@ enum L10n { /// - ru: Готово static let statusWritable: String.LocalizationValue = "folder_status_writable" } + enum Format { + /// {first} · {second} + /// + /// Key: `format_separated_pair` + /// Context: Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values. + /// + /// - en: {first} · {second} + /// - fr: {first} · {second} + /// - es: {first} · {second} + /// - it: {first} · {second} + /// - de: {first} · {second} + /// - pt: {first} · {second} + /// - pl: {first} · {second} + /// - nl: {first} · {second} + /// - ru: {first} · {second} + static func separatedPair(first: String, second: String) -> String { + String(format: String(localized: "format_separated_pair"), first, second) + } + /// {first} {second} · {third} + /// + /// Key: `format_separated_triple` + /// Context: Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values. + /// + /// - en: {first} {second} · {third} + /// - fr: {first} {second} · {third} + /// - es: {first} {second} · {third} + /// - it: {first} {second} · {third} + /// - de: {first} {second} · {third} + /// - pt: {first} {second} · {third} + /// - pl: {first} {second} · {third} + /// - nl: {first} {second} · {third} + /// - ru: {first} {second} · {third} + static func separatedTriple(first: String, second: String, third: String) -> String { + String(format: String(localized: "format_separated_triple"), first, second, third) + } + } enum Metadata { /// Files /// diff --git a/apple/VniDrop/Platform/AppDependencies+iOS.swift b/apple/VniDrop/Platform/AppDependencies+iOS.swift index 1c27c43..e4d5bd2 100644 --- a/apple/VniDrop/Platform/AppDependencies+iOS.swift +++ b/apple/VniDrop/Platform/AppDependencies+iOS.swift @@ -36,7 +36,7 @@ private struct IosDeviceInfoProvider: DeviceInfoProvider { device.isBatteryMonitoringEnabled = true defer { device.isBatteryMonitoringEnabled = wasMonitoring } let level = device.batteryLevel - return level >= 0 ? "\(Int(level * 100))%" : nil + return level >= 0 ? L10n.Battery.levelValue(level: "\(Int(level * 100))") : nil }() return DeviceInfo( deviceName: device.name, diff --git a/apple/VniDrop/Resources/Localizable.xcstrings b/apple/VniDrop/Resources/Localizable.xcstrings index 17af5c5..4b811be 100644 --- a/apple/VniDrop/Resources/Localizable.xcstrings +++ b/apple/VniDrop/Resources/Localizable.xcstrings @@ -1,134 +1,6 @@ { "sourceLanguage": "en", "strings": { - "%@": { - "comment": "Apple format passthrough placeholder (single value). Legacy literal key — rename to a semantic key.", - "extractionState": "manual" - }, - "%@ %@ · %@": { - "comment": "Apple format template composing three values with a middot separator (e.g. metadata rows). Legacy literal key — rename.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@ %2$@ · %3$@" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - } - } - }, - "%@ · %@": { - "comment": "Apple format template composing two values with a middot separator. Legacy literal key — rename.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@ · %2$@" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - } - } - }, - "%@%%": { - "comment": "Apple format template appending a percent sign to a value (e.g. battery level). Legacy literal key — rename.", - "extractionState": "manual" - }, "about_bug_report": { "comment": "About screen: button/link that opens the bug report form.", "extractionState": "manual", @@ -2109,6 +1981,66 @@ } } }, + "battery_level_value": { + "comment": "Device information: battery charge formatted as a percentage. {level} = integer percent value.", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@%%" + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "nl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "pt": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + } + } + }, "bug_report_contact_hint": { "comment": "Bug report form: placeholder text in the contact email field.", "extractionState": "manual", @@ -6729,6 +6661,126 @@ } } }, + "format_separated_pair": { + "comment": "Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values.", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ · %2$@" + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "nl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "pt": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + } + } + }, + "format_separated_triple": { + "comment": "Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values.", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ %2$@ · %3$@" + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "nl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "pt": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + } + } + }, "metadata_files": { "comment": "Transfer metadata label: number of files.", "extractionState": "manual", diff --git a/localization/strings.json b/localization/strings.json index 6b46ecb..dec300d 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -13,76 +13,6 @@ "ru" ], "strings": { - "%@": { - "context": "Apple format passthrough placeholder (single value). Legacy literal key — rename to a semantic key.", - "targets": [ - "apple" - ] - }, - "%@ %@ · %@": { - "context": "Apple format template composing three values with a middot separator (e.g. metadata rows). Legacy literal key — rename.", - "targets": [ - "apple" - ], - "args": [ - { - "name": "arg1", - "type": "string" - }, - { - "name": "arg2", - "type": "string" - }, - { - "name": "arg3", - "type": "string" - } - ], - "translations": { - "en": "{arg1} {arg2} · {arg3}", - "fr": "{arg1} {arg2} · {arg3}", - "es": "{arg1} {arg2} · {arg3}", - "it": "{arg1} {arg2} · {arg3}", - "de": "{arg1} {arg2} · {arg3}", - "pt": "{arg1} {arg2} · {arg3}", - "pl": "{arg1} {arg2} · {arg3}", - "nl": "{arg1} {arg2} · {arg3}", - "ru": "{arg1} {arg2} · {arg3}" - } - }, - "%@ · %@": { - "context": "Apple format template composing two values with a middot separator. Legacy literal key — rename.", - "targets": [ - "apple" - ], - "args": [ - { - "name": "arg1", - "type": "string" - }, - { - "name": "arg2", - "type": "string" - } - ], - "translations": { - "en": "{arg1} · {arg2}", - "fr": "{arg1} · {arg2}", - "es": "{arg1} · {arg2}", - "it": "{arg1} · {arg2}", - "de": "{arg1} · {arg2}", - "pt": "{arg1} · {arg2}", - "pl": "{arg1} · {arg2}", - "nl": "{arg1} · {arg2}", - "ru": "{arg1} · {arg2}" - } - }, - "%@%%": { - "context": "Apple format template appending a percent sign to a value (e.g. battery level). Legacy literal key — rename.", - "targets": [ - "apple" - ] - }, "about_bug_report": { "context": "About screen: button/link that opens the bug report form.", "translations": { @@ -567,6 +497,29 @@ "ru": "Уровень заряда" } }, + "battery_level_value": { + "context": "Device information: battery charge formatted as a percentage. {level} = integer percent value.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "level", + "type": "string" + } + ], + "translations": { + "en": "{level}%", + "fr": "{level} %", + "es": "{level} %", + "it": "{level}%", + "de": "{level} %", + "pt": "{level}%", + "pl": "{level}%", + "nl": "{level}%", + "ru": "{level} %" + } + }, "bug_report_contact_hint": { "context": "Bug report form: placeholder text in the contact email field.", "translations": { @@ -1645,6 +1598,64 @@ "ru": "Готово" } }, + "format_separated_pair": { + "context": "Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "first", + "type": "string" + }, + { + "name": "second", + "type": "string" + } + ], + "translations": { + "en": "{first} · {second}", + "fr": "{first} · {second}", + "es": "{first} · {second}", + "it": "{first} · {second}", + "de": "{first} · {second}", + "pt": "{first} · {second}", + "pl": "{first} · {second}", + "nl": "{first} · {second}", + "ru": "{first} · {second}" + } + }, + "format_separated_triple": { + "context": "Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "first", + "type": "string" + }, + { + "name": "second", + "type": "string" + }, + { + "name": "third", + "type": "string" + } + ], + "translations": { + "en": "{first} {second} · {third}", + "fr": "{first} {second} · {third}", + "es": "{first} {second} · {third}", + "it": "{first} {second} · {third}", + "de": "{first} {second} · {third}", + "pt": "{first} {second} · {third}", + "pl": "{first} {second} · {third}", + "nl": "{first} {second} · {third}", + "ru": "{first} {second} · {third}" + } + }, "metadata_files": { "context": "Transfer metadata label: number of files.", "translations": { From 3c8267adc5f951039deb65cd451ea92fdef5e64b Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:00:29 +0200 Subject: [PATCH 04/36] refactor(apple): type core event phase/kind/direction as enums MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the stringly-typed transfer-event phase/kind/direction values throughout the progress-derivation logic with EventPhase, EventKind and EventDirection enums (String-backed to match the core's wire values). CoreEventModel keeps the raw wire strings as a faithful boundary DTO but exposes typed eventPhase/eventKind/eventDirection accessors; all logic — progressForTransfer/Receiver, humanProgressLabel, aggregateReceiverProgress, the refresh trigger, and the SendScreen snapshots — now compares enum cases instead of literals. TransferProgress.phase/kind are the enums directly, so constructions read `phase: .transfer, kind: .progress`. The two ad-hoc phase/kind Sets collapse into "is a recognized case" (non-nil) checks. --- apple/Tests/ProgressDerivationTests.swift | 2 +- apple/VniDrop/Core/CoreModels.swift | 46 +++++++++ apple/VniDrop/Core/CoreRepository.swift | 9 +- apple/VniDrop/Core/TransferProgress.swift | 94 ++++++++----------- .../Receive/ReceiveInvitationActions.swift | 2 +- .../Features/Receive/ReceiveModel.swift | 2 +- apple/VniDrop/Features/Send/SendScreen.swift | 4 +- 7 files changed, 96 insertions(+), 63 deletions(-) diff --git a/apple/Tests/ProgressDerivationTests.swift b/apple/Tests/ProgressDerivationTests.swift index 6e6de33..ea54eb8 100644 --- a/apple/Tests/ProgressDerivationTests.swift +++ b/apple/Tests/ProgressDerivationTests.swift @@ -57,7 +57,7 @@ final class ProgressDerivationTests: XCTestCase { remoteEndpointId: "peer-a", totalSizeHint: 100 ) - XCTAssertEqual(progress?.kind, "completed") + XCTAssertEqual(progress?.kind, .completed) XCTAssertEqual(progress?.labelKey, L10n.Progress.completed) XCTAssertEqual(progress?.progress, 1) } diff --git a/apple/VniDrop/Core/CoreModels.swift b/apple/VniDrop/Core/CoreModels.swift index 33fe647..df40304 100644 --- a/apple/VniDrop/Core/CoreModels.swift +++ b/apple/VniDrop/Core/CoreModels.swift @@ -15,10 +15,56 @@ struct CoreEventModel: Equatable, Identifiable, Sendable { let timestamp: Int64 let scope: String let transferId: UInt64? + /// Raw wire values as emitted by the core. Interpret them through the typed + /// `eventDirection` / `eventPhase` / `eventKind` accessors below — logic code + /// should never compare these strings directly. let direction: String? let phase: String let kind: String let dataJson: String + + var eventDirection: EventDirection? { direction.flatMap(EventDirection.init(rawValue:)) } + var eventPhase: EventPhase? { EventPhase(rawValue: phase) } + var eventKind: EventKind? { EventKind(rawValue: kind) } +} + +/// Direction of a core event, matching the wire strings the core emits. +enum EventDirection: String, Equatable, Sendable { + case send + case receive +} + +/// Phase of a core progress event (the `phase` wire field). +enum EventPhase: String, Equatable, Sendable { + case importing = "import" + case ticket + case access + case transfer + case download + case export + case lifecycle + case network + case handshake + case error +} + +/// Kind of a core progress event (the `kind` wire field). +enum EventKind: String, Equatable, Sendable { + case started + case copyProgress = "copy-progress" + case copyDone = "copy-done" + case outboardProgress = "outboard-progress" + case done + case created + case progress + case completed + case aborted + case failed + case connecting + case connected + case foundCollection = "found-collection" + case cancelled + case shareStopped = "share-stopped" } enum ShareAccessPolicy: Equatable, Sendable { diff --git a/apple/VniDrop/Core/CoreRepository.swift b/apple/VniDrop/Core/CoreRepository.swift index 141e54d..f455a81 100644 --- a/apple/VniDrop/Core/CoreRepository.swift +++ b/apple/VniDrop/Core/CoreRepository.swift @@ -298,14 +298,15 @@ private extension CoreEvent { } } -private let refreshPhases: Set = ["lifecycle", "error", "ticket", "import", "download", "export", "handshake"] -private let refreshKinds: Set = [ - "started", "done", "created", "failed", "cancelled", "share-stopped", "found-collection", "connected", +private let refreshPhases: Set = [.lifecycle, .error, .ticket, .importing, .download, .export, .handshake] +private let refreshKinds: Set = [ + .started, .done, .created, .failed, .cancelled, .shareStopped, .foundCollection, .connected, ] private extension CoreEventModel { var shouldRefreshTransfers: Bool { - refreshPhases.contains(phase) && refreshKinds.contains(kind) + guard let eventPhase, let eventKind else { return false } + return refreshPhases.contains(eventPhase) && refreshKinds.contains(eventKind) } } diff --git a/apple/VniDrop/Core/TransferProgress.swift b/apple/VniDrop/Core/TransferProgress.swift index 227dbc4..9c8fe3d 100644 --- a/apple/VniDrop/Core/TransferProgress.swift +++ b/apple/VniDrop/Core/TransferProgress.swift @@ -18,8 +18,8 @@ func windowClassFor(width: Double) -> WindowClass { /// resolved at the view layer. struct TransferProgress: Equatable { let transferId: UInt64? - let phase: String - let kind: String + let phase: EventPhase + let kind: EventKind let labelKey: String.LocalizationValue let progress: Double? var detail: String? = nil @@ -40,32 +40,19 @@ func statusLabelKey(_ status: TransferStatus) -> String.LocalizationValue { } } -private let progressPhases: Set = [ - "import", "ticket", "access", "transfer", "download", "export", - "lifecycle", "network", "handshake", "error", -] - -private let progressKinds: Set = [ - "started", "copy-progress", "copy-done", "outboard-progress", "done", - "created", "progress", "completed", "aborted", "failed", - "connecting", "connected", "found-collection", - "cancelled", "share-stopped", -] - -/// Latest progress snapshot for a transfer. Events are newest-first. +/// Latest progress snapshot for a transfer. Events are newest-first. Only events +/// whose `phase` and `kind` map to known cases participate. func progressForTransfer(events: [CoreEventModel], transferId: UInt64) -> TransferProgress? { let relevant = events.filter { event in - event.transferId == transferId - && progressPhases.contains(event.phase) - && progressKinds.contains(event.kind) + event.transferId == transferId && event.eventPhase != nil && event.eventKind != nil } - guard let latest = relevant.first else { return nil } + guard let latest = relevant.first, let phase = latest.eventPhase, let kind = latest.eventKind else { return nil } let sizeHint = findKnownSize(events: events, transferId: transferId) return TransferProgress( transferId: transferId, - phase: latest.phase, - kind: latest.kind, - labelKey: humanProgressLabel(latest), + phase: phase, + kind: kind, + labelKey: humanProgressLabel(phase: phase, kind: kind), progress: parseProgress(latest.dataJson, sizeHint: sizeHint), detail: progressDetail(latest) ) @@ -80,32 +67,31 @@ func progressForReceiver( ) -> TransferProgress? { if remoteEndpointId.isEmpty { return nil } let connectionIds = connectionIdsForEndpoint(events: events, remoteEndpointId: remoteEndpointId) + let receiverKinds: Set = [.started, .progress, .completed, .aborted] let transferEvents = events.filter { event in event.transferId == transferId - && event.direction == "send" - && event.phase == "transfer" - && ["started", "progress", "completed", "aborted"].contains(event.kind) + && event.eventDirection == .send + && event.eventPhase == .transfer + && (event.eventKind.map(receiverKinds.contains) ?? false) && eventBelongsToReceiver(event, remoteEndpointId: remoteEndpointId, connectionIds: connectionIds) } - if transferEvents.isEmpty { return nil } - - let latest = transferEvents[0] - if latest.kind == "aborted" { + guard let latest = transferEvents.first, let latestKind = latest.eventKind else { return nil } + if latestKind == .aborted { return TransferProgress( - transferId: transferId, phase: "transfer", kind: "aborted", + transferId: transferId, phase: .transfer, kind: .aborted, labelKey: L10n.Progress.interrupted, progress: nil, detail: nil ) } - if latest.kind == "completed" && !transferEvents.contains(where: { $0.kind == "progress" || $0.kind == "started" }) { + if latestKind == .completed && !transferEvents.contains(where: { $0.eventKind == .progress || $0.eventKind == .started }) { return TransferProgress( - transferId: transferId, phase: "transfer", kind: "completed", + transferId: transferId, phase: .transfer, kind: .completed, labelKey: L10n.Progress.completed, progress: 1, detail: nil ) } let progress = aggregateReceiverProgress(events: transferEvents, totalSizeHint: totalSizeHint) return TransferProgress( - transferId: transferId, phase: "transfer", kind: latest.kind, + transferId: transferId, phase: .transfer, kind: latestKind, labelKey: L10n.Progress.sending, progress: progress, detail: progressDetail(latest) ) } @@ -127,25 +113,25 @@ func formatBytes(_ size: UInt64) -> String { // MARK: - Internals (ported literally from AppUiModels.kt) -private func humanProgressLabel(_ event: CoreEventModel) -> String.LocalizationValue { - switch (event.phase, event.kind) { - case ("import", "copy-progress"), ("import", "outboard-progress"), ("import", "started"): +private func humanProgressLabel(phase: EventPhase, kind: EventKind) -> String.LocalizationValue { + switch (phase, kind) { + case (.importing, .copyProgress), (.importing, .outboardProgress), (.importing, .started): return L10n.Progress.preparing - case ("import", "done"): return L10n.Progress.ready - case ("ticket", "created"): return L10n.Progress.shareReady - case ("network", "connecting"): return L10n.Progress.connecting - case ("network", "connected"): return L10n.Progress.connected - case ("download", "found-collection"): return L10n.Progress.gettingReady - case ("download", "progress"): return L10n.Progress.downloading - case ("export", "progress"): return L10n.Progress.saving - case ("transfer", "progress"): return L10n.Progress.sending - case ("transfer", "started"): return L10n.Progress.connected - case ("transfer", "completed"): return L10n.Progress.completed - case ("lifecycle", "done"): return L10n.Progress.completed - case ("lifecycle", "cancelled"): return L10n.Progress.cancelled + case (.importing, .done): return L10n.Progress.ready + case (.ticket, .created): return L10n.Progress.shareReady + case (.network, .connecting): return L10n.Progress.connecting + case (.network, .connected): return L10n.Progress.connected + case (.download, .foundCollection): return L10n.Progress.gettingReady + case (.download, .progress): return L10n.Progress.downloading + case (.export, .progress): return L10n.Progress.saving + case (.transfer, .progress): return L10n.Progress.sending + case (.transfer, .started): return L10n.Progress.connected + case (.transfer, .completed): return L10n.Progress.completed + case (.lifecycle, .done): return L10n.Progress.completed + case (.lifecycle, .cancelled): return L10n.Progress.cancelled default: - if event.phase == "handshake" { return L10n.Progress.requestingAccess } - if event.kind == "failed" { return L10n.Progress.failed } + if phase == .handshake { return L10n.Progress.requestingAccess } + if kind == .failed { return L10n.Progress.failed } return L10n.Progress.working } } @@ -217,14 +203,14 @@ private func aggregateReceiverProgress(events: [CoreEventModel], totalSizeHint: order.append(requestKey) } if let size, size > 0 { state.size = size } - switch event.kind { - case "progress", "started": + switch event.eventKind { + case .progress, .started: if let endOffset { state.offset = max(state.offset, endOffset) } state.aborted = false - case "completed": + case .completed: state.completed = true if let s = state.size { state.offset = s } - case "aborted": + case .aborted: state.aborted = true default: break diff --git a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift index ee80794..38141f1 100644 --- a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift +++ b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift @@ -129,7 +129,7 @@ struct InvitationReviewPanel: View { if state.isReceiving { let progressId = state.activeReceiveTransferId - ?? model.coreState.events.first { $0.direction == "receive" && $0.transferId != nil }?.transferId + ?? model.coreState.events.first { $0.eventDirection == .receive && $0.transferId != nil }?.transferId let progress = progressId.flatMap { progressForTransfer(events: model.coreState.events, transferId: $0) } ProgressRow(labelKey: progress?.labelKey ?? L10n.Progress.receiving, progress: progress?.progress, detail: progress?.detail) SecondaryButton(title: String(localized: L10n.Button.cancelReceive), action: model.cancelActiveReceive) diff --git a/apple/VniDrop/Features/Receive/ReceiveModel.swift b/apple/VniDrop/Features/Receive/ReceiveModel.swift index aa7f82f..31c95d2 100644 --- a/apple/VniDrop/Features/Receive/ReceiveModel.swift +++ b/apple/VniDrop/Features/Receive/ReceiveModel.swift @@ -202,7 +202,7 @@ final class ReceiveModel: ObservableObject { func cancelActiveReceive() { let transferId = state.activeReceiveTransferId ?? coreState.transfers.first { $0.direction == .receive && $0.status == .receiving }?.transferId - ?? coreState.events.first { $0.direction == "receive" && $0.transferId != nil }?.transferId + ?? coreState.events.first { $0.eventDirection == .receive && $0.transferId != nil }?.transferId guard let transferId else { return } Task { let result = await repository.cancel(transferId: transferId) diff --git a/apple/VniDrop/Features/Send/SendScreen.swift b/apple/VniDrop/Features/Send/SendScreen.swift index ab9e3a4..ee8b55d 100644 --- a/apple/VniDrop/Features/Send/SendScreen.swift +++ b/apple/VniDrop/Features/Send/SendScreen.swift @@ -134,10 +134,10 @@ struct SendScreen: View { } let combined = fractions.isEmpty ? nil : fractions.reduce(0, +) / Double(fractions.count) if active.count == 1 { - return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress", + return TransferProgress(transferId: transfer.transferId, phase: .transfer, kind: .progress, labelKey: L10n.Progress.sending, progress: combined) } - return TransferProgress(transferId: transfer.transferId, phase: "transfer", kind: "progress", + return TransferProgress(transferId: transfer.transferId, phase: .transfer, kind: .progress, labelKey: L10n.Progress.sending, progress: combined, label: L10n.Progress.sendingToCount(count: active.count)) } From 081b59815c232c4a869c8c09c181f89735fab7e8 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:30:55 +0200 Subject: [PATCH 05/36] fix(apple): make receive-cancel actually cancel the transfer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Cancel button on an in-progress receive did nothing. CoreRepository funnelled every core call through one serial DispatchQueue, but `receive` is a blocking core call that occupies that queue for the whole transfer. The tapped `cancelTransfer` was enqueued behind the in-flight `receive` on the same serial queue, so it could never run until `receive` returned — which it never would, because it was waiting to be cancelled. A deadlock the button couldn't escape. The Rust core is explicitly designed for cancel to arrive from another thread mid-receive (VnidropCore.block_on uses a shared runtime handle for exactly this). Extracts the two-lane dispatch into a CoreDispatcher: a serial lane for ordered calls and a separate concurrent lane for interrupt-style calls, and routes cancel through the latter so the signal reaches the core and unblocks the receive. Adds CoreDispatcherTests, including a regression guard that an interrupt completes while the serial lane is blocked. --- apple/Tests/CoreDispatcherTests.swift | 50 +++++++++++++++++++++++++ apple/VniDrop/Core/CoreDispatcher.swift | 41 ++++++++++++++++++++ apple/VniDrop/Core/CoreRepository.swift | 25 ++++++------- 3 files changed, 103 insertions(+), 13 deletions(-) create mode 100644 apple/Tests/CoreDispatcherTests.swift create mode 100644 apple/VniDrop/Core/CoreDispatcher.swift diff --git a/apple/Tests/CoreDispatcherTests.swift b/apple/Tests/CoreDispatcherTests.swift new file mode 100644 index 0000000..4a71272 --- /dev/null +++ b/apple/Tests/CoreDispatcherTests.swift @@ -0,0 +1,50 @@ +import XCTest +@testable import VniDrop + +final class CoreDispatcherTests: XCTestCase { + + /// Regression guard for the receive-cancel deadlock: an interrupt-lane call + /// must complete even while the serial lane is occupied by a blocking call. + /// With a single shared queue (the old design) the interrupt would be stuck + /// behind the blocked `receive`, and this would time out. + func testInterruptCompletesWhileSerialLaneIsBlocked() async { + let dispatcher = CoreDispatcher() + let serialEntered = DispatchSemaphore(value: 0) + let releaseSerial = DispatchSemaphore(value: 0) + + // Occupy the serial lane with a call that blocks until we release it. + let serialTask = Task { + await dispatcher.run { + serialEntered.signal() + releaseSerial.wait() + } + } + XCTAssertEqual(serialEntered.wait(timeout: .now() + 2), .success, "serial lane never started") + + // The interrupt lane must run despite the serial lane being blocked. + let interruptDone = DispatchSemaphore(value: 0) + Task.detached { + _ = await dispatcher.runInterrupt { 42 } + interruptDone.signal() + } + XCTAssertEqual( + interruptDone.wait(timeout: .now() + 2), .success, + "interrupt lane was blocked behind the occupied serial lane") + + releaseSerial.signal() + _ = await serialTask.value + } + + func testRunPropagatesValuesAndErrors() async { + let dispatcher = CoreDispatcher() + + let value = await dispatcher.run { 7 } + XCTAssertEqual(try? value.get(), 7) + + let failure = await dispatcher.run { () -> Int in throw TestError.unimplemented } + switch failure { + case .success: XCTFail("expected the thrown error to propagate") + case .failure(let error): XCTAssertTrue(error is TestError) + } + } +} diff --git a/apple/VniDrop/Core/CoreDispatcher.swift b/apple/VniDrop/Core/CoreDispatcher.swift new file mode 100644 index 0000000..efffaea --- /dev/null +++ b/apple/VniDrop/Core/CoreDispatcher.swift @@ -0,0 +1,41 @@ +import Foundation + +/// Dispatch-queue labels for the core's serial and interrupt lanes. +enum QueueLabel { + static let core = "com.vnidrop.core" + static let interrupt = "com.vnidrop.core.interrupt" +} + +/// Two-lane dispatcher for blocking core calls. +/// +/// `run` serializes calls on one queue so the core is driven from a single lane. +/// `runInterrupt` uses a *separate* concurrent lane, so an interrupt-style call +/// (cancel) can reach the core while a blocking call (`receive`) still occupies +/// the serial lane. The core is internally synchronized and explicitly supports +/// cancel arriving from another thread mid-receive (see VnidropCore.block_on); +/// a single shared queue would deadlock it. +final class CoreDispatcher: Sendable { + private let serialQueue: DispatchQueue + private let interruptQueue: DispatchQueue + + init(label: String = QueueLabel.core, interruptLabel: String = QueueLabel.interrupt) { + serialQueue = DispatchQueue(label: label, qos: .userInitiated) + interruptQueue = DispatchQueue(label: interruptLabel, qos: .userInitiated, attributes: .concurrent) + } + + /// Runs a blocking core call on the serial lane and hops the result back. + func run(_ block: @escaping @Sendable () throws -> T) async -> Result { + await withCheckedContinuation { continuation in + serialQueue.async { continuation.resume(returning: Result { try block() }) } + } + } + + /// Like `run`, but off the serial lane so it can interrupt a blocking call in + /// flight there (e.g. cancel a `receive`). Only use for core calls that are + /// safe to run concurrently with another core call. + func runInterrupt(_ block: @escaping @Sendable () throws -> T) async -> Result { + await withCheckedContinuation { continuation in + interruptQueue.async { continuation.resume(returning: Result { try block() }) } + } + } +} diff --git a/apple/VniDrop/Core/CoreRepository.swift b/apple/VniDrop/Core/CoreRepository.swift index f455a81..f7ea1a3 100644 --- a/apple/VniDrop/Core/CoreRepository.swift +++ b/apple/VniDrop/Core/CoreRepository.swift @@ -21,7 +21,7 @@ final class CoreRepository: ObservableObject, CoreGateway { // `runCore`; the underlying core is internally synchronized, so this crossing // is safe. `nonisolated(unsafe)` documents that contract for Swift 6. private nonisolated(unsafe) var core: VnidropCore? - private let queue = DispatchQueue(label: "com.vnidrop.core", qos: .userInitiated) + private let dispatcher = CoreDispatcher() private lazy var sink = RepositoryEventSink { [weak self] event in Task { @MainActor in self?.handle(event: event) } } @@ -119,7 +119,9 @@ final class CoreRepository: ObservableObject, CoreGateway { // MARK: - Lifecycle actions func cancel(transferId: UInt64) async -> Result { - await runCore { + // Off the serial `queue`: a receive in flight is blocking it, and the + // cancel signal must reach the core to unblock that receive. + await runInterrupt { try self.requireCore().cancelTransfer(transferId: transferId) }.map { self.refreshSnapshot() } } @@ -253,17 +255,14 @@ final class CoreRepository: ObservableObject, CoreGateway { /// Runs a blocking core call off the main actor and hops the result back. private nonisolated func runCore(_ block: @escaping @Sendable () throws -> T) async -> Result { - await withCheckedContinuation { continuation in - queue.async { - let result: Result - do { - result = .success(try block()) - } catch { - result = .failure(error) - } - continuation.resume(returning: result) - } - } + await dispatcher.run(block) + } + + /// Like `runCore`, but off the serial lane so it can interrupt a blocking + /// call in flight there (e.g. cancel a `receive`). Only use for core calls + /// that are safe to run concurrently with another core call. + private nonisolated func runInterrupt(_ block: @escaping @Sendable () throws -> T) async -> Result { + await dispatcher.runInterrupt(block) } private nonisolated static func nextTransferId() -> UInt64 { From 0e0d43bc4d6f281115067b557561c74849e099f2 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:48:03 +0200 Subject: [PATCH 06/36] build(apple): stop tracking generated localization outputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Localizable.xcstrings and Generated/L10n.swift are generated from localization/strings.json — the single source of truth — yet were committed, which caused redundant tracking and a spurious ~10k-line diff every time Xcode reformatted the catalog on build. Treat them like the (already gitignored) Rust bindings: generate at build time instead of tracking them. gitignore both; make the `apple-project` target depend on `localization` so `bun run generate` recreates them before xcodegen; install Bun in the Apple CI job and trigger it on localization/**. Android strings.xml stays tracked — it has no reformatting churn and its build doesn't run the generator. --- .github/workflows/apple.yml | 7 + Makefile | 2 +- apple/.gitignore | 4 + apple/VniDrop/Generated/L10n.swift | 3976 ---- apple/VniDrop/Resources/Localizable.xcstrings | 15678 ---------------- apple/project.yml | 5 +- 6 files changed, 16 insertions(+), 19656 deletions(-) delete mode 100644 apple/VniDrop/Generated/L10n.swift delete mode 100644 apple/VniDrop/Resources/Localizable.xcstrings diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml index 0157181..0871d58 100644 --- a/.github/workflows/apple.yml +++ b/.github/workflows/apple.yml @@ -12,6 +12,7 @@ on: - "config.mk" - "make/**" - ".github/workflows/apple.yml" + - "localization/**" push: branches: - master @@ -25,6 +26,7 @@ on: - "config.mk" - "make/**" - ".github/workflows/apple.yml" + - "localization/**" permissions: contents: read @@ -63,5 +65,10 @@ jobs: - name: Install XcodeGen run: brew install xcodegen + - name: Install Bun + # The Apple l10n catalog (Localizable.xcstrings) and L10n.swift are + # generated from localization/strings.json at build time, not tracked. + uses: oven-sh/setup-bun@v2 + - name: Build and test Apple app run: make check-apple diff --git a/Makefile b/Makefile index 5cf49b3..81b36df 100644 --- a/Makefile +++ b/Makefile @@ -113,7 +113,7 @@ apple-core: ## Build the Rust XCFramework and generated Swift bindings. @test "$(HOST_OS)" = macos || { printf 'Apple builds require macOS.\n' >&2; exit 1; } cd $(ROOT) && apple/scripts/build-core.sh $(APPLE_PROFILE) -apple-project: apple-core ## Generate the native Apple Xcode project. +apple-project: apple-core localization ## Generate the native Apple Xcode project. cd $(ROOT)/apple && $(XCODEGEN) generate open-apple-project: apple-project ## Generate and open the native Apple Xcode project. diff --git a/apple/.gitignore b/apple/.gitignore index 3910689..40759f5 100644 --- a/apple/.gitignore +++ b/apple/.gitignore @@ -3,6 +3,10 @@ VnidropCore/vnidrop.xcframework/ VnidropCore/Sources/VnidropCore/Vnidrop.swift +# Generated from localization/strings.json (cd localization && bun run src/cli.ts generate) +VniDrop/Resources/Localizable.xcstrings +VniDrop/Generated/ + # Generated by XcodeGen from project.yml VniDrop.xcodeproj/ diff --git a/apple/VniDrop/Generated/L10n.swift b/apple/VniDrop/Generated/L10n.swift deleted file mode 100644 index e1532c4..0000000 --- a/apple/VniDrop/Generated/L10n.swift +++ /dev/null @@ -1,3976 +0,0 @@ -// Generated by localization/ (bun run src/cli.ts generate). Do not edit. -// Keys resolve through Apple's String Catalog exactly as a literal would; these -// accessors only make the key compile-time-checked. If a runtime language -// switcher (live `.environment(\.locale)`) is ever added, switch the plain -// `static let` constants to computed `static var` so the locale is not frozen. -import Foundation - -enum L10n { - enum About { - /// Report a bug - /// - /// Key: `about_bug_report` - /// Context: About screen: button/link that opens the bug report form. - /// - /// - en: Report a bug - /// - fr: Signaler un bug - /// - es: Informar de un error - /// - it: Segnala un bug - /// - de: Fehler melden - /// - pt: Comunicar um erro - /// - pl: Zgłoś błąd - /// - nl: Een fout melden - /// - ru: Сообщить об ошибке - static let bugReport: String.LocalizationValue = "about_bug_report" - /// VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done. - /// - /// Key: `about_description` - /// Context: About screen: intro paragraph describing what VniDrop does. - /// - /// - en: VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done. - /// - fr: VniDrop transfère fichiers et dossiers directement d’un appareil à un autre sur le réseau, sans les téléverser vers un service d’hébergement. Aucun compte à créer, et aucune copie de votre transfert ne reste dans le cloud une fois terminé. - /// - es: VniDrop transfiere archivos y carpetas directamente de un dispositivo a otro a través de la red, sin subirlos a ningún servicio de alojamiento. No hay que crear ninguna cuenta y no queda ninguna copia de su transferencia en la nube una vez que termina. - /// - it: VniDrop trasferisce file e cartelle direttamente da un dispositivo a un altro sulla rete, senza caricarli su alcun servizio di hosting. Non c’è alcun account da creare e nessuna copia del suo trasferimento rimane nel cloud una volta terminato. - /// - de: VniDrop überträgt Dateien und Ordner direkt von einem Gerät auf ein anderes über das Netzwerk, ohne sie auf einen Hosting-Dienst hochzuladen. Es muss kein Konto erstellt werden, und nach Abschluss verbleibt keine Kopie Ihrer Übertragung in der Cloud. - /// - pt: O VniDrop transfere ficheiros e pastas diretamente de um dispositivo para outro através da rede, sem os carregar para qualquer serviço de alojamento. Não é necessário criar qualquer conta e não fica nenhuma cópia da sua transferência na nuvem depois de terminar. - /// - pl: VniDrop przesyła pliki i foldery bezpośrednio z jednego urządzenia na drugie przez sieć, bez przesyłania ich do jakiejkolwiek usługi hostingowej. Nie trzeba zakładać konta, a po zakończeniu żadna kopia transferu nie pozostaje w chmurze. - /// - nl: VniDrop verplaatst bestanden en mappen rechtstreeks van het ene apparaat naar het andere via het netwerk, zonder ze naar een hostingdienst te uploaden. U hoeft geen account aan te maken en er blijft geen kopie van uw overdracht in de cloud achter zodra u klaar bent. - /// - ru: VniDrop передаёт файлы и папки напрямую с одного устройства на другое по сети, не загружая их в какой-либо хостинг-сервис. Не нужно создавать учётную запись, и после завершения ни одна копия вашей передачи не остаётся в облаке. - static let description: String.LocalizationValue = "about_description" - /// A direct device-to-device transfer — your files go straight to the receiver. - /// - /// Key: `about_is_direct` - /// Context: About screen, 'What VniDrop is' list: point about direct device-to-device transfer. - /// - /// - en: A direct device-to-device transfer — your files go straight to the receiver. - /// - fr: Un transfert direct d’appareil à appareil — vos fichiers vont droit au destinataire. - /// - es: Una transferencia directa entre dispositivos: sus archivos van directamente al destinatario. - /// - it: Un trasferimento diretto da dispositivo a dispositivo: i suoi file vanno direttamente al destinatario. - /// - de: Eine direkte Gerät-zu-Gerät-Übertragung – Ihre Dateien gehen direkt an den Empfänger. - /// - pt: Uma transferência direta entre dispositivos — os seus ficheiros vão diretamente para o destinatário. - /// - pl: Bezpośredni transfer między urządzeniami — pliki trafiają prosto do odbiorcy. - /// - nl: Een directe overdracht van apparaat naar apparaat — uw bestanden gaan rechtstreeks naar de ontvanger. - /// - ru: Прямая передача с устройства на устройство — ваши файлы попадают напрямую к получателю. - static let isDirect: String.LocalizationValue = "about_is_direct" - /// Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash. - /// - /// Key: `about_is_encrypted` - /// Context: About screen, 'What VniDrop is' list: point about encrypted, verified connections. - /// - /// - en: Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash. - /// - fr: Les connexions sont authentifiées et chiffrées de bout en bout (via Iroh), et les fichiers entrants sont vérifiés par leur empreinte de contenu. - /// - es: Las conexiones están autenticadas y cifradas de extremo a extremo (mediante Iroh), y los archivos entrantes se verifican por su huella de contenido. - /// - it: Le connessioni sono autenticate e cifrate end-to-end (tramite Iroh) e i file in arrivo vengono verificati tramite l’impronta del loro contenuto. - /// - de: Verbindungen sind authentifiziert und Ende-zu-Ende-verschlüsselt (über Iroh), und eingehende Dateien werden anhand ihres Inhalts-Hashs überprüft. - /// - pt: As ligações são autenticadas e cifradas ponto a ponto (através do Iroh), e os ficheiros recebidos são verificados pela impressão digital do seu conteúdo. - /// - pl: Połączenia są uwierzytelniane i szyfrowane od końca do końca (przez Iroh), a przychodzące pliki są weryfikowane na podstawie skrótu ich zawartości. - /// - nl: Verbindingen zijn geverifieerd en end-to-end versleuteld (via Iroh), en binnenkomende bestanden worden gecontroleerd aan de hand van hun inhouds-hash. - /// - ru: Соединения аутентифицированы и зашифрованы сквозным шифрованием (через Iroh), а входящие файлы проверяются по хешу их содержимого. - static let isEncrypted: String.LocalizationValue = "about_is_encrypted" - /// You decide who receives: approve each request, or open a transfer to anyone holding the invitation. - /// - /// Key: `about_is_in_control` - /// Context: About screen, 'What VniDrop is' list: point about controlling who receives. - /// - /// - en: You decide who receives: approve each request, or open a transfer to anyone holding the invitation. - /// - fr: Vous décidez qui reçoit : approuvez chaque demande, ou ouvrez un transfert à toute personne disposant de l’invitation. - /// - es: Usted decide quién recibe: apruebe cada solicitud o abra una transferencia a cualquiera que tenga la invitación. - /// - it: Lei decide chi riceve: approvi ogni richiesta oppure apra un trasferimento a chiunque disponga dell’invito. - /// - de: Sie entscheiden, wer empfängt: Genehmigen Sie jede Anfrage oder öffnen Sie eine Übertragung für alle, die die Einladung besitzen. - /// - pt: É você quem decide quem recebe: aprove cada pedido ou abra uma transferência a qualquer pessoa que tenha o convite. - /// - pl: To Ty decydujesz, kto otrzymuje: zatwierdź każde żądanie lub udostępnij transfer każdemu, kto ma zaproszenie. - /// - nl: U bepaalt wie ontvangt: keur elk verzoek goed, of stel een overdracht open voor iedereen met de uitnodiging. - /// - ru: Вы решаете, кто получает: одобряйте каждый запрос или откройте передачу всем, у кого есть приглашение. - static let isInControl: String.LocalizationValue = "about_is_in_control" - /// Account-free — there’s nothing to sign up for. - /// - /// Key: `about_is_no_account` - /// Context: About screen, 'What VniDrop is' list: point about being account-free. - /// - /// - en: Account-free — there’s nothing to sign up for. - /// - fr: Sans compte — il n’y a rien à créer. - /// - es: Sin cuenta: no hay nada que registrar. - /// - it: Senza account: non c’è nulla da registrare. - /// - de: Ohne Konto – es gibt nichts zu registrieren. - /// - pt: Sem conta — não há nada para registar. - /// - pl: Bez konta — nie trzeba się rejestrować. - /// - nl: Zonder account — er is niets om aan te melden. - /// - ru: Без учётной записи — регистрироваться не нужно. - static let isNoAccount: String.LocalizationValue = "about_is_no_account" - /// Open source, released under the Apache 2.0 license. - /// - /// Key: `about_is_open` - /// Context: About screen, 'What VniDrop is' list: point about being open source. - /// - /// - en: Open source, released under the Apache 2.0 license. - /// - fr: Open source, publié sous licence Apache 2.0. - /// - es: Código abierto, publicado bajo la licencia Apache 2.0. - /// - it: Open source, rilasciato con licenza Apache 2.0. - /// - de: Open Source, veröffentlicht unter der Apache-2.0-Lizenz. - /// - pt: Código aberto, publicado sob a licença Apache 2.0. - /// - pl: Otwarte oprogramowanie, wydane na licencji Apache 2.0. - /// - nl: Opensource, uitgebracht onder de Apache 2.0-licentie. - /// - ru: Открытый исходный код, распространяется по лицензии Apache 2.0. - static let isOpen: String.LocalizationValue = "about_is_open" - /// What VniDrop is - /// - /// Key: `about_is_title` - /// Context: About screen: section heading for the 'What VniDrop is' list. - /// - /// - en: What VniDrop is - /// - fr: Ce qu’est VniDrop - /// - es: Qué es VniDrop - /// - it: Cos’è VniDrop - /// - de: Was VniDrop ist - /// - pt: O que o VniDrop é - /// - pl: Czym jest VniDrop - /// - nl: Wat VniDrop is - /// - ru: Что такое VniDrop - static let isTitle: String.LocalizationValue = "about_is_title" - /// Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer. - /// - /// Key: `about_isnt_cloud` - /// Context: About screen, 'What VniDrop isn't' list: point about not being cloud storage. - /// - /// - en: Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer. - /// - fr: Pas un stockage cloud — aucun serveur ne détient vos fichiers, et rien n’attend dans le cloud après un transfert. - /// - es: No es almacenamiento en la nube: ningún servidor guarda sus archivos y nada queda en la nube después de una transferencia. - /// - it: Non è un’archiviazione cloud: nessun server conserva i suoi file e nulla resta nel cloud dopo un trasferimento. - /// - de: Kein Cloud-Speicher – kein Server hält Ihre Dateien, und nach einer Übertragung wartet nichts in der Cloud. - /// - pt: Não é armazenamento na nuvem — nenhum servidor guarda os seus ficheiros e nada fica na nuvem após uma transferência. - /// - pl: To nie magazyn w chmurze — żaden serwer nie przechowuje Twoich plików i nic nie pozostaje w chmurze po transferze. - /// - nl: Geen cloudopslag — geen enkele server bewaart uw bestanden en er wacht niets in de cloud na een overdracht. - /// - ru: Это не облачное хранилище — ни один сервер не хранит ваши файлы, и после передачи в облаке ничего не остаётся. - static let isntCloud: String.LocalizationValue = "about_isnt_cloud" - /// Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby. - /// - /// Key: `about_isnt_public` - /// Context: About screen, 'What VniDrop isn't' list: point about invitations not being public broadcasts. - /// - /// - en: Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby. - /// - fr: Pas une diffusion publique — une invitation est un lien d’accès privé, pas une annonce à tout le voisinage. - /// - es: No es difusión pública: una invitación es un enlace de acceso privado, no un anuncio para todos los que estén cerca. - /// - it: Non è una diffusione pubblica: un invito è un link di accesso privato, non un annuncio a tutti nelle vicinanze. - /// - de: Keine öffentliche Übertragung – eine Einladung ist ein privater Zugangslink, keine Ankündigung an alle in der Nähe. - /// - pt: Não é difusão pública — um convite é uma ligação de acesso privado, não um anúncio a toda a gente por perto. - /// - pl: To nie publiczne rozgłaszanie — zaproszenie to prywatny link dostępu, a nie ogłoszenie dla wszystkich w pobliżu. - /// - nl: Geen openbare uitzending — een uitnodiging is een privétoegangslink, geen aankondiging aan iedereen in de buurt. - /// - ru: Это не публичная рассылка — приглашение является частной ссылкой доступа, а не объявлением для всех поблизости. - static let isntPublic: String.LocalizationValue = "about_isnt_public" - /// Not a sync or backup service. - /// - /// Key: `about_isnt_sync` - /// Context: About screen, 'What VniDrop isn't' list: point about not being sync/backup. - /// - /// - en: Not a sync or backup service. - /// - fr: Pas un service de synchronisation ou de sauvegarde. - /// - es: No es un servicio de sincronización ni de copia de seguridad. - /// - it: Non è un servizio di sincronizzazione o di backup. - /// - de: Kein Synchronisierungs- oder Backup-Dienst. - /// - pt: Não é um serviço de sincronização ou de cópia de segurança. - /// - pl: To nie usługa synchronizacji ani kopii zapasowej. - /// - nl: Geen synchronisatie- of back-updienst. - /// - ru: Это не служба синхронизации или резервного копирования. - static let isntSync: String.LocalizationValue = "about_isnt_sync" - /// What VniDrop isn’t - /// - /// Key: `about_isnt_title` - /// Context: About screen: section heading for the 'What VniDrop isn't' list. - /// - /// - en: What VniDrop isn’t - /// - fr: Ce que VniDrop n’est pas - /// - es: Qué no es VniDrop - /// - it: Cosa non è VniDrop - /// - de: Was VniDrop nicht ist - /// - pt: O que o VniDrop não é - /// - pl: Czym VniDrop nie jest - /// - nl: Wat VniDrop niet is - /// - ru: Чем VniDrop не является - static let isntTitle: String.LocalizationValue = "about_isnt_title" - /// License - /// - /// Key: `about_license_label` - /// Context: About screen: label for the license row. - /// - /// - en: License - /// - fr: Licence - /// - es: Licencia - /// - it: Licenza - /// - de: Lizenz - /// - pt: Licença - /// - pl: Licencja - /// - nl: Licentie - /// - ru: Лицензия - static let licenseLabel: String.LocalizationValue = "about_license_label" - /// Privacy policy - /// - /// Key: `about_privacy` - /// Context: About screen: link to the privacy policy. - /// - /// - en: Privacy policy - /// - fr: Politique de confidentialité - /// - es: Política de privacidad - /// - it: Informativa sulla privacy - /// - de: Datenschutzrichtlinie - /// - pt: Política de privacidade - /// - pl: Polityka prywatności - /// - nl: Privacybeleid - /// - ru: Политика конфиденциальности - static let privacy: String.LocalizationValue = "about_privacy" - /// Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.” - /// - /// Key: `about_privacy_capability` - /// Context: About screen, Privacy & security list: point that invitations are capabilities to share carefully. - /// - /// - en: Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.” - /// - fr: Les invitations sont des clés d’accès. Traitez un QR code, un tag NFC ou un fichier .vnd comme un lien d’accès privé et ne le partagez qu’avec les personnes visées — en particulier avec « Toute personne disposant de ce transfert ». - /// - es: Las invitaciones son claves de acceso. Trate un código QR, una etiqueta NFC o un archivo .vnd como un enlace de acceso privado y compártalo solo con las personas previstas, especialmente con «Cualquiera que tenga esta transferencia». - /// - it: Gli inviti sono chiavi di accesso. Tratti un codice QR, un tag NFC o un file .vnd come un link di accesso privato e lo condivida solo con le persone previste, soprattutto con «Chiunque abbia questo trasferimento». - /// - de: Einladungen sind Zugangsschlüssel. Behandeln Sie einen QR-Code, ein NFC-Tag oder eine .vnd-Datei wie einen privaten Zugangslink und teilen Sie ihn nur mit den vorgesehenen Personen – insbesondere bei „Jeder mit dieser Übertragung“. - /// - pt: Os convites são chaves de acesso. Trate um código QR, uma etiqueta NFC ou um ficheiro .vnd como uma ligação de acesso privado e partilhe-o apenas com as pessoas pretendidas — sobretudo com «Qualquer pessoa com esta transferência». - /// - pl: Zaproszenia są kluczami dostępu. Traktuj kod QR, tag NFC lub plik .vnd jak prywatny link dostępu i udostępniaj go tylko zamierzonym osobom — zwłaszcza przy opcji „Każdy, kto ma ten transfer”. - /// - nl: Uitnodigingen zijn toegangssleutels. Behandel een QR-code, NFC-tag of .vnd-bestand als een privétoegangslink en deel deze alleen met de bedoelde personen — vooral bij ‘Iedereen met deze overdracht’. - /// - ru: Приглашения — это ключи доступа. Относитесь к QR-коду, NFC-метке или файлу .vnd как к частной ссылке доступа и делитесь ими только с теми, для кого они предназначены, особенно при варианте «Любой, у кого есть эта передача». - static let privacyCapability: String.LocalizationValue = "about_privacy_capability" - /// Deny by default — VniDrop serves only the content of an active share and rejects unknown requests. - /// - /// Key: `about_privacy_deny` - /// Context: About screen, Privacy & security list: point about denying unknown requests by default. - /// - /// - en: Deny by default — VniDrop serves only the content of an active share and rejects unknown requests. - /// - fr: Refus par défaut — VniDrop ne sert que le contenu d’un partage actif et rejette les demandes inconnues. - /// - es: Denegación por defecto: VniDrop solo sirve el contenido de un recurso compartido activo y rechaza las solicitudes desconocidas. - /// - it: Rifiuto predefinito: VniDrop serve solo il contenuto di una condivisione attiva e rifiuta le richieste sconosciute. - /// - de: Standardmäßig ablehnen – VniDrop stellt nur den Inhalt einer aktiven Freigabe bereit und weist unbekannte Anfragen ab. - /// - pt: Recusa por predefinição — o VniDrop apenas fornece o conteúdo de uma partilha ativa e rejeita pedidos desconhecidos. - /// - pl: Domyślnie odmowa — VniDrop udostępnia tylko zawartość aktywnego udostępnienia i odrzuca nieznane żądania. - /// - nl: Standaard weigeren — VniDrop levert alleen de inhoud van een actieve deling en wijst onbekende verzoeken af. - /// - ru: Отказ по умолчанию — VniDrop предоставляет только содержимое активной передачи и отклоняет неизвестные запросы. - static let privacyDeny: String.LocalizationValue = "about_privacy_deny" - /// Received files are saved on your device and never silently overwrite an existing file. - /// - /// Key: `about_privacy_local` - /// Context: About screen, Privacy & security list: point about received files staying local and never overwriting. - /// - /// - en: Received files are saved on your device and never silently overwrite an existing file. - /// - fr: Les fichiers reçus sont enregistrés sur votre appareil et n’écrasent jamais un fichier existant. - /// - es: Los archivos recibidos se guardan en su dispositivo y nunca sobrescriben un archivo existente. - /// - it: I file ricevuti vengono salvati sul suo dispositivo e non sovrascrivono mai un file esistente. - /// - de: Empfangene Dateien werden auf Ihrem Gerät gespeichert und überschreiben niemals eine vorhandene Datei. - /// - pt: Os ficheiros recebidos são guardados no seu dispositivo e nunca substituem um ficheiro existente. - /// - pl: Odebrane pliki są zapisywane na Twoim urządzeniu i nigdy nie nadpisują istniejącego pliku. - /// - nl: Ontvangen bestanden worden op uw apparaat bewaard en overschrijven nooit een bestaand bestand. - /// - ru: Полученные файлы сохраняются на вашем устройстве и никогда не перезаписывают существующий файл. - static let privacyLocal: String.LocalizationValue = "about_privacy_local" - /// Privacy policy - /// - /// Key: `about_privacy_policy_label` - /// Context: About screen: label for the privacy policy row. - /// - /// - en: Privacy policy - /// - fr: Politique de confidentialité - /// - es: Política de privacidad - /// - it: Informativa sulla privacy - /// - de: Datenschutzrichtlinie - /// - pt: Política de privacidade - /// - pl: Polityka prywatności - /// - nl: Privacybeleid - /// - ru: Политика конфиденциальности - static let privacyPolicyLabel: String.LocalizationValue = "about_privacy_policy_label" - /// If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files. - /// - /// Key: `about_privacy_relay` - /// Context: About screen, Privacy & security list: point explaining encrypted relays. - /// - /// - en: If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files. - /// - fr: Si deux appareils ne peuvent pas se connecter directement, la connexion chiffrée peut être relayée. Les relais ne transmettent que des paquets chiffrés ; ils ne stockent jamais vos fichiers. - /// - es: Si dos dispositivos no pueden conectarse directamente, la conexión cifrada puede retransmitirse. Los relés solo reenvían paquetes cifrados; nunca almacenan sus archivos. - /// - it: Se due dispositivi non riescono a connettersi direttamente, la connessione cifrata può essere instradata tramite relay. I relay inoltrano solo pacchetti cifrati; non memorizzano mai i suoi file. - /// - de: Wenn zwei Geräte sich nicht direkt verbinden können, kann die verschlüsselte Verbindung weitergeleitet werden. Relays leiten nur verschlüsselte Pakete weiter; sie speichern niemals Ihre Dateien. - /// - pt: Se dois dispositivos não conseguirem ligar-se diretamente, a ligação cifrada pode ser retransmitida. Os retransmissores apenas encaminham pacotes cifrados; nunca guardam os seus ficheiros. - /// - pl: Jeśli dwa urządzenia nie mogą połączyć się bezpośrednio, szyfrowane połączenie może być przekazywane przez przekaźnik. Przekaźniki przekazują tylko zaszyfrowane pakiety; nigdy nie przechowują Twoich plików. - /// - nl: Als twee apparaten niet rechtstreeks verbinding kunnen maken, kan de versleutelde verbinding via een relay worden doorgestuurd. Relays sturen alleen versleutelde pakketten door; ze slaan uw bestanden nooit op. - /// - ru: Если два устройства не могут соединиться напрямую, зашифрованное соединение может передаваться через ретранслятор. Ретрансляторы пересылают только зашифрованные пакеты; они никогда не хранят ваши файлы. - static let privacyRelay: String.LocalizationValue = "about_privacy_relay" - /// Privacy & security - /// - /// Key: `about_privacy_title` - /// Context: About screen: section heading for the Privacy & security list. - /// - /// - en: Privacy & security - /// - fr: Confidentialité et sécurité - /// - es: Privacidad y seguridad - /// - it: Privacy e sicurezza - /// - de: Datenschutz & Sicherheit - /// - pt: Privacidade e segurança - /// - pl: Prywatność i bezpieczeństwo - /// - nl: Privacy en beveiliging - /// - ru: Конфиденциальность и безопасность - static let privacyTitle: String.LocalizationValue = "about_privacy_title" - /// Send files directly. Stay in control of who receives them. - /// - /// Key: `about_tagline` - /// Context: About screen: short tagline under the app name. - /// - /// - en: Send files directly. Stay in control of who receives them. - /// - fr: Envoyez des fichiers directement. Gardez le contrôle de qui les reçoit. - /// - es: Envíe archivos directamente. Mantenga el control de quién los recibe. - /// - it: Invii file direttamente. Mantenga il controllo su chi li riceve. - /// - de: Senden Sie Dateien direkt. Behalten Sie die Kontrolle darüber, wer sie empfängt. - /// - pt: Envie ficheiros diretamente. Mantenha o controlo sobre quem os recebe. - /// - pl: Wysyłaj pliki bezpośrednio. Zachowaj kontrolę nad tym, kto je otrzymuje. - /// - nl: Verstuur bestanden rechtstreeks. Houd controle over wie ze ontvangt. - /// - ru: Отправляйте файлы напрямую. Сохраняйте контроль над тем, кто их получает. - static let tagline: String.LocalizationValue = "about_tagline" - /// About - /// - /// Key: `about_title` - /// Context: About screen: navigation/screen title. - /// - /// - en: About - /// - fr: À propos - /// - es: Acerca de - /// - it: Informazioni - /// - de: Über - /// - pt: Acerca de - /// - pl: Informacje - /// - nl: Over - /// - ru: О приложении - static let title: String.LocalizationValue = "about_title" - } - enum Appearance { - /// Match this device’s light or dark appearance. - /// - /// Key: `appearance_auto_description` - /// Context: Settings > Appearance: description for the System/auto option. - /// - /// - en: Match this device’s light or dark appearance. - /// - fr: Suivre l’apparence claire ou sombre de cet appareil. - /// - es: Seguir la apariencia clara u oscura de este dispositivo. - /// - it: Segue l’aspetto chiaro o scuro di questo dispositivo. - /// - de: Der hellen oder dunklen Darstellung dieses Geräts folgen. - /// - pt: Acompanhar o aspeto claro ou escuro deste dispositivo. - /// - pl: Dopasuj do jasnego lub ciemnego wyglądu tego urządzenia. - /// - nl: De lichte of donkere weergave van dit apparaat volgen. - /// - ru: Следовать светлому или тёмному оформлению этого устройства. - static let autoDescription: String.LocalizationValue = "appearance_auto_description" - /// Dark mode - /// - /// Key: `appearance_dark_mode` - /// Context: Settings > Appearance: label for the dark theme option. - /// - /// - en: Dark mode - /// - fr: Mode sombre - /// - es: Modo oscuro - /// - it: Modalità scura - /// - de: Dunkelmodus - /// - pt: Modo escuro - /// - pl: Tryb ciemny - /// - nl: Donkere modus - /// - ru: Тёмный режим - static let darkMode: String.LocalizationValue = "appearance_dark_mode" - /// Light mode - /// - /// Key: `appearance_light_mode` - /// Context: Settings > Appearance: label for the light theme option. - /// - /// - en: Light mode - /// - fr: Mode clair - /// - es: Modo claro - /// - it: Modalità chiara - /// - de: Hellmodus - /// - pt: Modo claro - /// - pl: Tryb jasny - /// - nl: Lichte modus - /// - ru: Светлый режим - static let lightMode: String.LocalizationValue = "appearance_light_mode" - /// System - /// - /// Key: `appearance_system_mode` - /// Context: Settings > Appearance: label for the follow-system option. - /// - /// - en: System - /// - fr: Système - /// - es: Sistema - /// - it: Sistema - /// - de: System - /// - pt: Sistema - /// - pl: System - /// - nl: Systeem - /// - ru: Системный - static let systemMode: String.LocalizationValue = "appearance_system_mode" - /// Appearance - /// - /// Key: `appearance_title` - /// Context: Settings > Appearance: section title. - /// - /// - en: Appearance - /// - fr: Apparence - /// - es: Apariencia - /// - it: Aspetto - /// - de: Darstellung - /// - pt: Aspeto - /// - pl: Wygląd - /// - nl: Weergave - /// - ru: Оформление - static let title: String.LocalizationValue = "appearance_title" - } - enum Approval { - /// Receive request - /// - /// Key: `approval_connection_request` - /// Context: Approval prompt: title when a receiver requests to download a transfer. - /// - /// - en: Receive request - /// - fr: Demande de réception - /// - es: Solicitud de recepción - /// - it: Richiesta di ricezione - /// - de: Empfangsanfrage - /// - pt: Pedido de receção - /// - pl: Prośba o odbiór - /// - nl: Ontvangstverzoek - /// - ru: Запрос на получение - static let connectionRequest: String.LocalizationValue = "approval_connection_request" - /// Device ID: {deviceId} - /// - /// Key: `approval_endpoint_id` - /// Context: Approval prompt: shows the requesting device's ID. {deviceId} = device/endpoint identifier. - /// - /// - en: Device ID: {deviceId} - /// - fr: Identifiant de l’appareil : {deviceId} - /// - es: ID del dispositivo: {deviceId} - /// - it: ID dispositivo: {deviceId} - /// - de: Geräte-ID: {deviceId} - /// - pt: ID do dispositivo: {deviceId} - /// - pl: Identyfikator urządzenia: {deviceId} - /// - nl: Apparaat-ID: {deviceId} - /// - ru: Идентификатор устройства: {deviceId} - static func endpointId(deviceId: String) -> String { - String(format: String(localized: "approval_endpoint_id"), deviceId) - } - /// A nearby device - /// - /// Key: `approval_nearby_device` - /// Context: Approval prompt: fallback label for a requester with no display name. - /// - /// - en: A nearby device - /// - fr: Un appareil à proximité - /// - es: Un dispositivo cercano - /// - it: Un dispositivo nelle vicinanze - /// - de: Ein Gerät in der Nähe - /// - pt: Um dispositivo próximo - /// - pl: Urządzenie w pobliżu - /// - nl: Een apparaat in de buurt - /// - ru: Устройство поблизости - static let nearbyDevice: String.LocalizationValue = "approval_nearby_device" - /// {count} requests waiting - /// - /// Key: `approval_pending_count` - /// Context: Send/transfer list: badge showing how many receive requests are awaiting approval. {count} = pending requests. - /// - /// - en: {count} requests waiting - /// - fr: {count} demandes en attente - /// - es: {count} solicitudes en espera - /// - it: {count} richieste in attesa - /// - de: {count} Anfragen warten - /// - pt: {count} pedidos em espera - /// - pl: Oczekujące prośby: {count} - /// - nl: {count} verzoeken in behandeling - /// - ru: Ожидающих запросов: {count} - static func pendingCount(count: Int) -> String { - String(format: String(localized: "approval_pending_count"), count) - } - /// {receiver} wants to receive “{transferName}”. - /// - /// Key: `approval_request_body` - /// Context: Approval prompt body: '{receiver} wants to receive "{transferName}".' {receiver} = requester name, {transferName} = transfer name. - /// - /// - en: {receiver} wants to receive “{transferName}”. - /// - fr: {receiver} souhaite recevoir « {transferName} ». - /// - es: {receiver} quiere recibir «{transferName}». - /// - it: {receiver} vuole ricevere «{transferName}». - /// - de: {receiver} möchte „{transferName}“ empfangen. - /// - pt: {receiver} quer receber «{transferName}». - /// - pl: {receiver} chce odebrać „{transferName}”. - /// - nl: {receiver} wil ‘{transferName}’ ontvangen. - /// - ru: {receiver} хочет получить «{transferName}». - static func requestBody(receiver: String, transferName: String) -> String { - String(format: String(localized: "approval_request_body"), receiver, transferName) - } - } - enum Battery { - /// Battery level - /// - /// Key: `battery_level_title` - /// Context: Device information row: battery level label. - /// - /// - en: Battery level - /// - fr: Niveau de batterie - /// - es: Nivel de batería - /// - it: Livello batteria - /// - de: Batteriestand - /// - pt: Nível da bateria - /// - pl: Poziom baterii - /// - nl: Batterijniveau - /// - ru: Уровень заряда - static let levelTitle: String.LocalizationValue = "battery_level_title" - /// {level}% - /// - /// Key: `battery_level_value` - /// Context: Device information: battery charge formatted as a percentage. {level} = integer percent value. - /// - /// - en: {level}% - /// - fr: {level} % - /// - es: {level} % - /// - it: {level}% - /// - de: {level} % - /// - pt: {level}% - /// - pl: {level}% - /// - nl: {level}% - /// - ru: {level} % - static func levelValue(level: String) -> String { - String(format: String(localized: "battery_level_value"), level) - } - } - enum Bug { - /// name@example.com - /// - /// Key: `bug_report_contact_hint` - /// Context: Bug report form: placeholder text in the contact email field. - /// - /// - en: name@example.com - /// - fr: nom@exemple.com - /// - es: nombre@ejemplo.com - /// - it: nome@esempio.com - /// - de: name@beispiel.com - /// - pt: nome@exemplo.com - /// - pl: nazwa@przyklad.com - /// - nl: naam@voorbeeld.com - /// - ru: имя@пример.com - static let reportContactHint: String.LocalizationValue = "bug_report_contact_hint" - /// Contact email (optional) - /// - /// Key: `bug_report_contact_label` - /// Context: Bug report form: label for the optional contact email field. - /// - /// - en: Contact email (optional) - /// - fr: E-mail de contact (facultatif) - /// - es: Correo de contacto (opcional) - /// - it: Email di contatto (facoltativa) - /// - de: Kontakt-E-Mail (optional) - /// - pt: E-mail de contacto (opcional) - /// - pl: E-mail kontaktowy (opcjonalnie) - /// - nl: Contact-e-mail (optioneel) - /// - ru: Контактный e-mail (необязательно) - static let reportContactLabel: String.LocalizationValue = "bug_report_contact_label" - /// Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted). - /// - /// Key: `bug_report_description` - /// Context: Bug report form: intro text explaining what gets attached. - /// - /// - en: Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted). - /// - fr: Dites-nous ce qui s’est passé. Nous joignons les infos de l’appareil et, en option, les journaux récents (valeurs sensibles masquées). - /// - es: Cuéntenos qué salió mal. Adjuntamos información del dispositivo y, opcionalmente, registros recientes (con los valores sensibles ocultos). - /// - it: Ci dica cosa è andato storto. Alleghiamo le informazioni sul dispositivo e, facoltativamente, i log recenti (con i valori sensibili oscurati). - /// - de: Sagen Sie uns, was schiefgelaufen ist. Wir hängen Geräteinformationen und optional aktuelle Protokolle an (sensible Werte werden geschwärzt). - /// - pt: Diga-nos o que correu mal. Anexamos informações do dispositivo e, opcionalmente, registos recentes (com os valores sensíveis ocultados). - /// - pl: Napisz, co poszło nie tak. Dołączamy informacje o urządzeniu i opcjonalnie ostatnie dzienniki (z ukrytymi wrażliwymi wartościami). - /// - nl: Vertel ons wat er misging. We voegen apparaatgegevens toe en optioneel recente logbestanden (met gevoelige waarden onleesbaar gemaakt). - /// - ru: Расскажите, что пошло не так. Мы прикладываем сведения об устройстве и, при желании, недавние журналы (конфиденциальные значения скрыты). - static let reportDescription: String.LocalizationValue = "bug_report_description" - /// Device information - /// - /// Key: `bug_report_device_section` - /// Context: Bug report form: heading for the attached device information section. - /// - /// - en: Device information - /// - fr: Informations sur l’appareil - /// - es: Información del dispositivo - /// - it: Informazioni sul dispositivo - /// - de: Geräteinformationen - /// - pt: Informações do dispositivo - /// - pl: Informacje o urządzeniu - /// - nl: Apparaatgegevens - /// - ru: Сведения об устройстве - static let reportDeviceSection: String.LocalizationValue = "bug_report_device_section" - /// Describe what you expected to happen - /// - /// Key: `bug_report_expected_hint` - /// Context: Bug report form: placeholder in the 'what did you expect' field. - /// - /// - en: Describe what you expected to happen - /// - fr: Décrivez ce à quoi vous vous attendiez - /// - es: Describa lo que esperaba que ocurriera - /// - it: Descriva cosa si aspettava che accadesse - /// - de: Beschreiben Sie, was Sie erwartet haben - /// - pt: Descreva o que esperava que acontecesse - /// - pl: Opisz oczekiwane zachowanie - /// - nl: Beschrijf wat u verwachtte - /// - ru: Опишите, что вы ожидали - static let reportExpectedHint: String.LocalizationValue = "bug_report_expected_hint" - /// What did you expect? - /// - /// Key: `bug_report_expected_label` - /// Context: Bug report form: label for the expected-behavior field. - /// - /// - en: What did you expect? - /// - fr: À quoi vous attendiez-vous ? - /// - es: ¿Qué esperaba? - /// - it: Cosa si aspettava? - /// - de: Was haben Sie erwartet? - /// - pt: O que esperava? - /// - pl: Czego oczekiwano? - /// - nl: Wat verwachtte u? - /// - ru: Что вы ожидали? - static let reportExpectedLabel: String.LocalizationValue = "bug_report_expected_label" - /// Include recent logs - /// - /// Key: `bug_report_include_logs` - /// Context: Bug report form: toggle label to attach recent logs. - /// - /// - en: Include recent logs - /// - fr: Inclure les journaux récents - /// - es: Incluir registros recientes - /// - it: Includi i log recenti - /// - de: Aktuelle Protokolle einschließen - /// - pt: Incluir registos recentes - /// - pl: Dołącz ostatnie dzienniki - /// - nl: Recente logbestanden meesturen - /// - ru: Включить недавние журналы - static let reportIncludeLogs: String.LocalizationValue = "bug_report_include_logs" - /// Helps us diagnose the issue. Sensitive values are redacted before sending. - /// - /// Key: `bug_report_include_logs_description` - /// Context: Bug report form: explanation under the include-logs toggle. - /// - /// - en: Helps us diagnose the issue. Sensitive values are redacted before sending. - /// - fr: Nous aide à diagnostiquer le problème. Les valeurs sensibles sont masquées avant l’envoi. - /// - es: Nos ayuda a diagnosticar el problema. Los valores sensibles se ocultan antes de enviarlos. - /// - it: Ci aiuta a diagnosticare il problema. I valori sensibili vengono oscurati prima dell’invio. - /// - de: Hilft uns, das Problem zu diagnostizieren. Sensible Werte werden vor dem Senden geschwärzt. - /// - pt: Ajuda-nos a diagnosticar o problema. Os valores sensíveis são ocultados antes do envio. - /// - pl: Pomaga nam zdiagnozować problem. Wrażliwe wartości są ukrywane przed wysłaniem. - /// - nl: Helpt ons het probleem te diagnosticeren. Gevoelige waarden worden vóór verzending onleesbaar gemaakt. - /// - ru: Помогает нам диагностировать проблему. Конфиденциальные значения скрываются перед отправкой. - static let reportIncludeLogsDescription: String.LocalizationValue = "bug_report_include_logs_description" - /// Log attachment size - /// - /// Key: `bug_report_logs_size` - /// Context: Bug report form: label showing the size of the log attachment. - /// - /// - en: Log attachment size - /// - fr: Taille de la pièce jointe des journaux - /// - es: Tamaño del archivo de registros - /// - it: Dimensione dell’allegato dei log - /// - de: Größe des Protokollanhangs - /// - pt: Tamanho do anexo de registos - /// - pl: Rozmiar załącznika z dziennikami - /// - nl: Grootte van logbijlage - /// - ru: Размер вложения с журналами - static let reportLogsSize: String.LocalizationValue = "bug_report_logs_size" - /// Please describe what you expected. - /// - /// Key: `bug_report_missing_expected` - /// Context: Bug report form: validation error when the expected-behavior field is empty. - /// - /// - en: Please describe what you expected. - /// - fr: Veuillez décrire ce à quoi vous vous attendiez. - /// - es: Describa lo que esperaba. - /// - it: Descriva cosa si aspettava. - /// - de: Bitte beschreiben Sie, was Sie erwartet haben. - /// - pt: Descreva o que esperava. - /// - pl: Opisz oczekiwane zachowanie. - /// - nl: Beschrijf wat u verwachtte. - /// - ru: Опишите, что вы ожидали. - static let reportMissingExpected: String.LocalizationValue = "bug_report_missing_expected" - /// Please describe what happened. - /// - /// Key: `bug_report_missing_what` - /// Context: Bug report form: validation error when the what-happened field is empty. - /// - /// - en: Please describe what happened. - /// - fr: Veuillez décrire ce qui s’est passé. - /// - es: Describa lo que ocurrió. - /// - it: Descriva cosa è accaduto. - /// - de: Bitte beschreiben Sie, was passiert ist. - /// - pt: Descreva o que aconteceu. - /// - pl: Opisz, co się stało. - /// - nl: Beschrijf wat er gebeurde. - /// - ru: Опишите, что произошло. - static let reportMissingWhat: String.LocalizationValue = "bug_report_missing_what" - /// List the steps to reproduce the issue - /// - /// Key: `bug_report_steps_hint` - /// Context: Bug report form: placeholder in the steps-to-reproduce field. - /// - /// - en: List the steps to reproduce the issue - /// - fr: Listez les étapes pour reproduire le problème - /// - es: Enumere los pasos para reproducir el problema - /// - it: Elenchi i passaggi per riprodurre il problema - /// - de: Listen Sie die Schritte zum Reproduzieren des Problems auf - /// - pt: Enumere os passos para reproduzir o problema - /// - pl: Wymień kroki umożliwiające odtworzenie problemu - /// - nl: Noem de stappen om het probleem te reproduceren - /// - ru: Перечислите шаги для воспроизведения проблемы - static let reportStepsHint: String.LocalizationValue = "bug_report_steps_hint" - /// Steps to reproduce (optional) - /// - /// Key: `bug_report_steps_label` - /// Context: Bug report form: label for the optional steps-to-reproduce field. - /// - /// - en: Steps to reproduce (optional) - /// - fr: Étapes pour reproduire (facultatif) - /// - es: Pasos para reproducir (opcional) - /// - it: Passaggi per riprodurre (facoltativi) - /// - de: Schritte zum Reproduzieren (optional) - /// - pt: Passos para reproduzir (opcional) - /// - pl: Kroki do odtworzenia (opcjonalnie) - /// - nl: Stappen om te reproduceren (optioneel) - /// - ru: Шаги для воспроизведения (необязательно) - static let reportStepsLabel: String.LocalizationValue = "bug_report_steps_label" - /// Submit report - /// - /// Key: `bug_report_submit` - /// Context: Bug report form: submit button. - /// - /// - en: Submit report - /// - fr: Envoyer le rapport - /// - es: Enviar informe - /// - it: Invia segnalazione - /// - de: Bericht senden - /// - pt: Enviar relatório - /// - pl: Wyślij zgłoszenie - /// - nl: Rapport versturen - /// - ru: Отправить отчёт - static let reportSubmit: String.LocalizationValue = "bug_report_submit" - /// Could not submit the bug report. Try again later. - /// - /// Key: `bug_report_submit_failed` - /// Context: Bug report form: error message when submission fails. - /// - /// - en: Could not submit the bug report. Try again later. - /// - fr: Impossible d’envoyer le rapport de bug. Réessayez plus tard. - /// - es: No se pudo enviar el informe de error. Inténtelo de nuevo más tarde. - /// - it: Impossibile inviare la segnalazione. Riprovi più tardi. - /// - de: Der Fehlerbericht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut. - /// - pt: Não foi possível enviar o relatório de erro. Tente novamente mais tarde. - /// - pl: Nie udało się wysłać zgłoszenia błędu. Spróbuj ponownie później. - /// - nl: Het foutrapport kon niet worden verstuurd. Probeer het later opnieuw. - /// - ru: Не удалось отправить отчёт об ошибке. Повторите попытку позже. - static let reportSubmitFailed: String.LocalizationValue = "bug_report_submit_failed" - /// Thanks — your bug report was recorded. - /// - /// Key: `bug_report_submitted` - /// Context: Bug report form: confirmation message after a successful submission. - /// - /// - en: Thanks — your bug report was recorded. - /// - fr: Merci — votre rapport de bug a été enregistré. - /// - es: Gracias: su informe de error se ha registrado. - /// - it: Grazie: la sua segnalazione è stata registrata. - /// - de: Danke – Ihr Fehlerbericht wurde erfasst. - /// - pt: Obrigado — o seu relatório de erro foi registado. - /// - pl: Dziękujemy — Twoje zgłoszenie błędu zostało zapisane. - /// - nl: Bedankt — uw foutrapport is vastgelegd. - /// - ru: Спасибо — ваш отчёт об ошибке записан. - static let reportSubmitted: String.LocalizationValue = "bug_report_submitted" - /// Submitting… - /// - /// Key: `bug_report_submitting` - /// Context: Bug report form: progress label while the report is being sent. - /// - /// - en: Submitting… - /// - fr: Envoi… - /// - es: Enviando… - /// - it: Invio in corso… - /// - de: Wird gesendet… - /// - pt: A enviar… - /// - pl: Wysyłanie… - /// - nl: Bezig met versturen… - /// - ru: Отправка… - static let reportSubmitting: String.LocalizationValue = "bug_report_submitting" - /// Describe what happened - /// - /// Key: `bug_report_what_hint` - /// Context: Bug report form: placeholder in the what-happened field. - /// - /// - en: Describe what happened - /// - fr: Décrivez ce qui s’est passé - /// - es: Describa lo que ocurrió - /// - it: Descriva cosa è accaduto - /// - de: Beschreiben Sie, was passiert ist - /// - pt: Descreva o que aconteceu - /// - pl: Opisz, co się stało - /// - nl: Beschrijf wat er gebeurde - /// - ru: Опишите, что произошло - static let reportWhatHint: String.LocalizationValue = "bug_report_what_hint" - /// What happened? - /// - /// Key: `bug_report_what_label` - /// Context: Bug report form: label for the what-happened field. - /// - /// - en: What happened? - /// - fr: Que s’est-il passé ? - /// - es: ¿Qué ocurrió? - /// - it: Cosa è accaduto? - /// - de: Was ist passiert? - /// - pt: O que aconteceu? - /// - pl: Co się stało? - /// - nl: Wat is er gebeurd? - /// - ru: Что произошло? - static let reportWhatLabel: String.LocalizationValue = "bug_report_what_label" - } - enum Button { - /// Approve - /// - /// Key: `button_approve` - /// Context: Button: approve a receiver's request. - /// - /// - en: Approve - /// - fr: Approuver - /// - es: Aprobar - /// - it: Approva - /// - de: Genehmigen - /// - pt: Aprovar - /// - pl: Zatwierdź - /// - nl: Goedkeuren - /// - ru: Одобрить - static let approve: String.LocalizationValue = "button_approve" - /// Back - /// - /// Key: `button_back` - /// Context: Button: go back to the previous step/screen. - /// - /// - en: Back - /// - fr: Retour - /// - es: Atrás - /// - it: Indietro - /// - de: Zurück - /// - pt: Voltar - /// - pl: Wstecz - /// - nl: Terug - /// - ru: Назад - static let back: String.LocalizationValue = "button_back" - /// Cancel - /// - /// Key: `button_cancel` - /// Context: Button: cancel the current action or dialog. - /// - /// - en: Cancel - /// - fr: Annuler - /// - es: Cancelar - /// - it: Annulla - /// - de: Abbrechen - /// - pt: Cancelar - /// - pl: Anuluj - /// - nl: Annuleren - /// - ru: Отмена - static let cancel: String.LocalizationValue = "button_cancel" - /// Cancel - /// - /// Key: `button_cancel_receive` - /// Context: Button: cancel an in-progress receive. - /// - /// - en: Cancel - /// - fr: Annuler - /// - es: Cancelar - /// - it: Annulla - /// - de: Abbrechen - /// - pt: Cancelar - /// - pl: Anuluj - /// - nl: Annuleren - /// - ru: Отмена - static let cancelReceive: String.LocalizationValue = "button_cancel_receive" - /// Change files - /// - /// Key: `button_change_files` - /// Context: Button: change the selected files in the send flow. - /// - /// - en: Change files - /// - fr: Modifier les fichiers - /// - es: Cambiar archivos - /// - it: Cambia file - /// - de: Dateien ändern - /// - pt: Alterar ficheiros - /// - pl: Zmień pliki - /// - nl: Bestanden wijzigen - /// - ru: Изменить файлы - static let changeFiles: String.LocalizationValue = "button_change_files" - /// Choose files - /// - /// Key: `button_choose_files` - /// Context: Button: open the file picker to choose files to send. - /// - /// - en: Choose files - /// - fr: Choisir des fichiers - /// - es: Elegir archivos - /// - it: Scegli file - /// - de: Dateien auswählen - /// - pt: Escolher ficheiros - /// - pl: Wybierz pliki - /// - nl: Bestanden kiezen - /// - ru: Выбрать файлы - static let chooseFiles: String.LocalizationValue = "button_choose_files" - /// Choose folder - /// - /// Key: `button_choose_folder` - /// Context: Button: open the folder picker (send selection or receive folder). - /// - /// - en: Choose folder - /// - fr: Choisir un dossier - /// - es: Elegir carpeta - /// - it: Scegli cartella - /// - de: Ordner auswählen - /// - pt: Escolher pasta - /// - pl: Wybierz folder - /// - nl: Map kiezen - /// - ru: Выбрать папку - static let chooseFolder: String.LocalizationValue = "button_choose_folder" - /// Clear - /// - /// Key: `button_clear` - /// Context: Button: clear the current input or selection. - /// - /// - en: Clear - /// - fr: Effacer - /// - es: Borrar - /// - it: Cancella - /// - de: Löschen - /// - pt: Limpar - /// - pl: Wyczyść - /// - nl: Wissen - /// - ru: Очистить - static let clear: String.LocalizationValue = "button_clear" - /// Close - /// - /// Key: `button_close` - /// Context: Button: close the current sheet/dialog. - /// - /// - en: Close - /// - fr: Fermer - /// - es: Cerrar - /// - it: Chiudi - /// - de: Schließen - /// - pt: Fechar - /// - pl: Zamknij - /// - nl: Sluiten - /// - ru: Закрыть - static let close: String.LocalizationValue = "button_close" - /// New transfer - /// - /// Key: `button_create_new_transfer` - /// Context: Button: start creating a new transfer (Send tab). - /// - /// - en: New transfer - /// - fr: Nouveau transfert - /// - es: Nueva transferencia - /// - it: Nuovo trasferimento - /// - de: Neue Übertragung - /// - pt: Nova transferência - /// - pl: Nowy transfer - /// - nl: Nieuwe overdracht - /// - ru: Новая передача - static let createNewTransfer: String.LocalizationValue = "button_create_new_transfer" - /// Delete transfer - /// - /// Key: `button_delete_transfer` - /// Context: Button: delete a transfer. - /// - /// - en: Delete transfer - /// - fr: Supprimer le transfert - /// - es: Eliminar transferencia - /// - it: Elimina trasferimento - /// - de: Übertragung löschen - /// - pt: Eliminar transferência - /// - pl: Usuń transfer - /// - nl: Overdracht verwijderen - /// - ru: Удалить передачу - static let deleteTransfer: String.LocalizationValue = "button_delete_transfer" - /// Save .vnd file - /// - /// Key: `button_download_invitation` - /// Context: Button: save the invitation as a .vnd file. - /// - /// - en: Save .vnd file - /// - fr: Enregistrer le fichier .vnd - /// - es: Guardar archivo .vnd - /// - it: Salva file .vnd - /// - de: .vnd-Datei sichern - /// - pt: Guardar ficheiro .vnd - /// - pl: Zapisz plik .vnd - /// - nl: .vnd-bestand bewaren - /// - ru: Сохранить файл .vnd - static let downloadInvitation: String.LocalizationValue = "button_download_invitation" - /// Share invitation - /// - /// Key: `button_native_share` - /// Context: Button: open the OS share sheet to share the invitation. - /// - /// - en: Share invitation - /// - fr: Partager l’invitation - /// - es: Compartir invitación - /// - it: Condividi invito - /// - de: Einladung teilen - /// - pt: Partilhar convite - /// - pl: Udostępnij zaproszenie - /// - nl: Uitnodiging delen - /// - ru: Поделиться приглашением - static let nativeShare: String.LocalizationValue = "button_native_share" - /// Open Settings - /// - /// Key: `button_open_settings` - /// Context: Button: open the OS Settings app (e.g. for permissions). - /// - /// - en: Open Settings - /// - fr: Ouvrir les Réglages - /// - es: Abrir Ajustes - /// - it: Apri Impostazioni - /// - de: Einstellungen öffnen - /// - pt: Abrir Definições - /// - pl: Otwórz Ustawienia - /// - nl: Instellingen openen - /// - ru: Открыть Настройки - static let openSettings: String.LocalizationValue = "button_open_settings" - /// Receive - /// - /// Key: `button_receive` - /// Context: Button: receive label (Receive action). - /// - /// - en: Receive - /// - fr: Recevoir - /// - es: Recibir - /// - it: Ricevi - /// - de: Empfangen - /// - pt: Receber - /// - pl: Odbierz - /// - nl: Ontvangen - /// - ru: Получить - static let receive: String.LocalizationValue = "button_receive" - /// Start receiving - /// - /// Key: `button_receive_files` - /// Context: Button: start receiving a transfer. - /// - /// - en: Start receiving - /// - fr: Commencer à recevoir - /// - es: Empezar a recibir - /// - it: Inizia a ricevere - /// - de: Empfang starten - /// - pt: Começar a receber - /// - pl: Rozpocznij odbieranie - /// - nl: Ontvangen starten - /// - ru: Начать получение - static let receiveFiles: String.LocalizationValue = "button_receive_files" - /// Refuse - /// - /// Key: `button_refuse` - /// Context: Button: refuse a receiver's request. - /// - /// - en: Refuse - /// - fr: Refuser - /// - es: Rechazar - /// - it: Rifiuta - /// - de: Ablehnen - /// - pt: Recusar - /// - pl: Odrzuć - /// - nl: Weigeren - /// - ru: Отклонить - static let refuse: String.LocalizationValue = "button_refuse" - /// Remove file - /// - /// Key: `button_remove_file` - /// Context: Button: remove a single file from the send selection. - /// - /// - en: Remove file - /// - fr: Retirer le fichier - /// - es: Quitar archivo - /// - it: Rimuovi file - /// - de: Datei entfernen - /// - pt: Remover ficheiro - /// - pl: Usuń plik - /// - nl: Bestand verwijderen - /// - ru: Удалить файл - static let removeFile: String.LocalizationValue = "button_remove_file" - /// Use default - /// - /// Key: `button_reset_default` - /// Context: Button: reset a setting to its default value. - /// - /// - en: Use default - /// - fr: Valeur par défaut - /// - es: Usar predeterminado - /// - it: Usa predefinito - /// - de: Standard verwenden - /// - pt: Usar predefinição - /// - pl: Użyj domyślnego - /// - nl: Standaard gebruiken - /// - ru: По умолчанию - static let resetDefault: String.LocalizationValue = "button_reset_default" - /// Retry - /// - /// Key: `button_retry` - /// Context: Button: retry after a failed operation. - /// - /// - en: Retry - /// - fr: Réessayer - /// - es: Reintentar - /// - it: Riprova - /// - de: Wiederholen - /// - pt: Tentar novamente - /// - pl: Spróbuj ponownie - /// - nl: Opnieuw proberen - /// - ru: Повторить - static let retry: String.LocalizationValue = "button_retry" - /// Start sharing - /// - /// Key: `button_share_file` - /// Context: Button: start sharing the transfer (make it available). - /// - /// - en: Start sharing - /// - fr: Commencer le partage - /// - es: Empezar a compartir - /// - it: Inizia a condividere - /// - de: Freigabe starten - /// - pt: Começar a partilhar - /// - pl: Rozpocznij udostępnianie - /// - nl: Delen starten - /// - ru: Начать общий доступ - static let shareFile: String.LocalizationValue = "button_share_file" - /// Preparing transfer… - /// - /// Key: `button_sharing_file` - /// Context: Button: disabled/loading state while the transfer is being prepared. - /// - /// - en: Preparing transfer… - /// - fr: Préparation du transfert… - /// - es: Preparando la transferencia… - /// - it: Preparazione del trasferimento… - /// - de: Übertragung wird vorbereitet… - /// - pt: A preparar a transferência… - /// - pl: Przygotowywanie transferu… - /// - nl: Overdracht voorbereiden… - /// - ru: Подготовка передачи… - static let sharingFile: String.LocalizationValue = "button_sharing_file" - /// Show in Files - /// - /// Key: `button_show_in_files` - /// Context: Button: reveal a received file in the Files app. - /// - /// - en: Show in Files - /// - fr: Afficher dans Fichiers - /// - es: Mostrar en Archivos - /// - it: Mostra in File - /// - de: In „Dateien“ anzeigen - /// - pt: Mostrar em Ficheiros - /// - pl: Pokaż w Plikach - /// - nl: Toon in Bestanden - /// - ru: Показать в Файлах - static let showInFiles: String.LocalizationValue = "button_show_in_files" - /// Write to NFC tag - /// - /// Key: `button_write_nfc` - /// Context: Button: write the invitation to an NFC tag. - /// - /// - en: Write to NFC tag - /// - fr: Écrire sur un tag NFC - /// - es: Escribir en etiqueta NFC - /// - it: Scrivi su tag NFC - /// - de: Auf NFC-Tag schreiben - /// - pt: Escrever em etiqueta NFC - /// - pl: Zapisz na tagu NFC - /// - nl: Naar NFC-tag schrijven - /// - ru: Записать на NFC-метку - static let writeNfc: String.LocalizationValue = "button_write_nfc" - } - enum Device { - /// Device model - /// - /// Key: `device_model_title` - /// Context: Device information row: device model label. - /// - /// - en: Device model - /// - fr: Modèle de l’appareil - /// - es: Modelo del dispositivo - /// - it: Modello del dispositivo - /// - de: Gerätemodell - /// - pt: Modelo do dispositivo - /// - pl: Model urządzenia - /// - nl: Apparaatmodel - /// - ru: Модель устройства - static let modelTitle: String.LocalizationValue = "device_model_title" - /// Device name - /// - /// Key: `device_name_title` - /// Context: Device information row: device name label. - /// - /// - en: Device name - /// - fr: Nom de l’appareil - /// - es: Nombre del dispositivo - /// - it: Nome del dispositivo - /// - de: Gerätename - /// - pt: Nome do dispositivo - /// - pl: Nazwa urządzenia - /// - nl: Apparaatnaam - /// - ru: Имя устройства - static let nameTitle: String.LocalizationValue = "device_name_title" - } - enum Diagnostics { - /// Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included. - /// - /// Key: `diagnostics_description` - /// Context: Settings > Diagnostics: explanation of what anonymous diagnostics collect. - /// - /// - en: Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included. - /// - fr: Envoyer des rapports de plantage et des événements d’utilisation anonymes pour nous aider à améliorer VniDrop. Vous pouvez désactiver cela à tout moment. Les invitations, chemins de fichiers et contenus de transfert ne sont jamais inclus. - /// - es: Enviar informes de fallos y eventos de uso anónimos para ayudarnos a mejorar VniDrop. Puede desactivarlo en cualquier momento. Las invitaciones, las rutas de archivos y el contenido de las transferencias nunca se incluyen. - /// - it: Invia report di arresto anomalo ed eventi d’uso anonimi per aiutarci a migliorare VniDrop. Può disattivarlo in qualsiasi momento. Inviti, percorsi dei file e contenuti dei trasferimenti non vengono mai inclusi. - /// - de: Anonyme Absturzberichte und Nutzungsereignisse senden, damit wir VniDrop verbessern können. Sie können dies jederzeit deaktivieren. Einladungen, Dateipfade und Übertragungsinhalte werden niemals einbezogen. - /// - pt: Enviar relatórios de falhas e eventos de utilização anónimos para nos ajudar a melhorar o VniDrop. Pode desativar isto a qualquer momento. Convites, caminhos de ficheiros e conteúdos das transferências nunca são incluídos. - /// - pl: Wysyłaj anonimowe raporty o awariach i zdarzenia użytkowania, aby pomóc nam ulepszać VniDrop. Możesz to wyłączyć w dowolnej chwili. Zaproszenia, ścieżki plików i zawartość transferów nigdy nie są dołączane. - /// - nl: Verstuur anonieme crashrapporten en gebruiksgebeurtenissen zodat we VniDrop kunnen verbeteren. U kunt dit op elk moment uitschakelen. Uitnodigingen, bestandspaden en overdrachtsinhoud worden nooit meegestuurd. - /// - ru: Отправлять анонимные отчёты о сбоях и события использования, чтобы помочь нам улучшать VniDrop. Вы можете отключить это в любой момент. Приглашения, пути к файлам и содержимое передач никогда не включаются. - static let description: String.LocalizationValue = "diagnostics_description" - /// Diagnostics sharing is off. - /// - /// Key: `diagnostics_disabled_message` - /// Context: Settings > Diagnostics: confirmation shown when diagnostics are turned off. - /// - /// - en: Diagnostics sharing is off. - /// - fr: Le partage des diagnostics est désactivé. - /// - es: El uso compartido de diagnósticos está desactivado. - /// - it: La condivisione dei dati diagnostici è disattivata. - /// - de: Die Freigabe von Diagnosedaten ist deaktiviert. - /// - pt: A partilha de diagnósticos está desativada. - /// - pl: Udostępnianie diagnostyki jest wyłączone. - /// - nl: Het delen van diagnostische gegevens is uitgeschakeld. - /// - ru: Передача диагностики отключена. - static let disabledMessage: String.LocalizationValue = "diagnostics_disabled_message" - /// Diagnostics sharing is on. - /// - /// Key: `diagnostics_enabled_message` - /// Context: Settings > Diagnostics: confirmation shown when diagnostics are turned on. - /// - /// - en: Diagnostics sharing is on. - /// - fr: Le partage des diagnostics est activé. - /// - es: El uso compartido de diagnósticos está activado. - /// - it: La condivisione dei dati diagnostici è attivata. - /// - de: Die Freigabe von Diagnosedaten ist aktiviert. - /// - pt: A partilha de diagnósticos está ativada. - /// - pl: Udostępnianie diagnostyki jest włączone. - /// - nl: Het delen van diagnostische gegevens is ingeschakeld. - /// - ru: Передача диагностики включена. - static let enabledMessage: String.LocalizationValue = "diagnostics_enabled_message" - /// Share diagnostics - /// - /// Key: `diagnostics_title` - /// Context: Settings > Diagnostics: toggle title. - /// - /// - en: Share diagnostics - /// - fr: Partager les diagnostics - /// - es: Compartir diagnósticos - /// - it: Condividi dati diagnostici - /// - de: Diagnosedaten teilen - /// - pt: Partilhar diagnósticos - /// - pl: Udostępniaj diagnostykę - /// - nl: Diagnostische gegevens delen - /// - ru: Делиться диагностикой - static let title: String.LocalizationValue = "diagnostics_title" - } - enum Error { - /// Camera access is required to scan a QR code. - /// - /// Key: `error_camera` - /// Context: Error: camera permission is needed to scan a QR code. - /// - /// - en: Camera access is required to scan a QR code. - /// - fr: L’accès à la caméra est nécessaire pour scanner un QR code. - /// - es: Se necesita acceso a la cámara para escanear un código QR. - /// - it: Per scansionare un codice QR è necessario l’accesso alla fotocamera. - /// - de: Für das Scannen eines QR-Codes ist Kamerazugriff erforderlich. - /// - pt: É necessário acesso à câmara para ler um código QR. - /// - pl: Do zeskanowania kodu QR wymagany jest dostęp do aparatu. - /// - nl: Voor het scannen van een QR-code is toegang tot de camera vereist. - /// - ru: Для сканирования QR-кода требуется доступ к камере. - static let camera: String.LocalizationValue = "error_camera" - /// Could not load device information. - /// - /// Key: `error_device_info` - /// Context: Error: device information could not be loaded. - /// - /// - en: Could not load device information. - /// - fr: Impossible de charger les informations de l’appareil. - /// - es: No se pudo cargar la información del dispositivo. - /// - it: Impossibile caricare le informazioni sul dispositivo. - /// - de: Geräteinformationen konnten nicht geladen werden. - /// - pt: Não foi possível carregar as informações do dispositivo. - /// - pl: Nie udało się wczytać informacji o urządzeniu. - /// - nl: Apparaatgegevens konden niet worden geladen. - /// - ru: Не удалось загрузить сведения об устройстве. - static let deviceInfo: String.LocalizationValue = "error_device_info" - /// A file with the same name already exists in the destination. Choose another folder or remove the existing file. - /// - /// Key: `error_destination_exists` - /// Context: Error: a received file would overwrite an existing destination file. - /// - /// - en: A file with the same name already exists in the destination. Choose another folder or remove the existing file. - /// - fr: Un fichier portant le même nom existe déjà dans la destination. Choisissez un autre dossier ou supprimez le fichier existant. - /// - es: Ya existe un archivo con el mismo nombre en el destino. Elija otra carpeta o elimine el archivo existente. - /// - it: Nella destinazione esiste già un file con lo stesso nome. Scelga un’altra cartella o rimuova il file esistente. - /// - de: Am Ziel ist bereits eine Datei mit demselben Namen vorhanden. Wählen Sie einen anderen Ordner oder entfernen Sie die vorhandene Datei. - /// - pt: Já existe um ficheiro com o mesmo nome no destino. Escolha outra pasta ou remova o ficheiro existente. - /// - pl: W miejscu docelowym istnieje już plik o tej samej nazwie. Wybierz inny folder lub usuń istniejący plik. - /// - nl: Er staat al een bestand met dezelfde naam in de doelmap. Kies een andere map of verwijder het bestaande bestand. - /// - ru: В папке назначения уже есть файл с таким именем. Выберите другую папку или удалите существующий файл. - static let destinationExists: String.LocalizationValue = "error_destination_exists" - /// VniDrop could not access the selected files or folder. Check permissions and try again. - /// - /// Key: `error_filesystem` - /// Context: Error: the selected files/folder could not be accessed. - /// - /// - en: VniDrop could not access the selected files or folder. Check permissions and try again. - /// - fr: VniDrop n’a pas pu accéder aux fichiers ou au dossier sélectionnés. Vérifiez les autorisations et réessayez. - /// - es: VniDrop no pudo acceder a los archivos o la carpeta seleccionados. Compruebe los permisos e inténtelo de nuevo. - /// - it: VniDrop non ha potuto accedere ai file o alla cartella selezionati. Controlli le autorizzazioni e riprovi. - /// - de: VniDrop konnte nicht auf die ausgewählten Dateien oder den Ordner zugreifen. Überprüfen Sie die Berechtigungen und versuchen Sie es erneut. - /// - pt: O VniDrop não conseguiu aceder aos ficheiros ou à pasta selecionados. Verifique as permissões e tente novamente. - /// - pl: VniDrop nie mógł uzyskać dostępu do wybranych plików lub folderu. Sprawdź uprawnienia i spróbuj ponownie. - /// - nl: VniDrop kon geen toegang krijgen tot de geselecteerde bestanden of map. Controleer de machtigingen en probeer het opnieuw. - /// - ru: VniDrop не удалось получить доступ к выбранным файлам или папке. Проверьте разрешения и повторите попытку. - static let filesystem: String.LocalizationValue = "error_filesystem" - /// Something went wrong. Try again. - /// - /// Key: `error_generic` - /// Context: Error: generic fallback message. - /// - /// - en: Something went wrong. Try again. - /// - fr: Une erreur est survenue. Réessayez. - /// - es: Algo salió mal. Inténtelo de nuevo. - /// - it: Qualcosa è andato storto. Riprovi. - /// - de: Etwas ist schiefgelaufen. Versuchen Sie es erneut. - /// - pt: Algo correu mal. Tente novamente. - /// - pl: Coś poszło nie tak. Spróbuj ponownie. - /// - nl: Er is iets misgegaan. Probeer het opnieuw. - /// - ru: Что-то пошло не так. Повторите попытку. - static let generic: String.LocalizationValue = "error_generic" - /// VniDrop could not finish starting up. Close the app and try again. - /// - /// Key: `error_initialization` - /// Context: Error: the app failed to finish starting up. - /// - /// - en: VniDrop could not finish starting up. Close the app and try again. - /// - fr: VniDrop n’a pas pu terminer son démarrage. Fermez l’app et réessayez. - /// - es: VniDrop no pudo terminar de iniciarse. Cierre la app e inténtelo de nuevo. - /// - it: VniDrop non ha potuto completare l’avvio. Chiuda l’app e riprovi. - /// - de: VniDrop konnte den Start nicht abschließen. Schließen Sie die App und versuchen Sie es erneut. - /// - pt: O VniDrop não conseguiu concluir o arranque. Feche a app e tente novamente. - /// - pl: VniDrop nie mógł dokończyć uruchamiania. Zamknij aplikację i spróbuj ponownie. - /// - nl: VniDrop kon het opstarten niet voltooien. Sluit de app en probeer het opnieuw. - /// - ru: VniDrop не удалось завершить запуск. Закройте приложение и повторите попытку. - static let initialization: String.LocalizationValue = "error_initialization" - /// Some transfer information is invalid. Review your selection or ask the sender to share again. - /// - /// Key: `error_invalid_input` - /// Context: Error: transfer input or metadata is invalid. - /// - /// - en: Some transfer information is invalid. Review your selection or ask the sender to share again. - /// - fr: Certaines informations du transfert ne sont pas valides. Vérifiez votre sélection ou demandez à l’expéditeur de partager à nouveau. - /// - es: Parte de la información de la transferencia no es válida. Revise la selección o pida al remitente que vuelva a compartirla. - /// - it: Alcune informazioni del trasferimento non sono valide. Controlli la selezione o chieda al mittente di condividere di nuovo. - /// - de: Einige Übertragungsinformationen sind ungültig. Prüfen Sie Ihre Auswahl oder bitten Sie den Absender, erneut zu teilen. - /// - pt: Algumas informações da transferência são inválidas. Reveja a seleção ou peça ao remetente para partilhar novamente. - /// - pl: Niektóre informacje o transferze są nieprawidłowe. Sprawdź wybór lub poproś nadawcę o ponowne udostępnienie. - /// - nl: Sommige overdrachtsgegevens zijn ongeldig. Controleer uw selectie of vraag de afzender opnieuw te delen. - /// - ru: Некоторые данные передачи недействительны. Проверьте выбор или попросите отправителя поделиться снова. - static let invalidInput: String.LocalizationValue = "error_invalid_input" - /// This invitation could not be read. Ask the sender for a new one. - /// - /// Key: `error_invalid_ticket` - /// Context: Error: the invitation/ticket could not be parsed. - /// - /// - en: This invitation could not be read. Ask the sender for a new one. - /// - fr: Cette invitation n’a pas pu être lue. Demandez-en une nouvelle à l’expéditeur. - /// - es: No se pudo leer esta invitación. Pida al remitente una nueva. - /// - it: Impossibile leggere questo invito. Ne chieda uno nuovo al mittente. - /// - de: Diese Einladung konnte nicht gelesen werden. Bitten Sie den Absender um eine neue. - /// - pt: Não foi possível ler este convite. Peça um novo ao remetente. - /// - pl: Nie udało się odczytać tego zaproszenia. Poproś nadawcę o nowe. - /// - nl: Deze uitnodiging kon niet worden gelezen. Vraag de afzender om een nieuwe. - /// - ru: Не удалось прочитать это приглашение. Попросите отправителя прислать новое. - static let invalidTicket: String.LocalizationValue = "error_invalid_ticket" - /// That invitation is empty. Try opening it again. - /// - /// Key: `error_invitation_empty` - /// Context: Error: the opened invitation contained no data. - /// - /// - en: That invitation is empty. Try opening it again. - /// - fr: Cette invitation est vide. Essayez de l’ouvrir à nouveau. - /// - es: Esa invitación está vacía. Intente abrirla de nuevo. - /// - it: Questo invito è vuoto. Provi ad aprirlo di nuovo. - /// - de: Diese Einladung ist leer. Versuchen Sie, sie erneut zu öffnen. - /// - pt: Esse convite está vazio. Tente abri-lo novamente. - /// - pl: To zaproszenie jest puste. Spróbuj otworzyć je ponownie. - /// - nl: Die uitnodiging is leeg. Probeer deze opnieuw te openen. - /// - ru: Это приглашение пустое. Попробуйте открыть его снова. - static let invitationEmpty: String.LocalizationValue = "error_invitation_empty" - /// The native VniDrop library is missing from this build. - /// - /// Key: `error_missing_native_library` - /// Context: Error: the native library is missing from the build. - /// - /// - en: The native VniDrop library is missing from this build. - /// - fr: La bibliothèque native de VniDrop est absente de cette version. - /// - es: Falta la biblioteca nativa de VniDrop en esta versión. - /// - it: La libreria nativa di VniDrop non è presente in questa build. - /// - de: Die native VniDrop-Bibliothek fehlt in diesem Build. - /// - pt: A biblioteca nativa do VniDrop está em falta nesta compilação. - /// - pl: W tej kompilacji brakuje natywnej biblioteki VniDrop. - /// - nl: De native VniDrop-bibliotheek ontbreekt in deze build. - /// - ru: В этой сборке отсутствует нативная библиотека VniDrop. - static let missingNativeLibrary: String.LocalizationValue = "error_missing_native_library" - /// VniDrop could not reach the sender. Check the connection on both devices and try again. - /// - /// Key: `error_network` - /// Context: Error: the sender could not be reached over the local network. - /// - /// - en: VniDrop could not reach the sender. Check the connection on both devices and try again. - /// - fr: VniDrop n’a pas pu joindre l’expéditeur. Vérifiez la connexion sur les deux appareils et réessayez. - /// - es: VniDrop no pudo contactar con el remitente. Compruebe la conexión en ambos dispositivos e inténtelo de nuevo. - /// - it: VniDrop non è riuscito a raggiungere il mittente. Controlli la connessione su entrambi i dispositivi e riprovi. - /// - de: VniDrop konnte den Absender nicht erreichen. Prüfen Sie die Verbindung auf beiden Geräten und versuchen Sie es erneut. - /// - pt: O VniDrop não conseguiu contactar o remetente. Verifique a ligação nos dois dispositivos e tente novamente. - /// - pl: VniDrop nie mógł połączyć się z nadawcą. Sprawdź połączenie na obu urządzeniach i spróbuj ponownie. - /// - nl: VniDrop kon de afzender niet bereiken. Controleer de verbinding op beide apparaten en probeer het opnieuw. - /// - ru: VniDrop не удалось связаться с отправителем. Проверьте подключение на обоих устройствах и повторите попытку. - static let network: String.LocalizationValue = "error_network" - /// This NFC tag could not be used. Try another tag. - /// - /// Key: `error_nfc` - /// Context: Error: the NFC tag could not be read/used. - /// - /// - en: This NFC tag could not be used. Try another tag. - /// - fr: Ce tag NFC n’a pas pu être utilisé. Essayez-en un autre. - /// - es: No se pudo usar esta etiqueta NFC. Pruebe con otra. - /// - it: Impossibile usare questo tag NFC. Ne provi un altro. - /// - de: Dieses NFC-Tag konnte nicht verwendet werden. Versuchen Sie ein anderes. - /// - pt: Não foi possível utilizar esta etiqueta NFC. Tente outra. - /// - pl: Nie udało się użyć tego tagu NFC. Spróbuj innego. - /// - nl: Deze NFC-tag kon niet worden gebruikt. Probeer een andere. - /// - ru: Не удалось использовать эту NFC-метку. Попробуйте другую. - static let nfc: String.LocalizationValue = "error_nfc" - /// The sender has not approved this transfer, or it was refused. - /// - /// Key: `error_permission` - /// Context: Error: the transfer was not approved or was refused by the sender. - /// - /// - en: The sender has not approved this transfer, or it was refused. - /// - fr: L’expéditeur n’a pas approuvé ce transfert, ou il a été refusé. - /// - es: El remitente no ha aprobado esta transferencia, o fue rechazada. - /// - it: Il mittente non ha approvato questo trasferimento, oppure è stato rifiutato. - /// - de: Der Absender hat diese Übertragung nicht genehmigt oder sie wurde abgelehnt. - /// - pt: O remetente não aprovou esta transferência, ou foi recusada. - /// - pl: Nadawca nie zatwierdził tego transferu lub został on odrzucony. - /// - nl: De afzender heeft deze overdracht niet goedgekeurd, of deze is geweigerd. - /// - ru: Отправитель не одобрил эту передачу, или она была отклонена. - static let permission: String.LocalizationValue = "error_permission" - /// VniDrop could not save transfer data on this device. - /// - /// Key: `error_repository` - /// Context: Error: transfer data could not be saved locally. - /// - /// - en: VniDrop could not save transfer data on this device. - /// - fr: VniDrop n’a pas pu enregistrer les données de transfert sur cet appareil. - /// - es: VniDrop no pudo guardar los datos de la transferencia en este dispositivo. - /// - it: VniDrop non ha potuto salvare i dati del trasferimento su questo dispositivo. - /// - de: VniDrop konnte die Übertragungsdaten auf diesem Gerät nicht speichern. - /// - pt: O VniDrop não conseguiu guardar os dados da transferência neste dispositivo. - /// - pl: VniDrop nie mógł zapisać danych transferu na tym urządzeniu. - /// - nl: VniDrop kon de overdrachtsgegevens niet op dit apparaat bewaren. - /// - ru: VniDrop не удалось сохранить данные передачи на этом устройстве. - static let repository: String.LocalizationValue = "error_repository" - /// Could not open the selected item. Try choosing it again. - /// - /// Key: `error_selection_failed` - /// Context: Error: the selected item could not be opened. - /// - /// - en: Could not open the selected item. Try choosing it again. - /// - fr: Impossible d’ouvrir l’élément sélectionné. Essayez de le choisir à nouveau. - /// - es: No se pudo abrir el elemento seleccionado. Intente elegirlo de nuevo. - /// - it: Impossibile aprire l’elemento selezionato. Provi a sceglierlo di nuovo. - /// - de: Das ausgewählte Objekt konnte nicht geöffnet werden. Versuchen Sie, es erneut auszuwählen. - /// - pt: Não foi possível abrir o item selecionado. Tente escolhê-lo novamente. - /// - pl: Nie udało się otworzyć wybranego elementu. Spróbuj wybrać go ponownie. - /// - nl: Het geselecteerde item kon niet worden geopend. Probeer het opnieuw te kiezen. - /// - ru: Не удалось открыть выбранный объект. Попробуйте выбрать его снова. - static let selectionFailed: String.LocalizationValue = "error_selection_failed" - /// Select at least one item to share. - /// - /// Key: `error_share_empty` - /// Context: Error: attempted to share with nothing selected. - /// - /// - en: Select at least one item to share. - /// - fr: Sélectionnez au moins un élément à partager. - /// - es: Seleccione al menos un elemento para compartir. - /// - it: Selezioni almeno un elemento da condividere. - /// - de: Wählen Sie mindestens ein Objekt zum Teilen aus. - /// - pt: Selecione pelo menos um item para partilhar. - /// - pl: Wybierz co najmniej jeden element do udostępnienia. - /// - nl: Selecteer minstens één item om te delen. - /// - ru: Выберите хотя бы один объект для отправки. - static let shareEmpty: String.LocalizationValue = "error_share_empty" - /// VniDrop could not open its network sockets on this device. - /// - /// Key: `error_socket_bind` - /// Context: Error: the app could not open its network sockets. - /// - /// - en: VniDrop could not open its network sockets on this device. - /// - fr: VniDrop n’a pas pu ouvrir ses sockets réseau sur cet appareil. - /// - es: VniDrop no pudo abrir sus sockets de red en este dispositivo. - /// - it: VniDrop non ha potuto aprire i suoi socket di rete su questo dispositivo. - /// - de: VniDrop konnte seine Netzwerk-Sockets auf diesem Gerät nicht öffnen. - /// - pt: O VniDrop não conseguiu abrir os seus sockets de rede neste dispositivo. - /// - pl: VniDrop nie mógł otworzyć gniazd sieciowych na tym urządzeniu. - /// - nl: VniDrop kon zijn netwerksockets niet openen op dit apparaat. - /// - ru: VniDrop не удалось открыть сетевые сокеты на этом устройстве. - static let socketBind: String.LocalizationValue = "error_socket_bind" - /// VniDrop is still starting. Open the invitation again in a moment. - /// - /// Key: `error_starting_up` - /// Context: Error: an invitation was opened before startup finished. - /// - /// - en: VniDrop is still starting. Open the invitation again in a moment. - /// - fr: VniDrop démarre encore. Rouvrez l’invitation dans un instant. - /// - es: VniDrop todavía se está iniciando. Vuelva a abrir la invitación en un momento. - /// - it: VniDrop è ancora in fase di avvio. Riapra l’invito tra un momento. - /// - de: VniDrop startet noch. Öffnen Sie die Einladung gleich erneut. - /// - pt: O VniDrop ainda está a iniciar. Abra o convite novamente dentro de momentos. - /// - pl: VniDrop jeszcze się uruchamia. Otwórz zaproszenie ponownie za chwilę. - /// - nl: VniDrop is nog aan het opstarten. Open de uitnodiging zo meteen opnieuw. - /// - ru: VniDrop ещё запускается. Откройте приглашение снова через мгновение. - static let startingUp: String.LocalizationValue = "error_starting_up" - /// There is not enough storage space to save this transfer. Free up space and try again. - /// - /// Key: `error_storage_full` - /// Context: Error: the destination does not have enough free storage. - /// - /// - en: There is not enough storage space to save this transfer. Free up space and try again. - /// - fr: L’espace de stockage est insuffisant pour enregistrer ce transfert. Libérez de l’espace et réessayez. - /// - es: No hay suficiente espacio de almacenamiento para guardar esta transferencia. Libere espacio e inténtelo de nuevo. - /// - it: Lo spazio di archiviazione non è sufficiente per salvare il trasferimento. Liberi spazio e riprovi. - /// - de: Zum Speichern dieser Übertragung ist nicht genügend Speicherplatz vorhanden. Geben Sie Speicherplatz frei und versuchen Sie es erneut. - /// - pt: Não existe espaço de armazenamento suficiente para guardar esta transferência. Liberte espaço e tente novamente. - /// - pl: Brakuje miejsca na zapisanie tego transferu. Zwolnij miejsce i spróbuj ponownie. - /// - nl: Er is onvoldoende opslagruimte om deze overdracht op te slaan. Maak ruimte vrij en probeer het opnieuw. - /// - ru: Недостаточно места для сохранения этой передачи. Освободите место и повторите попытку. - static let storageFull: String.LocalizationValue = "error_storage_full" - /// The transfer data could not be processed. Ask the sender to share it again. - /// - /// Key: `error_transfer` - /// Context: Error: transfer data could not be processed; network failures use error_network. - /// - /// - en: The transfer data could not be processed. Ask the sender to share it again. - /// - fr: Les données du transfert n’ont pas pu être traitées. Demandez à l’expéditeur de les partager à nouveau. - /// - es: No se pudieron procesar los datos de la transferencia. Pida al remitente que vuelva a compartirlos. - /// - it: Non è stato possibile elaborare i dati del trasferimento. Chieda al mittente di condividerli di nuovo. - /// - de: Die Übertragungsdaten konnten nicht verarbeitet werden. Bitten Sie den Absender, sie erneut zu teilen. - /// - pt: Não foi possível processar os dados da transferência. Peça ao remetente para os partilhar novamente. - /// - pl: Nie udało się przetworzyć danych transferu. Poproś nadawcę o ponowne udostępnienie. - /// - nl: De overdrachtsgegevens konden niet worden verwerkt. Vraag de afzender ze opnieuw te delen. - /// - ru: Не удалось обработать данные передачи. Попросите отправителя поделиться ими снова. - static let transfer: String.LocalizationValue = "error_transfer" - } - enum Field { - /// Receiver name - /// - /// Key: `field_receiver_name` - /// Context: Text field label: the receiver's display name. - /// - /// - en: Receiver name - /// - fr: Nom du destinataire - /// - es: Nombre del destinatario - /// - it: Nome del destinatario - /// - de: Empfängername - /// - pt: Nome do destinatário - /// - pl: Nazwa odbiorcy - /// - nl: Naam van ontvanger - /// - ru: Имя получателя - static let receiverName: String.LocalizationValue = "field_receiver_name" - /// Sender name - /// - /// Key: `field_sender_name` - /// Context: Text field label: the sender's display name. - /// - /// - en: Sender name - /// - fr: Nom de l’expéditeur - /// - es: Nombre del remitente - /// - it: Nome del mittente - /// - de: Absendername - /// - pt: Nome do remetente - /// - pl: Nazwa nadawcy - /// - nl: Naam van afzender - /// - ru: Имя отправителя - static let senderName: String.LocalizationValue = "field_sender_name" - /// Transfer name - /// - /// Key: `field_transfer_name` - /// Context: Text field label: the name given to a transfer. - /// - /// - en: Transfer name - /// - fr: Nom du transfert - /// - es: Nombre de la transferencia - /// - it: Nome del trasferimento - /// - de: Übertragungsname - /// - pt: Nome da transferência - /// - pl: Nazwa transferu - /// - nl: Naam van overdracht - /// - ru: Название передачи - static let transferName: String.LocalizationValue = "field_transfer_name" - /// Display name - /// - /// Key: `field_username` - /// Context: Settings text field label: this device's display name. - /// - /// - en: Display name - /// - fr: Nom d’affichage - /// - es: Nombre visible - /// - it: Nome visualizzato - /// - de: Anzeigename - /// - pt: Nome a apresentar - /// - pl: Nazwa wyświetlana - /// - nl: Weergavenaam - /// - ru: Отображаемое имя - static let username: String.LocalizationValue = "field_username" - } - enum Folder { - /// Permission needed - /// - /// Key: `folder_status_permission_required` - /// Context: Receive-folder status: permission is required to write to the chosen folder. - /// - /// - en: Permission needed - /// - fr: Autorisation requise - /// - es: Permiso necesario - /// - it: Autorizzazione necessaria - /// - de: Berechtigung erforderlich - /// - pt: Permissão necessária - /// - pl: Wymagane uprawnienie - /// - nl: Machtiging vereist - /// - ru: Требуется разрешение - static let statusPermissionRequired: String.LocalizationValue = "folder_status_permission_required" - /// Unavailable - /// - /// Key: `folder_status_unavailable` - /// Context: Receive-folder status: the chosen folder is unavailable. - /// - /// - en: Unavailable - /// - fr: Indisponible - /// - es: No disponible - /// - it: Non disponibile - /// - de: Nicht verfügbar - /// - pt: Indisponível - /// - pl: Niedostępny - /// - nl: Niet beschikbaar - /// - ru: Недоступно - static let statusUnavailable: String.LocalizationValue = "folder_status_unavailable" - /// Checking folder… - /// - /// Key: `folder_status_validating` - /// Context: Receive-folder status: the folder is being checked. - /// - /// - en: Checking folder… - /// - fr: Vérification du dossier… - /// - es: Comprobando carpeta… - /// - it: Controllo della cartella… - /// - de: Ordner wird geprüft… - /// - pt: A verificar a pasta… - /// - pl: Sprawdzanie folderu… - /// - nl: Map controleren… - /// - ru: Проверка папки… - static let statusValidating: String.LocalizationValue = "folder_status_validating" - /// Ready - /// - /// Key: `folder_status_writable` - /// Context: Receive-folder status: the folder is valid and writable. - /// - /// - en: Ready - /// - fr: Prêt - /// - es: Listo - /// - it: Pronta - /// - de: Bereit - /// - pt: Pronto - /// - pl: Gotowy - /// - nl: Gereed - /// - ru: Готово - static let statusWritable: String.LocalizationValue = "folder_status_writable" - } - enum Format { - /// {first} · {second} - /// - /// Key: `format_separated_pair` - /// Context: Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values. - /// - /// - en: {first} · {second} - /// - fr: {first} · {second} - /// - es: {first} · {second} - /// - it: {first} · {second} - /// - de: {first} · {second} - /// - pt: {first} · {second} - /// - pl: {first} · {second} - /// - nl: {first} · {second} - /// - ru: {first} · {second} - static func separatedPair(first: String, second: String) -> String { - String(format: String(localized: "format_separated_pair"), first, second) - } - /// {first} {second} · {third} - /// - /// Key: `format_separated_triple` - /// Context: Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values. - /// - /// - en: {first} {second} · {third} - /// - fr: {first} {second} · {third} - /// - es: {first} {second} · {third} - /// - it: {first} {second} · {third} - /// - de: {first} {second} · {third} - /// - pt: {first} {second} · {third} - /// - pl: {first} {second} · {third} - /// - nl: {first} {second} · {third} - /// - ru: {first} {second} · {third} - static func separatedTriple(first: String, second: String, third: String) -> String { - String(format: String(localized: "format_separated_triple"), first, second, third) - } - } - enum Metadata { - /// Files - /// - /// Key: `metadata_files` - /// Context: Transfer metadata label: number of files. - /// - /// - en: Files - /// - fr: Fichiers - /// - es: Archivos - /// - it: File - /// - de: Dateien - /// - pt: Ficheiros - /// - pl: Pliki - /// - nl: Bestanden - /// - ru: Файлы - static let files: String.LocalizationValue = "metadata_files" - /// Size - /// - /// Key: `metadata_size` - /// Context: Transfer metadata label: total size. - /// - /// - en: Size - /// - fr: Taille - /// - es: Tamaño - /// - it: Dimensione - /// - de: Größe - /// - pt: Tamanho - /// - pl: Rozmiar - /// - nl: Grootte - /// - ru: Размер - static let size: String.LocalizationValue = "metadata_size" - /// Status - /// - /// Key: `metadata_status` - /// Context: Transfer metadata label: current status. - /// - /// - en: Status - /// - fr: Statut - /// - es: Estado - /// - it: Stato - /// - de: Status - /// - pt: Estado - /// - pl: Status - /// - nl: Status - /// - ru: Статус - static let status: String.LocalizationValue = "metadata_status" - } - enum Nav { - /// Receive - /// - /// Key: `nav_receive` - /// Context: Bottom navigation: Receive tab label. - /// - /// - en: Receive - /// - fr: Recevoir - /// - es: Recibir - /// - it: Ricevi - /// - de: Empfangen - /// - pt: Receber - /// - pl: Odbierz - /// - nl: Ontvangen - /// - ru: Получить - static let receive: String.LocalizationValue = "nav_receive" - /// Send - /// - /// Key: `nav_send` - /// Context: Bottom navigation: Send tab label. - /// - /// - en: Send - /// - fr: Envoyer - /// - es: Enviar - /// - it: Invia - /// - de: Senden - /// - pt: Enviar - /// - pl: Wyślij - /// - nl: Versturen - /// - ru: Отправить - static let send: String.LocalizationValue = "nav_send" - /// Settings - /// - /// Key: `nav_settings` - /// Context: Bottom navigation: Settings tab label. - /// - /// - en: Settings - /// - fr: Réglages - /// - es: Ajustes - /// - it: Impostazioni - /// - de: Einstellungen - /// - pt: Definições - /// - pl: Ustawienia - /// - nl: Instellingen - /// - ru: Настройки - static let settings: String.LocalizationValue = "nav_settings" - } - enum Network { - /// Network - /// - /// Key: `network_title` - /// Context: Device information row: network label. - /// - /// - en: Network - /// - fr: Réseau - /// - es: Red - /// - it: Rete - /// - de: Netzwerk - /// - pt: Rede - /// - pl: Sieć - /// - nl: Netwerk - /// - ru: Сеть - static let title: String.LocalizationValue = "network_title" - } - enum Notifications { - /// Get notified about new receive requests while VniDrop is in the background. - /// - /// Key: `notifications_description` - /// Context: Settings > Notifications: explanation of what notifications are used for. - /// - /// - en: Get notified about new receive requests while VniDrop is in the background. - /// - fr: Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan. - /// - es: Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano. - /// - it: Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background. - /// - de: Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft. - /// - pt: Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano. - /// - pl: Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle. - /// - nl: Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait. - /// - ru: Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне. - static let description: String.LocalizationValue = "notifications_description" - /// Notifications enabled. - /// - /// Key: `notifications_enabled_message` - /// Context: Settings > Notifications: confirmation when notifications are enabled. - /// - /// - en: Notifications enabled. - /// - fr: Notifications activées. - /// - es: Notificaciones activadas. - /// - it: Notifiche attivate. - /// - de: Mitteilungen aktiviert. - /// - pt: Notificações ativadas. - /// - pl: Powiadomienia włączone. - /// - nl: Meldingen ingeschakeld. - /// - ru: Уведомления включены. - static let enabledMessage: String.LocalizationValue = "notifications_enabled_message" - /// Allow notifications - /// - /// Key: `notifications_local_title` - /// Context: Settings > Notifications: label for the allow-notifications action. - /// - /// - en: Allow notifications - /// - fr: Autoriser les notifications - /// - es: Permitir notificaciones - /// - it: Consenti le notifiche - /// - de: Mitteilungen erlauben - /// - pt: Permitir notificações - /// - pl: Zezwól na powiadomienia - /// - nl: Meldingen toestaan - /// - ru: Разрешить уведомления - static let localTitle: String.LocalizationValue = "notifications_local_title" - /// Notifications are turned off for VniDrop. You can enable them in Settings. - /// - /// Key: `notifications_permission_denied` - /// Context: Settings > Notifications: message when the OS permission is denied. - /// - /// - en: Notifications are turned off for VniDrop. You can enable them in Settings. - /// - fr: Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages. - /// - es: Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes. - /// - it: Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni. - /// - de: Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren. - /// - pt: As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições. - /// - pl: Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach. - /// - nl: Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen. - /// - ru: Уведомления отключены для VniDrop. Вы можете включить их в Настройках. - static let permissionDenied: String.LocalizationValue = "notifications_permission_denied" - /// Could not open notification settings. - /// - /// Key: `notifications_settings_open_failed` - /// Context: Settings > Notifications: error when the OS notification settings can't be opened. - /// - /// - en: Could not open notification settings. - /// - fr: Impossible d’ouvrir les réglages de notifications. - /// - es: No se pudieron abrir los ajustes de notificaciones. - /// - it: Impossibile aprire le impostazioni delle notifiche. - /// - de: Die Mitteilungseinstellungen konnten nicht geöffnet werden. - /// - pt: Não foi possível abrir as definições de notificações. - /// - pl: Nie udało się otworzyć ustawień powiadomień. - /// - nl: De meldingsinstellingen konden niet worden geopend. - /// - ru: Не удалось открыть настройки уведомлений. - static let settingsOpenFailed: String.LocalizationValue = "notifications_settings_open_failed" - /// Notifications - /// - /// Key: `notifications_title` - /// Context: Settings > Notifications: section title. - /// - /// - en: Notifications - /// - fr: Notifications - /// - es: Notificaciones - /// - it: Notifiche - /// - de: Mitteilungen - /// - pt: Notificações - /// - pl: Powiadomienia - /// - nl: Meldingen - /// - ru: Уведомления - static let title: String.LocalizationValue = "notifications_title" - /// Notifications are not available on this device. - /// - /// Key: `notifications_unsupported` - /// Context: Settings > Notifications: message when notifications aren't supported on the device. - /// - /// - en: Notifications are not available on this device. - /// - fr: Les notifications ne sont pas disponibles sur cet appareil. - /// - es: Las notificaciones no están disponibles en este dispositivo. - /// - it: Le notifiche non sono disponibili su questo dispositivo. - /// - de: Mitteilungen sind auf diesem Gerät nicht verfügbar. - /// - pt: As notificações não estão disponíveis neste dispositivo. - /// - pl: Powiadomienia nie są dostępne na tym urządzeniu. - /// - nl: Meldingen zijn niet beschikbaar op dit apparaat. - /// - ru: Уведомления недоступны на этом устройстве. - static let unsupported: String.LocalizationValue = "notifications_unsupported" - } - enum Os { - /// Operating system - /// - /// Key: `os_version_title` - /// Context: Device information row: operating system version label. - /// - /// - en: Operating system - /// - fr: Système d’exploitation - /// - es: Sistema operativo - /// - it: Sistema operativo - /// - de: Betriebssystem - /// - pt: Sistema operativo - /// - pl: System operacyjny - /// - nl: Besturingssysteem - /// - ru: Операционная система - static let versionTitle: String.LocalizationValue = "os_version_title" - } - enum Preferences { - /// Save received transfers to - /// - /// Key: `preferences_receive_folder_title` - /// Context: Settings > Preferences: label for the received-files destination folder. - /// - /// - en: Save received transfers to - /// - fr: Enregistrer les transferts reçus dans - /// - es: Guardar las transferencias recibidas en - /// - it: Salva i trasferimenti ricevuti in - /// - de: Empfangene Übertragungen sichern in - /// - pt: Guardar as transferências recebidas em - /// - pl: Zapisuj odebrane transfery w - /// - nl: Ontvangen overdrachten bewaren in - /// - ru: Сохранять полученные передачи в - static let receiveFolderTitle: String.LocalizationValue = "preferences_receive_folder_title" - /// Preferences - /// - /// Key: `preferences_title` - /// Context: Settings > Preferences: section title. - /// - /// - en: Preferences - /// - fr: Préférences - /// - es: Preferencias - /// - it: Preferenze - /// - de: Voreinstellungen - /// - pt: Preferências - /// - pl: Preferencje - /// - nl: Voorkeuren - /// - ru: Параметры - static let title: String.LocalizationValue = "preferences_title" - } - enum Progress { - /// Cancelled - /// - /// Key: `progress_cancelled` - /// Context: Transfer progress label: cancelled. - /// - /// - en: Cancelled - /// - fr: Annulé - /// - es: Cancelado - /// - it: Annullato - /// - de: Abgebrochen - /// - pt: Cancelado - /// - pl: Anulowano - /// - nl: Geannuleerd - /// - ru: Отменено - static let cancelled: String.LocalizationValue = "progress_cancelled" - /// Completed - /// - /// Key: `progress_completed` - /// Context: Transfer progress label: completed. - /// - /// - en: Completed - /// - fr: Terminé - /// - es: Completado - /// - it: Completato - /// - de: Abgeschlossen - /// - pt: Concluído - /// - pl: Ukończono - /// - nl: Voltooid - /// - ru: Завершено - static let completed: String.LocalizationValue = "progress_completed" - /// Connected - /// - /// Key: `progress_connected` - /// Context: Transfer progress label: connected. - /// - /// - en: Connected - /// - fr: Connecté - /// - es: Conectado - /// - it: Connesso - /// - de: Verbunden - /// - pt: Ligado - /// - pl: Połączono - /// - nl: Verbonden - /// - ru: Подключено - static let connected: String.LocalizationValue = "progress_connected" - /// Connecting - /// - /// Key: `progress_connecting` - /// Context: Transfer progress label: connecting. - /// - /// - en: Connecting - /// - fr: Connexion - /// - es: Conectando - /// - it: Connessione - /// - de: Verbinden - /// - pt: A ligar - /// - pl: Łączenie - /// - nl: Verbinden - /// - ru: Подключение - static let connecting: String.LocalizationValue = "progress_connecting" - /// Downloading - /// - /// Key: `progress_downloading` - /// Context: Transfer progress label: downloading. - /// - /// - en: Downloading - /// - fr: Téléchargement - /// - es: Descargando - /// - it: Download - /// - de: Wird geladen - /// - pt: A descarregar - /// - pl: Pobieranie - /// - nl: Downloaden - /// - ru: Загрузка - static let downloading: String.LocalizationValue = "progress_downloading" - /// Failed - /// - /// Key: `progress_failed` - /// Context: Transfer progress label: failed. - /// - /// - en: Failed - /// - fr: Échec - /// - es: Fallido - /// - it: Non riuscito - /// - de: Fehlgeschlagen - /// - pt: Falhou - /// - pl: Niepowodzenie - /// - nl: Mislukt - /// - ru: Ошибка - static let failed: String.LocalizationValue = "progress_failed" - /// Getting ready - /// - /// Key: `progress_getting_ready` - /// Context: Transfer progress label: getting ready. - /// - /// - en: Getting ready - /// - fr: Préparation - /// - es: Preparándose - /// - it: Preparazione - /// - de: Wird bereitgemacht - /// - pt: A preparar-se - /// - pl: Przygotowywanie - /// - nl: Klaarmaken - /// - ru: Подготовка - static let gettingReady: String.LocalizationValue = "progress_getting_ready" - /// Transfer interrupted - /// - /// Key: `progress_interrupted` - /// Context: Transfer progress label: the transfer was interrupted. - /// - /// - en: Transfer interrupted - /// - fr: Transfert interrompu - /// - es: Transferencia interrumpida - /// - it: Trasferimento interrotto - /// - de: Übertragung unterbrochen - /// - pt: Transferência interrompida - /// - pl: Transfer przerwany - /// - nl: Overdracht onderbroken - /// - ru: Передача прервана - static let interrupted: String.LocalizationValue = "progress_interrupted" - /// Preparing - /// - /// Key: `progress_preparing` - /// Context: Transfer progress label: preparing. - /// - /// - en: Preparing - /// - fr: Préparation - /// - es: Preparando - /// - it: Preparazione - /// - de: Wird vorbereitet - /// - pt: A preparar - /// - pl: Przygotowywanie - /// - nl: Voorbereiden - /// - ru: Подготовка - static let preparing: String.LocalizationValue = "progress_preparing" - /// Ready - /// - /// Key: `progress_ready` - /// Context: Transfer progress label: ready. - /// - /// - en: Ready - /// - fr: Prêt - /// - es: Listo - /// - it: Pronto - /// - de: Bereit - /// - pt: Pronto - /// - pl: Gotowe - /// - nl: Gereed - /// - ru: Готово - static let ready: String.LocalizationValue = "progress_ready" - /// Receiving - /// - /// Key: `progress_receiving` - /// Context: Transfer progress label: receiving. - /// - /// - en: Receiving - /// - fr: Réception - /// - es: Recibiendo - /// - it: Ricezione - /// - de: Wird empfangen - /// - pt: A receber - /// - pl: Odbieranie - /// - nl: Ontvangen - /// - ru: Получение - static let receiving: String.LocalizationValue = "progress_receiving" - /// Requesting access - /// - /// Key: `progress_requesting_access` - /// Context: Transfer progress label: requesting access from the sender. - /// - /// - en: Requesting access - /// - fr: Demande d’accès - /// - es: Solicitando acceso - /// - it: Richiesta di accesso - /// - de: Zugriff wird angefragt - /// - pt: A pedir acesso - /// - pl: Prośba o dostęp - /// - nl: Toegang aanvragen - /// - ru: Запрос доступа - static let requestingAccess: String.LocalizationValue = "progress_requesting_access" - /// Saving - /// - /// Key: `progress_saving` - /// Context: Transfer progress label: saving received files. - /// - /// - en: Saving - /// - fr: Enregistrement - /// - es: Guardando - /// - it: Salvataggio - /// - de: Wird gesichert - /// - pt: A guardar - /// - pl: Zapisywanie - /// - nl: Bewaren - /// - ru: Сохранение - static let saving: String.LocalizationValue = "progress_saving" - /// Sending - /// - /// Key: `progress_sending` - /// Context: Transfer progress label: sending. - /// - /// - en: Sending - /// - fr: Envoi - /// - es: Enviando - /// - it: Invio - /// - de: Wird gesendet - /// - pt: A enviar - /// - pl: Wysyłanie - /// - nl: Versturen - /// - ru: Отправка - static let sending: String.LocalizationValue = "progress_sending" - /// Sending to {count} - /// - /// Key: `progress_sending_to_count` - /// Context: Transfer progress label: sending to N receivers. %lld = receiver count. - /// - /// - en: Sending to {count} - /// - fr: Envoi à {count} - /// - es: Enviando a {count} - /// - it: Invio a {count} - /// - de: Senden an {count} - /// - pt: A enviar para {count} - /// - pl: Wysyłanie do {count} - /// - nl: Versturen naar {count} - /// - ru: Отправка получателям: {count} - static func sendingToCount(count: Int) -> String { - String(format: String(localized: "progress_sending_to_count"), count) - } - /// Ready to share - /// - /// Key: `progress_share_ready` - /// Context: Transfer progress label: the share is ready. - /// - /// - en: Ready to share - /// - fr: Prêt à partager - /// - es: Listo para compartir - /// - it: Pronto per la condivisione - /// - de: Bereit zum Teilen - /// - pt: Pronto para partilhar - /// - pl: Gotowe do udostępnienia - /// - nl: Klaar om te delen - /// - ru: Готово к отправке - static let shareReady: String.LocalizationValue = "progress_share_ready" - /// Working… - /// - /// Key: `progress_working` - /// Context: Transfer progress label: generic working/in-progress state. - /// - /// - en: Working… - /// - fr: En cours… - /// - es: Trabajando… - /// - it: Elaborazione… - /// - de: Wird bearbeitet… - /// - pt: A processar… - /// - pl: Przetwarzanie… - /// - nl: Bezig… - /// - ru: Обработка… - static let working: String.LocalizationValue = "progress_working" - } - enum Receive { - /// Choose the invitation method available to you. - /// - /// Key: `receive_choose_method_body` - /// Context: Receive flow: body text prompting the user to pick an invitation method. - /// - /// - en: Choose the invitation method available to you. - /// - fr: Choisissez la méthode d’invitation à votre disposition. - /// - es: Elija el método de invitación de que disponga. - /// - it: Scelga il metodo di invito a sua disposizione. - /// - de: Wählen Sie die Ihnen zur Verfügung stehende Einladungsmethode. - /// - pt: Escolha o método de convite ao seu dispor. - /// - pl: Wybierz dostępną metodę zaproszenia. - /// - nl: Kies de uitnodigingsmethode die u ter beschikking staat. - /// - ru: Выберите доступный вам способ приглашения. - static let chooseMethodBody: String.LocalizationValue = "receive_choose_method_body" - /// How would you like to connect? - /// - /// Key: `receive_choose_method_title` - /// Context: Receive flow: title of the connect-method chooser. - /// - /// - en: How would you like to connect? - /// - fr: Comment souhaitez-vous vous connecter ? - /// - es: ¿Cómo quiere conectarse? - /// - it: Come vuole connettersi? - /// - de: Wie möchten Sie sich verbinden? - /// - pt: Como pretende ligar-se? - /// - pl: Jak chcesz się połączyć? - /// - nl: Hoe wilt u verbinding maken? - /// - ru: Как вы хотите подключиться? - static let chooseMethodTitle: String.LocalizationValue = "receive_choose_method_title" - /// Clear history - /// - /// Key: `receive_clear_history` - /// Context: Receive history: action to clear all history. - /// - /// - en: Clear history - /// - fr: Effacer l’historique - /// - es: Borrar historial - /// - it: Cancella cronologia - /// - de: Verlauf löschen - /// - pt: Limpar histórico - /// - pl: Wyczyść historię - /// - nl: Geschiedenis wissen - /// - ru: Очистить историю - static let clearHistory: String.LocalizationValue = "receive_clear_history" - /// All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device. - /// - /// Key: `receive_clear_history_description` - /// Context: Receive history: confirmation dialog body for clearing all history. - /// - /// - en: All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device. - /// - fr: Toutes les réceptions terminées, échouées et annulées seront retirées de l’historique de VniDrop. Les fichiers téléchargés resteront sur cet appareil. - /// - es: Todas las recepciones completadas, fallidas y canceladas se eliminarán del historial de VniDrop. Los archivos descargados permanecerán en este dispositivo. - /// - it: Tutte le ricezioni completate, non riuscite e annullate verranno rimosse dalla cronologia di VniDrop. I file scaricati rimarranno su questo dispositivo. - /// - de: Alle abgeschlossenen, fehlgeschlagenen und abgebrochenen Empfänge werden aus dem Verlauf von VniDrop entfernt. Heruntergeladene Dateien verbleiben auf diesem Gerät. - /// - pt: Todas as receções concluídas, falhadas e canceladas serão removidas do histórico do VniDrop. Os ficheiros descarregados permanecerão neste dispositivo. - /// - pl: Wszystkie ukończone, nieudane i anulowane odbiory zostaną usunięte z historii VniDrop. Pobrane pliki pozostaną na tym urządzeniu. - /// - nl: Alle voltooide, mislukte en geannuleerde ontvangsten worden uit de geschiedenis van VniDrop verwijderd. Gedownloade bestanden blijven op dit apparaat. - /// - ru: Все завершённые, неудачные и отменённые получения будут удалены из истории VniDrop. Загруженные файлы останутся на этом устройстве. - static let clearHistoryDescription: String.LocalizationValue = "receive_clear_history_description" - /// Clear receive history? - /// - /// Key: `receive_clear_history_title` - /// Context: Receive history: confirmation dialog title for clearing all history. - /// - /// - en: Clear receive history? - /// - fr: Effacer l’historique de réception ? - /// - es: ¿Borrar el historial de recepción? - /// - it: Cancellare la cronologia di ricezione? - /// - de: Empfangsverlauf löschen? - /// - pt: Limpar o histórico de receção? - /// - pl: Wyczyścić historię odbioru? - /// - nl: Ontvangstgeschiedenis wissen? - /// - ru: Очистить историю получения? - static let clearHistoryTitle: String.LocalizationValue = "receive_clear_history_title" - /// Transfer received. - /// - /// Key: `receive_completed` - /// Context: Receive flow: toast/message when a transfer finishes downloading. - /// - /// - en: Transfer received. - /// - fr: Transfert reçu. - /// - es: Transferencia recibida. - /// - it: Trasferimento ricevuto. - /// - de: Übertragung empfangen. - /// - pt: Transferência recebida. - /// - pl: Transfer odebrany. - /// - nl: Overdracht ontvangen. - /// - ru: Передача получена. - static let completed: String.LocalizationValue = "receive_completed" - /// “{transferName}” will be removed from VniDrop’s history. The downloaded file will remain on this device. - /// - /// Key: `receive_delete_history_description` - /// Context: Receive history: confirmation body for removing one item. {transferName} = transfer name. - /// - /// - en: “{transferName}” will be removed from VniDrop’s history. The downloaded file will remain on this device. - /// - fr: « {transferName} » sera retiré de l’historique de VniDrop. Le fichier téléchargé restera sur cet appareil. - /// - es: «{transferName}» se eliminará del historial de VniDrop. El archivo descargado permanecerá en este dispositivo. - /// - it: «{transferName}» verrà rimosso dalla cronologia di VniDrop. Il file scaricato rimarrà su questo dispositivo. - /// - de: „{transferName}“ wird aus dem Verlauf von VniDrop entfernt. Die heruntergeladene Datei verbleibt auf diesem Gerät. - /// - pt: «{transferName}» será removido do histórico do VniDrop. O ficheiro descarregado permanecerá neste dispositivo. - /// - pl: „{transferName}” zostanie usunięty z historii VniDrop. Pobrany plik pozostanie na tym urządzeniu. - /// - nl: ‘{transferName}’ wordt uit de geschiedenis van VniDrop verwijderd. Het gedownloade bestand blijft op dit apparaat. - /// - ru: «{transferName}» будет удалён из истории VniDrop. Загруженный файл останется на этом устройстве. - static func deleteHistoryDescription(transferName: String) -> String { - String(format: String(localized: "receive_delete_history_description"), transferName) - } - /// Delete from receive history - /// - /// Key: `receive_delete_history_item` - /// Context: Receive history: swipe/menu action to delete a single history item. - /// - /// - en: Delete from receive history - /// - fr: Supprimer de l’historique de réception - /// - es: Eliminar del historial de recepción - /// - it: Elimina dalla cronologia di ricezione - /// - de: Aus Empfangsverlauf löschen - /// - pt: Eliminar do histórico de receção - /// - pl: Usuń z historii odbioru - /// - nl: Uit ontvangstgeschiedenis verwijderen - /// - ru: Удалить из истории получения - static let deleteHistoryItem: String.LocalizationValue = "receive_delete_history_item" - /// Remove from history? - /// - /// Key: `receive_delete_history_title` - /// Context: Receive history: confirmation title for removing one item. - /// - /// - en: Remove from history? - /// - fr: Retirer de l’historique ? - /// - es: ¿Quitar del historial? - /// - it: Rimuovere dalla cronologia? - /// - de: Aus dem Verlauf entfernen? - /// - pt: Remover do histórico? - /// - pl: Usunąć z historii? - /// - nl: Uit geschiedenis verwijderen? - /// - ru: Удалить из истории? - static let deleteHistoryTitle: String.LocalizationValue = "receive_delete_history_title" - /// Open a VniDrop invitation, scan a QR code, or hold near an NFC tag. - /// - /// Key: `receive_empty_body` - /// Context: Receive tab empty state: instructions on how to receive. - /// - /// - en: Open a VniDrop invitation, scan a QR code, or hold near an NFC tag. - /// - fr: Ouvrez une invitation VniDrop, scannez un QR code ou approchez un tag NFC. - /// - es: Abra una invitación de VniDrop, escanee un código QR o acerque una etiqueta NFC. - /// - it: Apra un invito VniDrop, scansioni un codice QR o avvicini un tag NFC. - /// - de: Öffnen Sie eine VniDrop-Einladung, scannen Sie einen QR-Code oder halten Sie das Gerät an ein NFC-Tag. - /// - pt: Abra um convite do VniDrop, leia um código QR ou aproxime uma etiqueta NFC. - /// - pl: Otwórz zaproszenie VniDrop, zeskanuj kod QR lub zbliż tag NFC. - /// - nl: Open een VniDrop-uitnodiging, scan een QR-code of houd het apparaat bij een NFC-tag. - /// - ru: Откройте приглашение VniDrop, отсканируйте QR-код или поднесите NFC-метку. - static let emptyBody: String.LocalizationValue = "receive_empty_body" - /// Nothing received yet - /// - /// Key: `receive_empty_title` - /// Context: Receive tab empty state: title when nothing has been received. - /// - /// - en: Nothing received yet - /// - fr: Rien reçu pour l’instant - /// - es: Aún no se ha recibido nada - /// - it: Ancora nulla di ricevuto - /// - de: Noch nichts empfangen - /// - pt: Ainda não recebeu nada - /// - pl: Nic jeszcze nie odebrano - /// - nl: Nog niets ontvangen - /// - ru: Пока ничего не получено - static let emptyTitle: String.LocalizationValue = "receive_empty_title" - /// Receive history cleared. - /// - /// Key: `receive_history_cleared` - /// Context: Receive history: toast confirming history was cleared. - /// - /// - en: Receive history cleared. - /// - fr: Historique de réception effacé. - /// - es: Historial de recepción borrado. - /// - it: Cronologia di ricezione cancellata. - /// - de: Empfangsverlauf gelöscht. - /// - pt: Histórico de receção limpo. - /// - pl: Historia odbioru wyczyszczona. - /// - nl: Ontvangstgeschiedenis gewist. - /// - ru: История получения очищена. - static let historyCleared: String.LocalizationValue = "receive_history_cleared" - /// History - /// - /// Key: `receive_history_title` - /// Context: Receive tab: History section title. - /// - /// - en: History - /// - fr: Historique - /// - es: Historial - /// - it: Cronologia - /// - de: Verlauf - /// - pt: Histórico - /// - pl: Historia - /// - nl: Geschiedenis - /// - ru: История - static let historyTitle: String.LocalizationValue = "receive_history_title" - /// Open a .vnd invitation - /// - /// Key: `receive_method_file` - /// Context: Receive method: open a .vnd invitation file. - /// - /// - en: Open a .vnd invitation - /// - fr: Ouvrir une invitation .vnd - /// - es: Abrir una invitación .vnd - /// - it: Apri un invito .vnd - /// - de: Eine .vnd-Einladung öffnen - /// - pt: Abrir um convite .vnd - /// - pl: Otwórz zaproszenie .vnd - /// - nl: Een .vnd-uitnodiging openen - /// - ru: Открыть приглашение .vnd - static let methodFile: String.LocalizationValue = "receive_method_file" - /// Choose an invitation saved or shared to this device. - /// - /// Key: `receive_method_file_description` - /// Context: Receive method description: open a saved/shared invitation file. - /// - /// - en: Choose an invitation saved or shared to this device. - /// - fr: Choisissez une invitation enregistrée ou partagée sur cet appareil. - /// - es: Elija una invitación guardada o compartida en este dispositivo. - /// - it: Scelga un invito salvato o condiviso su questo dispositivo. - /// - de: Wählen Sie eine auf diesem Gerät gespeicherte oder geteilte Einladung. - /// - pt: Escolha um convite guardado ou partilhado neste dispositivo. - /// - pl: Wybierz zaproszenie zapisane lub udostępnione na tym urządzeniu. - /// - nl: Kies een uitnodiging die op dit apparaat is bewaard of gedeeld. - /// - ru: Выберите приглашение, сохранённое или отправленное на это устройство. - static let methodFileDescription: String.LocalizationValue = "receive_method_file_description" - /// Read NFC tag - /// - /// Key: `receive_method_nfc` - /// Context: Receive method: read an NFC tag. - /// - /// - en: Read NFC tag - /// - fr: Lire un tag NFC - /// - es: Leer etiqueta NFC - /// - it: Leggi tag NFC - /// - de: NFC-Tag lesen - /// - pt: Ler etiqueta NFC - /// - pl: Odczytaj tag NFC - /// - nl: NFC-tag lezen - /// - ru: Прочитать NFC-метку - static let methodNfc: String.LocalizationValue = "receive_method_nfc" - /// Hold this device near the sender’s invitation tag. - /// - /// Key: `receive_method_nfc_description` - /// Context: Receive method description: hold near the sender's NFC tag. - /// - /// - en: Hold this device near the sender’s invitation tag. - /// - fr: Approchez cet appareil du tag d’invitation de l’expéditeur. - /// - es: Acerque este dispositivo a la etiqueta de invitación del remitente. - /// - it: Avvicini questo dispositivo al tag di invito del mittente. - /// - de: Halten Sie dieses Gerät an das Einladungs-Tag des Absenders. - /// - pt: Aproxime este dispositivo da etiqueta de convite do remetente. - /// - pl: Zbliż to urządzenie do tagu zaproszenia nadawcy. - /// - nl: Houd dit apparaat bij de uitnodigingstag van de afzender. - /// - ru: Поднесите это устройство к метке приглашения отправителя. - static let methodNfcDescription: String.LocalizationValue = "receive_method_nfc_description" - /// Scan QR code - /// - /// Key: `receive_method_scan` - /// Context: Receive method: scan a QR code. - /// - /// - en: Scan QR code - /// - fr: Scanner un QR code - /// - es: Escanear código QR - /// - it: Scansiona codice QR - /// - de: QR-Code scannen - /// - pt: Ler código QR - /// - pl: Zeskanuj kod QR - /// - nl: QR-code scannen - /// - ru: Сканировать QR-код - static let methodScan: String.LocalizationValue = "receive_method_scan" - /// Use the camera to scan the sender’s VniDrop code. - /// - /// Key: `receive_method_scan_description` - /// Context: Receive method description: use the camera to scan the sender's code. - /// - /// - en: Use the camera to scan the sender’s VniDrop code. - /// - fr: Utilisez la caméra pour scanner le code VniDrop de l’expéditeur. - /// - es: Use la cámara para escanear el código de VniDrop del remitente. - /// - it: Usi la fotocamera per scansionare il codice VniDrop del mittente. - /// - de: Verwenden Sie die Kamera, um den VniDrop-Code des Absenders zu scannen. - /// - pt: Use a câmara para ler o código VniDrop do remetente. - /// - pl: Użyj aparatu, aby zeskanować kod VniDrop nadawcy. - /// - nl: Gebruik de camera om de VniDrop-code van de afzender te scannen. - /// - ru: Используйте камеру, чтобы отсканировать код VniDrop отправителя. - static let methodScanDescription: String.LocalizationValue = "receive_method_scan_description" - /// Transfers you’ve received on this device. - /// - /// Key: `receive_new_subtitle` - /// Context: Receive tab: subtitle under the title. - /// - /// - en: Transfers you’ve received on this device. - /// - fr: Transferts que vous avez reçus sur cet appareil. - /// - es: Transferencias que ha recibido en este dispositivo. - /// - it: Trasferimenti che ha ricevuto su questo dispositivo. - /// - de: Übertragungen, die Sie auf diesem Gerät empfangen haben. - /// - pt: Transferências que recebeu neste dispositivo. - /// - pl: Transfery odebrane na tym urządzeniu. - /// - nl: Overdrachten die u op dit apparaat hebt ontvangen. - /// - ru: Передачи, полученные на этом устройстве. - static let newSubtitle: String.LocalizationValue = "receive_new_subtitle" - /// Hold near the NFC tag… - /// - /// Key: `receive_nfc_waiting` - /// Context: Receive flow: prompt while waiting to read an NFC tag. - /// - /// - en: Hold near the NFC tag… - /// - fr: Approchez du tag NFC… - /// - es: Acerque a la etiqueta NFC… - /// - it: Avvicini al tag NFC… - /// - de: Halten Sie das Gerät an das NFC-Tag… - /// - pt: Aproxime da etiqueta NFC… - /// - pl: Zbliż do tagu NFC… - /// - nl: Houd bij de NFC-tag… - /// - ru: Поднесите к NFC-метке… - static let nfcWaiting: String.LocalizationValue = "receive_nfc_waiting" - /// Couldn’t open VniDrop in Files. - /// - /// Key: `receive_open_files_failed` - /// Context: Receive flow: error when opening VniDrop's folder in Files fails. - /// - /// - en: Couldn’t open VniDrop in Files. - /// - fr: Impossible d’ouvrir VniDrop dans Fichiers. - /// - es: No se pudo abrir VniDrop en Archivos. - /// - it: Impossibile aprire VniDrop in File. - /// - de: VniDrop konnte in „Dateien“ nicht geöffnet werden. - /// - pt: Não foi possível abrir o VniDrop em Ficheiros. - /// - pl: Nie udało się otworzyć VniDrop w Plikach. - /// - nl: VniDrop kon niet worden geopend in Bestanden. - /// - ru: Не удалось открыть VniDrop в Файлах. - static let openFilesFailed: String.LocalizationValue = "receive_open_files_failed" - /// Review transfer - /// - /// Key: `receive_review_title` - /// Context: Receive flow: title of the review screen before accepting a transfer. - /// - /// - en: Review transfer - /// - fr: Vérifier le transfert - /// - es: Revisar transferencia - /// - it: Rivedi trasferimento - /// - de: Übertragung prüfen - /// - pt: Rever transferência - /// - pl: Przejrzyj transfer - /// - nl: Overdracht controleren - /// - ru: Проверить передачу - static let reviewTitle: String.LocalizationValue = "receive_review_title" - /// Receive - /// - /// Key: `receive_title` - /// Context: Receive tab: screen title. - /// - /// - en: Receive - /// - fr: Recevoir - /// - es: Recibir - /// - it: Ricevi - /// - de: Empfangen - /// - pt: Receber - /// - pl: Odbierz - /// - nl: Ontvangen - /// - ru: Получить - static let title: String.LocalizationValue = "receive_title" - /// VniDrop transfer - /// - /// Key: `receive_unknown_transfer` - /// Context: Receive flow: fallback name for a transfer with no title. - /// - /// - en: VniDrop transfer - /// - fr: Transfert VniDrop - /// - es: Transferencia de VniDrop - /// - it: Trasferimento VniDrop - /// - de: VniDrop-Übertragung - /// - pt: Transferência VniDrop - /// - pl: Transfer VniDrop - /// - nl: VniDrop-overdracht - /// - ru: Передача VniDrop - static let unknownTransfer: String.LocalizationValue = "receive_unknown_transfer" - } - enum Send { - /// Anyone with this transfer - /// - /// Key: `send_access_anyone` - /// Context: Send access option: anyone with the invitation can receive. - /// - /// - en: Anyone with this transfer - /// - fr: Toute personne disposant de ce transfert - /// - es: Cualquiera que tenga esta transferencia - /// - it: Chiunque abbia questo trasferimento - /// - de: Jeder mit dieser Übertragung - /// - pt: Qualquer pessoa com esta transferência - /// - pl: Każdy, kto ma ten transfer - /// - nl: Iedereen met deze overdracht - /// - ru: Любой, у кого есть эта передача - static let accessAnyone: String.LocalizationValue = "send_access_anyone" - /// No approval is required. Only use this for items you are comfortable sharing. - /// - /// Key: `send_access_anyone_description` - /// Context: Send access option description: no approval required. - /// - /// - en: No approval is required. Only use this for items you are comfortable sharing. - /// - fr: Aucune approbation requise. À n’utiliser que pour des éléments que vous êtes à l’aise de partager. - /// - es: No se requiere aprobación. Úselo solo para elementos que no le importe compartir. - /// - it: Nessuna approvazione richiesta. Da usare solo per elementi che non ha problemi a condividere. - /// - de: Keine Genehmigung erforderlich. Verwenden Sie dies nur für Objekte, die Sie unbedenklich teilen können. - /// - pt: Não é necessária aprovação. Utilize apenas para itens que não se importe de partilhar. - /// - pl: Nie jest wymagane zatwierdzenie. Używaj tylko dla elementów, które możesz swobodnie udostępniać. - /// - nl: Geen goedkeuring vereist. Gebruik dit alleen voor items die u gerust kunt delen. - /// - ru: Одобрение не требуется. Используйте только для объектов, которыми вы готовы поделиться. - static let accessAnyoneDescription: String.LocalizationValue = "send_access_anyone_description" - /// Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items. - /// - /// Key: `send_access_anyone_warning` - /// Context: Send access option warning: caution about open access. - /// - /// - en: Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items. - /// - fr: Toute personne disposant de l’invitation peut télécharger jusqu’à ce que vous arrêtiez le partage. Ne l’utilisez pas pour des éléments privés ou sensibles. - /// - es: Cualquiera que tenga la invitación puede descargar hasta que deje de compartir. No lo use para elementos privados o sensibles. - /// - it: Chiunque abbia l’invito può scaricare finché non interrompe la condivisione. Non lo usi per elementi privati o sensibili. - /// - de: Jeder mit der Einladung kann herunterladen, bis Sie die Freigabe beenden. Verwenden Sie dies nicht für private oder sensible Objekte. - /// - pt: Qualquer pessoa com o convite pode descarregar até parar de partilhar. Não utilize para itens privados ou sensíveis. - /// - pl: Każdy, kto ma zaproszenie, może pobierać, dopóki nie zatrzymasz udostępniania. Nie używaj tego dla prywatnych ani wrażliwych elementów. - /// - nl: Iedereen met de uitnodiging kan downloaden totdat u stopt met delen. Gebruik dit niet voor privé- of gevoelige items. - /// - ru: Любой, у кого есть приглашение, может загружать, пока вы не остановите общий доступ. Не используйте это для личных или конфиденциальных объектов. - static let accessAnyoneWarning: String.LocalizationValue = "send_access_anyone_warning" - /// Ask before each download - /// - /// Key: `send_access_approval` - /// Context: Send access option: approve each receiver. - /// - /// - en: Ask before each download - /// - fr: Demander avant chaque téléchargement - /// - es: Preguntar antes de cada descarga - /// - it: Chiedi prima di ogni download - /// - de: Vor jedem Download fragen - /// - pt: Perguntar antes de cada descarga - /// - pl: Pytaj przed każdym pobraniem - /// - nl: Vragen vóór elke download - /// - ru: Спрашивать перед каждой загрузкой - static let accessApproval: String.LocalizationValue = "send_access_approval" - /// You approve or refuse every new receiver. - /// - /// Key: `send_access_approval_description` - /// Context: Send access option description: you approve/refuse each receiver. - /// - /// - en: You approve or refuse every new receiver. - /// - fr: Vous approuvez ou refusez chaque nouveau destinataire. - /// - es: Usted aprueba o rechaza a cada nuevo destinatario. - /// - it: Approva o rifiuta ogni nuovo destinatario. - /// - de: Sie genehmigen oder lehnen jeden neuen Empfänger ab. - /// - pt: Aprova ou recusa cada novo destinatário. - /// - pl: Zatwierdzasz lub odrzucasz każdego nowego odbiorcę. - /// - nl: U keurt elke nieuwe ontvanger goed of weigert deze. - /// - ru: Вы одобряете или отклоняете каждого нового получателя. - static let accessApprovalDescription: String.LocalizationValue = "send_access_approval_description" - /// Who can receive it? - /// - /// Key: `send_access_title` - /// Context: Send flow: heading for the who-can-receive access chooser. - /// - /// - en: Who can receive it? - /// - fr: Qui peut le recevoir ? - /// - es: ¿Quién puede recibirla? - /// - it: Chi può riceverlo? - /// - de: Wer kann sie empfangen? - /// - pt: Quem a pode receber? - /// - pl: Kto może to odebrać? - /// - nl: Wie kan het ontvangen? - /// - ru: Кто может это получить? - static let accessTitle: String.LocalizationValue = "send_access_title" - /// Select files or a folder from this device. You can review the selection before creating the transfer. - /// - /// Key: `send_choose_file_body` - /// Context: Send flow: body text for the choose-what-to-share step. - /// - /// - en: Select files or a folder from this device. You can review the selection before creating the transfer. - /// - fr: Sélectionnez des fichiers ou un dossier sur cet appareil. Vous pourrez vérifier la sélection avant de créer le transfert. - /// - es: Seleccione archivos o una carpeta de este dispositivo. Podrá revisar la selección antes de crear la transferencia. - /// - it: Selezioni file o una cartella da questo dispositivo. Potrà rivedere la selezione prima di creare il trasferimento. - /// - de: Wählen Sie Dateien oder einen Ordner auf diesem Gerät aus. Sie können die Auswahl überprüfen, bevor Sie die Übertragung erstellen. - /// - pt: Selecione ficheiros ou uma pasta deste dispositivo. Poderá rever a seleção antes de criar a transferência. - /// - pl: Wybierz pliki lub folder z tego urządzenia. Przed utworzeniem transferu możesz przejrzeć wybór. - /// - nl: Selecteer bestanden of een map op dit apparaat. U kunt de selectie controleren voordat u de overdracht aanmaakt. - /// - ru: Выберите файлы или папку на этом устройстве. Вы сможете проверить выбор перед созданием передачи. - static let chooseFileBody: String.LocalizationValue = "send_choose_file_body" - /// Choose what to share - /// - /// Key: `send_choose_file_title` - /// Context: Send flow: title of the choose-what-to-share step. - /// - /// - en: Choose what to share - /// - fr: Choisissez quoi partager - /// - es: Elija qué compartir - /// - it: Scelga cosa condividere - /// - de: Wählen Sie, was Sie teilen möchten - /// - pt: Escolha o que partilhar - /// - pl: Wybierz, co udostępnić - /// - nl: Kies wat u wilt delen - /// - ru: Выберите, чем поделиться - static let chooseFileTitle: String.LocalizationValue = "send_choose_file_title" - /// Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file. - /// - /// Key: `send_empty_body` - /// Context: Send tab empty state: instructions on how to create a transfer. - /// - /// - en: Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file. - /// - fr: Créez un transfert, décidez qui peut le recevoir, puis invitez-les avec un QR code, un tag NFC ou un fichier d’invitation. - /// - es: Cree una transferencia, decida quién puede recibirla y luego invite a esas personas con un código QR, una etiqueta NFC o un archivo de invitación. - /// - it: Crei un trasferimento, decida chi può riceverlo, poi inviti queste persone con un codice QR, un tag NFC o un file di invito. - /// - de: Erstellen Sie eine Übertragung, entscheiden Sie, wer sie empfangen darf, und laden Sie diese Personen dann mit einem QR-Code, einem NFC-Tag oder einer Einladungsdatei ein. - /// - pt: Crie uma transferência, decida quem a pode receber e depois convide essas pessoas com um código QR, uma etiqueta NFC ou um ficheiro de convite. - /// - pl: Utwórz transfer, zdecyduj, kto może go odebrać, a następnie zaproś te osoby za pomocą kodu QR, tagu NFC lub pliku zaproszenia. - /// - nl: Maak een overdracht aan, bepaal wie deze kan ontvangen en nodig die personen vervolgens uit met een QR-code, NFC-tag of uitnodigingsbestand. - /// - ru: Создайте передачу, решите, кто может её получить, затем пригласите этих людей с помощью QR-кода, NFC-метки или файла приглашения. - static let emptyBody: String.LocalizationValue = "send_empty_body" - /// Nothing shared yet - /// - /// Key: `send_empty_title` - /// Context: Send tab empty state: title when nothing has been shared. - /// - /// - en: Nothing shared yet - /// - fr: Rien partagé pour l’instant - /// - es: Aún no se ha compartido nada - /// - it: Ancora nulla di condiviso - /// - de: Noch nichts geteilt - /// - pt: Ainda não partilhou nada - /// - pl: Nic jeszcze nie udostępniono - /// - nl: Nog niets gedeeld - /// - ru: Пока ничем не поделились - static let emptyTitle: String.LocalizationValue = "send_empty_title" - /// Size unavailable - /// - /// Key: `send_file_size_unknown` - /// Context: Send flow: shown when a selected file's size can't be determined. - /// - /// - en: Size unavailable - /// - fr: Taille indisponible - /// - es: Tamaño no disponible - /// - it: Dimensione non disponibile - /// - de: Größe nicht verfügbar - /// - pt: Tamanho indisponível - /// - pl: Rozmiar niedostępny - /// - nl: Grootte niet beschikbaar - /// - ru: Размер недоступен - static let fileSizeUnknown: String.LocalizationValue = "send_file_size_unknown" - /// Folder - /// - /// Key: `send_folder_label` - /// Context: Send flow: label indicating a selected item is a folder. - /// - /// - en: Folder - /// - fr: Dossier - /// - es: Carpeta - /// - it: Cartella - /// - de: Ordner - /// - pt: Pasta - /// - pl: Folder - /// - nl: Map - /// - ru: Папка - static let folderLabel: String.LocalizationValue = "send_folder_label" - /// Create a new transfer - /// - /// Key: `send_new_transfer_description` - /// Context: Send flow: description for the create-new-transfer entry. - /// - /// - en: Create a new transfer - /// - fr: Créer un nouveau transfert - /// - es: Crear una nueva transferencia - /// - it: Crea un nuovo trasferimento - /// - de: Eine neue Übertragung erstellen - /// - pt: Criar uma nova transferência - /// - pl: Utwórz nowy transfer - /// - nl: Een nieuwe overdracht aanmaken - /// - ru: Создать новую передачу - static let newTransferDescription: String.LocalizationValue = "send_new_transfer_description" - /// New transfer - /// - /// Key: `send_new_transfer_title` - /// Context: Send flow: title for the new-transfer step. - /// - /// - en: New transfer - /// - fr: Nouveau transfert - /// - es: Nueva transferencia - /// - it: Nuovo trasferimento - /// - de: Neue Übertragung - /// - pt: Nova transferência - /// - pl: Nowy transfer - /// - nl: Nieuwe overdracht - /// - ru: Новая передача - static let newTransferTitle: String.LocalizationValue = "send_new_transfer_title" - /// Review transfer - /// - /// Key: `send_review_title` - /// Context: Send flow: title of the review-transfer step before creating it. - /// - /// - en: Review transfer - /// - fr: Vérifier le transfert - /// - es: Revisar transferencia - /// - it: Rivedi trasferimento - /// - de: Übertragung prüfen - /// - pt: Rever transferência - /// - pl: Przejrzyj transfer - /// - nl: Overdracht controleren - /// - ru: Проверить передачу - static let reviewTitle: String.LocalizationValue = "send_review_title" - /// {count} files selected - /// - /// Key: `send_selected_files_count` - /// Context: Send flow: count of files chosen. {count} = selected files. NOTE: singular case reads '1 files' — should become a plural. - /// - /// - en: {count} files selected - /// - fr: {count} fichiers sélectionnés - /// - es: {count} archivos seleccionados - /// - it: {count} file selezionati - /// - de: {count} Dateien ausgewählt - /// - pt: {count} ficheiros selecionados - /// - pl: Wybrane pliki: {count} - /// - nl: {count} bestanden geselecteerd - /// - ru: Выбрано файлов: {count} - static func selectedFilesCount(count: Int) -> String { - String(format: String(localized: "send_selected_files_count"), count) - } - /// Stop sharing - /// - /// Key: `send_stop_sharing` - /// Context: Transfer details: action to stop sharing a transfer. - /// - /// - en: Stop sharing - /// - fr: Arrêter le partage - /// - es: Dejar de compartir - /// - it: Interrompi condivisione - /// - de: Freigabe beenden - /// - pt: Parar de partilhar - /// - pl: Zatrzymaj udostępnianie - /// - nl: Stoppen met delen - /// - ru: Остановить общий доступ - static let stopSharing: String.LocalizationValue = "send_stop_sharing" - /// This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped. - /// - /// Key: `send_stop_sharing_description` - /// Context: Transfer details: confirmation body for stopping sharing. - /// - /// - en: This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped. - /// - fr: Cela arrête le transfert et interrompt toute personne en train de le télécharger. Il reste dans votre historique en tant qu’Arrêté. - /// - es: Esto detiene la transferencia e interrumpe a quien esté descargándola. Permanece en su historial como Detenida. - /// - it: Questo interrompe il trasferimento e blocca chiunque lo stia scaricando. Rimane nella sua cronologia come Interrotto. - /// - de: Dies beendet die Übertragung und unterbricht alle, die sie gerade herunterladen. Sie verbleibt in Ihrem Verlauf als „Beendet“. - /// - pt: Isto para a transferência e interrompe quem estiver a descarregá-la. Permanece no seu histórico como Parada. - /// - pl: To zatrzymuje transfer i przerywa każdego, kto go właśnie pobiera. Pozostaje w historii jako Zatrzymany. - /// - nl: Hiermee stopt de overdracht en wordt iedereen die deze op dit moment downloadt onderbroken. De overdracht blijft in uw geschiedenis staan als Gestopt. - /// - ru: Это остановит передачу и прервёт всех, кто сейчас её загружает. Она останется в вашей истории со статусом «Остановлена». - static let stopSharingDescription: String.LocalizationValue = "send_stop_sharing_description" - /// Transfers you’re sharing from this device. - /// - /// Key: `send_subtitle` - /// Context: Send tab: subtitle under the title. - /// - /// - en: Transfers you’re sharing from this device. - /// - fr: Transferts que vous partagez depuis cet appareil. - /// - es: Transferencias que está compartiendo desde este dispositivo. - /// - it: Trasferimenti che sta condividendo da questo dispositivo. - /// - de: Übertragungen, die Sie von diesem Gerät aus teilen. - /// - pt: Transferências que está a partilhar a partir deste dispositivo. - /// - pl: Transfery udostępniane z tego urządzenia. - /// - nl: Overdrachten die u vanaf dit apparaat deelt. - /// - ru: Передачи, которыми вы делитесь с этого устройства. - static let subtitle: String.LocalizationValue = "send_subtitle" - /// Send - /// - /// Key: `send_title` - /// Context: Send tab: screen title. - /// - /// - en: Send - /// - fr: Envoyer - /// - es: Enviar - /// - it: Invia - /// - de: Senden - /// - pt: Enviar - /// - pl: Wyślij - /// - nl: Versturen - /// - ru: Отправить - static let title: String.LocalizationValue = "send_title" - /// Transfer created. - /// - /// Key: `send_transfer_created` - /// Context: Send flow: toast confirming a transfer was created. - /// - /// - en: Transfer created. - /// - fr: Transfert créé. - /// - es: Transferencia creada. - /// - it: Trasferimento creato. - /// - de: Übertragung erstellt. - /// - pt: Transferência criada. - /// - pl: Transfer utworzony. - /// - nl: Overdracht aangemaakt. - /// - ru: Передача создана. - static let transferCreated: String.LocalizationValue = "send_transfer_created" - /// Transfer details - /// - /// Key: `send_transfer_details_title` - /// Context: Send flow: title of the transfer details screen. - /// - /// - en: Transfer details - /// - fr: Détails du transfert - /// - es: Detalles de la transferencia - /// - it: Dettagli del trasferimento - /// - de: Übertragungsdetails - /// - pt: Detalhes da transferência - /// - pl: Szczegóły transferu - /// - nl: Overdrachtsdetails - /// - ru: Сведения о передаче - static let transferDetailsTitle: String.LocalizationValue = "send_transfer_details_title" - /// Your transfers - /// - /// Key: `send_transfers_title` - /// Context: Send tab: 'Your transfers' list heading. - /// - /// - en: Your transfers - /// - fr: Vos transferts - /// - es: Sus transferencias - /// - it: I suoi trasferimenti - /// - de: Ihre Übertragungen - /// - pt: As suas transferências - /// - pl: Twoje transfery - /// - nl: Uw overdrachten - /// - ru: Ваши передачи - static let transfersTitle: String.LocalizationValue = "send_transfers_title" - } - enum Settings { - /// Your name, where transfers are saved, appearance, and notifications. - /// - /// Key: `settings_subtitle` - /// Context: Settings screen: subtitle summarizing what's configurable. - /// - /// - en: Your name, where transfers are saved, appearance, and notifications. - /// - fr: Votre nom, l’emplacement d’enregistrement des transferts, l’apparence et les notifications. - /// - es: Su nombre, dónde se guardan las transferencias, la apariencia y las notificaciones. - /// - it: Il suo nome, dove vengono salvati i trasferimenti, l’aspetto e le notifiche. - /// - de: Ihr Name, wo Übertragungen gesichert werden, Darstellung und Mitteilungen. - /// - pt: O seu nome, onde as transferências são guardadas, o aspeto e as notificações. - /// - pl: Twoja nazwa, miejsce zapisu transferów, wygląd i powiadomienia. - /// - nl: Uw naam, waar overdrachten worden bewaard, weergave en meldingen. - /// - ru: Ваше имя, место сохранения передач, оформление и уведомления. - static let subtitle: String.LocalizationValue = "settings_subtitle" - /// Settings - /// - /// Key: `settings_title` - /// Context: Settings screen: title. - /// - /// - en: Settings - /// - fr: Réglages - /// - es: Ajustes - /// - it: Impostazioni - /// - de: Einstellungen - /// - pt: Definições - /// - pl: Ustawienia - /// - nl: Instellingen - /// - ru: Настройки - static let title: String.LocalizationValue = "settings_title" - } - enum Snackbar { - /// Dismiss - /// - /// Key: `snackbar_dismiss` - /// Context: Snackbar: accessibility label / action to dismiss the snackbar. - /// - /// - en: Dismiss - /// - fr: Ignorer - /// - es: Descartar - /// - it: Ignora - /// - de: Ausblenden - /// - pt: Ignorar - /// - pl: Zamknij - /// - nl: Sluiten - /// - ru: Закрыть - static let dismiss: String.LocalizationValue = "snackbar_dismiss" - } - enum Status { - /// Available - /// - /// Key: `status_available` - /// Context: Transfer status: available (actively shared). - /// - /// - en: Available - /// - fr: Disponible - /// - es: Disponible - /// - it: Disponibile - /// - de: Verfügbar - /// - pt: Disponível - /// - pl: Dostępny - /// - nl: Beschikbaar - /// - ru: Доступно - static let available: String.LocalizationValue = "status_available" - /// Cancelled - /// - /// Key: `status_cancelled` - /// Context: Transfer status: cancelled. - /// - /// - en: Cancelled - /// - fr: Annulé - /// - es: Cancelado - /// - it: Annullato - /// - de: Abgebrochen - /// - pt: Cancelado - /// - pl: Anulowany - /// - nl: Geannuleerd - /// - ru: Отменено - static let cancelled: String.LocalizationValue = "status_cancelled" - /// Completed - /// - /// Key: `status_completed` - /// Context: Transfer status: completed. - /// - /// - en: Completed - /// - fr: Terminé - /// - es: Completado - /// - it: Completato - /// - de: Abgeschlossen - /// - pt: Concluído - /// - pl: Ukończony - /// - nl: Voltooid - /// - ru: Завершено - static let completed: String.LocalizationValue = "status_completed" - /// Failed - /// - /// Key: `status_failed` - /// Context: Transfer status: failed. - /// - /// - en: Failed - /// - fr: Échec - /// - es: Fallido - /// - it: Non riuscito - /// - de: Fehlgeschlagen - /// - pt: Falhou - /// - pl: Niepowodzenie - /// - nl: Mislukt - /// - ru: Ошибка - static let failed: String.LocalizationValue = "status_failed" - /// Preparing - /// - /// Key: `status_preparing` - /// Context: Transfer status: preparing. - /// - /// - en: Preparing - /// - fr: Préparation - /// - es: Preparando - /// - it: Preparazione - /// - de: Wird vorbereitet - /// - pt: A preparar - /// - pl: Przygotowywanie - /// - nl: Voorbereiden - /// - ru: Подготовка - static let preparing: String.LocalizationValue = "status_preparing" - /// Receiving - /// - /// Key: `status_receiving` - /// Context: Transfer status: receiving. - /// - /// - en: Receiving - /// - fr: Réception - /// - es: Recibiendo - /// - it: Ricezione - /// - de: Wird empfangen - /// - pt: A receber - /// - pl: Odbieranie - /// - nl: Ontvangen - /// - ru: Получение - static let receiving: String.LocalizationValue = "status_receiving" - /// Stopped - /// - /// Key: `status_stopped` - /// Context: Transfer status: stopped. - /// - /// - en: Stopped - /// - fr: Arrêté - /// - es: Detenido - /// - it: Interrotto - /// - de: Beendet - /// - pt: Parada - /// - pl: Zatrzymany - /// - nl: Gestopt - /// - ru: Остановлено - static let stopped: String.LocalizationValue = "status_stopped" - } - enum Storage { - /// Calculating… - /// - /// Key: `storage_calculating` - /// Context: Settings > Storage: placeholder while a size is being calculated. - /// - /// - en: Calculating… - /// - fr: Calcul… - /// - es: Calculando… - /// - it: Calcolo… - /// - de: Wird berechnet… - /// - pt: A calcular… - /// - pl: Obliczanie… - /// - nl: Berekenen… - /// - ru: Вычисление… - static let calculating: String.LocalizationValue = "storage_calculating" - /// Delete all transfers - /// - /// Key: `storage_delete_transfers` - /// Context: Settings > Storage: button to delete all transfer records. - /// - /// - en: Delete all transfers - /// - fr: Supprimer tous les transferts - /// - es: Eliminar todas las transferencias - /// - it: Elimina tutti i trasferimenti - /// - de: Alle Übertragungen löschen - /// - pt: Eliminar todas as transferências - /// - pl: Usuń wszystkie transfery - /// - nl: Alle overdrachten verwijderen - /// - ru: Удалить все передачи - static let deleteTransfers: String.LocalizationValue = "storage_delete_transfers" - /// This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone. - /// - /// Key: `storage_delete_transfers_description` - /// Context: Settings > Storage: confirmation body for deleting all transfer records. - /// - /// - en: This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone. - /// - fr: Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible. - /// - es: Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer. - /// - it: Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata. - /// - de: Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden. - /// - pt: Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada. - /// - pl: Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć. - /// - nl: Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt. - /// - ru: Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить. - static let deleteTransfersDescription: String.LocalizationValue = "storage_delete_transfers_description" - /// App data - /// - /// Key: `storage_app_data` - /// Context: Settings > Storage: label for non-transfer application data. - /// - /// - en: App data - /// - fr: Données de l’app - /// - es: Datos de la aplicación - /// - it: Dati dell’app - /// - de: App-Daten - /// - pt: Dados da aplicação - /// - pl: Dane aplikacji - /// - nl: Appgegevens - /// - ru: Данные приложения - static let appData: String.LocalizationValue = "storage_app_data" - /// Deleting… - /// - /// Key: `storage_deleting` - /// Context: Settings > Storage: progress label while transfers are being deleted. - /// - /// - en: Deleting… - /// - fr: Suppression… - /// - es: Eliminando… - /// - it: Eliminazione… - /// - de: Wird gelöscht… - /// - pt: A eliminar… - /// - pl: Usuwanie… - /// - nl: Verwijderen… - /// - ru: Удаление… - static let deleting: String.LocalizationValue = "storage_deleting" - /// Transfer data includes your history and cached content for active shares. Unneeded cache is reclaimed automatically after transfer records are removed, which may take a little time. Received files are tracked for this summary but are never deleted here. - /// - /// Key: `storage_footer` - /// Context: Settings > Storage: footer explaining how storage is managed. - /// - /// - en: Transfer data includes your history and cached content for active shares. Unneeded cache is reclaimed automatically after transfer records are removed, which may take a little time. Received files are tracked for this summary but are never deleted here. - /// - fr: Les données de transfert comprennent votre historique et le contenu mis en cache pour les partages actifs. Le cache inutile est récupéré automatiquement après la suppression des enregistrements, ce qui peut prendre un peu de temps. Les fichiers reçus sont suivis pour ce récapitulatif, mais ne sont jamais supprimés ici. - /// - es: Los datos de transferencia incluyen su historial y el contenido en caché de los recursos compartidos activos. La caché innecesaria se recupera automáticamente tras eliminar los registros, lo que puede tardar un poco. Los archivos recibidos se registran para este resumen, pero nunca se eliminan aquí. - /// - it: I dati di trasferimento includono la cronologia e il contenuto nella cache per le condivisioni attive. La cache non necessaria viene recuperata automaticamente dopo la rimozione dei record, operazione che può richiedere un po’ di tempo. I file ricevuti vengono monitorati per questo riepilogo, ma non sono mai eliminati qui. - /// - de: Übertragungsdaten umfassen Ihren Verlauf und zwischengespeicherte Inhalte aktiver Freigaben. Nicht mehr benötigter Cache wird nach dem Löschen der Datensätze automatisch bereinigt; dies kann etwas dauern. Empfangene Dateien werden für diese Übersicht erfasst, aber hier niemals gelöscht. - /// - pt: Os dados de transferência incluem o histórico e o conteúdo em cache das partilhas ativas. A cache desnecessária é recuperada automaticamente após a remoção dos registos, o que pode demorar algum tempo. Os ficheiros recebidos são acompanhados para este resumo, mas nunca são eliminados aqui. - /// - pl: Dane transferu obejmują historię oraz zawartość w pamięci podręcznej dla aktywnych udostępnień. Niepotrzebna pamięć podręczna jest odzyskiwana automatycznie po usunięciu rekordów, co może chwilę potrwać. Odebrane pliki są śledzone na potrzeby tego podsumowania, ale nigdy nie są tu usuwane. - /// - nl: Overdrachtsgegevens omvatten uw geschiedenis en inhoud in de cache voor actieve shares. Onnodige cache wordt automatisch opgeruimd nadat overdrachtsrecords zijn verwijderd; dit kan enige tijd duren. Ontvangen bestanden worden voor dit overzicht bijgehouden, maar hier nooit verwijderd. - /// - ru: Данные передачи включают историю и кэшированное содержимое активных раздач. Ненужный кэш освобождается автоматически после удаления записей; это может занять некоторое время. Полученные файлы учитываются в этой сводке, но никогда не удаляются здесь. - static let footer: String.LocalizationValue = "storage_footer" - /// Received files - /// - /// Key: `storage_received_files` - /// Context: Settings > Storage: label for the received-files size row. - /// - /// - en: Received files - /// - fr: Fichiers reçus - /// - es: Archivos recibidos - /// - it: File ricevuti - /// - de: Empfangene Dateien - /// - pt: Ficheiros recebidos - /// - pl: Odebrane pliki - /// - nl: Ontvangen bestanden - /// - ru: Полученные файлы - static let receivedFiles: String.LocalizationValue = "storage_received_files" - /// Temporary files - /// - /// Key: `storage_temporary` - /// Context: Settings > Storage: label for the temporary-files size row. - /// - /// - en: Temporary files - /// - fr: Fichiers temporaires - /// - es: Archivos temporales - /// - it: File temporanei - /// - de: Temporäre Dateien - /// - pt: Ficheiros temporários - /// - pl: Pliki tymczasowe - /// - nl: Tijdelijke bestanden - /// - ru: Временные файлы - static let temporary: String.LocalizationValue = "storage_temporary" - /// Storage - /// - /// Key: `storage_title` - /// Context: Settings > Storage: section title. - /// - /// - en: Storage - /// - fr: Stockage - /// - es: Almacenamiento - /// - it: Archiviazione - /// - de: Speicher - /// - pt: Armazenamento - /// - pl: Pamięć - /// - nl: Opslag - /// - ru: Хранилище - static let title: String.LocalizationValue = "storage_title" - /// Total - /// - /// Key: `storage_total` - /// Context: Settings > Storage: label for the total-size row. - /// - /// - en: Total - /// - fr: Total - /// - es: Total - /// - it: Totale - /// - de: Gesamt - /// - pt: Total - /// - pl: Łącznie - /// - nl: Totaal - /// - ru: Всего - static let total: String.LocalizationValue = "storage_total" - /// Transfer data - /// - /// Key: `storage_transfer_data` - /// Context: Settings > Storage: label for the transfer-engine data size row. - /// - /// - en: Transfer data - /// - fr: Données de transfert - /// - es: Datos de transferencia - /// - it: Dati di trasferimento - /// - de: Übertragungsdaten - /// - pt: Dados de transferência - /// - pl: Dane transferu - /// - nl: Overdrachtsgegevens - /// - ru: Данные передачи - static let transferData: String.LocalizationValue = "storage_transfer_data" - /// All transfers deleted - /// - /// Key: `storage_transfers_deleted` - /// Context: Settings > Storage: toast confirming all transfers were deleted. - /// - /// - en: All transfers deleted - /// - fr: Tous les transferts supprimés - /// - es: Todas las transferencias eliminadas - /// - it: Tutti i trasferimenti eliminati - /// - de: Alle Übertragungen gelöscht - /// - pt: Todas as transferências eliminadas - /// - pl: Usunięto wszystkie transfery - /// - nl: Alle overdrachten verwijderd - /// - ru: Все передачи удалены - static let transfersDeleted: String.LocalizationValue = "storage_transfers_deleted" - } - enum Transfer { - /// See important updates for this transfer - /// - /// Key: `transfer_activity_description` - /// Context: Transfer details: description under the Activity section. - /// - /// - en: See important updates for this transfer - /// - fr: Consultez les mises à jour importantes de ce transfert - /// - es: Vea las actualizaciones importantes de esta transferencia - /// - it: Veda gli aggiornamenti importanti di questo trasferimento - /// - de: Wichtige Aktualisierungen zu dieser Übertragung ansehen - /// - pt: Ver as atualizações importantes desta transferência - /// - pl: Zobacz ważne aktualizacje tego transferu - /// - nl: Bekijk belangrijke updates voor deze overdracht - /// - ru: Просматривайте важные обновления этой передачи - static let activityDescription: String.LocalizationValue = "transfer_activity_description" - /// Activity - /// - /// Key: `transfer_activity_title` - /// Context: Transfer details: Activity section title. - /// - /// - en: Activity - /// - fr: Activité - /// - es: Actividad - /// - it: Attività - /// - de: Aktivität - /// - pt: Atividade - /// - pl: Aktywność - /// - nl: Activiteit - /// - ru: Активность - static let activityTitle: String.LocalizationValue = "transfer_activity_title" - /// “{transferName}” will stop being shared and its transfer history will be removed from this device. - /// - /// Key: `transfer_delete_description` - /// Context: Transfer details: confirmation body for deleting a transfer. {transferName} = transfer name. - /// - /// - en: “{transferName}” will stop being shared and its transfer history will be removed from this device. - /// - fr: « {transferName} » cessera d’être partagé et son historique de transfert sera retiré de cet appareil. - /// - es: «{transferName}» dejará de compartirse y su historial de transferencia se eliminará de este dispositivo. - /// - it: «{transferName}» non verrà più condiviso e la sua cronologia di trasferimento verrà rimossa da questo dispositivo. - /// - de: „{transferName}“ wird nicht mehr geteilt und sein Übertragungsverlauf wird von diesem Gerät entfernt. - /// - pt: «{transferName}» deixará de ser partilhado e o seu histórico de transferência será removido deste dispositivo. - /// - pl: „{transferName}” przestanie być udostępniany, a jego historia transferu zostanie usunięta z tego urządzenia. - /// - nl: ‘{transferName}’ wordt niet meer gedeeld en de overdrachtsgeschiedenis wordt van dit apparaat verwijderd. - /// - ru: Общий доступ к «{transferName}» будет остановлен, а история передачи будет удалена с этого устройства. - static func deleteDescription(transferName: String) -> String { - String(format: String(localized: "transfer_delete_description"), transferName) - } - /// Delete transfer? - /// - /// Key: `transfer_delete_title` - /// Context: Transfer details: confirmation title for deleting a transfer. - /// - /// - en: Delete transfer? - /// - fr: Supprimer le transfert ? - /// - es: ¿Eliminar la transferencia? - /// - it: Eliminare il trasferimento? - /// - de: Übertragung löschen? - /// - pt: Eliminar a transferência? - /// - pl: Usunąć transfer? - /// - nl: Overdracht verwijderen? - /// - ru: Удалить передачу? - static let deleteTitle: String.LocalizationValue = "transfer_delete_title" - /// Transfer deleted. - /// - /// Key: `transfer_deleted` - /// Context: Transfer details: toast confirming a transfer was deleted. - /// - /// - en: Transfer deleted. - /// - fr: Transfert supprimé. - /// - es: Transferencia eliminada. - /// - it: Trasferimento eliminato. - /// - de: Übertragung gelöscht. - /// - pt: Transferência eliminada. - /// - pl: Transfer usunięty. - /// - nl: Overdracht verwijderd. - /// - ru: Передача удалена. - static let deleted: String.LocalizationValue = "transfer_deleted" - /// Deleting… - /// - /// Key: `transfer_deleting` - /// Context: Transfer details: progress label while a transfer is being deleted. - /// - /// - en: Deleting… - /// - fr: Suppression… - /// - es: Eliminando… - /// - it: Eliminazione… - /// - de: Wird gelöscht… - /// - pt: A eliminar… - /// - pl: Usuwanie… - /// - nl: Verwijderen… - /// - ru: Удаление… - static let deleting: String.LocalizationValue = "transfer_deleting" - /// Transfer details - /// - /// Key: `transfer_details_title` - /// Context: Transfer details: screen title. - /// - /// - en: Transfer details - /// - fr: Détails du transfert - /// - es: Detalles de la transferencia - /// - it: Dettagli del trasferimento - /// - de: Übertragungsdetails - /// - pt: Detalhes da transferência - /// - pl: Szczegóły transferu - /// - nl: Overdrachtsdetails - /// - ru: Сведения о передаче - static let detailsTitle: String.LocalizationValue = "transfer_details_title" - /// Receiver access approved - /// - /// Key: `transfer_event_approved` - /// Context: Transfer activity event: a receiver's access was approved. - /// - /// - en: Receiver access approved - /// - fr: Accès du destinataire approuvé - /// - es: Acceso del destinatario aprobado - /// - it: Accesso del destinatario approvato - /// - de: Empfängerzugriff genehmigt - /// - pt: Acesso do destinatário aprovado - /// - pl: Zatwierdzono dostęp odbiorcy - /// - nl: Toegang ontvanger goedgekeurd - /// - ru: Доступ получателя одобрен - static let eventApproved: String.LocalizationValue = "transfer_event_approved" - /// A receiver completed the transfer - /// - /// Key: `transfer_event_completed` - /// Context: Transfer activity event: a receiver completed the transfer. - /// - /// - en: A receiver completed the transfer - /// - fr: Un destinataire a terminé le transfert - /// - es: Un destinatario completó la transferencia - /// - it: Un destinatario ha completato il trasferimento - /// - de: Ein Empfänger hat die Übertragung abgeschlossen - /// - pt: Um destinatário concluiu a transferência - /// - pl: Odbiorca ukończył transfer - /// - nl: Een ontvanger heeft de overdracht voltooid - /// - ru: Получатель завершил передачу - static let eventCompleted: String.LocalizationValue = "transfer_event_completed" - /// Connecting to sender - /// - /// Key: `transfer_event_connecting` - /// Context: Transfer activity event: connecting to the sender. - /// - /// - en: Connecting to sender - /// - fr: Connexion à l’expéditeur - /// - es: Conectando con el remitente - /// - it: Connessione al mittente - /// - de: Verbindung zum Absender - /// - pt: A ligar ao remetente - /// - pl: Łączenie z nadawcą - /// - nl: Verbinden met afzender - /// - ru: Подключение к отправителю - static let eventConnecting: String.LocalizationValue = "transfer_event_connecting" - /// Downloading - /// - /// Key: `transfer_event_downloading` - /// Context: Transfer activity event: downloading. - /// - /// - en: Downloading - /// - fr: Téléchargement - /// - es: Descargando - /// - it: Download - /// - de: Wird geladen - /// - pt: A descarregar - /// - pl: Pobieranie - /// - nl: Downloaden - /// - ru: Загрузка - static let eventDownloading: String.LocalizationValue = "transfer_event_downloading" - /// The transfer encountered a problem - /// - /// Key: `transfer_event_failed` - /// Context: Transfer activity event: the transfer hit a problem. - /// - /// - en: The transfer encountered a problem - /// - fr: Le transfert a rencontré un problème - /// - es: La transferencia tuvo un problema - /// - it: Il trasferimento ha riscontrato un problema - /// - de: Bei der Übertragung ist ein Problem aufgetreten - /// - pt: A transferência teve um problema - /// - pl: Podczas transferu wystąpił problem - /// - nl: Er is een probleem opgetreden bij de overdracht - /// - ru: При передаче возникла проблема - static let eventFailed: String.LocalizationValue = "transfer_event_failed" - /// Preparing your transfer - /// - /// Key: `transfer_event_preparing` - /// Context: Transfer activity event: preparing the transfer. - /// - /// - en: Preparing your transfer - /// - fr: Préparation de votre transfert - /// - es: Preparando su transferencia - /// - it: Preparazione del trasferimento - /// - de: Ihre Übertragung wird vorbereitet - /// - pt: A preparar a sua transferência - /// - pl: Przygotowywanie transferu - /// - nl: Uw overdracht voorbereiden - /// - ru: Подготовка вашей передачи - static let eventPreparing: String.LocalizationValue = "transfer_event_preparing" - /// Ready to share - /// - /// Key: `transfer_event_ready` - /// Context: Transfer activity event: ready to share. - /// - /// - en: Ready to share - /// - fr: Prêt à partager - /// - es: Listo para compartir - /// - it: Pronto per la condivisione - /// - de: Bereit zum Teilen - /// - pt: Pronto para partilhar - /// - pl: Gotowe do udostępnienia - /// - nl: Klaar om te delen - /// - ru: Готово к отправке - static let eventReady: String.LocalizationValue = "transfer_event_ready" - /// Receiver access refused - /// - /// Key: `transfer_event_refused` - /// Context: Transfer activity event: a receiver's access was refused. - /// - /// - en: Receiver access refused - /// - fr: Accès du destinataire refusé - /// - es: Acceso del destinatario rechazado - /// - it: Accesso del destinatario rifiutato - /// - de: Empfängerzugriff abgelehnt - /// - pt: Acesso do destinatário recusado - /// - pl: Odrzucono dostęp odbiorcy - /// - nl: Toegang ontvanger geweigerd - /// - ru: Доступ получателя отклонён - static let eventRefused: String.LocalizationValue = "transfer_event_refused" - /// A receiver requested access - /// - /// Key: `transfer_event_requested` - /// Context: Transfer activity event: a receiver requested access. - /// - /// - en: A receiver requested access - /// - fr: Un destinataire a demandé l’accès - /// - es: Un destinatario solicitó acceso - /// - it: Un destinatario ha richiesto l’accesso - /// - de: Ein Empfänger hat Zugriff angefragt - /// - pt: Um destinatário pediu acesso - /// - pl: Odbiorca poprosił o dostęp - /// - nl: Een ontvanger heeft toegang aangevraagd - /// - ru: Получатель запросил доступ - static let eventRequested: String.LocalizationValue = "transfer_event_requested" - /// Saving - /// - /// Key: `transfer_event_saving` - /// Context: Transfer activity event: saving received files. - /// - /// - en: Saving - /// - fr: Enregistrement - /// - es: Guardando - /// - it: Salvataggio - /// - de: Wird gesichert - /// - pt: A guardar - /// - pl: Zapisywanie - /// - nl: Bewaren - /// - ru: Сохранение - static let eventSaving: String.LocalizationValue = "transfer_event_saving" - /// Sharing stopped - /// - /// Key: `transfer_event_stopped` - /// Context: Transfer activity event: sharing was stopped. - /// - /// - en: Sharing stopped - /// - fr: Partage arrêté - /// - es: Se dejó de compartir - /// - it: Condivisione interrotta - /// - de: Freigabe beendet - /// - pt: Partilha parada - /// - pl: Zatrzymano udostępnianie - /// - nl: Delen gestopt - /// - ru: Раздача остановлена - static let eventStopped: String.LocalizationValue = "transfer_event_stopped" - /// Transfer updated - /// - /// Key: `transfer_event_updated` - /// Context: Transfer activity event: the transfer was updated. - /// - /// - en: Transfer updated - /// - fr: Transfert mis à jour - /// - es: Transferencia actualizada - /// - it: Trasferimento aggiornato - /// - de: Übertragung aktualisiert - /// - pt: Transferência atualizada - /// - pl: Transfer zaktualizowany - /// - nl: Overdracht bijgewerkt - /// - ru: Передача обновлена - static let eventUpdated: String.LocalizationValue = "transfer_event_updated" - /// {count} files - /// - /// Key: `transfer_file_count` - /// Context: Transfer subtitle: file count with pluralization. {count} = number of files. - /// - /// - en: {count} files - /// - fr: {count} fichiers - /// - es: {count} archivos - /// - it: {count} file - /// - de: {count} Dateien - /// - pt: {count} ficheiros - /// - pl: {count} pliku - /// - nl: {count} bestanden - /// - ru: {count} файла - static func fileCount(count: Int) -> String { - String(format: String(localized: "transfer_file_count"), count) - } - /// Invitation saved. - /// - /// Key: `transfer_invitation_saved` - /// Context: Transfer share: toast confirming the invitation file was saved. - /// - /// - en: Invitation saved. - /// - fr: Invitation enregistrée. - /// - es: Invitación guardada. - /// - it: Invito salvato. - /// - de: Einladung gesichert. - /// - pt: Convite guardado. - /// - pl: Zaproszenie zapisane. - /// - nl: Uitnodiging bewaard. - /// - ru: Приглашение сохранено. - static let invitationSaved: String.LocalizationValue = "transfer_invitation_saved" - /// Nearby device - /// - /// Key: `transfer_nearby_device` - /// Context: Transfer share/receivers: label for a nearby device. - /// - /// - en: Nearby device - /// - fr: Appareil à proximité - /// - es: Dispositivo cercano - /// - it: Dispositivo nelle vicinanze - /// - de: Gerät in der Nähe - /// - pt: Dispositivo próximo - /// - pl: Urządzenie w pobliżu - /// - nl: Apparaat in de buurt - /// - ru: Устройство поблизости - static let nearbyDevice: String.LocalizationValue = "transfer_nearby_device" - /// NFC tag writing is not available on this device. - /// - /// Key: `transfer_nfc_unavailable` - /// Context: Transfer share: message when NFC writing isn't available on the device. - /// - /// - en: NFC tag writing is not available on this device. - /// - fr: L’écriture de tag NFC n’est pas disponible sur cet appareil. - /// - es: La escritura de etiquetas NFC no está disponible en este dispositivo. - /// - it: La scrittura dei tag NFC non è disponibile su questo dispositivo. - /// - de: Das Beschreiben von NFC-Tags ist auf diesem Gerät nicht verfügbar. - /// - pt: A escrita de etiquetas NFC não está disponível neste dispositivo. - /// - pl: Zapis tagów NFC nie jest dostępny na tym urządzeniu. - /// - nl: Het schrijven van NFC-tags is niet beschikbaar op dit apparaat. - /// - ru: Запись NFC-меток недоступна на этом устройстве. - static let nfcUnavailable: String.LocalizationValue = "transfer_nfc_unavailable" - /// Hold your device near a writable NFC tag. - /// - /// Key: `transfer_nfc_waiting` - /// Context: Transfer share: prompt while waiting to write an NFC tag. - /// - /// - en: Hold your device near a writable NFC tag. - /// - fr: Approchez votre appareil d’un tag NFC inscriptible. - /// - es: Acerque su dispositivo a una etiqueta NFC grabable. - /// - it: Avvicini il dispositivo a un tag NFC scrivibile. - /// - de: Halten Sie Ihr Gerät an ein beschreibbares NFC-Tag. - /// - pt: Aproxime o seu dispositivo de uma etiqueta NFC gravável. - /// - pl: Zbliż urządzenie do zapisywalnego tagu NFC. - /// - nl: Houd uw apparaat bij een beschrijfbare NFC-tag. - /// - ru: Поднесите устройство к записываемой NFC-метке. - static let nfcWaiting: String.LocalizationValue = "transfer_nfc_waiting" - /// Invitation written to the NFC tag. - /// - /// Key: `transfer_nfc_written` - /// Context: Transfer share: confirmation the invitation was written to the NFC tag. - /// - /// - en: Invitation written to the NFC tag. - /// - fr: Invitation écrite sur le tag NFC. - /// - es: Invitación escrita en la etiqueta NFC. - /// - it: Invito scritto sul tag NFC. - /// - de: Einladung auf das NFC-Tag geschrieben. - /// - pt: Convite escrito na etiqueta NFC. - /// - pl: Zaproszenie zapisane na tagu NFC. - /// - nl: Uitnodiging naar de NFC-tag geschreven. - /// - ru: Приглашение записано на NFC-метку. - static let nfcWritten: String.LocalizationValue = "transfer_nfc_written" - /// There is no activity to show yet. - /// - /// Key: `transfer_no_activity` - /// Context: Transfer details: empty state for the Activity section. - /// - /// - en: There is no activity to show yet. - /// - fr: Aucune activité à afficher pour l’instant. - /// - es: Todavía no hay actividad que mostrar. - /// - it: Non c’è ancora alcuna attività da mostrare. - /// - de: Es gibt noch keine Aktivität anzuzeigen. - /// - pt: Ainda não há atividade para mostrar. - /// - pl: Nie ma jeszcze aktywności do wyświetlenia. - /// - nl: Er is nog geen activiteit om weer te geven. - /// - ru: Пока нет активности для отображения. - static let noActivity: String.LocalizationValue = "transfer_no_activity" - /// Nobody has requested this transfer yet. - /// - /// Key: `transfer_no_receivers` - /// Context: Transfer details: empty state for the Receivers section. - /// - /// - en: Nobody has requested this transfer yet. - /// - fr: Personne n’a encore demandé ce transfert. - /// - es: Nadie ha solicitado aún esta transferencia. - /// - it: Nessuno ha ancora richiesto questo trasferimento. - /// - de: Noch niemand hat diese Übertragung angefragt. - /// - pt: Ainda ninguém pediu esta transferência. - /// - pl: Nikt jeszcze nie poprosił o ten transfer. - /// - nl: Nog niemand heeft deze overdracht aangevraagd. - /// - ru: Никто ещё не запросил эту передачу. - static let noReceivers: String.LocalizationValue = "transfer_no_receivers" - /// Approved — waiting for completion - /// - /// Key: `transfer_receiver_accepted` - /// Context: Receiver status: approved, waiting for the download to complete. - /// - /// - en: Approved — waiting for completion - /// - fr: Approuvé — en attente de la fin - /// - es: Aprobado: esperando a que se complete - /// - it: Approvato: in attesa del completamento - /// - de: Genehmigt – wartet auf Abschluss - /// - pt: Aprovado — a aguardar conclusão - /// - pl: Zatwierdzono — oczekiwanie na ukończenie - /// - nl: Goedgekeurd — wachten op voltooiing - /// - ru: Одобрено — ожидание завершения - static let receiverAccepted: String.LocalizationValue = "transfer_receiver_accepted" - /// Received successfully - /// - /// Key: `transfer_receiver_completed` - /// Context: Receiver status: received successfully. - /// - /// - en: Received successfully - /// - fr: Reçu avec succès - /// - es: Recibido correctamente - /// - it: Ricevuto correttamente - /// - de: Erfolgreich empfangen - /// - pt: Recebido com sucesso - /// - pl: Odebrano pomyślnie - /// - nl: Succesvol ontvangen - /// - ru: Успешно получено - static let receiverCompleted: String.LocalizationValue = "transfer_receiver_completed" - /// Request expired - /// - /// Key: `transfer_receiver_expired` - /// Context: Receiver status: the request expired. - /// - /// - en: Request expired - /// - fr: Demande expirée - /// - es: Solicitud caducada - /// - it: Richiesta scaduta - /// - de: Anfrage abgelaufen - /// - pt: Pedido expirado - /// - pl: Prośba wygasła - /// - nl: Verzoek verlopen - /// - ru: Запрос истёк - static let receiverExpired: String.LocalizationValue = "transfer_receiver_expired" - /// Request refused - /// - /// Key: `transfer_receiver_refused` - /// Context: Receiver status: the request was refused. - /// - /// - en: Request refused - /// - fr: Demande refusée - /// - es: Solicitud rechazada - /// - it: Richiesta rifiutata - /// - de: Anfrage abgelehnt - /// - pt: Pedido recusado - /// - pl: Prośba odrzucona - /// - nl: Verzoek geweigerd - /// - ru: Запрос отклонён - static let receiverRefused: String.LocalizationValue = "transfer_receiver_refused" - /// Waiting for your approval - /// - /// Key: `transfer_receiver_requested` - /// Context: Receiver status: waiting for the sender's approval. - /// - /// - en: Waiting for your approval - /// - fr: En attente de votre approbation - /// - es: Esperando su aprobación - /// - it: In attesa della sua approvazione - /// - de: Wartet auf Ihre Genehmigung - /// - pt: A aguardar a sua aprovação - /// - pl: Oczekiwanie na Twoje zatwierdzenie - /// - nl: Wachten op uw goedkeuring - /// - ru: Ожидание вашего одобрения - static let receiverRequested: String.LocalizationValue = "transfer_receiver_requested" - /// Status unavailable - /// - /// Key: `transfer_receiver_unknown` - /// Context: Receiver status: status unavailable/unknown. - /// - /// - en: Status unavailable - /// - fr: Statut indisponible - /// - es: Estado no disponible - /// - it: Stato non disponibile - /// - de: Status nicht verfügbar - /// - pt: Estado indisponível - /// - pl: Status niedostępny - /// - nl: Status niet beschikbaar - /// - ru: Статус недоступен - static let receiverUnknown: String.LocalizationValue = "transfer_receiver_unknown" - /// {count} completed - /// - /// Key: `transfer_receivers_completed_count` - /// Context: Receivers summary: how many receivers completed. {count} = completed receivers. - /// - /// - en: {count} completed - /// - fr: {count} terminés - /// - es: {count} completados - /// - it: {count} completati - /// - de: {count} abgeschlossen - /// - pt: {count} concluídos - /// - pl: Ukończone: {count} - /// - nl: {count} voltooid - /// - ru: Завершено: {count} - static func receiversCompletedCount(count: Int) -> String { - String(format: String(localized: "transfer_receivers_completed_count"), count) - } - /// Requests, approvals, and completed deliveries - /// - /// Key: `transfer_receivers_description` - /// Context: Transfer details: description under the Receivers section. - /// - /// - en: Requests, approvals, and completed deliveries - /// - fr: Demandes, approbations et livraisons terminées - /// - es: Solicitudes, aprobaciones y entregas completadas - /// - it: Richieste, approvazioni e consegne completate - /// - de: Anfragen, Genehmigungen und abgeschlossene Zustellungen - /// - pt: Pedidos, aprovações e entregas concluídas - /// - pl: Prośby, zatwierdzenia i ukończone dostawy - /// - nl: Verzoeken, goedkeuringen en voltooide leveringen - /// - ru: Запросы, одобрения и завершённые доставки - static let receiversDescription: String.LocalizationValue = "transfer_receivers_description" - /// {count} waiting - /// - /// Key: `transfer_receivers_pending` - /// Context: Receivers summary: how many requests are waiting. {count} = pending receivers. - /// - /// - en: {count} waiting - /// - fr: {count} en attente - /// - es: {count} en espera - /// - it: {count} in attesa - /// - de: {count} warten - /// - pt: {count} em espera - /// - pl: Oczekujące: {count} - /// - nl: {count} in behandeling - /// - ru: Ожидают: {count} - static func receiversPending(count: Int) -> String { - String(format: String(localized: "transfer_receivers_pending"), count) - } - /// Receivers - /// - /// Key: `transfer_receivers_title` - /// Context: Transfer details: Receivers section title. - /// - /// - en: Receivers - /// - fr: Destinataires - /// - es: Destinatarios - /// - it: Destinatari - /// - de: Empfänger - /// - pt: Destinatários - /// - pl: Odbiorcy - /// - nl: Ontvangers - /// - ru: Получатели - static let receiversTitle: String.LocalizationValue = "transfer_receivers_title" - /// Scan with VniDrop to receive this transfer - /// - /// Key: `transfer_scan_qr` - /// Context: Transfer share: caption under the QR code. - /// - /// - en: Scan with VniDrop to receive this transfer - /// - fr: Scannez avec VniDrop pour recevoir ce transfert - /// - es: Escanee con VniDrop para recibir esta transferencia - /// - it: Scansioni con VniDrop per ricevere questo trasferimento - /// - de: Mit VniDrop scannen, um diese Übertragung zu empfangen - /// - pt: Leia com o VniDrop para receber esta transferência - /// - pl: Zeskanuj za pomocą VniDrop, aby odebrać ten transfer - /// - nl: Scan met VniDrop om deze overdracht te ontvangen - /// - ru: Отсканируйте с помощью VniDrop, чтобы получить эту передачу - static let scanQr: String.LocalizationValue = "transfer_scan_qr" - /// QR code, invitation file, and nearby options - /// - /// Key: `transfer_share_description` - /// Context: Transfer details: description under the Share section. - /// - /// - en: QR code, invitation file, and nearby options - /// - fr: QR code, fichier d’invitation et options à proximité - /// - es: Código QR, archivo de invitación y opciones cercanas - /// - it: Codice QR, file di invito e opzioni nelle vicinanze - /// - de: QR-Code, Einladungsdatei und Optionen in der Nähe - /// - pt: Código QR, ficheiro de convite e opções por perto - /// - pl: Kod QR, plik zaproszenia i opcje w pobliżu - /// - nl: QR-code, uitnodigingsbestand en opties in de buurt - /// - ru: QR-код, файл приглашения и варианты поблизости - static let shareDescription: String.LocalizationValue = "transfer_share_description" - /// Share - /// - /// Key: `transfer_share_title` - /// Context: Transfer details: Share section title. - /// - /// - en: Share - /// - fr: Partager - /// - es: Compartir - /// - it: Condividi - /// - de: Teilen - /// - pt: Partilhar - /// - pl: Udostępnij - /// - nl: Delen - /// - ru: Поделиться - static let shareTitle: String.LocalizationValue = "transfer_share_title" - } - enum Value { - /// Not available - /// - /// Key: `value_unavailable` - /// Context: Placeholder shown when a device-info or metadata value can't be read. - /// - /// - en: Not available - /// - fr: Non disponible - /// - es: No disponible - /// - it: Non disponibile - /// - de: Nicht verfügbar - /// - pt: Indisponível - /// - pl: Niedostępne - /// - nl: Niet beschikbaar - /// - ru: Недоступно - static let unavailable: String.LocalizationValue = "value_unavailable" - } - enum Version { - /// App version - /// - /// Key: `version_title` - /// Context: Device information / Settings row: app version label. - /// - /// - en: App version - /// - fr: Version de l’app - /// - es: Versión de la app - /// - it: Versione dell’app - /// - de: App-Version - /// - pt: Versão da app - /// - pl: Wersja aplikacji - /// - nl: App-versie - /// - ru: Версия приложения - static let title: String.LocalizationValue = "version_title" - } -} diff --git a/apple/VniDrop/Resources/Localizable.xcstrings b/apple/VniDrop/Resources/Localizable.xcstrings deleted file mode 100644 index 4b811be..0000000 --- a/apple/VniDrop/Resources/Localizable.xcstrings +++ /dev/null @@ -1,15678 +0,0 @@ -{ - "sourceLanguage": "en", - "strings": { - "about_bug_report": { - "comment": "About screen: button/link that opens the bug report form.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Fehler melden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Report a bug" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Informar de un error" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Signaler un bug" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Segnala un bug" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Een fout melden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zgłoś błąd" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Comunicar um erro" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сообщить об ошибке" - } - } - } - }, - "about_description": { - "comment": "About screen: intro paragraph describing what VniDrop does.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop überträgt Dateien und Ordner direkt von einem Gerät auf ein anderes über das Netzwerk, ohne sie auf einen Hosting-Dienst hochzuladen. Es muss kein Konto erstellt werden, und nach Abschluss verbleibt keine Kopie Ihrer Übertragung in der Cloud." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop moves files and folders straight from one device to another over the network, without uploading them to any file-hosting service. There’s no account to create, and no copy of your transfer is left in the cloud once you’re done." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop transfiere archivos y carpetas directamente de un dispositivo a otro a través de la red, sin subirlos a ningún servicio de alojamiento. No hay que crear ninguna cuenta y no queda ninguna copia de su transferencia en la nube una vez que termina." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop transfère fichiers et dossiers directement d’un appareil à un autre sur le réseau, sans les téléverser vers un service d’hébergement. Aucun compte à créer, et aucune copie de votre transfert ne reste dans le cloud une fois terminé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop trasferisce file e cartelle direttamente da un dispositivo a un altro sulla rete, senza caricarli su alcun servizio di hosting. Non c’è alcun account da creare e nessuna copia del suo trasferimento rimane nel cloud una volta terminato." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop verplaatst bestanden en mappen rechtstreeks van het ene apparaat naar het andere via het netwerk, zonder ze naar een hostingdienst te uploaden. U hoeft geen account aan te maken en er blijft geen kopie van uw overdracht in de cloud achter zodra u klaar bent." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop przesyła pliki i foldery bezpośrednio z jednego urządzenia na drugie przez sieć, bez przesyłania ich do jakiejkolwiek usługi hostingowej. Nie trzeba zakładać konta, a po zakończeniu żadna kopia transferu nie pozostaje w chmurze." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O VniDrop transfere ficheiros e pastas diretamente de um dispositivo para outro através da rede, sem os carregar para qualquer serviço de alojamento. Não é necessário criar qualquer conta e não fica nenhuma cópia da sua transferência na nuvem depois de terminar." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop передаёт файлы и папки напрямую с одного устройства на другое по сети, не загружая их в какой-либо хостинг-сервис. Не нужно создавать учётную запись, и после завершения ни одна копия вашей передачи не остаётся в облаке." - } - } - } - }, - "about_is_direct": { - "comment": "About screen, 'What VniDrop is' list: point about direct device-to-device transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Eine direkte Gerät-zu-Gerät-Übertragung – Ihre Dateien gehen direkt an den Empfänger." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "A direct device-to-device transfer — your files go straight to the receiver." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Una transferencia directa entre dispositivos: sus archivos van directamente al destinatario." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Un transfert direct d’appareil à appareil — vos fichiers vont droit au destinataire." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Un trasferimento diretto da dispositivo a dispositivo: i suoi file vanno direttamente al destinatario." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Een directe overdracht van apparaat naar apparaat — uw bestanden gaan rechtstreeks naar de ontvanger." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Bezpośredni transfer między urządzeniami — pliki trafiają prosto do odbiorcy." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Uma transferência direta entre dispositivos — os seus ficheiros vão diretamente para o destinatário." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Прямая передача с устройства на устройство — ваши файлы попадают напрямую к получателю." - } - } - } - }, - "about_is_encrypted": { - "comment": "About screen, 'What VniDrop is' list: point about encrypted, verified connections.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verbindungen sind authentifiziert und Ende-zu-Ende-verschlüsselt (über Iroh), und eingehende Dateien werden anhand ihres Inhalts-Hashs überprüft." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Connections are authenticated and end-to-end encrypted (via Iroh), and incoming files are verified by their content hash." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Las conexiones están autenticadas y cifradas de extremo a extremo (mediante Iroh), y los archivos entrantes se verifican por su huella de contenido." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Les connexions sont authentifiées et chiffrées de bout en bout (via Iroh), et les fichiers entrants sont vérifiés par leur empreinte de contenu." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Le connessioni sono autenticate e cifrate end-to-end (tramite Iroh) e i file in arrivo vengono verificati tramite l’impronta del loro contenuto." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verbindingen zijn geverifieerd en end-to-end versleuteld (via Iroh), en binnenkomende bestanden worden gecontroleerd aan de hand van hun inhouds-hash." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Połączenia są uwierzytelniane i szyfrowane od końca do końca (przez Iroh), a przychodzące pliki są weryfikowane na podstawie skrótu ich zawartości." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "As ligações são autenticadas e cifradas ponto a ponto (através do Iroh), e os ficheiros recebidos são verificados pela impressão digital do seu conteúdo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Соединения аутентифицированы и зашифрованы сквозным шифрованием (через Iroh), а входящие файлы проверяются по хешу их содержимого." - } - } - } - }, - "about_is_in_control": { - "comment": "About screen, 'What VniDrop is' list: point about controlling who receives.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Sie entscheiden, wer empfängt: Genehmigen Sie jede Anfrage oder öffnen Sie eine Übertragung für alle, die die Einladung besitzen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "You decide who receives: approve each request, or open a transfer to anyone holding the invitation." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Usted decide quién recibe: apruebe cada solicitud o abra una transferencia a cualquiera que tenga la invitación." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Vous décidez qui reçoit : approuvez chaque demande, ou ouvrez un transfert à toute personne disposant de l’invitation." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Lei decide chi riceve: approvi ogni richiesta oppure apra un trasferimento a chiunque disponga dell’invito." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "U bepaalt wie ontvangt: keur elk verzoek goed, of stel een overdracht open voor iedereen met de uitnodiging." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "To Ty decydujesz, kto otrzymuje: zatwierdź każde żądanie lub udostępnij transfer każdemu, kto ma zaproszenie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "É você quem decide quem recebe: aprove cada pedido ou abra uma transferência a qualquer pessoa que tenha o convite." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Вы решаете, кто получает: одобряйте каждый запрос или откройте передачу всем, у кого есть приглашение." - } - } - } - }, - "about_is_no_account": { - "comment": "About screen, 'What VniDrop is' list: point about being account-free.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ohne Konto – es gibt nichts zu registrieren." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Account-free — there’s nothing to sign up for." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Sin cuenta: no hay nada que registrar." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Sans compte — il n’y a rien à créer." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Senza account: non c’è nulla da registrare." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Zonder account — er is niets om aan te melden." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Bez konta — nie trzeba się rejestrować." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Sem conta — não há nada para registar." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Без учётной записи — регистрироваться не нужно." - } - } - } - }, - "about_is_open": { - "comment": "About screen, 'What VniDrop is' list: point about being open source.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Open Source, veröffentlicht unter der Apache-2.0-Lizenz." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Open source, released under the Apache 2.0 license." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Código abierto, publicado bajo la licencia Apache 2.0." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Open source, publié sous licence Apache 2.0." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Open source, rilasciato con licenza Apache 2.0." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Opensource, uitgebracht onder de Apache 2.0-licentie." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Otwarte oprogramowanie, wydane na licencji Apache 2.0." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Código aberto, publicado sob a licença Apache 2.0." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Открытый исходный код, распространяется по лицензии Apache 2.0." - } - } - } - }, - "about_is_title": { - "comment": "About screen: section heading for the 'What VniDrop is' list.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Was VniDrop ist" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "What VniDrop is" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Qué es VniDrop" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Ce qu’est VniDrop" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cos’è VniDrop" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Wat VniDrop is" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Czym jest VniDrop" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O que o VniDrop é" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Что такое VniDrop" - } - } - } - }, - "about_isnt_cloud": { - "comment": "About screen, 'What VniDrop isn't' list: point about not being cloud storage.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Kein Cloud-Speicher – kein Server hält Ihre Dateien, und nach einer Übertragung wartet nichts in der Cloud." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Not cloud storage — no server holds your files, and nothing waits in the cloud after a transfer." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No es almacenamiento en la nube: ningún servidor guarda sus archivos y nada queda en la nube después de una transferencia." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Pas un stockage cloud — aucun serveur ne détient vos fichiers, et rien n’attend dans le cloud après un transfert." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non è un’archiviazione cloud: nessun server conserva i suoi file e nulla resta nel cloud dopo un trasferimento." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geen cloudopslag — geen enkele server bewaart uw bestanden en er wacht niets in de cloud na een overdracht." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "To nie magazyn w chmurze — żaden serwer nie przechowuje Twoich plików i nic nie pozostaje w chmurze po transferze." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não é armazenamento na nuvem — nenhum servidor guarda os seus ficheiros e nada fica na nuvem após uma transferência." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Это не облачное хранилище — ни один сервер не хранит ваши файлы, и после передачи в облаке ничего не остаётся." - } - } - } - }, - "about_isnt_public": { - "comment": "About screen, 'What VniDrop isn't' list: point about invitations not being public broadcasts.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Keine öffentliche Übertragung – eine Einladung ist ein privater Zugangslink, keine Ankündigung an alle in der Nähe." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Not public broadcasting — an invitation is a private access link, not an announcement to everyone nearby." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No es difusión pública: una invitación es un enlace de acceso privado, no un anuncio para todos los que estén cerca." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Pas une diffusion publique — une invitation est un lien d’accès privé, pas une annonce à tout le voisinage." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non è una diffusione pubblica: un invito è un link di accesso privato, non un annuncio a tutti nelle vicinanze." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geen openbare uitzending — een uitnodiging is een privétoegangslink, geen aankondiging aan iedereen in de buurt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "To nie publiczne rozgłaszanie — zaproszenie to prywatny link dostępu, a nie ogłoszenie dla wszystkich w pobliżu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não é difusão pública — um convite é uma ligação de acesso privado, não um anúncio a toda a gente por perto." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Это не публичная рассылка — приглашение является частной ссылкой доступа, а не объявлением для всех поблизости." - } - } - } - }, - "about_isnt_sync": { - "comment": "About screen, 'What VniDrop isn't' list: point about not being sync/backup.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Kein Synchronisierungs- oder Backup-Dienst." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Not a sync or backup service." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No es un servicio de sincronización ni de copia de seguridad." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Pas un service de synchronisation ou de sauvegarde." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non è un servizio di sincronizzazione o di backup." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geen synchronisatie- of back-updienst." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "To nie usługa synchronizacji ani kopii zapasowej." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não é um serviço de sincronização ou de cópia de segurança." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Это не служба синхронизации или резервного копирования." - } - } - } - }, - "about_isnt_title": { - "comment": "About screen: section heading for the 'What VniDrop isn't' list.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Was VniDrop nicht ist" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "What VniDrop isn’t" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Qué no es VniDrop" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Ce que VniDrop n’est pas" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cosa non è VniDrop" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Wat VniDrop niet is" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Czym VniDrop nie jest" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O que o VniDrop não é" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Чем VniDrop не является" - } - } - } - }, - "about_license_label": { - "comment": "About screen: label for the license row.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Lizenz" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "License" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Licencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Licence" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Licenza" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Licentie" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Licencja" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Licença" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Лицензия" - } - } - } - }, - "about_privacy": { - "comment": "About screen: link to the privacy policy.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Datenschutzrichtlinie" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Privacy policy" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Política de privacidad" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Politique de confidentialité" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Informativa sulla privacy" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Privacybeleid" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Polityka prywatności" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Política de privacidade" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Политика конфиденциальности" - } - } - } - }, - "about_privacy_capability": { - "comment": "About screen, Privacy & security list: point that invitations are capabilities to share carefully.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einladungen sind Zugangsschlüssel. Behandeln Sie einen QR-Code, ein NFC-Tag oder eine .vnd-Datei wie einen privaten Zugangslink und teilen Sie ihn nur mit den vorgesehenen Personen – insbesondere bei „Jeder mit dieser Übertragung“." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Invitations are capabilities. Treat a QR code, NFC tag, or .vnd file like a private access link and share it only with the people you intend — especially with “Anyone with this transfer.”" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Las invitaciones son claves de acceso. Trate un código QR, una etiqueta NFC o un archivo .vnd como un enlace de acceso privado y compártalo solo con las personas previstas, especialmente con «Cualquiera que tenga esta transferencia»." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Les invitations sont des clés d’accès. Traitez un QR code, un tag NFC ou un fichier .vnd comme un lien d’accès privé et ne le partagez qu’avec les personnes visées — en particulier avec « Toute personne disposant de ce transfert »." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Gli inviti sono chiavi di accesso. Tratti un codice QR, un tag NFC o un file .vnd come un link di accesso privato e lo condivida solo con le persone previste, soprattutto con «Chiunque abbia questo trasferimento»." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uitnodigingen zijn toegangssleutels. Behandel een QR-code, NFC-tag of .vnd-bestand als een privétoegangslink en deel deze alleen met de bedoelde personen — vooral bij ‘Iedereen met deze overdracht’." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zaproszenia są kluczami dostępu. Traktuj kod QR, tag NFC lub plik .vnd jak prywatny link dostępu i udostępniaj go tylko zamierzonym osobom — zwłaszcza przy opcji „Każdy, kto ma ten transfer”." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Os convites são chaves de acesso. Trate um código QR, uma etiqueta NFC ou um ficheiro .vnd como uma ligação de acesso privado e partilhe-o apenas com as pessoas pretendidas — sobretudo com «Qualquer pessoa com esta transferência»." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Приглашения — это ключи доступа. Относитесь к QR-коду, NFC-метке или файлу .vnd как к частной ссылке доступа и делитесь ими только с теми, для кого они предназначены, особенно при варианте «Любой, у кого есть эта передача»." - } - } - } - }, - "about_privacy_deny": { - "comment": "About screen, Privacy & security list: point about denying unknown requests by default.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Standardmäßig ablehnen – VniDrop stellt nur den Inhalt einer aktiven Freigabe bereit und weist unbekannte Anfragen ab." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Deny by default — VniDrop serves only the content of an active share and rejects unknown requests." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Denegación por defecto: VniDrop solo sirve el contenido de un recurso compartido activo y rechaza las solicitudes desconocidas." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Refus par défaut — VniDrop ne sert que le contenu d’un partage actif et rejette les demandes inconnues." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Rifiuto predefinito: VniDrop serve solo il contenuto di una condivisione attiva e rifiuta le richieste sconosciute." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Standaard weigeren — VniDrop levert alleen de inhoud van een actieve deling en wijst onbekende verzoeken af." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Domyślnie odmowa — VniDrop udostępnia tylko zawartość aktywnego udostępnienia i odrzuca nieznane żądania." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Recusa por predefinição — o VniDrop apenas fornece o conteúdo de uma partilha ativa e rejeita pedidos desconhecidos." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отказ по умолчанию — VniDrop предоставляет только содержимое активной передачи и отклоняет неизвестные запросы." - } - } - } - }, - "about_privacy_local": { - "comment": "About screen, Privacy & security list: point about received files staying local and never overwriting.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangene Dateien werden auf Ihrem Gerät gespeichert und überschreiben niemals eine vorhandene Datei." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Received files are saved on your device and never silently overwrite an existing file." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Los archivos recibidos se guardan en su dispositivo y nunca sobrescriben un archivo existente." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Les fichiers reçus sont enregistrés sur votre appareil et n’écrasent jamais un fichier existant." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "I file ricevuti vengono salvati sul suo dispositivo e non sovrascrivono mai un file esistente." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen bestanden worden op uw apparaat bewaard en overschrijven nooit een bestaand bestand." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odebrane pliki są zapisywane na Twoim urządzeniu i nigdy nie nadpisują istniejącego pliku." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Os ficheiros recebidos são guardados no seu dispositivo e nunca substituem um ficheiro existente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Полученные файлы сохраняются на вашем устройстве и никогда не перезаписывают существующий файл." - } - } - } - }, - "about_privacy_policy_label": { - "comment": "About screen: label for the privacy policy row.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Datenschutzrichtlinie" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Privacy policy" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Política de privacidad" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Politique de confidentialité" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Informativa sulla privacy" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Privacybeleid" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Polityka prywatności" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Política de privacidade" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Политика конфиденциальности" - } - } - } - }, - "about_privacy_relay": { - "comment": "About screen, Privacy & security list: point explaining encrypted relays.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wenn zwei Geräte sich nicht direkt verbinden können, kann die verschlüsselte Verbindung weitergeleitet werden. Relays leiten nur verschlüsselte Pakete weiter; sie speichern niemals Ihre Dateien." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "If two devices can’t connect directly, the encrypted connection may be relayed. Relays forward encrypted packets only; they never store your files." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Si dos dispositivos no pueden conectarse directamente, la conexión cifrada puede retransmitirse. Los relés solo reenvían paquetes cifrados; nunca almacenan sus archivos." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Si deux appareils ne peuvent pas se connecter directement, la connexion chiffrée peut être relayée. Les relais ne transmettent que des paquets chiffrés ; ils ne stockent jamais vos fichiers." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Se due dispositivi non riescono a connettersi direttamente, la connessione cifrata può essere instradata tramite relay. I relay inoltrano solo pacchetti cifrati; non memorizzano mai i suoi file." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Als twee apparaten niet rechtstreeks verbinding kunnen maken, kan de versleutelde verbinding via een relay worden doorgestuurd. Relays sturen alleen versleutelde pakketten door; ze slaan uw bestanden nooit op." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Jeśli dwa urządzenia nie mogą połączyć się bezpośrednio, szyfrowane połączenie może być przekazywane przez przekaźnik. Przekaźniki przekazują tylko zaszyfrowane pakiety; nigdy nie przechowują Twoich plików." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Se dois dispositivos não conseguirem ligar-se diretamente, a ligação cifrada pode ser retransmitida. Os retransmissores apenas encaminham pacotes cifrados; nunca guardam os seus ficheiros." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Если два устройства не могут соединиться напрямую, зашифрованное соединение может передаваться через ретранслятор. Ретрансляторы пересылают только зашифрованные пакеты; они никогда не хранят ваши файлы." - } - } - } - }, - "about_privacy_title": { - "comment": "About screen: section heading for the Privacy & security list.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Datenschutz & Sicherheit" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Privacy & security" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Privacidad y seguridad" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Confidentialité et sécurité" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Privacy e sicurezza" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Privacy en beveiliging" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Prywatność i bezpieczeństwo" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Privacidade e segurança" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Конфиденциальность и безопасность" - } - } - } - }, - "about_tagline": { - "comment": "About screen: short tagline under the app name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Senden Sie Dateien direkt. Behalten Sie die Kontrolle darüber, wer sie empfängt." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Send files directly. Stay in control of who receives them." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Envíe archivos directamente. Mantenga el control de quién los recibe." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoyez des fichiers directement. Gardez le contrôle de qui les reçoit." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invii file direttamente. Mantenga il controllo su chi li riceve." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verstuur bestanden rechtstreeks. Houd controle over wie ze ontvangt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wysyłaj pliki bezpośrednio. Zachowaj kontrolę nad tym, kto je otrzymuje." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Envie ficheiros diretamente. Mantenha o controlo sobre quem os recebe." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправляйте файлы напрямую. Сохраняйте контроль над тем, кто их получает." - } - } - } - }, - "about_title": { - "comment": "About screen: navigation/screen title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Über" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "About" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Acerca de" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "À propos" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Informazioni" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Over" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Informacje" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Acerca de" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "О приложении" - } - } - } - }, - "appearance_auto_description": { - "comment": "Settings > Appearance: description for the System/auto option.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Der hellen oder dunklen Darstellung dieses Geräts folgen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Match this device’s light or dark appearance." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Seguir la apariencia clara u oscura de este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Suivre l’apparence claire ou sombre de cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Segue l’aspetto chiaro o scuro di questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "De lichte of donkere weergave van dit apparaat volgen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Dopasuj do jasnego lub ciemnego wyglądu tego urządzenia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Acompanhar o aspeto claro ou escuro deste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Следовать светлому или тёмному оформлению этого устройства." - } - } - } - }, - "appearance_dark_mode": { - "comment": "Settings > Appearance: label for the dark theme option.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Dunkelmodus" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Dark mode" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Modo oscuro" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Mode sombre" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Modalità scura" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Donkere modus" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Tryb ciemny" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Modo escuro" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Тёмный режим" - } - } - } - }, - "appearance_light_mode": { - "comment": "Settings > Appearance: label for the light theme option.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Hellmodus" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Light mode" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Modo claro" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Mode clair" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Modalità chiara" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Lichte modus" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Tryb jasny" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Modo claro" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Светлый режим" - } - } - } - }, - "appearance_system_mode": { - "comment": "Settings > Appearance: label for the follow-system option.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "System" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "System" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Sistema" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Système" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Sistema" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Systeem" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "System" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Sistema" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Системный" - } - } - } - }, - "appearance_title": { - "comment": "Settings > Appearance: section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Darstellung" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Appearance" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Apariencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Apparence" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Aspetto" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Weergave" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wygląd" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Aspeto" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Оформление" - } - } - } - }, - "approval_connection_request": { - "comment": "Approval prompt: title when a receiver requests to download a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangsanfrage" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receive request" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Solicitud de recepción" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Demande de réception" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Richiesta di ricezione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangstverzoek" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Prośba o odbiór" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pedido de receção" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Запрос на получение" - } - } - } - }, - "approval_endpoint_id": { - "comment": "Approval prompt: shows the requesting device's ID. {deviceId} = device/endpoint identifier.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Geräte-ID: %1$@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Device ID: %1$@" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "ID del dispositivo: %1$@" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Identifiant de l’appareil : %1$@" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "ID dispositivo: %1$@" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Apparaat-ID: %1$@" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Identyfikator urządzenia: %1$@" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "ID do dispositivo: %1$@" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Идентификатор устройства: %1$@" - } - } - } - }, - "approval_nearby_device": { - "comment": "Approval prompt: fallback label for a requester with no display name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ein Gerät in der Nähe" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "A nearby device" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Un dispositivo cercano" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Un appareil à proximité" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Un dispositivo nelle vicinanze" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Een apparaat in de buurt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Urządzenie w pobliżu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Um dispositivo próximo" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Устройство поблизости" - } - } - } - }, - "approval_pending_count": { - "comment": "Send/transfer list: badge showing how many receive requests are awaiting approval. {count} = pending requests.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d Anfragen warten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$d requests waiting" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d solicitudes en espera" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d demandes en attente" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d richieste in attesa" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d verzoeken in behandeling" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Oczekujące prośby: %1$d" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d pedidos em espera" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Ожидающих запросов: %1$d" - } - } - } - }, - "approval_request_body": { - "comment": "Approval prompt body: '{receiver} wants to receive \"{transferName}\".' {receiver} = requester name, {transferName} = transfer name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ möchte „%2$@“ empfangen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@ wants to receive “%2$@”." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ quiere recibir «%2$@»." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ souhaite recevoir « %2$@ »." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ vuole ricevere «%2$@»." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ wil ‘%2$@’ ontvangen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ chce odebrać „%2$@”." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ quer receber «%2$@»." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ хочет получить «%2$@»." - } - } - } - }, - "battery_level_title": { - "comment": "Device information row: battery level label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Batteriestand" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Battery level" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nivel de batería" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Niveau de batterie" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Livello batteria" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Batterijniveau" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Poziom baterii" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nível da bateria" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Уровень заряда" - } - } - } - }, - "battery_level_value": { - "comment": "Device information: battery charge formatted as a percentage. {level} = integer percent value.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %%" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@%%" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %%" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %%" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@%%" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@%%" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@%%" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@%%" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %%" - } - } - } - }, - "bug_report_contact_hint": { - "comment": "Bug report form: placeholder text in the contact email field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "name@beispiel.com" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "name@example.com" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "nombre@ejemplo.com" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "nom@exemple.com" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "nome@esempio.com" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "naam@voorbeeld.com" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "nazwa@przyklad.com" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "nome@exemplo.com" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "имя@пример.com" - } - } - } - }, - "bug_report_contact_label": { - "comment": "Bug report form: label for the optional contact email field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Kontakt-E-Mail (optional)" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Contact email (optional)" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Correo de contacto (opcional)" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "E-mail de contact (facultatif)" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Email di contatto (facoltativa)" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Contact-e-mail (optioneel)" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "E-mail kontaktowy (opcjonalnie)" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "E-mail de contacto (opcional)" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Контактный e-mail (необязательно)" - } - } - } - }, - "bug_report_description": { - "comment": "Bug report form: intro text explaining what gets attached.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Sagen Sie uns, was schiefgelaufen ist. Wir hängen Geräteinformationen und optional aktuelle Protokolle an (sensible Werte werden geschwärzt)." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Tell us what went wrong. We attach device info and optional recent logs (with sensitive values redacted)." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cuéntenos qué salió mal. Adjuntamos información del dispositivo y, opcionalmente, registros recientes (con los valores sensibles ocultos)." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Dites-nous ce qui s’est passé. Nous joignons les infos de l’appareil et, en option, les journaux récents (valeurs sensibles masquées)." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ci dica cosa è andato storto. Alleghiamo le informazioni sul dispositivo e, facoltativamente, i log recenti (con i valori sensibili oscurati)." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Vertel ons wat er misging. We voegen apparaatgegevens toe en optioneel recente logbestanden (met gevoelige waarden onleesbaar gemaakt)." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Napisz, co poszło nie tak. Dołączamy informacje o urządzeniu i opcjonalnie ostatnie dzienniki (z ukrytymi wrażliwymi wartościami)." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Diga-nos o que correu mal. Anexamos informações do dispositivo e, opcionalmente, registos recentes (com os valores sensíveis ocultados)." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Расскажите, что пошло не так. Мы прикладываем сведения об устройстве и, при желании, недавние журналы (конфиденциальные значения скрыты)." - } - } - } - }, - "bug_report_device_section": { - "comment": "Bug report form: heading for the attached device information section.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Geräteinformationen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Device information" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Información del dispositivo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Informations sur l’appareil" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Informazioni sul dispositivo" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Apparaatgegevens" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Informacje o urządzeniu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Informações do dispositivo" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сведения об устройстве" - } - } - } - }, - "bug_report_expected_hint": { - "comment": "Bug report form: placeholder in the 'what did you expect' field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Beschreiben Sie, was Sie erwartet haben" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Describe what you expected to happen" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Describa lo que esperaba que ocurriera" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Décrivez ce à quoi vous vous attendiez" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Descriva cosa si aspettava che accadesse" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Beschrijf wat u verwachtte" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Opisz oczekiwane zachowanie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Descreva o que esperava que acontecesse" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Опишите, что вы ожидали" - } - } - } - }, - "bug_report_expected_label": { - "comment": "Bug report form: label for the expected-behavior field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Was haben Sie erwartet?" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "What did you expect?" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "¿Qué esperaba?" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "À quoi vous attendiez-vous ?" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cosa si aspettava?" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Wat verwachtte u?" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Czego oczekiwano?" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O que esperava?" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Что вы ожидали?" - } - } - } - }, - "bug_report_include_logs": { - "comment": "Bug report form: toggle label to attach recent logs.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Aktuelle Protokolle einschließen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Include recent logs" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Incluir registros recientes" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Inclure les journaux récents" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Includi i log recenti" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Recente logbestanden meesturen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Dołącz ostatnie dzienniki" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Incluir registos recentes" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Включить недавние журналы" - } - } - } - }, - "bug_report_include_logs_description": { - "comment": "Bug report form: explanation under the include-logs toggle.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Hilft uns, das Problem zu diagnostizieren. Sensible Werte werden vor dem Senden geschwärzt." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Helps us diagnose the issue. Sensitive values are redacted before sending." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nos ayuda a diagnosticar el problema. Los valores sensibles se ocultan antes de enviarlos." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nous aide à diagnostiquer le problème. Les valeurs sensibles sont masquées avant l’envoi." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ci aiuta a diagnosticare il problema. I valori sensibili vengono oscurati prima dell’invio." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Helpt ons het probleem te diagnosticeren. Gevoelige waarden worden vóór verzending onleesbaar gemaakt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pomaga nam zdiagnozować problem. Wrażliwe wartości są ukrywane przed wysłaniem." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ajuda-nos a diagnosticar o problema. Os valores sensíveis são ocultados antes do envio." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Помогает нам диагностировать проблему. Конфиденциальные значения скрываются перед отправкой." - } - } - } - }, - "bug_report_logs_size": { - "comment": "Bug report form: label showing the size of the log attachment.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Größe des Protokollanhangs" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Log attachment size" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Tamaño del archivo de registros" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Taille de la pièce jointe des journaux" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dimensione dell’allegato dei log" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Grootte van logbijlage" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Rozmiar załącznika z dziennikami" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Tamanho do anexo de registos" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Размер вложения с журналами" - } - } - } - }, - "bug_report_missing_expected": { - "comment": "Bug report form: validation error when the expected-behavior field is empty.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bitte beschreiben Sie, was Sie erwartet haben." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Please describe what you expected." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Describa lo que esperaba." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Veuillez décrire ce à quoi vous vous attendiez." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Descriva cosa si aspettava." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Beschrijf wat u verwachtte." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Opisz oczekiwane zachowanie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Descreva o que esperava." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Опишите, что вы ожидали." - } - } - } - }, - "bug_report_missing_what": { - "comment": "Bug report form: validation error when the what-happened field is empty.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bitte beschreiben Sie, was passiert ist." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Please describe what happened." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Describa lo que ocurrió." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Veuillez décrire ce qui s’est passé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Descriva cosa è accaduto." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Beschrijf wat er gebeurde." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Opisz, co się stało." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Descreva o que aconteceu." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Опишите, что произошло." - } - } - } - }, - "bug_report_steps_hint": { - "comment": "Bug report form: placeholder in the steps-to-reproduce field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Listen Sie die Schritte zum Reproduzieren des Problems auf" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "List the steps to reproduce the issue" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enumere los pasos para reproducir el problema" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Listez les étapes pour reproduire le problème" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Elenchi i passaggi per riprodurre il problema" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Noem de stappen om het probleem te reproduceren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wymień kroki umożliwiające odtworzenie problemu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Enumere os passos para reproduzir o problema" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Перечислите шаги для воспроизведения проблемы" - } - } - } - }, - "bug_report_steps_label": { - "comment": "Bug report form: label for the optional steps-to-reproduce field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Schritte zum Reproduzieren (optional)" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Steps to reproduce (optional)" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Pasos para reproducir (opcional)" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Étapes pour reproduire (facultatif)" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Passaggi per riprodurre (facoltativi)" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Stappen om te reproduceren (optioneel)" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Kroki do odtworzenia (opcjonalnie)" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Passos para reproduzir (opcional)" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Шаги для воспроизведения (необязательно)" - } - } - } - }, - "bug_report_submit": { - "comment": "Bug report form: submit button.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bericht senden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Submit report" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar informe" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoyer le rapport" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invia segnalazione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Rapport versturen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wyślij zgłoszenie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar relatório" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправить отчёт" - } - } - } - }, - "bug_report_submit_failed": { - "comment": "Bug report form: error message when submission fails.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Der Fehlerbericht konnte nicht gesendet werden. Bitte versuchen Sie es später erneut." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Could not submit the bug report. Try again later." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudo enviar el informe de error. Inténtelo de nuevo más tarde." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Impossible d’envoyer le rapport de bug. Réessayez plus tard." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impossibile inviare la segnalazione. Riprovi più tardi." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Het foutrapport kon niet worden verstuurd. Probeer het later opnieuw." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się wysłać zgłoszenia błędu. Spróbuj ponownie później." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível enviar o relatório de erro. Tente novamente mais tarde." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось отправить отчёт об ошибке. Повторите попытку позже." - } - } - } - }, - "bug_report_submitted": { - "comment": "Bug report form: confirmation message after a successful submission.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Danke – Ihr Fehlerbericht wurde erfasst." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Thanks — your bug report was recorded." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Gracias: su informe de error se ha registrado." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Merci — votre rapport de bug a été enregistré." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Grazie: la sua segnalazione è stata registrata." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bedankt — uw foutrapport is vastgelegd." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Dziękujemy — Twoje zgłoszenie błędu zostało zapisane." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Obrigado — o seu relatório de erro foi registado." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Спасибо — ваш отчёт об ошибке записан." - } - } - } - }, - "bug_report_submitting": { - "comment": "Bug report form: progress label while the report is being sent.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird gesendet…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Submitting…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enviando…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoi…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invio in corso…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bezig met versturen…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wysyłanie…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A enviar…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправка…" - } - } - } - }, - "bug_report_what_hint": { - "comment": "Bug report form: placeholder in the what-happened field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Beschreiben Sie, was passiert ist" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Describe what happened" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Describa lo que ocurrió" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Décrivez ce qui s’est passé" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Descriva cosa è accaduto" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Beschrijf wat er gebeurde" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Opisz, co się stało" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Descreva o que aconteceu" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Опишите, что произошло" - } - } - } - }, - "bug_report_what_label": { - "comment": "Bug report form: label for the what-happened field.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Was ist passiert?" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "What happened?" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "¿Qué ocurrió?" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Que s’est-il passé ?" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cosa è accaduto?" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Wat is er gebeurd?" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Co się stało?" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O que aconteceu?" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Что произошло?" - } - } - } - }, - "button_approve": { - "comment": "Button: approve a receiver's request.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Genehmigen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Approve" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Aprobar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Approuver" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Approva" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Goedkeuren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zatwierdź" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Aprovar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Одобрить" - } - } - } - }, - "button_back": { - "comment": "Button: go back to the previous step/screen.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Zurück" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Back" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Atrás" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Retour" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Indietro" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Terug" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wstecz" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Voltar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Назад" - } - } - } - }, - "button_cancel": { - "comment": "Button: cancel the current action or dialog.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Abbrechen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cancel" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Annuler" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Annulla" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Annuleren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Anuluj" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отмена" - } - } - } - }, - "button_cancel_receive": { - "comment": "Button: cancel an in-progress receive.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Abbrechen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cancel" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Annuler" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Annulla" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Annuleren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Anuluj" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отмена" - } - } - } - }, - "button_change_files": { - "comment": "Button: change the selected files in the send flow.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Dateien ändern" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Change files" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cambiar archivos" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Modifier les fichiers" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cambia file" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bestanden wijzigen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zmień pliki" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Alterar ficheiros" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Изменить файлы" - } - } - } - }, - "button_choose_files": { - "comment": "Button: open the file picker to choose files to send.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Dateien auswählen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Choose files" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Elegir archivos" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Choisir des fichiers" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scegli file" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bestanden kiezen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybierz pliki" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Escolher ficheiros" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выбрать файлы" - } - } - } - }, - "button_choose_folder": { - "comment": "Button: open the folder picker (send selection or receive folder).", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ordner auswählen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Choose folder" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Elegir carpeta" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Choisir un dossier" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scegli cartella" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Map kiezen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybierz folder" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Escolher pasta" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выбрать папку" - } - } - } - }, - "button_clear": { - "comment": "Button: clear the current input or selection.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Löschen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Clear" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Borrar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Effacer" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cancella" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Wissen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wyczyść" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Limpar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Очистить" - } - } - } - }, - "button_close": { - "comment": "Button: close the current sheet/dialog.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Schließen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Close" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cerrar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Fermer" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Chiudi" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Sluiten" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zamknij" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Fechar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Закрыть" - } - } - } - }, - "button_create_new_transfer": { - "comment": "Button: start creating a new transfer (Send tab).", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Neue Übertragung" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "New transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nueva transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nouveau transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nuovo trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Nieuwe overdracht" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nowy transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nova transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Новая передача" - } - } - } - }, - "button_delete_transfer": { - "comment": "Button: delete a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung löschen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Delete transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminar transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Supprimer le transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Elimina trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht verwijderen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usuń transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminar transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удалить передачу" - } - } - } - }, - "button_download_invitation": { - "comment": "Button: save the invitation as a .vnd file.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": ".vnd-Datei sichern" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Save .vnd file" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Guardar archivo .vnd" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Enregistrer le fichier .vnd" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Salva file .vnd" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": ".vnd-bestand bewaren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zapisz plik .vnd" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Guardar ficheiro .vnd" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сохранить файл .vnd" - } - } - } - }, - "button_native_share": { - "comment": "Button: open the OS share sheet to share the invitation.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einladung teilen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Share invitation" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Compartir invitación" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Partager l’invitation" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Condividi invito" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uitnodiging delen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Udostępnij zaproszenie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Partilhar convite" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Поделиться приглашением" - } - } - } - }, - "button_open_settings": { - "comment": "Button: open the OS Settings app (e.g. for permissions).", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einstellungen öffnen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Open Settings" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Abrir Ajustes" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Ouvrir les Réglages" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Apri Impostazioni" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Instellingen openen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Otwórz Ustawienia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Abrir Definições" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Открыть Настройки" - } - } - } - }, - "button_receive": { - "comment": "Button: receive label (Receive action).", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receive" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Recibir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Recevoir" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ricevi" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbierz" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Receber" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получить" - } - } - } - }, - "button_receive_files": { - "comment": "Button: start receiving a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfang starten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Start receiving" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Empezar a recibir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Commencer à recevoir" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Inizia a ricevere" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen starten" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Rozpocznij odbieranie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Começar a receber" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Начать получение" - } - } - } - }, - "button_refuse": { - "comment": "Button: refuse a receiver's request.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ablehnen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Refuse" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Rechazar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Refuser" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Rifiuta" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Weigeren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odrzuć" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Recusar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отклонить" - } - } - } - }, - "button_remove_file": { - "comment": "Button: remove a single file from the send selection.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Datei entfernen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Remove file" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Quitar archivo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Retirer le fichier" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Rimuovi file" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bestand verwijderen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usuń plik" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Remover ficheiro" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удалить файл" - } - } - } - }, - "button_reset_default": { - "comment": "Button: reset a setting to its default value.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Standard verwenden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Use default" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Usar predeterminado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Valeur par défaut" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Usa predefinito" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Standaard gebruiken" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Użyj domyślnego" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Usar predefinição" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "По умолчанию" - } - } - } - }, - "button_retry": { - "comment": "Button: retry after a failed operation.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wiederholen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Retry" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Reintentar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Réessayer" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Riprova" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Opnieuw proberen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Spróbuj ponownie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Tentar novamente" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Повторить" - } - } - } - }, - "button_share_file": { - "comment": "Button: start sharing the transfer (make it available).", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Freigabe starten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Start sharing" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Empezar a compartir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Commencer le partage" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Inizia a condividere" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Delen starten" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Rozpocznij udostępnianie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Começar a partilhar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Начать общий доступ" - } - } - } - }, - "button_sharing_file": { - "comment": "Button: disabled/loading state while the transfer is being prepared.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung wird vorbereitet…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Preparing transfer…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Preparando la transferencia…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Préparation du transfert…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Preparazione del trasferimento…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht voorbereiden…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przygotowywanie transferu…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A preparar a transferência…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подготовка передачи…" - } - } - } - }, - "button_show_in_files": { - "comment": "Button: reveal a received file in the Files app.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "In „Dateien“ anzeigen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Show in Files" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Mostrar en Archivos" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Afficher dans Fichiers" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Mostra in File" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Toon in Bestanden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pokaż w Plikach" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Mostrar em Ficheiros" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Показать в Файлах" - } - } - } - }, - "button_write_nfc": { - "comment": "Button: write the invitation to an NFC tag.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Auf NFC-Tag schreiben" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Write to NFC tag" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Escribir en etiqueta NFC" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Écrire sur un tag NFC" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scrivi su tag NFC" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Naar NFC-tag schrijven" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zapisz na tagu NFC" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Escrever em etiqueta NFC" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Записать на NFC-метку" - } - } - } - }, - "device_model_title": { - "comment": "Device information row: device model label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Gerätemodell" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Device model" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Modelo del dispositivo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Modèle de l’appareil" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Modello del dispositivo" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Apparaatmodel" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Model urządzenia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Modelo do dispositivo" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Модель устройства" - } - } - } - }, - "device_name_title": { - "comment": "Device information row: device name label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Gerätename" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Device name" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nombre del dispositivo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nom de l’appareil" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nome del dispositivo" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Apparaatnaam" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nazwa urządzenia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nome do dispositivo" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Имя устройства" - } - } - } - }, - "diagnostics_description": { - "comment": "Settings > Diagnostics: explanation of what anonymous diagnostics collect.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Anonyme Absturzberichte und Nutzungsereignisse senden, damit wir VniDrop verbessern können. Sie können dies jederzeit deaktivieren. Einladungen, Dateipfade und Übertragungsinhalte werden niemals einbezogen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Send anonymous crash reports and usage events so we can improve VniDrop. You can turn this off anytime. Invitations, file paths, and transfer contents are never included." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar informes de fallos y eventos de uso anónimos para ayudarnos a mejorar VniDrop. Puede desactivarlo en cualquier momento. Las invitaciones, las rutas de archivos y el contenido de las transferencias nunca se incluyen." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoyer des rapports de plantage et des événements d’utilisation anonymes pour nous aider à améliorer VniDrop. Vous pouvez désactiver cela à tout moment. Les invitations, chemins de fichiers et contenus de transfert ne sont jamais inclus." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invia report di arresto anomalo ed eventi d’uso anonimi per aiutarci a migliorare VniDrop. Può disattivarlo in qualsiasi momento. Inviti, percorsi dei file e contenuti dei trasferimenti non vengono mai inclusi." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verstuur anonieme crashrapporten en gebruiksgebeurtenissen zodat we VniDrop kunnen verbeteren. U kunt dit op elk moment uitschakelen. Uitnodigingen, bestandspaden en overdrachtsinhoud worden nooit meegestuurd." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wysyłaj anonimowe raporty o awariach i zdarzenia użytkowania, aby pomóc nam ulepszać VniDrop. Możesz to wyłączyć w dowolnej chwili. Zaproszenia, ścieżki plików i zawartość transferów nigdy nie są dołączane." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar relatórios de falhas e eventos de utilização anónimos para nos ajudar a melhorar o VniDrop. Pode desativar isto a qualquer momento. Convites, caminhos de ficheiros e conteúdos das transferências nunca são incluídos." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправлять анонимные отчёты о сбоях и события использования, чтобы помочь нам улучшать VniDrop. Вы можете отключить это в любой момент. Приглашения, пути к файлам и содержимое передач никогда не включаются." - } - } - } - }, - "diagnostics_disabled_message": { - "comment": "Settings > Diagnostics: confirmation shown when diagnostics are turned off.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Die Freigabe von Diagnosedaten ist deaktiviert." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Diagnostics sharing is off." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "El uso compartido de diagnósticos está desactivado." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Le partage des diagnostics est désactivé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "La condivisione dei dati diagnostici è disattivata." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Het delen van diagnostische gegevens is uitgeschakeld." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Udostępnianie diagnostyki jest wyłączone." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A partilha de diagnósticos está desativada." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача диагностики отключена." - } - } - } - }, - "diagnostics_enabled_message": { - "comment": "Settings > Diagnostics: confirmation shown when diagnostics are turned on.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Die Freigabe von Diagnosedaten ist aktiviert." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Diagnostics sharing is on." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "El uso compartido de diagnósticos está activado." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Le partage des diagnostics est activé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "La condivisione dei dati diagnostici è attivata." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Het delen van diagnostische gegevens is ingeschakeld." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Udostępnianie diagnostyki jest włączone." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A partilha de diagnósticos está ativada." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача диагностики включена." - } - } - } - }, - "diagnostics_title": { - "comment": "Settings > Diagnostics: toggle title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Diagnosedaten teilen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Share diagnostics" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Compartir diagnósticos" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Partager les diagnostics" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Condividi dati diagnostici" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Diagnostische gegevens delen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Udostępniaj diagnostykę" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Partilhar diagnósticos" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Делиться диагностикой" - } - } - } - }, - "error_camera": { - "comment": "Error: camera permission is needed to scan a QR code.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Für das Scannen eines QR-Codes ist Kamerazugriff erforderlich." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Camera access is required to scan a QR code." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Se necesita acceso a la cámara para escanear un código QR." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "L’accès à la caméra est nécessaire pour scanner un QR code." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Per scansionare un codice QR è necessario l’accesso alla fotocamera." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Voor het scannen van een QR-code is toegang tot de camera vereist." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Do zeskanowania kodu QR wymagany jest dostęp do aparatu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "É necessário acesso à câmara para ler um código QR." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Для сканирования QR-кода требуется доступ к камере." - } - } - } - }, - "error_destination_exists": { - "comment": "Error: a received file would overwrite an existing destination file.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Am Ziel ist bereits eine Datei mit demselben Namen vorhanden. Wählen Sie einen anderen Ordner oder entfernen Sie die vorhandene Datei." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "A file with the same name already exists in the destination. Choose another folder or remove the existing file." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Ya existe un archivo con el mismo nombre en el destino. Elija otra carpeta o elimine el archivo existente." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Un fichier portant le même nom existe déjà dans la destination. Choisissez un autre dossier ou supprimez le fichier existant." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nella destinazione esiste già un file con lo stesso nome. Scelga un’altra cartella o rimuova il file esistente." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Er staat al een bestand met dezelfde naam in de doelmap. Kies een andere map of verwijder het bestaande bestand." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "W miejscu docelowym istnieje już plik o tej samej nazwie. Wybierz inny folder lub usuń istniejący plik." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Já existe um ficheiro com o mesmo nome no destino. Escolha outra pasta ou remova o ficheiro existente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "В папке назначения уже есть файл с таким именем. Выберите другую папку или удалите существующий файл." - } - } - } - }, - "error_device_info": { - "comment": "Error: device information could not be loaded.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Geräteinformationen konnten nicht geladen werden." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Could not load device information." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudo cargar la información del dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Impossible de charger les informations de l’appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impossibile caricare le informazioni sul dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Apparaatgegevens konden niet worden geladen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się wczytać informacji o urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível carregar as informações do dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось загрузить сведения об устройстве." - } - } - } - }, - "error_filesystem": { - "comment": "Error: the selected files/folder could not be accessed.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop konnte nicht auf die ausgewählten Dateien oder den Ordner zugreifen. Überprüfen Sie die Berechtigungen und versuchen Sie es erneut." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop could not access the selected files or folder. Check permissions and try again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop no pudo acceder a los archivos o la carpeta seleccionados. Compruebe los permisos e inténtelo de nuevo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop n’a pas pu accéder aux fichiers ou au dossier sélectionnés. Vérifiez les autorisations et réessayez." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop non ha potuto accedere ai file o alla cartella selezionati. Controlli le autorizzazioni e riprovi." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop kon geen toegang krijgen tot de geselecteerde bestanden of map. Controleer de machtigingen en probeer het opnieuw." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop nie mógł uzyskać dostępu do wybranych plików lub folderu. Sprawdź uprawnienia i spróbuj ponownie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O VniDrop não conseguiu aceder aos ficheiros ou à pasta selecionados. Verifique as permissões e tente novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop не удалось получить доступ к выбранным файлам или папке. Проверьте разрешения и повторите попытку." - } - } - } - }, - "error_generic": { - "comment": "Error: generic fallback message.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Etwas ist schiefgelaufen. Versuchen Sie es erneut." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Something went wrong. Try again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Algo salió mal. Inténtelo de nuevo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Une erreur est survenue. Réessayez." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Qualcosa è andato storto. Riprovi." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Er is iets misgegaan. Probeer het opnieuw." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Coś poszło nie tak. Spróbuj ponownie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Algo correu mal. Tente novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Что-то пошло не так. Повторите попытку." - } - } - } - }, - "error_initialization": { - "comment": "Error: the app failed to finish starting up.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop konnte den Start nicht abschließen. Schließen Sie die App und versuchen Sie es erneut." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop could not finish starting up. Close the app and try again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop no pudo terminar de iniciarse. Cierre la app e inténtelo de nuevo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop n’a pas pu terminer son démarrage. Fermez l’app et réessayez." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop non ha potuto completare l’avvio. Chiuda l’app e riprovi." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop kon het opstarten niet voltooien. Sluit de app en probeer het opnieuw." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop nie mógł dokończyć uruchamiania. Zamknij aplikację i spróbuj ponownie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O VniDrop não conseguiu concluir o arranque. Feche a app e tente novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop не удалось завершить запуск. Закройте приложение и повторите попытку." - } - } - } - }, - "error_invalid_input": { - "comment": "Error: transfer input or metadata is invalid.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einige Übertragungsinformationen sind ungültig. Prüfen Sie Ihre Auswahl oder bitten Sie den Absender, erneut zu teilen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Some transfer information is invalid. Review your selection or ask the sender to share again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Parte de la información de la transferencia no es válida. Revise la selección o pida al remitente que vuelva a compartirla." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Certaines informations du transfert ne sont pas valides. Vérifiez votre sélection ou demandez à l’expéditeur de partager à nouveau." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Alcune informazioni del trasferimento non sono valide. Controlli la selezione o chieda al mittente di condividere di nuovo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Sommige overdrachtsgegevens zijn ongeldig. Controleer uw selectie of vraag de afzender opnieuw te delen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Niektóre informacje o transferze są nieprawidłowe. Sprawdź wybór lub poproś nadawcę o ponowne udostępnienie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Algumas informações da transferência são inválidas. Reveja a seleção ou peça ao remetente para partilhar novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Некоторые данные передачи недействительны. Проверьте выбор или попросите отправителя поделиться снова." - } - } - } - }, - "error_invalid_ticket": { - "comment": "Error: the invitation/ticket could not be parsed.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Diese Einladung konnte nicht gelesen werden. Bitten Sie den Absender um eine neue." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This invitation could not be read. Ask the sender for a new one." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudo leer esta invitación. Pida al remitente una nueva." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Cette invitation n’a pas pu être lue. Demandez-en une nouvelle à l’expéditeur." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impossibile leggere questo invito. Ne chieda uno nuovo al mittente." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Deze uitnodiging kon niet worden gelezen. Vraag de afzender om een nieuwe." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się odczytać tego zaproszenia. Poproś nadawcę o nowe." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível ler este convite. Peça um novo ao remetente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось прочитать это приглашение. Попросите отправителя прислать новое." - } - } - } - }, - "error_invitation_empty": { - "comment": "Error: the opened invitation contained no data.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Diese Einladung ist leer. Versuchen Sie, sie erneut zu öffnen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "That invitation is empty. Try opening it again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Esa invitación está vacía. Intente abrirla de nuevo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Cette invitation est vide. Essayez de l’ouvrir à nouveau." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Questo invito è vuoto. Provi ad aprirlo di nuovo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Die uitnodiging is leeg. Probeer deze opnieuw te openen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "To zaproszenie jest puste. Spróbuj otworzyć je ponownie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Esse convite está vazio. Tente abri-lo novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Это приглашение пустое. Попробуйте открыть его снова." - } - } - } - }, - "error_missing_native_library": { - "comment": "Error: the native library is missing from the build.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Die native VniDrop-Bibliothek fehlt in diesem Build." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "The native VniDrop library is missing from this build." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Falta la biblioteca nativa de VniDrop en esta versión." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "La bibliothèque native de VniDrop est absente de cette version." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "La libreria nativa di VniDrop non è presente in questa build." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "De native VniDrop-bibliotheek ontbreekt in deze build." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "W tej kompilacji brakuje natywnej biblioteki VniDrop." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A biblioteca nativa do VniDrop está em falta nesta compilação." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "В этой сборке отсутствует нативная библиотека VniDrop." - } - } - } - }, - "error_network": { - "comment": "Error: the sender could not be reached over the local network.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop konnte den Absender nicht erreichen. Prüfen Sie die Verbindung auf beiden Geräten und versuchen Sie es erneut." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop could not reach the sender. Check the connection on both devices and try again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop no pudo contactar con el remitente. Compruebe la conexión en ambos dispositivos e inténtelo de nuevo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop n’a pas pu joindre l’expéditeur. Vérifiez la connexion sur les deux appareils et réessayez." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop non è riuscito a raggiungere il mittente. Controlli la connessione su entrambi i dispositivi e riprovi." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop kon de afzender niet bereiken. Controleer de verbinding op beide apparaten en probeer het opnieuw." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop nie mógł połączyć się z nadawcą. Sprawdź połączenie na obu urządzeniach i spróbuj ponownie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O VniDrop não conseguiu contactar o remetente. Verifique a ligação nos dois dispositivos e tente novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop не удалось связаться с отправителем. Проверьте подключение на обоих устройствах и повторите попытку." - } - } - } - }, - "error_nfc": { - "comment": "Error: the NFC tag could not be read/used.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Dieses NFC-Tag konnte nicht verwendet werden. Versuchen Sie ein anderes." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This NFC tag could not be used. Try another tag." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudo usar esta etiqueta NFC. Pruebe con otra." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Ce tag NFC n’a pas pu être utilisé. Essayez-en un autre." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impossibile usare questo tag NFC. Ne provi un altro." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Deze NFC-tag kon niet worden gebruikt. Probeer een andere." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się użyć tego tagu NFC. Spróbuj innego." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível utilizar esta etiqueta NFC. Tente outra." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось использовать эту NFC-метку. Попробуйте другую." - } - } - } - }, - "error_permission": { - "comment": "Error: the transfer was not approved or was refused by the sender.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Der Absender hat diese Übertragung nicht genehmigt oder sie wurde abgelehnt." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "The sender has not approved this transfer, or it was refused." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "El remitente no ha aprobado esta transferencia, o fue rechazada." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "L’expéditeur n’a pas approuvé ce transfert, ou il a été refusé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Il mittente non ha approvato questo trasferimento, oppure è stato rifiutato." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "De afzender heeft deze overdracht niet goedgekeurd, of deze is geweigerd." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nadawca nie zatwierdził tego transferu lub został on odrzucony." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O remetente não aprovou esta transferência, ou foi recusada." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправитель не одобрил эту передачу, или она была отклонена." - } - } - } - }, - "error_repository": { - "comment": "Error: transfer data could not be saved locally.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop konnte die Übertragungsdaten auf diesem Gerät nicht speichern." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop could not save transfer data on this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop no pudo guardar los datos de la transferencia en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop n’a pas pu enregistrer les données de transfert sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop non ha potuto salvare i dati del trasferimento su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop kon de overdrachtsgegevens niet op dit apparaat bewaren." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop nie mógł zapisać danych transferu na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O VniDrop não conseguiu guardar os dados da transferência neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop не удалось сохранить данные передачи на этом устройстве." - } - } - } - }, - "error_selection_failed": { - "comment": "Error: the selected item could not be opened.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Das ausgewählte Objekt konnte nicht geöffnet werden. Versuchen Sie, es erneut auszuwählen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Could not open the selected item. Try choosing it again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudo abrir el elemento seleccionado. Intente elegirlo de nuevo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Impossible d’ouvrir l’élément sélectionné. Essayez de le choisir à nouveau." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impossibile aprire l’elemento selezionato. Provi a sceglierlo di nuovo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Het geselecteerde item kon niet worden geopend. Probeer het opnieuw te kiezen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się otworzyć wybranego elementu. Spróbuj wybrać go ponownie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível abrir o item selecionado. Tente escolhê-lo novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось открыть выбранный объект. Попробуйте выбрать его снова." - } - } - } - }, - "error_share_empty": { - "comment": "Error: attempted to share with nothing selected.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wählen Sie mindestens ein Objekt zum Teilen aus." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Select at least one item to share." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Seleccione al menos un elemento para compartir." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Sélectionnez au moins un élément à partager." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Selezioni almeno un elemento da condividere." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Selecteer minstens één item om te delen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybierz co najmniej jeden element do udostępnienia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Selecione pelo menos um item para partilhar." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выберите хотя бы один объект для отправки." - } - } - } - }, - "error_socket_bind": { - "comment": "Error: the app could not open its network sockets.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop konnte seine Netzwerk-Sockets auf diesem Gerät nicht öffnen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop could not open its network sockets on this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop no pudo abrir sus sockets de red en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop n’a pas pu ouvrir ses sockets réseau sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop non ha potuto aprire i suoi socket di rete su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop kon zijn netwerksockets niet openen op dit apparaat." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop nie mógł otworzyć gniazd sieciowych na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O VniDrop não conseguiu abrir os seus sockets de rede neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop не удалось открыть сетевые сокеты на этом устройстве." - } - } - } - }, - "error_starting_up": { - "comment": "Error: an invitation was opened before startup finished.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop startet noch. Öffnen Sie die Einladung gleich erneut." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop is still starting. Open the invitation again in a moment." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop todavía se está iniciando. Vuelva a abrir la invitación en un momento." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop démarre encore. Rouvrez l’invitation dans un instant." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop è ancora in fase di avvio. Riapra l’invito tra un momento." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop is nog aan het opstarten. Open de uitnodiging zo meteen opnieuw." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop jeszcze się uruchamia. Otwórz zaproszenie ponownie za chwilę." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O VniDrop ainda está a iniciar. Abra o convite novamente dentro de momentos." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop ещё запускается. Откройте приглашение снова через мгновение." - } - } - } - }, - "error_storage_full": { - "comment": "Error: the destination does not have enough free storage.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Zum Speichern dieser Übertragung ist nicht genügend Speicherplatz vorhanden. Geben Sie Speicherplatz frei und versuchen Sie es erneut." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "There is not enough storage space to save this transfer. Free up space and try again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No hay suficiente espacio de almacenamiento para guardar esta transferencia. Libere espacio e inténtelo de nuevo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "L’espace de stockage est insuffisant pour enregistrer ce transfert. Libérez de l’espace et réessayez." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Lo spazio di archiviazione non è sufficiente per salvare il trasferimento. Liberi spazio e riprovi." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Er is onvoldoende opslagruimte om deze overdracht op te slaan. Maak ruimte vrij en probeer het opnieuw." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Brakuje miejsca na zapisanie tego transferu. Zwolnij miejsce i spróbuj ponownie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não existe espaço de armazenamento suficiente para guardar esta transferência. Liberte espaço e tente novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Недостаточно места для сохранения этой передачи. Освободите место и повторите попытку." - } - } - } - }, - "error_transfer": { - "comment": "Error: transfer data could not be processed; network failures use error_network.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Die Übertragungsdaten konnten nicht verarbeitet werden. Bitten Sie den Absender, sie erneut zu teilen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "The transfer data could not be processed. Ask the sender to share it again." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudieron procesar los datos de la transferencia. Pida al remitente que vuelva a compartirlos." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Les données du transfert n’ont pas pu être traitées. Demandez à l’expéditeur de les partager à nouveau." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non è stato possibile elaborare i dati del trasferimento. Chieda al mittente di condividerli di nuovo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "De overdrachtsgegevens konden niet worden verwerkt. Vraag de afzender ze opnieuw te delen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się przetworzyć danych transferu. Poproś nadawcę o ponowne udostępnienie." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível processar os dados da transferência. Peça ao remetente para os partilhar novamente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось обработать данные передачи. Попросите отправителя поделиться ими снова." - } - } - } - }, - "field_receiver_name": { - "comment": "Text field label: the receiver's display name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfängername" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receiver name" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nombre del destinatario" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nom du destinataire" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nome del destinatario" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Naam van ontvanger" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nazwa odbiorcy" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nome do destinatário" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Имя получателя" - } - } - } - }, - "field_sender_name": { - "comment": "Text field label: the sender's display name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Absendername" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Sender name" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nombre del remitente" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nom de l’expéditeur" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nome del mittente" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Naam van afzender" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nazwa nadawcy" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nome do remetente" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Имя отправителя" - } - } - } - }, - "field_transfer_name": { - "comment": "Text field label: the name given to a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragungsname" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer name" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nombre de la transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nom du transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nome del trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Naam van overdracht" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nazwa transferu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nome da transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Название передачи" - } - } - } - }, - "field_username": { - "comment": "Settings text field label: this device's display name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Anzeigename" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Display name" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nombre visible" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nom d’affichage" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nome visualizzato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Weergavenaam" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nazwa wyświetlana" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nome a apresentar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отображаемое имя" - } - } - } - }, - "folder_status_permission_required": { - "comment": "Receive-folder status: permission is required to write to the chosen folder.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Berechtigung erforderlich" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Permission needed" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Permiso necesario" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Autorisation requise" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Autorizzazione necessaria" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Machtiging vereist" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wymagane uprawnienie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Permissão necessária" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Требуется разрешение" - } - } - } - }, - "folder_status_unavailable": { - "comment": "Receive-folder status: the chosen folder is unavailable.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Nicht verfügbar" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Unavailable" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No disponible" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Indisponible" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non disponibile" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Niet beschikbaar" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Niedostępny" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Indisponível" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Недоступно" - } - } - } - }, - "folder_status_validating": { - "comment": "Receive-folder status: the folder is being checked.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ordner wird geprüft…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Checking folder…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Comprobando carpeta…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Vérification du dossier…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Controllo della cartella…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Map controleren…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Sprawdzanie folderu…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A verificar a pasta…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Проверка папки…" - } - } - } - }, - "folder_status_writable": { - "comment": "Receive-folder status: the folder is valid and writable.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bereit" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Ready" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Listo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Prêt" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Pronta" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Gereed" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Gotowy" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pronto" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Готово" - } - } - } - }, - "format_separated_pair": { - "comment": "Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@ · %2$@" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - } - } - }, - "format_separated_triple": { - "comment": "Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@ %2$@ · %3$@" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - } - } - }, - "metadata_files": { - "comment": "Transfer metadata label: number of files.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Dateien" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Files" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Archivos" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Fichiers" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "File" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bestanden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pliki" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ficheiros" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Файлы" - } - } - } - }, - "metadata_size": { - "comment": "Transfer metadata label: total size.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Größe" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Size" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Tamaño" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Taille" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dimensione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Grootte" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Rozmiar" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Tamanho" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Размер" - } - } - } - }, - "metadata_status": { - "comment": "Transfer metadata label: current status.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Status" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Status" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Estado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Statut" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Stato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Status" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Status" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Estado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Статус" - } - } - } - }, - "nav_receive": { - "comment": "Bottom navigation: Receive tab label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receive" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Recibir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Recevoir" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ricevi" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbierz" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Receber" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получить" - } - } - } - }, - "nav_send": { - "comment": "Bottom navigation: Send tab label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Senden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Send" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoyer" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invia" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Versturen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wyślij" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправить" - } - } - } - }, - "nav_settings": { - "comment": "Bottom navigation: Settings tab label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einstellungen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Settings" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Ajustes" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Réglages" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impostazioni" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Instellingen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Ustawienia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Definições" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Настройки" - } - } - } - }, - "network_title": { - "comment": "Device information row: network label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Netzwerk" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Network" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Red" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Réseau" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Rete" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Netwerk" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Sieć" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Rede" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сеть" - } - } - } - }, - "notifications_description": { - "comment": "Settings > Notifications: explanation of what notifications are used for.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Get notified about new receive requests while VniDrop is in the background." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне." - } - } - } - }, - "notifications_enabled_message": { - "comment": "Settings > Notifications: confirmation when notifications are enabled.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Mitteilungen aktiviert." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Notifications enabled." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Notificaciones activadas." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Notifications activées." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Notifiche attivate." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Meldingen ingeschakeld." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Powiadomienia włączone." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Notificações ativadas." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Уведомления включены." - } - } - } - }, - "notifications_local_title": { - "comment": "Settings > Notifications: label for the allow-notifications action.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Mitteilungen erlauben" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Allow notifications" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Permitir notificaciones" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Autoriser les notifications" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Consenti le notifiche" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Meldingen toestaan" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zezwól na powiadomienia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Permitir notificações" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Разрешить уведомления" - } - } - } - }, - "notifications_permission_denied": { - "comment": "Settings > Notifications: message when the OS permission is denied.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Notifications are turned off for VniDrop. You can enable them in Settings." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Уведомления отключены для VniDrop. Вы можете включить их в Настройках." - } - } - } - }, - "notifications_settings_open_failed": { - "comment": "Settings > Notifications: error when the OS notification settings can't be opened.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Die Mitteilungseinstellungen konnten nicht geöffnet werden." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Could not open notification settings." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudieron abrir los ajustes de notificaciones." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Impossible d’ouvrir les réglages de notifications." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impossibile aprire le impostazioni delle notifiche." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "De meldingsinstellingen konden niet worden geopend." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się otworzyć ustawień powiadomień." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível abrir as definições de notificações." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось открыть настройки уведомлений." - } - } - } - }, - "notifications_title": { - "comment": "Settings > Notifications: section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Mitteilungen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Notifications" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Notificaciones" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Notifications" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Notifiche" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Meldingen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Powiadomienia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Notificações" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Уведомления" - } - } - } - }, - "notifications_unsupported": { - "comment": "Settings > Notifications: message when notifications aren't supported on the device.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Mitteilungen sind auf diesem Gerät nicht verfügbar." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Notifications are not available on this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Las notificaciones no están disponibles en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Les notifications ne sont pas disponibles sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Le notifiche non sono disponibili su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Meldingen zijn niet beschikbaar op dit apparaat." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Powiadomienia nie są dostępne na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "As notificações não estão disponíveis neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Уведомления недоступны на этом устройстве." - } - } - } - }, - "os_version_title": { - "comment": "Device information row: operating system version label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Betriebssystem" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Operating system" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Sistema operativo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Système d’exploitation" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Sistema operativo" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Besturingssysteem" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "System operacyjny" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Sistema operativo" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Операционная система" - } - } - } - }, - "preferences_receive_folder_title": { - "comment": "Settings > Preferences: label for the received-files destination folder.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangene Übertragungen sichern in" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Save received transfers to" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Guardar las transferencias recibidas en" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Enregistrer les transferts reçus dans" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Salva i trasferimenti ricevuti in" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen overdrachten bewaren in" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zapisuj odebrane transfery w" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Guardar as transferências recebidas em" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сохранять полученные передачи в" - } - } - } - }, - "preferences_title": { - "comment": "Settings > Preferences: section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Voreinstellungen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Preferences" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Preferencias" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Préférences" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Preferenze" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Voorkeuren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Preferencje" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Preferências" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Параметры" - } - } - } - }, - "progress_cancelled": { - "comment": "Transfer progress label: cancelled.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Abgebrochen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cancelled" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Annulé" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Annullato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geannuleerd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Anulowano" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отменено" - } - } - } - }, - "progress_completed": { - "comment": "Transfer progress label: completed.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Abgeschlossen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Completed" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Completado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Terminé" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Completato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Voltooid" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Ukończono" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Concluído" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Завершено" - } - } - } - }, - "progress_connected": { - "comment": "Transfer progress label: connected.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verbunden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Connected" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Conectado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Connecté" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Connesso" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verbonden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Połączono" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ligado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подключено" - } - } - } - }, - "progress_connecting": { - "comment": "Transfer progress label: connecting.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verbinden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Connecting" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Conectando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Connexion" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Connessione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verbinden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Łączenie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A ligar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подключение" - } - } - } - }, - "progress_downloading": { - "comment": "Transfer progress label: downloading.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird geladen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Downloading" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Descargando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Téléchargement" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Download" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Downloaden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pobieranie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A descarregar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Загрузка" - } - } - } - }, - "progress_failed": { - "comment": "Transfer progress label: failed.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Fehlgeschlagen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Failed" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Fallido" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Échec" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non riuscito" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Mislukt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Niepowodzenie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Falhou" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Ошибка" - } - } - } - }, - "progress_getting_ready": { - "comment": "Transfer progress label: getting ready.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird bereitgemacht" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Getting ready" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Preparándose" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Préparation" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Preparazione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Klaarmaken" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przygotowywanie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A preparar-se" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подготовка" - } - } - } - }, - "progress_interrupted": { - "comment": "Transfer progress label: the transfer was interrupted.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung unterbrochen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer interrupted" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencia interrumpida" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transfert interrompu" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimento interrotto" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht onderbroken" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfer przerwany" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferência interrompida" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача прервана" - } - } - } - }, - "progress_preparing": { - "comment": "Transfer progress label: preparing.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird vorbereitet" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Preparing" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Preparando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Préparation" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Preparazione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Voorbereiden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przygotowywanie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A preparar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подготовка" - } - } - } - }, - "progress_ready": { - "comment": "Transfer progress label: ready.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bereit" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Ready" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Listo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Prêt" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Pronto" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Gereed" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Gotowe" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pronto" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Готово" - } - } - } - }, - "progress_receiving": { - "comment": "Transfer progress label: receiving.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receiving" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Recibiendo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Réception" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ricezione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbieranie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A receber" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получение" - } - } - } - }, - "progress_requesting_access": { - "comment": "Transfer progress label: requesting access from the sender.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Zugriff wird angefragt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Requesting access" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Solicitando acceso" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Demande d’accès" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Richiesta di accesso" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Toegang aanvragen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Prośba o dostęp" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A pedir acesso" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Запрос доступа" - } - } - } - }, - "progress_saving": { - "comment": "Transfer progress label: saving received files.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird gesichert" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Saving" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Guardando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Enregistrement" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Salvataggio" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bewaren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zapisywanie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A guardar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сохранение" - } - } - } - }, - "progress_sending": { - "comment": "Transfer progress label: sending.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird gesendet" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Sending" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enviando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoi" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invio" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Versturen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wysyłanie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A enviar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправка" - } - } - } - }, - "progress_sending_to_count": { - "comment": "Transfer progress label: sending to N receivers. %lld = receiver count.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Senden an %1$d" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Sending to %1$d" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enviando a %1$d" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoi à %1$d" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invio a %1$d" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Versturen naar %1$d" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wysyłanie do %1$d" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A enviar para %1$d" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправка получателям: %1$d" - } - } - } - }, - "progress_share_ready": { - "comment": "Transfer progress label: the share is ready.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bereit zum Teilen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Ready to share" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Listo para compartir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Prêt à partager" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Pronto per la condivisione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Klaar om te delen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Gotowe do udostępnienia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pronto para partilhar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Готово к отправке" - } - } - } - }, - "progress_working": { - "comment": "Transfer progress label: generic working/in-progress state.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird bearbeitet…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Working…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Trabajando…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "En cours…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Elaborazione…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bezig…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przetwarzanie…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A processar…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Обработка…" - } - } - } - }, - "receive_choose_method_body": { - "comment": "Receive flow: body text prompting the user to pick an invitation method.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wählen Sie die Ihnen zur Verfügung stehende Einladungsmethode." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Choose the invitation method available to you." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Elija el método de invitación de que disponga." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Choisissez la méthode d’invitation à votre disposition." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scelga il metodo di invito a sua disposizione." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Kies de uitnodigingsmethode die u ter beschikking staat." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybierz dostępną metodę zaproszenia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Escolha o método de convite ao seu dispor." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выберите доступный вам способ приглашения." - } - } - } - }, - "receive_choose_method_title": { - "comment": "Receive flow: title of the connect-method chooser.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wie möchten Sie sich verbinden?" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "How would you like to connect?" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "¿Cómo quiere conectarse?" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Comment souhaitez-vous vous connecter ?" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Come vuole connettersi?" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Hoe wilt u verbinding maken?" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Jak chcesz się połączyć?" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Como pretende ligar-se?" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Как вы хотите подключиться?" - } - } - } - }, - "receive_clear_history": { - "comment": "Receive history: action to clear all history.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verlauf löschen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Clear history" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Borrar historial" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Effacer l’historique" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cancella cronologia" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geschiedenis wissen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wyczyść historię" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Limpar histórico" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Очистить историю" - } - } - } - }, - "receive_clear_history_description": { - "comment": "Receive history: confirmation dialog body for clearing all history.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Alle abgeschlossenen, fehlgeschlagenen und abgebrochenen Empfänge werden aus dem Verlauf von VniDrop entfernt. Heruntergeladene Dateien verbleiben auf diesem Gerät." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "All completed, failed, and cancelled receives will be removed from VniDrop’s history. Downloaded files will remain on this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Todas las recepciones completadas, fallidas y canceladas se eliminarán del historial de VniDrop. Los archivos descargados permanecerán en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Toutes les réceptions terminées, échouées et annulées seront retirées de l’historique de VniDrop. Les fichiers téléchargés resteront sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Tutte le ricezioni completate, non riuscite e annullate verranno rimosse dalla cronologia di VniDrop. I file scaricati rimarranno su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Alle voltooide, mislukte en geannuleerde ontvangsten worden uit de geschiedenis van VniDrop verwijderd. Gedownloade bestanden blijven op dit apparaat." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wszystkie ukończone, nieudane i anulowane odbiory zostaną usunięte z historii VniDrop. Pobrane pliki pozostaną na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Todas as receções concluídas, falhadas e canceladas serão removidas do histórico do VniDrop. Os ficheiros descarregados permanecerão neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Все завершённые, неудачные и отменённые получения будут удалены из истории VniDrop. Загруженные файлы останутся на этом устройстве." - } - } - } - }, - "receive_clear_history_title": { - "comment": "Receive history: confirmation dialog title for clearing all history.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangsverlauf löschen?" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Clear receive history?" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "¿Borrar el historial de recepción?" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Effacer l’historique de réception ?" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cancellare la cronologia di ricezione?" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangstgeschiedenis wissen?" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wyczyścić historię odbioru?" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Limpar o histórico de receção?" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Очистить историю получения?" - } - } - } - }, - "receive_completed": { - "comment": "Receive flow: toast/message when a transfer finishes downloading.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung empfangen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer received." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencia recibida." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transfert reçu." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimento ricevuto." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht ontvangen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfer odebrany." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferência recebida." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача получена." - } - } - } - }, - "receive_delete_history_description": { - "comment": "Receive history: confirmation body for removing one item. {transferName} = transfer name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "„%1$@“ wird aus dem Verlauf von VniDrop entfernt. Die heruntergeladene Datei verbleibt auf diesem Gerät." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "“%1$@” will be removed from VniDrop’s history. The downloaded file will remain on this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "«%1$@» se eliminará del historial de VniDrop. El archivo descargado permanecerá en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "« %1$@ » sera retiré de l’historique de VniDrop. Le fichier téléchargé restera sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "«%1$@» verrà rimosso dalla cronologia di VniDrop. Il file scaricato rimarrà su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "‘%1$@’ wordt uit de geschiedenis van VniDrop verwijderd. Het gedownloade bestand blijft op dit apparaat." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "„%1$@” zostanie usunięty z historii VniDrop. Pobrany plik pozostanie na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "«%1$@» será removido do histórico do VniDrop. O ficheiro descarregado permanecerá neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "«%1$@» будет удалён из истории VniDrop. Загруженный файл останется на этом устройстве." - } - } - } - }, - "receive_delete_history_item": { - "comment": "Receive history: swipe/menu action to delete a single history item.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Aus Empfangsverlauf löschen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Delete from receive history" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminar del historial de recepción" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Supprimer de l’historique de réception" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Elimina dalla cronologia di ricezione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uit ontvangstgeschiedenis verwijderen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usuń z historii odbioru" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminar do histórico de receção" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удалить из истории получения" - } - } - } - }, - "receive_delete_history_title": { - "comment": "Receive history: confirmation title for removing one item.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Aus dem Verlauf entfernen?" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Remove from history?" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "¿Quitar del historial?" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Retirer de l’historique ?" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Rimuovere dalla cronologia?" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uit geschiedenis verwijderen?" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usunąć z historii?" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Remover do histórico?" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удалить из истории?" - } - } - } - }, - "receive_empty_body": { - "comment": "Receive tab empty state: instructions on how to receive.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Öffnen Sie eine VniDrop-Einladung, scannen Sie einen QR-Code oder halten Sie das Gerät an ein NFC-Tag." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Open a VniDrop invitation, scan a QR code, or hold near an NFC tag." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Abra una invitación de VniDrop, escanee un código QR o acerque una etiqueta NFC." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Ouvrez une invitation VniDrop, scannez un QR code ou approchez un tag NFC." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Apra un invito VniDrop, scansioni un codice QR o avvicini un tag NFC." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Open een VniDrop-uitnodiging, scan een QR-code of houd het apparaat bij een NFC-tag." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Otwórz zaproszenie VniDrop, zeskanuj kod QR lub zbliż tag NFC." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Abra um convite do VniDrop, leia um código QR ou aproxime uma etiqueta NFC." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Откройте приглашение VniDrop, отсканируйте QR-код или поднесите NFC-метку." - } - } - } - }, - "receive_empty_title": { - "comment": "Receive tab empty state: title when nothing has been received.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Noch nichts empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Nothing received yet" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Aún no se ha recibido nada" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Rien reçu pour l’instant" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ancora nulla di ricevuto" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Nog niets ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nic jeszcze nie odebrano" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ainda não recebeu nada" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Пока ничего не получено" - } - } - } - }, - "receive_history_cleared": { - "comment": "Receive history: toast confirming history was cleared.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangsverlauf gelöscht." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receive history cleared." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Historial de recepción borrado." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Historique de réception effacé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cronologia di ricezione cancellata." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangstgeschiedenis gewist." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Historia odbioru wyczyszczona." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Histórico de receção limpo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "История получения очищена." - } - } - } - }, - "receive_history_title": { - "comment": "Receive tab: History section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verlauf" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "History" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Historial" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Historique" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cronologia" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geschiedenis" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Historia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Histórico" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "История" - } - } - } - }, - "receive_method_file": { - "comment": "Receive method: open a .vnd invitation file.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Eine .vnd-Einladung öffnen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Open a .vnd invitation" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Abrir una invitación .vnd" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Ouvrir une invitation .vnd" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Apri un invito .vnd" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Een .vnd-uitnodiging openen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Otwórz zaproszenie .vnd" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Abrir um convite .vnd" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Открыть приглашение .vnd" - } - } - } - }, - "receive_method_file_description": { - "comment": "Receive method description: open a saved/shared invitation file.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wählen Sie eine auf diesem Gerät gespeicherte oder geteilte Einladung." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Choose an invitation saved or shared to this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Elija una invitación guardada o compartida en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Choisissez une invitation enregistrée ou partagée sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scelga un invito salvato o condiviso su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Kies een uitnodiging die op dit apparaat is bewaard of gedeeld." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybierz zaproszenie zapisane lub udostępnione na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Escolha um convite guardado ou partilhado neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выберите приглашение, сохранённое или отправленное на это устройство." - } - } - } - }, - "receive_method_nfc": { - "comment": "Receive method: read an NFC tag.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "NFC-Tag lesen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Read NFC tag" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Leer etiqueta NFC" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Lire un tag NFC" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Leggi tag NFC" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "NFC-tag lezen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odczytaj tag NFC" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ler etiqueta NFC" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Прочитать NFC-метку" - } - } - } - }, - "receive_method_nfc_description": { - "comment": "Receive method description: hold near the sender's NFC tag.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Halten Sie dieses Gerät an das Einladungs-Tag des Absenders." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Hold this device near the sender’s invitation tag." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Acerque este dispositivo a la etiqueta de invitación del remitente." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Approchez cet appareil du tag d’invitation de l’expéditeur." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Avvicini questo dispositivo al tag di invito del mittente." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Houd dit apparaat bij de uitnodigingstag van de afzender." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zbliż to urządzenie do tagu zaproszenia nadawcy." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Aproxime este dispositivo da etiqueta de convite do remetente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Поднесите это устройство к метке приглашения отправителя." - } - } - } - }, - "receive_method_scan": { - "comment": "Receive method: scan a QR code.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "QR-Code scannen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Scan QR code" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Escanear código QR" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Scanner un QR code" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scansiona codice QR" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "QR-code scannen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zeskanuj kod QR" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ler código QR" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сканировать QR-код" - } - } - } - }, - "receive_method_scan_description": { - "comment": "Receive method description: use the camera to scan the sender's code.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verwenden Sie die Kamera, um den VniDrop-Code des Absenders zu scannen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Use the camera to scan the sender’s VniDrop code." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Use la cámara para escanear el código de VniDrop del remitente." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Utilisez la caméra pour scanner le code VniDrop de l’expéditeur." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Usi la fotocamera per scansionare il codice VniDrop del mittente." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Gebruik de camera om de VniDrop-code van de afzender te scannen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Użyj aparatu, aby zeskanować kod VniDrop nadawcy." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Use a câmara para ler o código VniDrop do remetente." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Используйте камеру, чтобы отсканировать код VniDrop отправителя." - } - } - } - }, - "receive_new_subtitle": { - "comment": "Receive tab: subtitle under the title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragungen, die Sie auf diesem Gerät empfangen haben." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfers you’ve received on this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencias que ha recibido en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transferts que vous avez reçus sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimenti che ha ricevuto su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdrachten die u op dit apparaat hebt ontvangen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfery odebrane na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferências que recebeu neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передачи, полученные на этом устройстве." - } - } - } - }, - "receive_nfc_waiting": { - "comment": "Receive flow: prompt while waiting to read an NFC tag.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Halten Sie das Gerät an das NFC-Tag…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Hold near the NFC tag…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Acerque a la etiqueta NFC…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Approchez du tag NFC…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Avvicini al tag NFC…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Houd bij de NFC-tag…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zbliż do tagu NFC…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Aproxime da etiqueta NFC…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Поднесите к NFC-метке…" - } - } - } - }, - "receive_open_files_failed": { - "comment": "Receive flow: error when opening VniDrop's folder in Files fails.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop konnte in „Dateien“ nicht geöffnet werden." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Couldn’t open VniDrop in Files." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se pudo abrir VniDrop en Archivos." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Impossible d’ouvrir VniDrop dans Fichiers." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impossibile aprire VniDrop in File." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop kon niet worden geopend in Bestanden." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie udało się otworzyć VniDrop w Plikach." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não foi possível abrir o VniDrop em Ficheiros." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Не удалось открыть VniDrop в Файлах." - } - } - } - }, - "receive_review_title": { - "comment": "Receive flow: title of the review screen before accepting a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung prüfen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Review transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Revisar transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Vérifier le transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Rivedi trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht controleren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przejrzyj transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Rever transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Проверить передачу" - } - } - } - }, - "receive_title": { - "comment": "Receive tab: screen title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receive" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Recibir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Recevoir" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ricevi" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbierz" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Receber" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получить" - } - } - } - }, - "receive_unknown_transfer": { - "comment": "Receive flow: fallback name for a transfer with no title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop-Übertragung" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "VniDrop transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencia de VniDrop" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transfert VniDrop" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimento VniDrop" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "VniDrop-overdracht" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfer VniDrop" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferência VniDrop" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача VniDrop" - } - } - } - }, - "send_access_anyone": { - "comment": "Send access option: anyone with the invitation can receive.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Jeder mit dieser Übertragung" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Anyone with this transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cualquiera que tenga esta transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Toute personne disposant de ce transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Chiunque abbia questo trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Iedereen met deze overdracht" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Każdy, kto ma ten transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Qualquer pessoa com esta transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Любой, у кого есть эта передача" - } - } - } - }, - "send_access_anyone_description": { - "comment": "Send access option description: no approval required.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Keine Genehmigung erforderlich. Verwenden Sie dies nur für Objekte, die Sie unbedenklich teilen können." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "No approval is required. Only use this for items you are comfortable sharing." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No se requiere aprobación. Úselo solo para elementos que no le importe compartir." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Aucune approbation requise. À n’utiliser que pour des éléments que vous êtes à l’aise de partager." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nessuna approvazione richiesta. Da usare solo per elementi che non ha problemi a condividere." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geen goedkeuring vereist. Gebruik dit alleen voor items die u gerust kunt delen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie jest wymagane zatwierdzenie. Używaj tylko dla elementów, które możesz swobodnie udostępniać." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Não é necessária aprovação. Utilize apenas para itens que não se importe de partilhar." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Одобрение не требуется. Используйте только для объектов, которыми вы готовы поделиться." - } - } - } - }, - "send_access_anyone_warning": { - "comment": "Send access option warning: caution about open access.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Jeder mit der Einladung kann herunterladen, bis Sie die Freigabe beenden. Verwenden Sie dies nicht für private oder sensible Objekte." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Anyone with the invitation can download until you stop sharing. Do not use this for private or sensitive items." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cualquiera que tenga la invitación puede descargar hasta que deje de compartir. No lo use para elementos privados o sensibles." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Toute personne disposant de l’invitation peut télécharger jusqu’à ce que vous arrêtiez le partage. Ne l’utilisez pas pour des éléments privés ou sensibles." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Chiunque abbia l’invito può scaricare finché non interrompe la condivisione. Non lo usi per elementi privati o sensibili." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Iedereen met de uitnodiging kan downloaden totdat u stopt met delen. Gebruik dit niet voor privé- of gevoelige items." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Każdy, kto ma zaproszenie, może pobierać, dopóki nie zatrzymasz udostępniania. Nie używaj tego dla prywatnych ani wrażliwych elementów." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Qualquer pessoa com o convite pode descarregar até parar de partilhar. Não utilize para itens privados ou sensíveis." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Любой, у кого есть приглашение, может загружать, пока вы не остановите общий доступ. Не используйте это для личных или конфиденциальных объектов." - } - } - } - }, - "send_access_approval": { - "comment": "Send access option: approve each receiver.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Vor jedem Download fragen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Ask before each download" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Preguntar antes de cada descarga" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Demander avant chaque téléchargement" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Chiedi prima di ogni download" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Vragen vóór elke download" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pytaj przed każdym pobraniem" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Perguntar antes de cada descarga" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Спрашивать перед каждой загрузкой" - } - } - } - }, - "send_access_approval_description": { - "comment": "Send access option description: you approve/refuse each receiver.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Sie genehmigen oder lehnen jeden neuen Empfänger ab." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "You approve or refuse every new receiver." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Usted aprueba o rechaza a cada nuevo destinatario." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Vous approuvez ou refusez chaque nouveau destinataire." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Approva o rifiuta ogni nuovo destinatario." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "U keurt elke nieuwe ontvanger goed of weigert deze." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zatwierdzasz lub odrzucasz każdego nowego odbiorcę." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Aprova ou recusa cada novo destinatário." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Вы одобряете или отклоняете каждого нового получателя." - } - } - } - }, - "send_access_title": { - "comment": "Send flow: heading for the who-can-receive access chooser.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wer kann sie empfangen?" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Who can receive it?" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "¿Quién puede recibirla?" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Qui peut le recevoir ?" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Chi può riceverlo?" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Wie kan het ontvangen?" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Kto może to odebrać?" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Quem a pode receber?" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Кто может это получить?" - } - } - } - }, - "send_choose_file_body": { - "comment": "Send flow: body text for the choose-what-to-share step.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wählen Sie Dateien oder einen Ordner auf diesem Gerät aus. Sie können die Auswahl überprüfen, bevor Sie die Übertragung erstellen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Select files or a folder from this device. You can review the selection before creating the transfer." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Seleccione archivos o una carpeta de este dispositivo. Podrá revisar la selección antes de crear la transferencia." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Sélectionnez des fichiers ou un dossier sur cet appareil. Vous pourrez vérifier la sélection avant de créer le transfert." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Selezioni file o una cartella da questo dispositivo. Potrà rivedere la selezione prima di creare il trasferimento." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Selecteer bestanden of een map op dit apparaat. U kunt de selectie controleren voordat u de overdracht aanmaakt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybierz pliki lub folder z tego urządzenia. Przed utworzeniem transferu możesz przejrzeć wybór." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Selecione ficheiros ou uma pasta deste dispositivo. Poderá rever a seleção antes de criar a transferência." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выберите файлы или папку на этом устройстве. Вы сможете проверить выбор перед созданием передачи." - } - } - } - }, - "send_choose_file_title": { - "comment": "Send flow: title of the choose-what-to-share step.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wählen Sie, was Sie teilen möchten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Choose what to share" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Elija qué compartir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Choisissez quoi partager" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scelga cosa condividere" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Kies wat u wilt delen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybierz, co udostępnić" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Escolha o que partilhar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выберите, чем поделиться" - } - } - } - }, - "send_empty_body": { - "comment": "Send tab empty state: instructions on how to create a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Erstellen Sie eine Übertragung, entscheiden Sie, wer sie empfangen darf, und laden Sie diese Personen dann mit einem QR-Code, einem NFC-Tag oder einer Einladungsdatei ein." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Create a transfer, decide who can receive it, then invite them with a QR code, NFC tag, or invitation file." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cree una transferencia, decida quién puede recibirla y luego invite a esas personas con un código QR, una etiqueta NFC o un archivo de invitación." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Créez un transfert, décidez qui peut le recevoir, puis invitez-les avec un QR code, un tag NFC ou un fichier d’invitation." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Crei un trasferimento, decida chi può riceverlo, poi inviti queste persone con un codice QR, un tag NFC o un file di invito." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Maak een overdracht aan, bepaal wie deze kan ontvangen en nodig die personen vervolgens uit met een QR-code, NFC-tag of uitnodigingsbestand." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Utwórz transfer, zdecyduj, kto może go odebrać, a następnie zaproś te osoby za pomocą kodu QR, tagu NFC lub pliku zaproszenia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Crie uma transferência, decida quem a pode receber e depois convide essas pessoas com um código QR, uma etiqueta NFC ou um ficheiro de convite." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Создайте передачу, решите, кто может её получить, затем пригласите этих людей с помощью QR-кода, NFC-метки или файла приглашения." - } - } - } - }, - "send_empty_title": { - "comment": "Send tab empty state: title when nothing has been shared.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Noch nichts geteilt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Nothing shared yet" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Aún no se ha compartido nada" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Rien partagé pour l’instant" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ancora nulla di condiviso" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Nog niets gedeeld" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nic jeszcze nie udostępniono" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ainda não partilhou nada" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Пока ничем не поделились" - } - } - } - }, - "send_file_size_unknown": { - "comment": "Send flow: shown when a selected file's size can't be determined.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Größe nicht verfügbar" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Size unavailable" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Tamaño no disponible" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Taille indisponible" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dimensione non disponibile" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Grootte niet beschikbaar" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Rozmiar niedostępny" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Tamanho indisponível" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Размер недоступен" - } - } - } - }, - "send_folder_label": { - "comment": "Send flow: label indicating a selected item is a folder.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ordner" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Folder" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Carpeta" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Dossier" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Cartella" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Map" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Folder" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pasta" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Папка" - } - } - } - }, - "send_new_transfer_description": { - "comment": "Send flow: description for the create-new-transfer entry.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Eine neue Übertragung erstellen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Create a new transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Crear una nueva transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Créer un nouveau transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Crea un nuovo trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Een nieuwe overdracht aanmaken" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Utwórz nowy transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Criar uma nova transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Создать новую передачу" - } - } - } - }, - "send_new_transfer_title": { - "comment": "Send flow: title for the new-transfer step.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Neue Übertragung" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "New transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nueva transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Nouveau transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nuovo trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Nieuwe overdracht" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nowy transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Nova transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Новая передача" - } - } - } - }, - "send_review_title": { - "comment": "Send flow: title of the review-transfer step before creating it.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung prüfen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Review transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Revisar transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Vérifier le transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Rivedi trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht controleren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przejrzyj transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Rever transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Проверить передачу" - } - } - } - }, - "send_selected_files_count": { - "comment": "Send flow: count of files chosen. {count} = selected files. NOTE: singular case reads '1 files' — should become a plural.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d Dateien ausgewählt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$d files selected" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d archivos seleccionados" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d fichiers sélectionnés" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d file selezionati" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d bestanden geselecteerd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wybrane pliki: %1$d" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d ficheiros selecionados" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Выбрано файлов: %1$d" - } - } - } - }, - "send_stop_sharing": { - "comment": "Transfer details: action to stop sharing a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Freigabe beenden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Stop sharing" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Dejar de compartir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Arrêter le partage" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Interrompi condivisione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Stoppen met delen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zatrzymaj udostępnianie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Parar de partilhar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Остановить общий доступ" - } - } - } - }, - "send_stop_sharing_description": { - "comment": "Transfer details: confirmation body for stopping sharing.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Dies beendet die Übertragung und unterbricht alle, die sie gerade herunterladen. Sie verbleibt in Ihrem Verlauf als „Beendet“." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This stops the transfer and interrupts anyone currently downloading it. It stays in your history as Stopped." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Esto detiene la transferencia e interrumpe a quien esté descargándola. Permanece en su historial como Detenida." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Cela arrête le transfert et interrompt toute personne en train de le télécharger. Il reste dans votre historique en tant qu’Arrêté." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Questo interrompe il trasferimento e blocca chiunque lo stia scaricando. Rimane nella sua cronologia come Interrotto." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Hiermee stopt de overdracht en wordt iedereen die deze op dit moment downloadt onderbroken. De overdracht blijft in uw geschiedenis staan als Gestopt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "To zatrzymuje transfer i przerywa każdego, kto go właśnie pobiera. Pozostaje w historii jako Zatrzymany." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Isto para a transferência e interrompe quem estiver a descarregá-la. Permanece no seu histórico como Parada." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Это остановит передачу и прервёт всех, кто сейчас её загружает. Она останется в вашей истории со статусом «Остановлена»." - } - } - } - }, - "send_subtitle": { - "comment": "Send tab: subtitle under the title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragungen, die Sie von diesem Gerät aus teilen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfers you’re sharing from this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencias que está compartiendo desde este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transferts que vous partagez depuis cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimenti che sta condividendo da questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdrachten die u vanaf dit apparaat deelt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfery udostępniane z tego urządzenia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferências que está a partilhar a partir deste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передачи, которыми вы делитесь с этого устройства." - } - } - } - }, - "send_title": { - "comment": "Send tab: screen title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Senden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Send" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Envoyer" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invia" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Versturen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wyślij" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Enviar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отправить" - } - } - } - }, - "send_transfer_created": { - "comment": "Send flow: toast confirming a transfer was created.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung erstellt." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer created." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencia creada." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transfert créé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimento creato." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht aangemaakt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfer utworzony." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferência criada." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача создана." - } - } - } - }, - "send_transfer_details_title": { - "comment": "Send flow: title of the transfer details screen.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragungsdetails" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer details" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Detalles de la transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Détails du transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dettagli del trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdrachtsdetails" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Szczegóły transferu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Detalhes da transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сведения о передаче" - } - } - } - }, - "send_transfers_title": { - "comment": "Send tab: 'Your transfers' list heading.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ihre Übertragungen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Your transfers" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Sus transferencias" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Vos transferts" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "I suoi trasferimenti" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uw overdrachten" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Twoje transfery" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "As suas transferências" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Ваши передачи" - } - } - } - }, - "settings_subtitle": { - "comment": "Settings screen: subtitle summarizing what's configurable.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ihr Name, wo Übertragungen gesichert werden, Darstellung und Mitteilungen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Your name, where transfers are saved, appearance, and notifications." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Su nombre, dónde se guardan las transferencias, la apariencia y las notificaciones." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Votre nom, l’emplacement d’enregistrement des transferts, l’apparence et les notifications." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Il suo nome, dove vengono salvati i trasferimenti, l’aspetto e le notifiche." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uw naam, waar overdrachten worden bewaard, weergave en meldingen." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Twoja nazwa, miejsce zapisu transferów, wygląd i powiadomienia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "O seu nome, onde as transferências são guardadas, o aspeto e as notificações." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Ваше имя, место сохранения передач, оформление и уведомления." - } - } - } - }, - "settings_title": { - "comment": "Settings screen: title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einstellungen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Settings" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Ajustes" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Réglages" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Impostazioni" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Instellingen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Ustawienia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Definições" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Настройки" - } - } - } - }, - "snackbar_dismiss": { - "comment": "Snackbar: accessibility label / action to dismiss the snackbar.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ausblenden" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Dismiss" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Descartar" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Ignorer" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ignora" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Sluiten" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zamknij" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ignorar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Закрыть" - } - } - } - }, - "status_available": { - "comment": "Transfer status: available (actively shared).", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verfügbar" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Available" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Disponible" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Disponible" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Disponibile" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Beschikbaar" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Dostępny" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Disponível" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Доступно" - } - } - } - }, - "status_cancelled": { - "comment": "Transfer status: cancelled.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Abgebrochen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cancelled" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Annulé" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Annullato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Geannuleerd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Anulowany" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Cancelado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отменено" - } - } - } - }, - "status_completed": { - "comment": "Transfer status: completed.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Abgeschlossen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Completed" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Completado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Terminé" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Completato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Voltooid" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Ukończony" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Concluído" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Завершено" - } - } - } - }, - "status_failed": { - "comment": "Transfer status: failed.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Fehlgeschlagen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Failed" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Fallido" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Échec" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non riuscito" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Mislukt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Niepowodzenie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Falhou" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Ошибка" - } - } - } - }, - "status_preparing": { - "comment": "Transfer status: preparing.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird vorbereitet" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Preparing" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Preparando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Préparation" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Preparazione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Voorbereiden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przygotowywanie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A preparar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подготовка" - } - } - } - }, - "status_receiving": { - "comment": "Transfer status: receiving.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receiving" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Recibiendo" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Réception" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ricezione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbieranie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A receber" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получение" - } - } - } - }, - "status_stopped": { - "comment": "Transfer status: stopped.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Beendet" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Stopped" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Detenido" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Arrêté" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Interrotto" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Gestopt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zatrzymany" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Parada" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Остановлено" - } - } - } - }, - "storage_app_data": { - "comment": "Settings > Storage: label for non-transfer application data.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "App-Daten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "App data" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Datos de la aplicación" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Données de l’app" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dati dell’app" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Appgegevens" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Dane aplikacji" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Dados da aplicação" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Данные приложения" - } - } - } - }, - "storage_calculating": { - "comment": "Settings > Storage: placeholder while a size is being calculated.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird berechnet…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Calculating…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Calculando…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Calcul…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Calcolo…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Berekenen…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Obliczanie…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A calcular…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Вычисление…" - } - } - } - }, - "storage_delete_transfers": { - "comment": "Settings > Storage: button to delete all transfer records.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Alle Übertragungen löschen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Delete all transfers" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminar todas las transferencias" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Supprimer tous les transferts" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Elimina tutti i trasferimenti" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Alle overdrachten verwijderen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usuń wszystkie transfery" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminar todas as transferências" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удалить все передачи" - } - } - } - }, - "storage_delete_transfers_description": { - "comment": "Settings > Storage: confirmation body for deleting all transfer records.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить." - } - } - } - }, - "storage_deleting": { - "comment": "Settings > Storage: progress label while transfers are being deleted.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird gelöscht…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Deleting…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminando…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Suppression…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminazione…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verwijderen…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usuwanie…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A eliminar…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удаление…" - } - } - } - }, - "storage_footer": { - "comment": "Settings > Storage: footer explaining how storage is managed.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragungsdaten umfassen Ihren Verlauf und zwischengespeicherte Inhalte aktiver Freigaben. Nicht mehr benötigter Cache wird nach dem Löschen der Datensätze automatisch bereinigt; dies kann etwas dauern. Empfangene Dateien werden für diese Übersicht erfasst, aber hier niemals gelöscht." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer data includes your history and cached content for active shares. Unneeded cache is reclaimed automatically after transfer records are removed, which may take a little time. Received files are tracked for this summary but are never deleted here." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Los datos de transferencia incluyen su historial y el contenido en caché de los recursos compartidos activos. La caché innecesaria se recupera automáticamente tras eliminar los registros, lo que puede tardar un poco. Los archivos recibidos se registran para este resumen, pero nunca se eliminan aquí." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Les données de transfert comprennent votre historique et le contenu mis en cache pour les partages actifs. Le cache inutile est récupéré automatiquement après la suppression des enregistrements, ce qui peut prendre un peu de temps. Les fichiers reçus sont suivis pour ce récapitulatif, mais ne sont jamais supprimés ici." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "I dati di trasferimento includono la cronologia e il contenuto nella cache per le condivisioni attive. La cache non necessaria viene recuperata automaticamente dopo la rimozione dei record, operazione che può richiedere un po’ di tempo. I file ricevuti vengono monitorati per questo riepilogo, ma non sono mai eliminati qui." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdrachtsgegevens omvatten uw geschiedenis en inhoud in de cache voor actieve shares. Onnodige cache wordt automatisch opgeruimd nadat overdrachtsrecords zijn verwijderd; dit kan enige tijd duren. Ontvangen bestanden worden voor dit overzicht bijgehouden, maar hier nooit verwijderd." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Dane transferu obejmują historię oraz zawartość w pamięci podręcznej dla aktywnych udostępnień. Niepotrzebna pamięć podręczna jest odzyskiwana automatycznie po usunięciu rekordów, co może chwilę potrwać. Odebrane pliki są śledzone na potrzeby tego podsumowania, ale nigdy nie są tu usuwane." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Os dados de transferência incluem o histórico e o conteúdo em cache das partilhas ativas. A cache desnecessária é recuperada automaticamente após a remoção dos registos, o que pode demorar algum tempo. Os ficheiros recebidos são acompanhados para este resumo, mas nunca são eliminados aqui." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Данные передачи включают историю и кэшированное содержимое активных раздач. Ненужный кэш освобождается автоматически после удаления записей; это может занять некоторое время. Полученные файлы учитываются в этой сводке, но никогда не удаляются здесь." - } - } - } - }, - "storage_received_files": { - "comment": "Settings > Storage: label for the received-files size row.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfangene Dateien" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Received files" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Archivos recibidos" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Fichiers reçus" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "File ricevuti" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangen bestanden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odebrane pliki" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ficheiros recebidos" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Полученные файлы" - } - } - } - }, - "storage_temporary": { - "comment": "Settings > Storage: label for the temporary-files size row.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Temporäre Dateien" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Temporary files" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Archivos temporales" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Fichiers temporaires" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "File temporanei" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Tijdelijke bestanden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pliki tymczasowe" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ficheiros temporários" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Временные файлы" - } - } - } - }, - "storage_title": { - "comment": "Settings > Storage: section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Speicher" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Storage" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Almacenamiento" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Stockage" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Archiviazione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Opslag" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pamięć" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Armazenamento" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Хранилище" - } - } - } - }, - "storage_total": { - "comment": "Settings > Storage: label for the total-size row.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Gesamt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Total" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Total" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Total" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Totale" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Totaal" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Łącznie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Total" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Всего" - } - } - } - }, - "storage_transfer_data": { - "comment": "Settings > Storage: label for the transfer-engine data size row.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragungsdaten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer data" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Datos de transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Données de transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dati di trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdrachtsgegevens" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Dane transferu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Dados de transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Данные передачи" - } - } - } - }, - "storage_transfers_deleted": { - "comment": "Settings > Storage: toast confirming all transfers were deleted.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Alle Übertragungen gelöscht" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "All transfers deleted" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Todas las transferencias eliminadas" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Tous les transferts supprimés" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Tutti i trasferimenti eliminati" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Alle overdrachten verwijderd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usunięto wszystkie transfery" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Todas as transferências eliminadas" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Все передачи удалены" - } - } - } - }, - "transfer_activity_description": { - "comment": "Transfer details: description under the Activity section.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wichtige Aktualisierungen zu dieser Übertragung ansehen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "See important updates for this transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Vea las actualizaciones importantes de esta transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Consultez les mises à jour importantes de ce transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Veda gli aggiornamenti importanti di questo trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bekijk belangrijke updates voor deze overdracht" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zobacz ważne aktualizacje tego transferu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ver as atualizações importantes desta transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Просматривайте важные обновления этой передачи" - } - } - } - }, - "transfer_activity_title": { - "comment": "Transfer details: Activity section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Aktivität" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Activity" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Actividad" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Activité" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Attività" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Activiteit" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Aktywność" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Atividade" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Активность" - } - } - } - }, - "transfer_delete_description": { - "comment": "Transfer details: confirmation body for deleting a transfer. {transferName} = transfer name.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "„%1$@“ wird nicht mehr geteilt und sein Übertragungsverlauf wird von diesem Gerät entfernt." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "“%1$@” will stop being shared and its transfer history will be removed from this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "«%1$@» dejará de compartirse y su historial de transferencia se eliminará de este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "« %1$@ » cessera d’être partagé et son historique de transfert sera retiré de cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "«%1$@» non verrà più condiviso e la sua cronologia di trasferimento verrà rimossa da questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "‘%1$@’ wordt niet meer gedeeld en de overdrachtsgeschiedenis wordt van dit apparaat verwijderd." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "„%1$@” przestanie być udostępniany, a jego historia transferu zostanie usunięta z tego urządzenia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "«%1$@» deixará de ser partilhado e o seu histórico de transferência será removido deste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Общий доступ к «%1$@» будет остановлен, а история передачи будет удалена с этого устройства." - } - } - } - }, - "transfer_delete_title": { - "comment": "Transfer details: confirmation title for deleting a transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung löschen?" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Delete transfer?" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "¿Eliminar la transferencia?" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Supprimer le transfert ?" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminare il trasferimento?" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht verwijderen?" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usunąć transfer?" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminar a transferência?" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удалить передачу?" - } - } - } - }, - "transfer_deleted": { - "comment": "Transfer details: toast confirming a transfer was deleted.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung gelöscht." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer deleted." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencia eliminada." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transfert supprimé." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimento eliminato." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht verwijderd." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfer usunięty." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferência eliminada." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача удалена." - } - } - } - }, - "transfer_deleting": { - "comment": "Transfer details: progress label while a transfer is being deleted.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird gelöscht…" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Deleting…" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminando…" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Suppression…" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Eliminazione…" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verwijderen…" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Usuwanie…" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A eliminar…" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Удаление…" - } - } - } - }, - "transfer_details_title": { - "comment": "Transfer details: screen title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragungsdetails" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer details" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Detalles de la transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Détails du transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dettagli del trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdrachtsdetails" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Szczegóły transferu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Detalhes da transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сведения о передаче" - } - } - } - }, - "transfer_event_approved": { - "comment": "Transfer activity event: a receiver's access was approved.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfängerzugriff genehmigt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receiver access approved" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Acceso del destinatario aprobado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Accès du destinataire approuvé" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Accesso del destinatario approvato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Toegang ontvanger goedgekeurd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zatwierdzono dostęp odbiorcy" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Acesso do destinatário aprovado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Доступ получателя одобрен" - } - } - } - }, - "transfer_event_completed": { - "comment": "Transfer activity event: a receiver completed the transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ein Empfänger hat die Übertragung abgeschlossen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "A receiver completed the transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Un destinatario completó la transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Un destinataire a terminé le transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Un destinatario ha completato il trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Een ontvanger heeft de overdracht voltooid" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbiorca ukończył transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Um destinatário concluiu a transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получатель завершил передачу" - } - } - } - }, - "transfer_event_connecting": { - "comment": "Transfer activity event: connecting to the sender.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Verbindung zum Absender" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Connecting to sender" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Conectando con el remitente" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Connexion à l’expéditeur" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Connessione al mittente" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verbinden met afzender" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Łączenie z nadawcą" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A ligar ao remetente" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подключение к отправителю" - } - } - } - }, - "transfer_event_downloading": { - "comment": "Transfer activity event: downloading.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird geladen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Downloading" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Descargando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Téléchargement" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Download" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Downloaden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Pobieranie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A descarregar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Загрузка" - } - } - } - }, - "transfer_event_failed": { - "comment": "Transfer activity event: the transfer hit a problem.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bei der Übertragung ist ein Problem aufgetreten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "The transfer encountered a problem" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "La transferencia tuvo un problema" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Le transfert a rencontré un problème" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Il trasferimento ha riscontrato un problema" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Er is een probleem opgetreden bij de overdracht" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Podczas transferu wystąpił problem" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A transferência teve um problema" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "При передаче возникла проблема" - } - } - } - }, - "transfer_event_preparing": { - "comment": "Transfer activity event: preparing the transfer.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ihre Übertragung wird vorbereitet" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Preparing your transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Preparando su transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Préparation de votre transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Preparazione del trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uw overdracht voorbereiden" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Przygotowywanie transferu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A preparar a sua transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Подготовка вашей передачи" - } - } - } - }, - "transfer_event_ready": { - "comment": "Transfer activity event: ready to share.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Bereit zum Teilen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Ready to share" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Listo para compartir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Prêt à partager" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Pronto per la condivisione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Klaar om te delen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Gotowe do udostępnienia" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pronto para partilhar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Готово к отправке" - } - } - } - }, - "transfer_event_refused": { - "comment": "Transfer activity event: a receiver's access was refused.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfängerzugriff abgelehnt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receiver access refused" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Acceso del destinatario rechazado" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Accès du destinataire refusé" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Accesso del destinatario rifiutato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Toegang ontvanger geweigerd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odrzucono dostęp odbiorcy" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Acesso do destinatário recusado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Доступ получателя отклонён" - } - } - } - }, - "transfer_event_requested": { - "comment": "Transfer activity event: a receiver requested access.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Ein Empfänger hat Zugriff angefragt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "A receiver requested access" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Un destinatario solicitó acceso" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Un destinataire a demandé l’accès" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Un destinatario ha richiesto l’accesso" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Een ontvanger heeft toegang aangevraagd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbiorca poprosił o dostęp" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Um destinatário pediu acesso" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получатель запросил доступ" - } - } - } - }, - "transfer_event_saving": { - "comment": "Transfer activity event: saving received files.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wird gesichert" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Saving" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Guardando" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Enregistrement" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Salvataggio" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Bewaren" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zapisywanie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A guardar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Сохранение" - } - } - } - }, - "transfer_event_stopped": { - "comment": "Transfer activity event: sharing was stopped.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Freigabe beendet" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Sharing stopped" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Se dejó de compartir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Partage arrêté" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Condivisione interrotta" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Delen gestopt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zatrzymano udostępnianie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Partilha parada" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Раздача остановлена" - } - } - } - }, - "transfer_event_updated": { - "comment": "Transfer activity event: the transfer was updated.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Übertragung aktualisiert" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Transfer updated" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Transferencia actualizada" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Transfert mis à jour" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Trasferimento aggiornato" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Overdracht bijgewerkt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Transfer zaktualizowany" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Transferência atualizada" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Передача обновлена" - } - } - } - }, - "transfer_file_count": { - "comment": "Transfer subtitle: file count with pluralization. {count} = number of files.", - "extractionState": "manual", - "localizations": { - "de": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d Datei" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d Dateien" - } - } - } - } - }, - "en": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "translated", - "value": "%1$d file" - } - }, - "other": { - "stringUnit": { - "state": "translated", - "value": "%1$d files" - } - } - } - } - }, - "es": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d archivo" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d archivos" - } - } - } - } - }, - "fr": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d fichier" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d fichiers" - } - } - } - } - }, - "it": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d file" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d file" - } - } - } - } - }, - "nl": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d bestand" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d bestanden" - } - } - } - } - }, - "pl": { - "variations": { - "plural": { - "few": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d pliki" - } - }, - "many": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d plików" - } - }, - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d plik" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d pliku" - } - } - } - } - }, - "pt": { - "variations": { - "plural": { - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d ficheiro" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d ficheiros" - } - } - } - } - }, - "ru": { - "variations": { - "plural": { - "few": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d файла" - } - }, - "many": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d файлов" - } - }, - "one": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d файл" - } - }, - "other": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d файла" - } - } - } - } - } - } - }, - "transfer_invitation_saved": { - "comment": "Transfer share: toast confirming the invitation file was saved.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einladung gesichert." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Invitation saved." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Invitación guardada." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Invitation enregistrée." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invito salvato." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uitnodiging bewaard." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zaproszenie zapisane." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Convite guardado." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Приглашение сохранено." - } - } - } - }, - "transfer_nearby_device": { - "comment": "Transfer share/receivers: label for a nearby device.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Gerät in der Nähe" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Nearby device" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Dispositivo cercano" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Appareil à proximité" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Dispositivo nelle vicinanze" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Apparaat in de buurt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Urządzenie w pobliżu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Dispositivo próximo" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Устройство поблизости" - } - } - } - }, - "transfer_nfc_unavailable": { - "comment": "Transfer share: message when NFC writing isn't available on the device.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Das Beschreiben von NFC-Tags ist auf diesem Gerät nicht verfügbar." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "NFC tag writing is not available on this device." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "La escritura de etiquetas NFC no está disponible en este dispositivo." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "L’écriture de tag NFC n’est pas disponible sur cet appareil." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "La scrittura dei tag NFC non è disponibile su questo dispositivo." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Het schrijven van NFC-tags is niet beschikbaar op dit apparaat." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zapis tagów NFC nie jest dostępny na tym urządzeniu." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A escrita de etiquetas NFC não está disponível neste dispositivo." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Запись NFC-меток недоступна на этом устройстве." - } - } - } - }, - "transfer_nfc_waiting": { - "comment": "Transfer share: prompt while waiting to write an NFC tag.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Halten Sie Ihr Gerät an ein beschreibbares NFC-Tag." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Hold your device near a writable NFC tag." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Acerque su dispositivo a una etiqueta NFC grabable." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Approchez votre appareil d’un tag NFC inscriptible." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Avvicini il dispositivo a un tag NFC scrivibile." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Houd uw apparaat bij een beschrijfbare NFC-tag." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zbliż urządzenie do zapisywalnego tagu NFC." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Aproxime o seu dispositivo de uma etiqueta NFC gravável." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Поднесите устройство к записываемой NFC-метке." - } - } - } - }, - "transfer_nfc_written": { - "comment": "Transfer share: confirmation the invitation was written to the NFC tag.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Einladung auf das NFC-Tag geschrieben." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Invitation written to the NFC tag." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Invitación escrita en la etiqueta NFC." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Invitation écrite sur le tag NFC." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Invito scritto sul tag NFC." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Uitnodiging naar de NFC-tag geschreven." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zaproszenie zapisane na tagu NFC." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Convite escrito na etiqueta NFC." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Приглашение записано на NFC-метку." - } - } - } - }, - "transfer_no_activity": { - "comment": "Transfer details: empty state for the Activity section.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Es gibt noch keine Aktivität anzuzeigen." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "There is no activity to show yet." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Todavía no hay actividad que mostrar." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Aucune activité à afficher pour l’instant." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non c’è ancora alcuna attività da mostrare." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Er is nog geen activiteit om weer te geven." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nie ma jeszcze aktywności do wyświetlenia." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ainda não há atividade para mostrar." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Пока нет активности для отображения." - } - } - } - }, - "transfer_no_receivers": { - "comment": "Transfer details: empty state for the Receivers section.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Noch niemand hat diese Übertragung angefragt." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Nobody has requested this transfer yet." - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Nadie ha solicitado aún esta transferencia." - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Personne n’a encore demandé ce transfert." - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Nessuno ha ancora richiesto questo trasferimento." - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Nog niemand heeft deze overdracht aangevraagd." - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Nikt jeszcze nie poprosił o ten transfer." - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Ainda ninguém pediu esta transferência." - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Никто ещё не запросил эту передачу." - } - } - } - }, - "transfer_receiver_accepted": { - "comment": "Receiver status: approved, waiting for the download to complete.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Genehmigt – wartet auf Abschluss" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Approved — waiting for completion" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Aprobado: esperando a que se complete" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Approuvé — en attente de la fin" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Approvato: in attesa del completamento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Goedgekeurd — wachten op voltooiing" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zatwierdzono — oczekiwanie na ukończenie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Aprovado — a aguardar conclusão" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Одобрено — ожидание завершения" - } - } - } - }, - "transfer_receiver_completed": { - "comment": "Receiver status: received successfully.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Erfolgreich empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Received successfully" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Recibido correctamente" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Reçu avec succès" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Ricevuto correttamente" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Succesvol ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odebrano pomyślnie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Recebido com sucesso" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Успешно получено" - } - } - } - }, - "transfer_receiver_expired": { - "comment": "Receiver status: the request expired.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Anfrage abgelaufen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Request expired" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Solicitud caducada" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Demande expirée" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Richiesta scaduta" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verzoek verlopen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Prośba wygasła" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pedido expirado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Запрос истёк" - } - } - } - }, - "transfer_receiver_refused": { - "comment": "Receiver status: the request was refused.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Anfrage abgelehnt" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Request refused" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Solicitud rechazada" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Demande refusée" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Richiesta rifiutata" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verzoek geweigerd" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Prośba odrzucona" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pedido recusado" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Запрос отклонён" - } - } - } - }, - "transfer_receiver_requested": { - "comment": "Receiver status: waiting for the sender's approval.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Wartet auf Ihre Genehmigung" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Waiting for your approval" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Esperando su aprobación" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "En attente de votre approbation" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "In attesa della sua approvazione" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Wachten op uw goedkeuring" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Oczekiwanie na Twoje zatwierdzenie" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "A aguardar a sua aprovação" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Ожидание вашего одобрения" - } - } - } - }, - "transfer_receiver_unknown": { - "comment": "Receiver status: status unavailable/unknown.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Status nicht verfügbar" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Status unavailable" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Estado no disponible" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Statut indisponible" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Stato non disponibile" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Status niet beschikbaar" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Status niedostępny" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Estado indisponível" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Статус недоступен" - } - } - } - }, - "transfer_receivers_completed_count": { - "comment": "Receivers summary: how many receivers completed. {count} = completed receivers.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d abgeschlossen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$d completed" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d completados" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d terminés" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d completati" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d voltooid" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Ukończone: %1$d" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d concluídos" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Завершено: %1$d" - } - } - } - }, - "transfer_receivers_description": { - "comment": "Transfer details: description under the Receivers section.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Anfragen, Genehmigungen und abgeschlossene Zustellungen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Requests, approvals, and completed deliveries" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Solicitudes, aprobaciones y entregas completadas" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Demandes, approbations et livraisons terminées" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Richieste, approvazioni e consegne completate" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Verzoeken, goedkeuringen en voltooide leveringen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Prośby, zatwierdzenia i ukończone dostawy" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Pedidos, aprovações e entregas concluídas" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Запросы, одобрения и завершённые доставки" - } - } - } - }, - "transfer_receivers_pending": { - "comment": "Receivers summary: how many requests are waiting. {count} = pending receivers.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d warten" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$d waiting" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d en espera" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d en attente" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d in attesa" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d in behandeling" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Oczekujące: %1$d" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$d em espera" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Ожидают: %1$d" - } - } - } - }, - "transfer_receivers_title": { - "comment": "Transfer details: Receivers section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Empfänger" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Receivers" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Destinatarios" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Destinataires" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Destinatari" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Ontvangers" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Odbiorcy" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Destinatários" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Получатели" - } - } - } - }, - "transfer_scan_qr": { - "comment": "Transfer share: caption under the QR code.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Mit VniDrop scannen, um diese Übertragung zu empfangen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Scan with VniDrop to receive this transfer" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Escanee con VniDrop para recibir esta transferencia" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Scannez avec VniDrop pour recevoir ce transfert" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Scansioni con VniDrop per ricevere questo trasferimento" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Scan met VniDrop om deze overdracht te ontvangen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Zeskanuj za pomocą VniDrop, aby odebrać ten transfer" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Leia com o VniDrop para receber esta transferência" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Отсканируйте с помощью VniDrop, чтобы получить эту передачу" - } - } - } - }, - "transfer_share_description": { - "comment": "Transfer details: description under the Share section.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "QR-Code, Einladungsdatei und Optionen in der Nähe" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "QR code, invitation file, and nearby options" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Código QR, archivo de invitación y opciones cercanas" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "QR code, fichier d’invitation et options à proximité" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Codice QR, file di invito e opzioni nelle vicinanze" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "QR-code, uitnodigingsbestand en opties in de buurt" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Kod QR, plik zaproszenia i opcje w pobliżu" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Código QR, ficheiro de convite e opções por perto" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "QR-код, файл приглашения и варианты поблизости" - } - } - } - }, - "transfer_share_title": { - "comment": "Transfer details: Share section title.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Teilen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Share" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Compartir" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Partager" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Condividi" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Delen" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Udostępnij" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Partilhar" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Поделиться" - } - } - } - }, - "value_unavailable": { - "comment": "Placeholder shown when a device-info or metadata value can't be read.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "Nicht verfügbar" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Not available" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "No disponible" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Non disponible" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Non disponibile" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "Niet beschikbaar" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Niedostępne" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Indisponível" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Недоступно" - } - } - } - }, - "version_title": { - "comment": "Device information / Settings row: app version label.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "App-Version" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "App version" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "Versión de la app" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "Version de l’app" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "Versione dell’app" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "App-versie" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "Wersja aplikacji" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "Versão da app" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "Версия приложения" - } - } - } - } - }, - "version": "1.0" -} diff --git a/apple/project.yml b/apple/project.yml index ff0b7fb..3ec7665 100644 --- a/apple/project.yml +++ b/apple/project.yml @@ -1,6 +1,9 @@ # XcodeGen spec for the native SwiftUI VniDrop app (iOS/iPadOS/macOS). # Regenerate the project with: xcodegen generate (run from apple/) -# Requires the Rust core first: apple/scripts/build-core.sh debug +# Requires two generated inputs first (both gitignored), before xcodegen: +# - Rust core: apple/scripts/build-core.sh debug +# - Localization: (cd localization && bun run src/cli.ts generate) +# -> VniDrop/Resources/Localizable.xcstrings, VniDrop/Generated/L10n.swift name: VniDrop options: bundleIdPrefix: com.vnidrop From 3469b122c25821af21ffdb38b1b158e93c820a82 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:29:40 +0200 Subject: [PATCH 07/36] feat(apple): local notifications for transfer lifecycle events Adds background notifications for the "the thing you were waiting for is done" moments, alongside the existing incoming-approval-request one: - a receive finished downloading (receive -> done) - a receive failed / was interrupted (receive -> failed) - a share you own failed (send -> failed) - a receiver finished downloading your share (receiver status completed) A new TransferNotificationCoordinator observes core state + signals and publishes these; the decision of which moments notify is a pure function (plannedTransferNotifications / plannedReceiverNotifications), unit-tested independently. The first state snapshot only primes existing history as seen so launch doesn't spam. Notification permission is now the single source of truth. The in-app notifications toggle and its decoupled UserDefaults preference are gone; the Settings section shows an "Allow notifications" button that requests the OS permission (or deep-links to Settings once decided), and notifications gate purely on `permission == .granted`. macOS delivery fixes: - add a UNUserNotificationCenterDelegate so banners present even while the app is active (the app window is usually open on macOS) - present-when-active on macOS, suppress-when-foregrounded on iOS - reserve the notification id before awaiting publish: the CombineLatest fired several times and re-added the same identifier, which macOS coalesces into a silent update with no banner - LocalNotificationService seeds its permission at init so gating can't race a not-yet-refreshed .notDetermined Eight localized title/body strings added (apple-only); the shared notifications_description copy is generalized from "receive requests" to "transfer activity". --- .../Tests/AppPreferencesRepositoryTests.swift | 3 - apple/Tests/ApprovalCoordinatorTests.swift | 1 - apple/Tests/TransferNotificationTests.swift | 44 +++++ apple/VniDrop/App/AppGraph.swift | 9 +- apple/VniDrop/Core/AppPreferences.swift | 10 - .../Core/LocalNotificationService.swift | 25 ++- .../Approvals/ApprovalCoordinator.swift | 35 ++-- .../TransferNotificationCoordinator.swift | 184 ++++++++++++++++++ .../Features/Settings/SettingsModel.swift | 49 +---- .../Features/Settings/SettingsSections.swift | 20 +- localization/strings.json | 182 ++++++++++++++++- .../composeResources/values-de/strings.xml | 2 +- .../composeResources/values-es/strings.xml | 2 +- .../composeResources/values-fr/strings.xml | 2 +- .../composeResources/values-it/strings.xml | 2 +- .../composeResources/values-nl/strings.xml | 2 +- .../composeResources/values-pl/strings.xml | 2 +- .../composeResources/values-pt/strings.xml | 2 +- .../composeResources/values-ru/strings.xml | 2 +- .../composeResources/values/strings.xml | 2 +- 20 files changed, 482 insertions(+), 98 deletions(-) create mode 100644 apple/Tests/TransferNotificationTests.swift create mode 100644 apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift diff --git a/apple/Tests/AppPreferencesRepositoryTests.swift b/apple/Tests/AppPreferencesRepositoryTests.swift index eb4ef11..b07d7fe 100644 --- a/apple/Tests/AppPreferencesRepositoryTests.swift +++ b/apple/Tests/AppPreferencesRepositoryTests.swift @@ -19,7 +19,6 @@ final class AppPreferencesRepositoryTests: XCTestCase { let repo = AppPreferencesRepository(defaults: defaults(), fallback: fallback()) XCTAssertEqual(repo.preferences.username, "Default") XCTAssertEqual(repo.preferences.themeMode, .system) - XCTAssertFalse(repo.preferences.notificationsEnabled) } func testValuesPersistAndReload() { @@ -28,14 +27,12 @@ final class AppPreferencesRepositoryTests: XCTestCase { let repo = AppPreferencesRepository(defaults: store, fallback: fb) repo.setUsername("Bob") repo.setThemeMode(.dark) - repo.setNotificationsEnabled(true) repo.setReceiveFolder(ReceiveFolder(kind: .iosSecurityScopedUrl, value: "file:///x", displayName: "Custom")) // A fresh repository over the same store reflects the persisted values. let reloaded = AppPreferencesRepository(defaults: store, fallback: fb) XCTAssertEqual(reloaded.preferences.username, "Bob") XCTAssertEqual(reloaded.preferences.themeMode, .dark) - XCTAssertTrue(reloaded.preferences.notificationsEnabled) XCTAssertEqual(reloaded.preferences.receiveFolder.displayName, "Custom") XCTAssertEqual(reloaded.preferences.receiveFolder.kind, .iosSecurityScopedUrl) } diff --git a/apple/Tests/ApprovalCoordinatorTests.swift b/apple/Tests/ApprovalCoordinatorTests.swift index 97ac8c0..617578a 100644 --- a/apple/Tests/ApprovalCoordinatorTests.swift +++ b/apple/Tests/ApprovalCoordinatorTests.swift @@ -11,7 +11,6 @@ final class ApprovalCoordinatorTests: XCTestCase { private func makeCoordinator(_ core: FakeCoreGateway) -> ApprovalCoordinator { ApprovalCoordinator( repository: core, - preferences: Fixtures.preferences(), notifications: LocalNotificationService(), visibility: AppVisibility(), messages: UiMessageController() diff --git a/apple/Tests/TransferNotificationTests.swift b/apple/Tests/TransferNotificationTests.swift new file mode 100644 index 0000000..e38e911 --- /dev/null +++ b/apple/Tests/TransferNotificationTests.swift @@ -0,0 +1,44 @@ +import XCTest +@testable import VniDrop + +@MainActor +final class TransferNotificationTests: XCTestCase { + + func testTransferNotificationsFireForTerminalStatesOnly() { + let transfers = [ + Fixtures.transfer(id: 1, direction: .send, status: .failed), + Fixtures.transfer(id: 2, direction: .receive, status: .done), + Fixtures.transfer(id: 3, direction: .receive, status: .failed), + Fixtures.transfer(id: 4, direction: .receive, status: .receiving), // in-flight, ignored + Fixtures.transfer(id: 5, direction: .send, status: .sharing), // active share, ignored + Fixtures.transfer(id: 6, direction: .send, status: .done), // send-done isn't notified + ] + let planned = plannedTransferNotifications(transfers, published: []) + XCTAssertEqual(planned.map(\.kind), [.sendFailed, .receiveCompleted, .receiveFailed]) + XCTAssertEqual(planned.map(\.id), ["send-failed-1", "receive-completed-2", "receive-failed-3"]) + XCTAssertEqual(planned.first?.transferName, "Photos") + } + + func testTransferNotificationsSkipAlreadyPublished() { + let transfers = [Fixtures.transfer(id: 2, direction: .receive, status: .done)] + XCTAssertTrue(plannedTransferNotifications(transfers, published: ["receive-completed-2"]).isEmpty) + } + + func testReceiverNotificationsFireOnlyForCompletedReceivers() { + let requests = [ + Fixtures.request(id: "a", requestedAt: 1, status: .completed), + Fixtures.request(id: "b", requestedAt: 2, status: .accepted), + Fixtures.request(id: "c", requestedAt: 3, status: .requested), + ] + let planned = plannedReceiverNotifications(requests, published: []) + XCTAssertEqual(planned.map(\.id), ["receiver-completed-a"]) + XCTAssertEqual(planned.first?.kind, .receiverCompleted) + XCTAssertEqual(planned.first?.receiver, "Peer") + XCTAssertEqual(planned.first?.transferName, "Photos") + } + + func testReceiverNotificationsSkipAlreadyPublished() { + let requests = [Fixtures.request(id: "a", requestedAt: 1, status: .completed)] + XCTAssertTrue(plannedReceiverNotifications(requests, published: ["receiver-completed-a"]).isEmpty) + } +} diff --git a/apple/VniDrop/App/AppGraph.swift b/apple/VniDrop/App/AppGraph.swift index dd029a9..d261499 100644 --- a/apple/VniDrop/App/AppGraph.swift +++ b/apple/VniDrop/App/AppGraph.swift @@ -12,6 +12,7 @@ final class AppGraph: ObservableObject { let preferencesRepository: AppPreferencesRepository let filePreviewRepository: FilePreviewRepository let approvalCoordinator: ApprovalCoordinator + let transferNotificationCoordinator: TransferNotificationCoordinator init(dependencies: AppDependencies, coreRepository: CoreRepository? = nil) { self.dependencies = dependencies @@ -23,13 +24,17 @@ final class AppGraph: ObservableObject { username: dependencies.environment.defaultUsername, receiveFolder: dependencies.fileSystemService.defaultReceiveFolder(), themeMode: .system, - notificationsEnabled: false, diagnosticsEnabled: false ) ) self.approvalCoordinator = ApprovalCoordinator( repository: coreRepository, - preferences: preferencesRepository, + notifications: dependencies.notificationService, + visibility: visibility, + messages: messages + ) + self.transferNotificationCoordinator = TransferNotificationCoordinator( + repository: coreRepository, notifications: dependencies.notificationService, visibility: visibility, messages: messages diff --git a/apple/VniDrop/Core/AppPreferences.swift b/apple/VniDrop/Core/AppPreferences.swift index 74049c0..1508d4d 100644 --- a/apple/VniDrop/Core/AppPreferences.swift +++ b/apple/VniDrop/Core/AppPreferences.swift @@ -25,7 +25,6 @@ struct AppPreferences: Equatable { var username: String var receiveFolder: ReceiveFolder var themeMode: ThemeMode - var notificationsEnabled: Bool var diagnosticsEnabled: Bool var diagnosticsInstallId: String } @@ -34,7 +33,6 @@ struct AppPreferencesDefaults { let username: String let receiveFolder: ReceiveFolder let themeMode: ThemeMode - var notificationsEnabled: Bool = false var diagnosticsEnabled: Bool = false } @@ -51,7 +49,6 @@ final class AppPreferencesRepository: ObservableObject { static let receiveFolderValue = "receive_folder_value" static let receiveFolderDisplayName = "receive_folder_display_name" static let themeMode = "theme_mode" - static let notificationsEnabled = "notifications_enabled" static let diagnosticsEnabled = "diagnostics_enabled" static let diagnosticsInstallId = "diagnostics_install_id" } @@ -66,14 +63,12 @@ final class AppPreferencesRepository: ObservableObject { let username = (defaults.string(forKey: Key.username)).flatMap { $0.isEmpty ? nil : $0 } ?? fallback.username let folder = resolveReceiveFolder(defaults, fallback: fallback.receiveFolder) let themeMode = defaults.string(forKey: Key.themeMode).flatMap(ThemeMode.init(rawValue:)) ?? fallback.themeMode - let notifications = defaults.object(forKey: Key.notificationsEnabled) as? Bool ?? fallback.notificationsEnabled let diagnostics = defaults.object(forKey: Key.diagnosticsEnabled) as? Bool ?? fallback.diagnosticsEnabled let installId = defaults.string(forKey: Key.diagnosticsInstallId) ?? "" return AppPreferences( username: username, receiveFolder: folder, themeMode: themeMode, - notificationsEnabled: notifications, diagnosticsEnabled: diagnostics, diagnosticsInstallId: installId ) @@ -113,11 +108,6 @@ final class AppPreferencesRepository: ObservableObject { reload() } - func setNotificationsEnabled(_ enabled: Bool) { - defaults.set(enabled, forKey: Key.notificationsEnabled) - reload() - } - func setDiagnosticsEnabled(_ enabled: Bool) { defaults.set(enabled, forKey: Key.diagnosticsEnabled) reload() diff --git a/apple/VniDrop/Core/LocalNotificationService.swift b/apple/VniDrop/Core/LocalNotificationService.swift index 4f04f68..4f7f11c 100644 --- a/apple/VniDrop/Core/LocalNotificationService.swift +++ b/apple/VniDrop/Core/LocalNotificationService.swift @@ -16,12 +16,32 @@ struct LocalNotification { let body: String } +/// Presents notifications even while the app is active. Without a delegate the +/// system drops the banner when the app is frontmost — very visible on macOS, +/// where the app window is usually open when a transfer completes. +private final class NotificationPresenter: NSObject, UNUserNotificationCenterDelegate { + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification + ) async -> UNNotificationPresentationOptions { + [.banner, .sound, .list] + } +} + /// Local notification service backed by `UNUserNotificationCenter`. @MainActor final class LocalNotificationService: ObservableObject { @Published private(set) var permission: NotificationPermission = .notDetermined private let center = UNUserNotificationCenter.current() + private let presenter = NotificationPresenter() + + init() { + center.delegate = presenter + // Seed the permission immediately so gating (approval/lifecycle + // notifications) never races a not-yet-refreshed `.notDetermined`. + Task { _ = await refreshPermission() } + } func refreshPermission() async -> NotificationPermission { let settings = await center.notificationSettings() @@ -75,11 +95,6 @@ final class LocalNotificationService: ObservableObject { center.removeDeliveredNotifications(withIdentifiers: [id]) } - func cancelAll() { - center.removeAllPendingNotificationRequests() - center.removeAllDeliveredNotifications() - } - private static func map(_ status: UNAuthorizationStatus) -> NotificationPermission { switch status { case .authorized, .provisional, .ephemeral: return .granted diff --git a/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift b/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift index 6b9a844..5b2bd7d 100644 --- a/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift +++ b/apple/VniDrop/Features/Approvals/ApprovalCoordinator.swift @@ -27,7 +27,6 @@ final class ApprovalCoordinator: ObservableObject { @Published private(set) var state = ApprovalState() private let repository: CoreGateway - private let preferences: AppPreferencesRepository private let notifications: LocalNotificationService private let visibility: AppVisibility private let messages: UiMessageController @@ -37,13 +36,11 @@ final class ApprovalCoordinator: ObservableObject { init( repository: CoreGateway, - preferences: AppPreferencesRepository, notifications: LocalNotificationService, visibility: AppVisibility, messages: UiMessageController ) { self.repository = repository - self.preferences = preferences self.notifications = notifications self.visibility = visibility self.messages = messages @@ -68,17 +65,15 @@ final class ApprovalCoordinator: ObservableObject { .store(in: &cancellables) // Recompute notifications when any input changes. - Publishers.CombineLatest4( - preferences.$preferences, + Publishers.CombineLatest3( visibility.$isForeground, $state, notifications.$permission ) - .sink { [weak self] preferences, foreground, approvalState, permission in + .sink { [weak self] foreground, approvalState, permission in guard let self else { return } Task { await self.synchronizeNotifications( - enabled: preferences.notificationsEnabled, foreground: foreground, pending: approvalState.pending, permission: permission @@ -137,16 +132,30 @@ final class ApprovalCoordinator: ObservableObject { } private func synchronizeNotifications( - enabled: Bool, foreground: Bool, pending: [PendingApproval], permission: NotificationPermission ) async { - if foreground || !enabled || permission != .granted { - notifications.cancelAll() + // iOS suppresses notifications while the user is in the app (the modal shows + // instead); macOS presents them even when active (the app window is usually + // open), relying on the presenter delegate. + #if os(iOS) + let suppressed = foreground || permission != .granted + #else + let suppressed = permission != .granted + #endif + if suppressed { + // Cancel only our own approval notifications — other coordinators + // (e.g. transfer-lifecycle) manage their own and must not be wiped. + for id in publishedNotificationIds { notifications.cancel(id: Self.notificationId(id)) } return } for request in pending where !publishedNotificationIds.contains(request.id) { + // Reserve the id *before* awaiting: the CombineLatest can fire several + // times near-simultaneously, and without this each pass re-adds the same + // notification identifier. macOS coalesces a repeated add of an in-flight + // id into a silent update and shows no banner. + publishedNotificationIds.insert(request.id) let receiver = request.receiverName ?? request.receiverDeviceName ?? String(localized: L10n.Approval.nearbyDevice) @@ -155,9 +164,9 @@ final class ApprovalCoordinator: ObservableObject { let result = await notifications.publish( LocalNotification(id: Self.notificationId(request.id), title: title, body: body) ) - switch result { - case .success: publishedNotificationIds.insert(request.id) - case .failure(let error): messages.error(error) + if case .failure(let error) = result { + publishedNotificationIds.remove(request.id) + messages.error(error) } } } diff --git a/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift b/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift new file mode 100644 index 0000000..0b166fc --- /dev/null +++ b/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift @@ -0,0 +1,184 @@ +import Combine +import Foundation + +/// A transfer-lifecycle moment worth a local notification. +enum TransferNotificationKind: Equatable { + case sendFailed // A share you own failed. + case receiveCompleted // An incoming transfer finished downloading. + case receiveFailed // An incoming transfer failed. + case receiverCompleted // A receiver finished downloading your shared transfer. +} + +/// A notification resolved from core state but not yet published. `transferName` +/// is the raw name (may be nil); the coordinator localizes and applies fallbacks. +struct PlannedNotification: Equatable { + let id: String + let kind: TransferNotificationKind + let transferName: String? + let receiver: String? +} + +/// Pure: transfer-status notifications for this snapshot, excluding already-published +/// ids. A terminal transfer yields at most one notification, keyed by (kind, id). +func plannedTransferNotifications(_ transfers: [Transfer], published: Set) -> [PlannedNotification] { + transfers.compactMap { transfer in + let kind: TransferNotificationKind + switch (transfer.direction, transfer.status) { + case (.send, .failed): kind = .sendFailed + case (.receive, .done): kind = .receiveCompleted + case (.receive, .failed): kind = .receiveFailed + default: return nil + } + let id = transferNotificationId(kind, transferId: transfer.transferId) + guard !published.contains(id) else { return nil } + return PlannedNotification(id: id, kind: kind, transferName: transfer.transferName, receiver: nil) + } +} + +/// Pure: one notification per receiver that has finished downloading a shared +/// transfer, excluding already-published ids. +func plannedReceiverNotifications(_ requests: [ReceiverRequestModel], published: Set) -> [PlannedNotification] { + requests.compactMap { request in + guard request.status == .completed else { return nil } + let id = "receiver-completed-\(request.id)" + guard !published.contains(id) else { return nil } + return PlannedNotification( + id: id, kind: .receiverCompleted, + transferName: request.transferName, + receiver: request.receiverName ?? request.receiverDeviceName + ) + } +} + +private func transferNotificationId(_ kind: TransferNotificationKind, transferId: UInt64) -> String { + switch kind { + case .sendFailed: return "send-failed-\(transferId)" + case .receiveCompleted: return "receive-completed-\(transferId)" + case .receiveFailed: return "receive-failed-\(transferId)" + case .receiverCompleted: return "receiver-completed-\(transferId)" + } +} + +/// Fires local notifications for transfer-lifecycle moments (a receive finishing +/// or failing, a share failing, a receiver completing), so a user who left the +/// app can see the outcome. Approval prompts are handled by `ApprovalCoordinator`. +/// +/// Gated on the OS notification permission (and, on iOS, on being backgrounded). +/// Each moment is terminal, so it is marked seen the first time it is observed and +/// never re-published. The first state snapshot — which includes existing history +/// such as past receives — only primes those ids as seen, so only new transitions +/// notify. +@MainActor +final class TransferNotificationCoordinator: ObservableObject { + private let repository: CoreGateway + private let notifications: LocalNotificationService + private let visibility: AppVisibility + private let messages: UiMessageController + + private var published = Set() + private var primedTransfers = false + private var cancellables = Set() + + init( + repository: CoreGateway, + notifications: LocalNotificationService, + visibility: AppVisibility, + messages: UiMessageController + ) { + self.repository = repository + self.notifications = notifications + self.visibility = visibility + self.messages = messages + + repository.statePublisher + .sink { [weak self] core in + guard let self, core.isInitialized else { return } + Task { await self.syncTransfers(core.transfers) } + } + .store(in: &cancellables) + + repository.signals + .sink { [weak self] signal in + guard let self else { return } + switch signal { + case .receiverHistoryChanged(let transferId), .transfersChanged(let transferId): + Task { await self.syncReceivers(transferId: transferId) } + case .approvalChanged: + break + } + } + .store(in: &cancellables) + } + + /// iOS suppresses notifications while the user is in the app (the convention); + /// macOS presents them even when active (also the convention — the app window + /// is usually open), relying on the presenter delegate to show the banner. + private var canPublish: Bool { + guard notifications.permission == .granted else { return false } + #if os(iOS) + return !visibility.isForeground + #else + return true + #endif + } + + private func syncTransfers(_ transfers: [Transfer]) async { + let planned = plannedTransferNotifications(transfers, published: published) + guard primedTransfers else { + // The first snapshot includes existing history (e.g. past receives). + // Mark those terminal transfers seen without notifying, so only new + // transitions notify. + primedTransfers = true + for plan in planned { published.insert(plan.id) } + return + } + for plan in planned { await deliver(plan) } + } + + private func syncReceivers(transferId: UInt64) async { + let result = await repository.receiverRequests(transferId: transferId) + switch result { + case .success(let requests): + for plan in plannedReceiverNotifications(requests, published: published) { + await deliver(plan) + } + case .failure(let error): + messages.error(error) + } + } + + /// Mark seen unconditionally (a terminal moment notifies at most once), then + /// publish only when the gate allows. + private func deliver(_ plan: PlannedNotification) async { + published.insert(plan.id) + guard canPublish else { return } + let name = plan.transferName ?? String(localized: L10n.Receive.unknownTransfer) + let notification: LocalNotification + switch plan.kind { + case .sendFailed: + notification = LocalNotification( + id: plan.id, + title: String(localized: L10n.Notifications.sendFailedTitle), + body: L10n.Notifications.sendFailedBody(transferName: name)) + case .receiveCompleted: + notification = LocalNotification( + id: plan.id, + title: String(localized: L10n.Notifications.receiveCompletedTitle), + body: L10n.Notifications.receiveCompletedBody(transferName: name)) + case .receiveFailed: + notification = LocalNotification( + id: plan.id, + title: String(localized: L10n.Notifications.receiveFailedTitle), + body: L10n.Notifications.receiveFailedBody(transferName: name)) + case .receiverCompleted: + let receiver = plan.receiver ?? String(localized: L10n.Approval.nearbyDevice) + notification = LocalNotification( + id: plan.id, + title: String(localized: L10n.Notifications.receiverCompletedTitle), + body: L10n.Notifications.receiverCompletedBody(receiver: receiver, transferName: name)) + } + if case .failure(let error) = await notifications.publish(notification) { + messages.error(error) + } + } +} diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift index ceef5b8..2ddc6b0 100644 --- a/apple/VniDrop/Features/Settings/SettingsModel.swift +++ b/apple/VniDrop/Features/Settings/SettingsModel.swift @@ -41,7 +41,6 @@ struct SettingsState: Equatable { var isValidatingFolder = false var supportsCustomReceiveFolders = true var themeMode: ThemeMode = .system - var notificationsEnabled = false var notificationPermission: NotificationPermission = .notDetermined var diagnosticsEnabled = false var deviceInfo: DeviceInfo? @@ -63,7 +62,7 @@ struct SettingsState: Equatable { && lhs.receiveFolder == rhs.receiveFolder && lhs.folderAccessStatus == rhs.folderAccessStatus && lhs.isValidatingFolder == rhs.isValidatingFolder && lhs.supportsCustomReceiveFolders == rhs.supportsCustomReceiveFolders - && lhs.themeMode == rhs.themeMode && lhs.notificationsEnabled == rhs.notificationsEnabled + && lhs.themeMode == rhs.themeMode && lhs.notificationPermission == rhs.notificationPermission && lhs.diagnosticsEnabled == rhs.diagnosticsEnabled && lhs.appVersion == rhs.appVersion && lhs.isLoadingDeviceInfo == rhs.isLoadingDeviceInfo @@ -93,7 +92,6 @@ final class SettingsModel: ObservableObject { private let bugReports: BugReportService private let diagnosticsIncluded: Bool - private var enableNotificationsAfterSettings = false private var usernamePersistTask: Task? private var hasLocalUsernameDraft = false private var cancellables = Set() @@ -131,7 +129,6 @@ final class SettingsModel: ObservableObject { self.state.username = self.hasLocalUsernameDraft ? self.state.username : prefs.username self.state.receiveFolder = folder self.state.themeMode = prefs.themeMode - self.state.notificationsEnabled = prefs.notificationsEnabled self.state.diagnosticsEnabled = prefs.diagnosticsEnabled if folder != previousFolder { Task { await self.validateFolder(folder) } } } @@ -171,26 +168,14 @@ final class SettingsModel: ObservableObject { func onReceiveFolderPickFailed(_ reason: String) { messages.error(InvitationError.message(reason)) } func resetReceiveFolder() { preferences.resetReceiveFolder() } - func setNotificationsEnabled(_ enabled: Bool) { + /// Ask the OS for notification permission. This is the only time the app can + /// grant it; disabling or fine-tuning afterwards happens in the Settings app. + func requestNotifications() { Task { - if !enabled { - preferences.setNotificationsEnabled(false) - notifications.cancelAll() - return - } let permission = await notifications.requestPermission() state.notificationPermission = permission - if permission == .granted { - await enableNotifications() - } else { - preferences.setNotificationsEnabled(false) - let key = permission == .unsupported ? L10n.Notifications.unsupported : L10n.Notifications.permissionDenied - messages.show(UiMessage( - text: .resource(key), - tone: .warning, - actionLabel: permission == .denied ? .resource(L10n.Button.openSettings) : nil, - onAction: permission == .denied ? { self.openNotificationSettings() } : nil - )) + if permission == .unsupported { + messages.show(UiMessage(text: .resource(L10n.Notifications.unsupported), tone: .warning)) } } } @@ -253,34 +238,20 @@ final class SettingsModel: ObservableObject { func openNotificationSettings() { Task { - enableNotificationsAfterSettings = true - let result = await notifications.openSettings() - if case .failure = result { - enableNotificationsAfterSettings = false + if case .failure = await notifications.openSettings() { messages.show(UiMessage(text: .resource(L10n.Notifications.settingsOpenFailed), tone: .error)) } } } + /// Re-read the OS permission (called on appear and when returning to the + /// foreground, e.g. after a trip to Settings) so the toggle stays in sync. func refreshNotificationPermission() { Task { - let permission = await notifications.refreshPermission() - state.notificationPermission = permission - if enableNotificationsAfterSettings { - enableNotificationsAfterSettings = false - if permission == .granted { await enableNotifications() } - } else if permission != .granted && state.notificationsEnabled { - preferences.setNotificationsEnabled(false) - notifications.cancelAll() - } + state.notificationPermission = await notifications.refreshPermission() } } - private func enableNotifications() async { - preferences.setNotificationsEnabled(true) - messages.show(UiMessage(text: .resource(L10n.Notifications.enabledMessage), tone: .success)) - } - // MARK: - Storage /// Recomputes the on-disk usage breakdown off the main actor. diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index 665bcb9..509c4b6 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -45,16 +45,22 @@ struct NotificationSettings: View { var body: some View { Section { - Toggle(isOn: Binding( - get: { model.state.notificationsEnabled }, - set: { model.setNotificationsEnabled($0) } - )) { - Text(String(localized: L10n.Notifications.localTitle)) - } - if model.state.notificationPermission == .denied { + Text(String(localized: L10n.Notifications.description)).foregroundStyle(.secondary) + switch model.state.notificationPermission { + case .notDetermined: + Button(String(localized: L10n.Notifications.localTitle), action: model.requestNotifications) + case .granted: + // Allowed — the OS Settings app is where you disable or fine-tune. + Text(String(localized: L10n.Notifications.enabledMessage)).foregroundStyle(.secondary) Button(String(localized: L10n.Button.openSettings), action: model.openNotificationSettings) + case .denied: + Text(String(localized: L10n.Notifications.permissionDenied)).foregroundStyle(.secondary) + Button(String(localized: L10n.Button.openSettings), action: model.openNotificationSettings) + case .unsupported: + Text(String(localized: L10n.Notifications.unsupported)).foregroundStyle(.secondary) } } + .onAppear { model.refreshNotificationPermission() } } } diff --git a/localization/strings.json b/localization/strings.json index dec300d..02cf6c7 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -1757,15 +1757,15 @@ "notifications_description": { "context": "Settings > Notifications: explanation of what notifications are used for.", "translations": { - "en": "Get notified about new receive requests while VniDrop is in the background.", - "fr": "Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan.", - "es": "Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano.", - "it": "Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background.", - "de": "Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft.", - "pt": "Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano.", - "pl": "Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle.", - "nl": "Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait.", - "ru": "Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне." + "en": "Get notified about transfer activity while VniDrop is in the background.", + "fr": "Soyez averti de l’activité des transferts lorsque VniDrop est en arrière-plan.", + "es": "Reciba avisos sobre la actividad de las transferencias cuando VniDrop está en segundo plano.", + "it": "Ricevi avvisi sull’attività dei trasferimenti quando VniDrop è in background.", + "de": "Werden Sie über Übertragungsaktivitäten benachrichtigt, während VniDrop im Hintergrund läuft.", + "pt": "Seja notificado sobre a atividade das transferências enquanto o VniDrop está em segundo plano.", + "pl": "Otrzymuj powiadomienia o aktywności transferów, gdy VniDrop działa w tle.", + "nl": "Ontvang meldingen over overdrachtsactiviteit terwijl VniDrop op de achtergrond draait.", + "ru": "Получайте уведомления об активности передач, пока VniDrop работает в фоне." } }, "notifications_enabled_message": { @@ -1810,6 +1810,170 @@ "ru": "Уведомления отключены для VniDrop. Вы можете включить их в Настройках." } }, + "notifications_receive_completed_body": { + "context": "Notification body shown when an incoming transfer finishes downloading. {transferName} = transfer name.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "transferName", + "type": "string" + } + ], + "translations": { + "en": "“{transferName}” finished downloading.", + "fr": "« {transferName} » a fini de se télécharger.", + "es": "«{transferName}» terminó de descargarse.", + "it": "«{transferName}» è stato scaricato.", + "de": "„{transferName}“ wurde vollständig heruntergeladen.", + "pt": "«{transferName}» concluiu a transferência.", + "pl": "Zakończono pobieranie „{transferName}”.", + "nl": "‘{transferName}’ is volledig gedownload.", + "ru": "«{transferName}» завершил загрузку." + } + }, + "notifications_receive_completed_title": { + "context": "Notification title shown when an incoming transfer finishes downloading.", + "targets": [ + "apple" + ], + "translations": { + "en": "Download complete", + "fr": "Téléchargement terminé", + "es": "Descarga completada", + "it": "Download completato", + "de": "Download abgeschlossen", + "pt": "Transferência concluída", + "pl": "Pobieranie zakończone", + "nl": "Download voltooid", + "ru": "Загрузка завершена" + } + }, + "notifications_receive_failed_body": { + "context": "Notification body shown when an incoming transfer fails. {transferName} = transfer name.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "transferName", + "type": "string" + } + ], + "translations": { + "en": "“{transferName}” couldn’t be received.", + "fr": "« {transferName} » n’a pas pu être reçu.", + "es": "No se pudo recibir «{transferName}».", + "it": "Impossibile ricevere «{transferName}».", + "de": "„{transferName}“ konnte nicht empfangen werden.", + "pt": "Não foi possível receber «{transferName}».", + "pl": "Nie udało się odebrać „{transferName}”.", + "nl": "‘{transferName}’ kon niet worden ontvangen.", + "ru": "Не удалось получить «{transferName}»." + } + }, + "notifications_receive_failed_title": { + "context": "Notification title shown when an incoming transfer fails.", + "targets": [ + "apple" + ], + "translations": { + "en": "Download failed", + "fr": "Échec du téléchargement", + "es": "Error en la descarga", + "it": "Download non riuscito", + "de": "Download fehlgeschlagen", + "pt": "Falha na transferência", + "pl": "Pobieranie nie powiodło się", + "nl": "Download mislukt", + "ru": "Ошибка загрузки" + } + }, + "notifications_receiver_completed_body": { + "context": "Notification body shown to the sender when a receiver finishes downloading a shared transfer. {receiver} = receiver name, {transferName} = transfer name.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "receiver", + "type": "string" + }, + { + "name": "transferName", + "type": "string" + } + ], + "translations": { + "en": "{receiver} finished receiving “{transferName}”.", + "fr": "{receiver} a fini de recevoir « {transferName} ».", + "es": "{receiver} terminó de recibir «{transferName}».", + "it": "{receiver} ha finito di ricevere «{transferName}».", + "de": "{receiver} hat „{transferName}“ vollständig empfangen.", + "pt": "{receiver} terminou de receber «{transferName}».", + "pl": "{receiver} zakończył odbieranie „{transferName}”.", + "nl": "{receiver} heeft ‘{transferName}’ volledig ontvangen.", + "ru": "{receiver} завершил получение «{transferName}»." + } + }, + "notifications_receiver_completed_title": { + "context": "Notification title shown to the sender when a receiver finishes downloading a shared transfer.", + "targets": [ + "apple" + ], + "translations": { + "en": "Transfer received", + "fr": "Transfert reçu", + "es": "Transferencia recibida", + "it": "Trasferimento ricevuto", + "de": "Übertragung empfangen", + "pt": "Transferência recebida", + "pl": "Transfer odebrany", + "nl": "Overdracht ontvangen", + "ru": "Передача получена" + } + }, + "notifications_send_failed_body": { + "context": "Notification body shown to the sender when a shared transfer fails. {transferName} = transfer name.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "transferName", + "type": "string" + } + ], + "translations": { + "en": "“{transferName}” couldn’t be shared.", + "fr": "« {transferName} » n’a pas pu être partagé.", + "es": "No se pudo compartir «{transferName}».", + "it": "Impossibile condividere «{transferName}».", + "de": "„{transferName}“ konnte nicht geteilt werden.", + "pt": "Não foi possível partilhar «{transferName}».", + "pl": "Nie udało się udostępnić „{transferName}”.", + "nl": "‘{transferName}’ kon niet worden gedeeld.", + "ru": "Не удалось поделиться «{transferName}»." + } + }, + "notifications_send_failed_title": { + "context": "Notification title shown to the sender when a shared transfer fails.", + "targets": [ + "apple" + ], + "translations": { + "en": "Sharing failed", + "fr": "Échec du partage", + "es": "Error al compartir", + "it": "Condivisione non riuscita", + "de": "Freigabe fehlgeschlagen", + "pt": "Falha na partilha", + "pl": "Udostępnianie nie powiodło się", + "nl": "Delen mislukt", + "ru": "Не удалось поделиться" + } + }, "notifications_settings_open_failed": { "context": "Settings > Notifications: error when the OS notification settings can't be opened.", "translations": { diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index f497235..da1a6ad 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -117,7 +117,7 @@ Senden Einstellungen Netzwerk - Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft. + Werden Sie über Übertragungsaktivitäten benachrichtigt, während VniDrop im Hintergrund läuft. Mitteilungen aktiviert. Mitteilungen erlauben Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren. diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index b106a93..b7ff17e 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -117,7 +117,7 @@ Enviar Ajustes Red - Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano. + Reciba avisos sobre la actividad de las transferencias cuando VniDrop está en segundo plano. Notificaciones activadas. Permitir notificaciones Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes. diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index d708fda..126bb38 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -117,7 +117,7 @@ Envoyer Réglages Réseau - Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan. + Soyez averti de l’activité des transferts lorsque VniDrop est en arrière-plan. Notifications activées. Autoriser les notifications Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages. diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index c1540af..10f1aad 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -117,7 +117,7 @@ Invia Impostazioni Rete - Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background. + Ricevi avvisi sull’attività dei trasferimenti quando VniDrop è in background. Notifiche attivate. Consenti le notifiche Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni. diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index f3f4f17..b281934 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -117,7 +117,7 @@ Versturen Instellingen Netwerk - Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait. + Ontvang meldingen over overdrachtsactiviteit terwijl VniDrop op de achtergrond draait. Meldingen ingeschakeld. Meldingen toestaan Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen. diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index 6d7c74f..e19c520 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -117,7 +117,7 @@ Wyślij Ustawienia Sieć - Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle. + Otrzymuj powiadomienia o aktywności transferów, gdy VniDrop działa w tle. Powiadomienia włączone. Zezwól na powiadomienia Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach. diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index d40618d..1233976 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -117,7 +117,7 @@ Enviar Definições Rede - Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano. + Seja notificado sobre a atividade das transferências enquanto o VniDrop está em segundo plano. Notificações ativadas. Permitir notificações As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições. diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index 5e271cb..513bb0c 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -117,7 +117,7 @@ Отправить Настройки Сеть - Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне. + Получайте уведомления об активности передач, пока VniDrop работает в фоне. Уведомления включены. Разрешить уведомления Уведомления отключены для VniDrop. Вы можете включить их в Настройках. diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 7767bf6..b4da6ac 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -117,7 +117,7 @@ Send Settings Network - Get notified about new receive requests while VniDrop is in the background. + Get notified about transfer activity while VniDrop is in the background. Notifications enabled. Allow notifications Notifications are turned off for VniDrop. You can enable them in Settings. From 2b2fe93293a9e643ce1932762fa09db71a8e9f02 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:30:03 +0200 Subject: [PATCH 08/36] build(apple): enable dead-code stripping and missing-localizability analyzer Adds project-wide build settings (applied to every target) so they persist in project.yml instead of the gitignored generated .xcodeproj: - DEAD_CODE_STRIPPING: strip unreachable code from release binaries - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: flag user-facing strings that aren't localized (the app ships 9 languages), surfaced during Analyze --- apple/project.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apple/project.yml b/apple/project.yml index 3ec7665..b819d39 100644 --- a/apple/project.yml +++ b/apple/project.yml @@ -12,6 +12,14 @@ options: macOS: "15.0" createIntermediateGroups: true +# Project-wide build settings (applied to every target/config). +settings: + base: + # Strip unreachable code from release binaries. + DEAD_CODE_STRIPPING: YES + # Flag user-facing strings that aren't localized (the app ships 9 languages). + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED: YES + packages: VnidropCore: path: VnidropCore From 6180cb95ceca0c507b4ba9c1f1c46b1875f6c210 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Thu, 23 Jul 2026 23:43:05 +0200 Subject: [PATCH 09/36] fix(apple): stop delete confirmation re-presenting on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirming a transfer/history deletion flashed the same confirmation alert a second time before it went away. The destructive button runs confirmDelete synchronously (setting isDeleting = true), while the alert's isPresented dismiss binding fires asynchronously and then no-ops because its `if !isDeleting` guard is already false — leaving the open flag set, so macOS re-reads the binding as true and re-presents the alert until the async delete finally clears it. Close the confirmation flag synchronously in confirmDeleteTransfer / confirmHistoryDelete so there's no window for re-presentation. Tests assert the flag clears immediately, before the async delete completes. --- apple/Tests/ReceiveModelTests.swift | 3 +++ apple/Tests/SendModelTests.swift | 3 +++ apple/VniDrop/Features/Receive/ReceiveModel.swift | 4 ++++ apple/VniDrop/Features/Send/SendModel.swift | 3 +++ 4 files changed, 13 insertions(+) diff --git a/apple/Tests/ReceiveModelTests.swift b/apple/Tests/ReceiveModelTests.swift index 0a83491..cb4975e 100644 --- a/apple/Tests/ReceiveModelTests.swift +++ b/apple/Tests/ReceiveModelTests.swift @@ -24,6 +24,9 @@ final class ReceiveModelTests: XCTestCase { XCTAssertEqual(model.state.historyDeleteTarget, .transfer(transferId: 5)) model.confirmHistoryDelete() + // Must close immediately (not after the async delete) so the alert can't + // re-present on macOS. + XCTAssertNil(model.state.historyDeleteTarget) await waitUntil { core.deletedTransfers.contains(5) } XCTAssertEqual(core.deletedTransfers, [5]) XCTAssertNil(model.state.historyDeleteTarget) diff --git a/apple/Tests/SendModelTests.swift b/apple/Tests/SendModelTests.swift index 616ae67..12b1801 100644 --- a/apple/Tests/SendModelTests.swift +++ b/apple/Tests/SendModelTests.swift @@ -32,6 +32,9 @@ final class SendModelTests: XCTestCase { XCTAssertTrue(model.state.isDeleteConfirmationOpen) model.confirmDeleteTransfer() + // Must close immediately (not after the async delete) so the alert can't + // re-present on macOS. + XCTAssertFalse(model.state.isDeleteConfirmationOpen) await waitUntil { core.deletedTransfers.contains(3) } XCTAssertEqual(core.deletedTransfers, [3]) XCTAssertNil(model.state.selectedTransferId) diff --git a/apple/VniDrop/Features/Receive/ReceiveModel.swift b/apple/VniDrop/Features/Receive/ReceiveModel.swift index 31c95d2..3927e92 100644 --- a/apple/VniDrop/Features/Receive/ReceiveModel.swift +++ b/apple/VniDrop/Features/Receive/ReceiveModel.swift @@ -121,6 +121,10 @@ final class ReceiveModel: ObservableObject { func confirmHistoryDelete() { guard let target = state.historyDeleteTarget, !state.isDeletingHistory else { return } state.isDeletingHistory = true + // Close synchronously: the alert's dismiss binding runs async and no-ops while + // `isDeletingHistory`, which would otherwise leave the target set and macOS + // re-present it. + state.historyDeleteTarget = nil Task { let result: Result switch target { diff --git a/apple/VniDrop/Features/Send/SendModel.swift b/apple/VniDrop/Features/Send/SendModel.swift index b9d74ed..325fe5c 100644 --- a/apple/VniDrop/Features/Send/SendModel.swift +++ b/apple/VniDrop/Features/Send/SendModel.swift @@ -207,6 +207,9 @@ final class SendModel: ObservableObject { func confirmDeleteTransfer() { guard let transferId = state.selectedTransferId, !state.isDeleting else { return } state.isDeleting = true + // Close synchronously: the alert's dismiss binding runs async and no-ops while + // `isDeleting`, which would otherwise leave the flag true and macOS re-present it. + state.isDeleteConfirmationOpen = false Task { let result = await repository.delete(transferId: transferId) switch result { From c9ef40f00f24d43a2dc385e60336539e46ff875a Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:28:34 +0200 Subject: [PATCH 10/36] refactor(apple): tidy the receive-folder preference row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Save received transfers to" section was a gray folder label that read like a disabled field, stacked above two full-width buttons. Replace it with the standard macOS "label · value · inline action" row: a folder icon + the current folder name with a trailing "Choose folder" button, long names truncated in the middle. "Use default" now shows only when a custom folder is actually set (hidden when already on the default, where it'd be a no-op). --- .../Features/Settings/SettingsModel.swift | 9 +++++++++ .../Features/Settings/SettingsSections.swift | 18 ++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift index 2ddc6b0..a6a3f3c 100644 --- a/apple/VniDrop/Features/Settings/SettingsModel.swift +++ b/apple/VniDrop/Features/Settings/SettingsModel.swift @@ -168,6 +168,15 @@ final class SettingsModel: ObservableObject { func onReceiveFolderPickFailed(_ reason: String) { messages.error(InvitationError.message(reason)) } func resetReceiveFolder() { preferences.resetReceiveFolder() } + /// Whether the current receive folder is the platform default (so the reset + /// action can be hidden when it would be a no-op). Compared by location, not + /// display name, which can differ once resolved. + var isUsingDefaultReceiveFolder: Bool { + guard let folder = state.receiveFolder else { return true } + let fallback = fileSystemService.defaultReceiveFolder() + return folder.kind == fallback.kind && folder.value == fallback.value + } + /// Ask the OS for notification permission. This is the only time the app can /// grant it; disabling or fine-tuning afterwards happens in the Settings app. func requestNotifications() { diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index 509c4b6..8457dd4 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -14,10 +14,20 @@ struct PreferencesSettings: View { } if model.state.supportsCustomReceiveFolders { Section(String(localized: L10n.Preferences.receiveFolderTitle)) { - Text(model.state.receiveFolder?.displayName ?? String(localized: L10n.Value.unavailable)) - .foregroundStyle(.secondary) - Button(String(localized: L10n.Button.chooseFolder), action: model.chooseReceiveFolder) - Button(String(localized: L10n.Button.resetDefault), action: model.resetReceiveFolder) + LabeledContent { + Button(String(localized: L10n.Button.chooseFolder), action: model.chooseReceiveFolder) + } label: { + Label { + Text(model.state.receiveFolder?.displayName ?? String(localized: L10n.Value.unavailable)) + .lineLimit(1) + .truncationMode(.middle) + } icon: { + Image(systemSymbol: .folder) + } + } + if !model.isUsingDefaultReceiveFolder { + Button(String(localized: L10n.Button.resetDefault), role: .cancel, action: model.resetReceiveFolder) + } } } } From 8b31c0fe78f275f01cf46eb9b1b177071539d6ae Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 00:37:49 +0200 Subject: [PATCH 11/36] feat(apple): use the brand purple as the app-wide accent color MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS sidebar selection and item icons rendered in the system default blue because the app had no global accent color — SwiftUI's `.tint` doesn't reach the AppKit-backed sidebar. Add an AccentColor asset (the exact sRGB of VniDropColors.brandPurple) and wire ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME so the accent applies at the OS level everywhere, including the sidebar. --- .../AccentColor.colorset/Contents.json | 20 +++++++++++++++++++ apple/VniDrop/UI/Theme/VniDropColors.swift | 4 +++- apple/project.yml | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 apple/VniDrop/Resources/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/apple/VniDrop/Resources/Assets.xcassets/AccentColor.colorset/Contents.json b/apple/VniDrop/Resources/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..e4c3f7f --- /dev/null +++ b/apple/VniDrop/Resources/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.9685", + "green" : "0.3315", + "red" : "0.6606" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apple/VniDrop/UI/Theme/VniDropColors.swift b/apple/VniDrop/UI/Theme/VniDropColors.swift index 3d7c8dd..f811b02 100644 --- a/apple/VniDrop/UI/Theme/VniDropColors.swift +++ b/apple/VniDrop/UI/Theme/VniDropColors.swift @@ -52,7 +52,9 @@ struct VniDropColors { } extension VniDropColors { - /// The single brand accent used app-wide as the SwiftUI tint. + /// The single brand accent used app-wide as the SwiftUI tint. Mirrored by the + /// `AccentColor` asset (the OS-level global accent for the macOS sidebar etc.); + /// keep the two in sync. static let brandPurple = Color.hsl(271, 91, 65) static let light = VniDropColors( diff --git a/apple/project.yml b/apple/project.yml index b819d39..843a363 100644 --- a/apple/project.yml +++ b/apple/project.yml @@ -55,6 +55,9 @@ targets: SWIFT_STRICT_CONCURRENCY: complete ENABLE_USER_SCRIPT_SANDBOXING: NO ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon + # App-wide accent (macOS sidebar selection, default control tints). The + # AccentColor asset mirrors VniDropColors.brandPurple — keep them in sync. + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor configs: debug: CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements From 42f569dcf0a79acf586d1e58e2dc7a43089f4c8f Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 12:27:09 +0200 Subject: [PATCH 12/36] feat(apple): allow only one macOS app instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set LSMultipleInstancesProhibited so re-launching VniDrop (or opening a vnidrop URL) activates the running instance instead of spawning a second copy. iOS ignores the key — it's single-instance already. --- apple/VniDrop/Resources/Info.plist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apple/VniDrop/Resources/Info.plist b/apple/VniDrop/Resources/Info.plist index 904953d..9ef9fc8 100644 --- a/apple/VniDrop/Resources/Info.plist +++ b/apple/VniDrop/Resources/Info.plist @@ -20,6 +20,10 @@ $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName VniDrop + + LSMultipleInstancesProhibited + CFBundleDocumentTypes From 20597e6e8888efba059f02cf1393032c30a1553f Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:19:26 +0200 Subject: [PATCH 13/36] fix(apple): enforce a single window on macOS and iPadOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS uses a single-instance `Window` scene instead of `WindowGroup`, which otherwise lets the app open multiple windows (via ⌘N). iPadOS sets `UIApplicationSupportsMultipleScenes = false` to block a second scene via Stage Manager / split view. (`LSMultipleInstancesProhibited` only blocks a second process, not a second window.) --- apple/VniDrop/App/VniDropApp.swift | 15 ++++++++++++++- apple/VniDrop/Resources/Info.plist | 4 ++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/apple/VniDrop/App/VniDropApp.swift b/apple/VniDrop/App/VniDropApp.swift index 0119980..3882b17 100644 --- a/apple/VniDrop/App/VniDropApp.swift +++ b/apple/VniDrop/App/VniDropApp.swift @@ -1,5 +1,8 @@ import SwiftUI +/// Scene identifier for the single main window. +private let mainWindowId = "main" + /// Native app entry point for iOS, iPadOS, and macOS. /// Opens `.vnd` invitations via `onOpenURL` and routes them to the receive flow. @main @@ -7,11 +10,21 @@ struct VniDropApp: App { @StateObject private var externalInvitations = ExternalInvitationController() var body: some Scene { - WindowGroup { + #if os(macOS) + // A single-instance `Window` (not `WindowGroup`): the app must never open a + // second window. `Window` also drops the ⌘N "New Window" command. + Window(Text(verbatim: "VniDrop"), id: mainWindowId) { RootView(dependencies: makeAppDependencies(externalInvitations: externalInvitations)) .ignoresSafeArea() .onOpenURL(perform: openInvitation) } + #else + WindowGroup(id: mainWindowId) { + RootView(dependencies: makeAppDependencies(externalInvitations: externalInvitations)) + .ignoresSafeArea() + .onOpenURL(perform: openInvitation) + } + #endif } /// Reads a `.vnd` invitation document under a security scope, enforcing the diff --git a/apple/VniDrop/Resources/Info.plist b/apple/VniDrop/Resources/Info.plist index 9ef9fc8..a2613e4 100644 --- a/apple/VniDrop/Resources/Info.plist +++ b/apple/VniDrop/Resources/Info.plist @@ -67,6 +67,10 @@ VniDrop uses the camera to scan transfer QR codes. NSLocalNetworkUsageDescription VniDrop needs local network access to send to other local devices if needed. + + UIApplicationSupportsMultipleScenes + UIBackgroundModes fetch From 677c3ce47faf800de243a33a2e601d4fd5e884dd Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 13:46:27 +0200 Subject: [PATCH 14/36] feat(apple): add a Free up space action to reclaim leaked storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete all transfers only clears core records, and the blob-store cache is reclaimed by the core's own timer. Neither touches the app's temporary directory (leftover picker/staging copies — hundreds of MB on macOS) or the stray .Trash folders that accumulate in app-owned directories and can't be removed via Files/Finder. Add a non-destructive Free up space button that empties the temp directory and removes .Trash folders under the core data dir (and, on iOS, the fixed Documents receive folder), reporting the bytes reclaimed. Guarded against running while a transfer is in flight; never touches received files, the core database, or user-chosen macOS receive folders. --- .../Features/Settings/SettingsModel.swift | 79 +++++++++++++++++++ .../Features/Settings/SettingsSections.swift | 17 ++++ localization/strings.json | 62 +++++++++++++++ .../composeResources/values-de/strings.xml | 4 + .../composeResources/values-es/strings.xml | 4 + .../composeResources/values-fr/strings.xml | 4 + .../composeResources/values-it/strings.xml | 4 + .../composeResources/values-nl/strings.xml | 4 + .../composeResources/values-pl/strings.xml | 4 + .../composeResources/values-pt/strings.xml | 4 + .../composeResources/values-ru/strings.xml | 4 + .../composeResources/values/strings.xml | 4 + 12 files changed, 194 insertions(+) diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift index a6a3f3c..6ccdb96 100644 --- a/apple/VniDrop/Features/Settings/SettingsModel.swift +++ b/apple/VniDrop/Features/Settings/SettingsModel.swift @@ -56,6 +56,7 @@ struct SettingsState: Equatable { var storage: StorageBreakdown? var isCalculatingStorage = false var isDeletingTransfers = false + var isCleaningStorage = false static func == (lhs: SettingsState, rhs: SettingsState) -> Bool { lhs.selectedSection == rhs.selectedSection && lhs.username == rhs.username @@ -72,6 +73,7 @@ struct SettingsState: Equatable { && lhs.bugLogPreviewBytes == rhs.bugLogPreviewBytes && lhs.storage == rhs.storage && lhs.isCalculatingStorage == rhs.isCalculatingStorage && lhs.isDeletingTransfers == rhs.isDeletingTransfers + && lhs.isCleaningStorage == rhs.isCleaningStorage && lhs.deviceInfo?.operatingSystem == rhs.deviceInfo?.operatingSystem } } @@ -315,6 +317,83 @@ final class SettingsModel: ObservableObject { } } + /// Reclaims disk space the core's transfer deletion doesn't touch: the app's + /// temporary directory (leftover picker/staging copies) and any stray `.Trash` + /// folders that accumulate inside app-owned directories. Never touches received + /// files, the core database, or user-chosen receive folders. + func freeUpSpace() { + if state.isCleaningStorage { return } + // Purging staging while a transfer is mid-flight could break it. + let hasActive = repository.state.transfers.contains { + $0.status == .sharing || $0.status == .importing || $0.status == .receiving + } + if hasActive { + messages.tryShow(UiMessage(text: .resource(L10n.Storage.cleanupBusy), tone: .warning)) + return + } + state.isCleaningStorage = true + let tempDir = NSTemporaryDirectory() + let dataDir = environment.defaultCoreDataDir + // Only clean the receive folder's trash when it is app-owned (iOS fixed + // Documents), never a user-chosen macOS folder like ~/Downloads. + let receiveTrashRoot = fileSystemService.supportsCustomReceiveFolders ? nil : state.receiveFolder?.value + Task { + let freed = await Task.detached { + SettingsModel.reclaimJunk(tempDir: tempDir, dataDir: dataDir, receiveTrashRoot: receiveTrashRoot) + }.value + state.isCleaningStorage = false + loadStorageUsage() + messages.show(UiMessage( + text: .dynamic(L10n.Storage.cleanupFreed(size: formatBytes(freed))), + tone: .success + )) + } + } + + /// Deletes temp-directory contents and `.Trash` folders under the given roots, + /// returning the number of bytes reclaimed. Runs off the main actor. + nonisolated static func reclaimJunk(tempDir: String, dataDir: String, receiveTrashRoot: String?) -> UInt64 { + let fm = FileManager.default + var freed: UInt64 = 0 + // Empty the temporary directory. + if let entries = try? fm.contentsOfDirectory(atPath: tempDir) { + for name in entries { + let path = (tempDir as NSString).appendingPathComponent(name) + freed += itemSize(path) + try? fm.removeItem(atPath: path) + } + } + // Remove stray `.Trash` folders inside app-owned directories. + for root in [dataDir, receiveTrashRoot].compactMap({ $0 }) { + for trash in trashDirectories(under: root) { + freed += directorySize(trash) + try? fm.removeItem(atPath: trash) + } + } + return freed + } + + /// Paths of every directory named `.Trash` under `root` (not descending into them). + private nonisolated static func trashDirectories(under root: String) -> [String] { + let url = URL(fileURLWithPath: root, isDirectory: true) + guard let enumerator = FileManager.default.enumerator( + at: url, includingPropertiesForKeys: [.isDirectoryKey] + ) else { return [] } + var result: [String] = [] + for case let fileURL as URL in enumerator where fileURL.lastPathComponent == ".Trash" { + result.append(fileURL.path) + enumerator.skipDescendants() + } + return result + } + + /// Allocated size of a file or directory (0 if missing). + private nonisolated static func itemSize(_ path: String) -> UInt64 { + var isDirectory: ObjCBool = false + guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { return 0 } + return isDirectory.boolValue ? directorySize(path) : fileSize(path) + } + nonisolated static func fileSize(_ path: String) -> UInt64 { let values = try? URL(fileURLWithPath: path).resourceValues( forKeys: [.isRegularFileKey, .totalFileAllocatedSizeKey, .fileSizeKey] diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index 8457dd4..5b639b0 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -99,6 +99,23 @@ struct StorageSettings: View { Text(String(localized: L10n.Storage.footer)) } + Section { + Button { + model.freeUpSpace() + } label: { + HStack { + Text(model.state.isCleaningStorage + ? String(localized: L10n.Storage.cleaning) + : String(localized: L10n.Storage.freeUpSpace)) + if model.state.isCleaningStorage { + Spacer() + ProgressView() + } + } + } + .disabled(model.state.isCleaningStorage) + } + Section { Button(role: .destructive) { showDeleteConfirmation = true diff --git a/localization/strings.json b/localization/strings.json index 02cf6c7..f8bed6c 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -3140,6 +3140,68 @@ "ru": "Вычисление…" } }, + "storage_cleaning": { + "context": "Settings > Storage: free-up-space button while cleanup runs.", + "translations": { + "en": "Cleaning up…", + "fr": "Nettoyage…", + "es": "Limpiando…", + "it": "Pulizia…", + "de": "Wird bereinigt…", + "pt": "A limpar…", + "pl": "Czyszczenie…", + "nl": "Opschonen…", + "ru": "Очистка…" + } + }, + "storage_cleanup_busy": { + "context": "Settings > Storage: shown when cleanup is blocked by in-flight transfers.", + "translations": { + "en": "Finish active transfers before freeing up space", + "fr": "Terminez les transferts en cours avant de libérer de l'espace", + "es": "Finaliza las transferencias activas antes de liberar espacio", + "it": "Completa i trasferimenti attivi prima di liberare spazio", + "de": "Beende aktive Übertragungen, bevor du Speicher freigibst", + "pt": "Conclui as transferências ativas antes de libertar espaço", + "pl": "Zakończ aktywne transfery przed zwolnieniem miejsca", + "nl": "Voltooi actieve overdrachten voordat je ruimte vrijmaakt", + "ru": "Завершите активные передачи перед освобождением места" + } + }, + "storage_cleanup_freed": { + "context": "Settings > Storage: cleanup success. {size} = amount freed.", + "args": [ + { + "name": "size", + "type": "string" + } + ], + "translations": { + "en": "Freed {size}", + "fr": "{size} libéré", + "es": "Se liberó {size}", + "it": "Liberati {size}", + "de": "{size} freigegeben", + "pt": "Libertado {size}", + "pl": "Zwolniono {size}", + "nl": "{size} vrijgemaakt", + "ru": "Освобождено {size}" + } + }, + "storage_free_up_space": { + "context": "Settings > Storage: button that clears temporary files and stray trash.", + "translations": { + "en": "Free up space", + "fr": "Libérer de l'espace", + "es": "Liberar espacio", + "it": "Libera spazio", + "de": "Speicher freigeben", + "pt": "Libertar espaço", + "pl": "Zwolnij miejsce", + "nl": "Ruimte vrijmaken", + "ru": "Освободить место" + } + }, "storage_delete_transfers": { "context": "Settings > Storage: button to delete all transfer records.", "translations": { diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index da1a6ad..b85e8d5 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -203,6 +203,10 @@ Wird empfangen Beendet Wird berechnet… + Wird bereinigt… + Beende aktive Übertragungen, bevor du Speicher freigibst + %1$s freigegeben + Speicher freigeben Alle Übertragungen löschen Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden. App-Daten diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index b7ff17e..a1dcb0e 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -203,6 +203,10 @@ Recibiendo Detenido Calculando… + Limpiando… + Finaliza las transferencias activas antes de liberar espacio + Se liberó %1$s + Liberar espacio Eliminar todas las transferencias Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer. Datos de la aplicación diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 126bb38..a49c270 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -203,6 +203,10 @@ Réception Arrêté Calcul… + Nettoyage… + Terminez les transferts en cours avant de libérer de l\'espace + %1$s libéré + Libérer de l\'espace Supprimer tous les transferts Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible. Données de l’app diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index 10f1aad..bb26acb 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -203,6 +203,10 @@ Ricezione Interrotto Calcolo… + Pulizia… + Completa i trasferimenti attivi prima di liberare spazio + Liberati %1$s + Libera spazio Elimina tutti i trasferimenti Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata. Dati dell’app diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index b281934..60d3791 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -203,6 +203,10 @@ Ontvangen Gestopt Berekenen… + Opschonen… + Voltooi actieve overdrachten voordat je ruimte vrijmaakt + %1$s vrijgemaakt + Ruimte vrijmaken Alle overdrachten verwijderen Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt. Appgegevens diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index e19c520..9de29f4 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -203,6 +203,10 @@ Odbieranie Zatrzymany Obliczanie… + Czyszczenie… + Zakończ aktywne transfery przed zwolnieniem miejsca + Zwolniono %1$s + Zwolnij miejsce Usuń wszystkie transfery Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć. Dane aplikacji diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index 1233976..b6d0c35 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -203,6 +203,10 @@ A receber Parada A calcular… + A limpar… + Conclui as transferências ativas antes de libertar espaço + Libertado %1$s + Libertar espaço Eliminar todas as transferências Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada. Dados da aplicação diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index 513bb0c..f8894e5 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -203,6 +203,10 @@ Получение Остановлено Вычисление… + Очистка… + Завершите активные передачи перед освобождением места + Освобождено %1$s + Освободить место Удалить все передачи Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить. Данные приложения diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index b4da6ac..4daed58 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -203,6 +203,10 @@ Receiving Stopped Calculating… + Cleaning up… + Finish active transfers before freeing up space + Freed %1$s + Free up space Delete all transfers This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone. App data From e22e395efcedf63d758e5f332abda966182eaa51 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:07:31 +0200 Subject: [PATCH 15/36] feat(apple): streamline the Storage screen and fix stuck usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesign the Storage screen for clarity: an "On this device" usage header with a manual Refresh control, symbol-led action buttons, and a caption under each action spelling out exactly what it does (Free up space = temp + trash, non-destructive; Delete all transfers = clears history + cached share content, keeps received files). Fix the summary sticking on "Calculating…": it loaded only on .onAppear and bailed when opened before the core finished its async launch, leaving the loading branch showing with nothing running (only a manual refresh recovered it). loadStorageUsage now waits for the core to become ready before reading usage, distinguishes a real failure (retry) from loading, loads via .task, and can be refreshed on demand. Use plain button styling with explicit tints so pressing an action no longer flips the label to the white selection highlight. --- .../Features/Settings/SettingsModel.swift | 15 +- .../Features/Settings/SettingsSections.swift | 133 +++++++++++++----- localization/strings.json | 70 +++++++++ .../composeResources/values-de/strings.xml | 5 + .../composeResources/values-es/strings.xml | 5 + .../composeResources/values-fr/strings.xml | 5 + .../composeResources/values-it/strings.xml | 5 + .../composeResources/values-nl/strings.xml | 5 + .../composeResources/values-pl/strings.xml | 5 + .../composeResources/values-pt/strings.xml | 5 + .../composeResources/values-ru/strings.xml | 5 + .../composeResources/values/strings.xml | 5 + 12 files changed, 224 insertions(+), 39 deletions(-) diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift index 6ccdb96..af29633 100644 --- a/apple/VniDrop/Features/Settings/SettingsModel.swift +++ b/apple/VniDrop/Features/Settings/SettingsModel.swift @@ -55,6 +55,7 @@ struct SettingsState: Equatable { var bugLogPreviewBytes = 0 var storage: StorageBreakdown? var isCalculatingStorage = false + var storageLoadFailed = false var isDeletingTransfers = false var isCleaningStorage = false @@ -72,6 +73,7 @@ struct SettingsState: Equatable { && lhs.bugIncludeLogs == rhs.bugIncludeLogs && lhs.isSubmittingBugReport == rhs.isSubmittingBugReport && lhs.bugLogPreviewBytes == rhs.bugLogPreviewBytes && lhs.storage == rhs.storage && lhs.isCalculatingStorage == rhs.isCalculatingStorage + && lhs.storageLoadFailed == rhs.storageLoadFailed && lhs.isDeletingTransfers == rhs.isDeletingTransfers && lhs.isCleaningStorage == rhs.isCleaningStorage && lhs.deviceInfo?.operatingSystem == rhs.deviceInfo?.operatingSystem @@ -265,17 +267,28 @@ final class SettingsModel: ObservableObject { // MARK: - Storage - /// Recomputes the on-disk usage breakdown off the main actor. + /// Recomputes the on-disk usage breakdown off the main actor. Safe to call + /// before the core is ready: it keeps the spinner up and waits for the core to + /// finish initializing (it starts asynchronously at launch) rather than bailing. func loadStorageUsage() { if state.isCalculatingStorage { return } state.isCalculatingStorage = true + state.storageLoadFailed = false let tempDir = NSTemporaryDirectory() Task { + // The core initializes asynchronously at launch; poll briefly so opening + // Storage early doesn't leave the summary stuck. + var attempts = 0 + while !repository.state.isInitialized && attempts < 100 { + try? await Task.sleep(nanoseconds: 100_000_000) + attempts += 1 + } let coreResult = await repository.storageUsage() let artifactsResult = await repository.receivedArtifacts() guard case .success(let core) = coreResult, case .success(let artifacts) = artifactsResult else { state.isCalculatingStorage = false + state.storageLoadFailed = true return } let diskSizes = await Task.detached { diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index 5b639b0..603b632 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -78,61 +78,71 @@ struct StorageSettings: View { @ObservedObject var model: SettingsModel @State private var showDeleteConfirmation = false + private var isBusy: Bool { + model.state.isCalculatingStorage || model.state.isCleaningStorage || model.state.isDeletingTransfers + } + var body: some View { Section { - if let storage = model.state.storage { - LabeledContent(String(localized: L10n.Storage.receivedFiles), value: formatBytes(storage.receivedFiles)) - LabeledContent(String(localized: L10n.Storage.transferData), value: formatBytes(storage.transferCache)) - LabeledContent(String(localized: L10n.Storage.appData), value: formatBytes(storage.appData)) - LabeledContent(String(localized: L10n.Storage.temporary), value: formatBytes(storage.temporary)) - LabeledContent(String(localized: L10n.Storage.total)) { - Text(formatBytes(storage.total)).fontWeight(.semibold) - } - } else { - HStack { - Text(String(localized: L10n.Storage.calculating)).foregroundStyle(.secondary) - Spacer() - ProgressView() + usageContent + } header: { + HStack { + Text(String(localized: L10n.Storage.usageHeader)) + Spacer() + if model.state.isCalculatingStorage { + ProgressView().controlSize(.small) + } else { + Button(action: model.loadStorageUsage) { + Label(String(localized: L10n.Storage.refresh), systemSymbol: .arrowClockwise) + .labelStyle(.iconOnly) + } + .buttonStyle(.borderless) + .disabled(isBusy) + .help(String(localized: L10n.Storage.refresh)) } } } footer: { Text(String(localized: L10n.Storage.footer)) } + // Reclaim reversible junk (temp + trash) — non-destructive to history. Section { - Button { - model.freeUpSpace() - } label: { - HStack { - Text(model.state.isCleaningStorage - ? String(localized: L10n.Storage.cleaning) - : String(localized: L10n.Storage.freeUpSpace)) - if model.state.isCleaningStorage { - Spacer() - ProgressView() - } - } + Button(action: model.freeUpSpace) { + actionLabel( + title: L10n.Storage.freeUpSpace, + busyTitle: L10n.Storage.cleaning, + isBusy: model.state.isCleaningStorage, + symbol: .sparkles, + tint: .accentColor + ) } - .disabled(model.state.isCleaningStorage) + // `.plain` so pressing the row dims the label instead of flipping it to + // the white selection-highlight that the default form button style uses. + .buttonStyle(.plain) + .disabled(isBusy) + } footer: { + Text(String(localized: L10n.Storage.freeUpSpaceCaption)) } + // Destructive: clears transfer history + cached share content. Section { - Button(role: .destructive) { + Button { showDeleteConfirmation = true } label: { - HStack { - Text(model.state.isDeletingTransfers - ? String(localized: L10n.Storage.deleting) - : String(localized: L10n.Storage.deleteTransfers)) - if model.state.isDeletingTransfers { - Spacer() - ProgressView() - } - } + actionLabel( + title: L10n.Storage.deleteTransfers, + busyTitle: L10n.Storage.deleting, + isBusy: model.state.isDeletingTransfers, + symbol: .trash, + tint: .red + ) } - .disabled(model.state.isDeletingTransfers) + .buttonStyle(.plain) + .disabled(isBusy) + } footer: { + Text(String(localized: L10n.Storage.deleteTransfersCaption)) } - .onAppear { model.loadStorageUsage() } + .task { model.loadStorageUsage() } .confirmationDialog( Text(String(localized: L10n.Storage.deleteTransfers)), isPresented: $showDeleteConfirmation, @@ -146,6 +156,53 @@ struct StorageSettings: View { Text(String(localized: L10n.Storage.deleteTransfersDescription)) } } + + @ViewBuilder + private var usageContent: some View { + if let storage = model.state.storage { + LabeledContent(String(localized: L10n.Storage.receivedFiles), value: formatBytes(storage.receivedFiles)) + LabeledContent(String(localized: L10n.Storage.transferData), value: formatBytes(storage.transferCache)) + LabeledContent(String(localized: L10n.Storage.appData), value: formatBytes(storage.appData)) + LabeledContent(String(localized: L10n.Storage.temporary), value: formatBytes(storage.temporary)) + LabeledContent(String(localized: L10n.Storage.total)) { + Text(formatBytes(storage.total)).fontWeight(.semibold) + } + } else if model.state.storageLoadFailed { + // Genuine failure (core reported an error) — offer a retry. + Button(action: model.loadStorageUsage) { + Label(String(localized: L10n.Storage.unavailable), systemSymbol: .arrowClockwise) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + } else { + // Loading, or waiting for the core to finish starting. + HStack { + Text(String(localized: L10n.Storage.calculating)).foregroundStyle(.secondary) + Spacer() + ProgressView().controlSize(.small) + } + } + } + + /// A tinted, full-width button label with a leading symbol and a trailing + /// spinner while busy. `.contentShape` keeps the whole row tappable. + private func actionLabel( + title: String.LocalizationValue, + busyTitle: String.LocalizationValue, + isBusy: Bool, + symbol: SFSymbol, + tint: Color + ) -> some View { + HStack { + Label(String(localized: isBusy ? busyTitle : title), systemSymbol: symbol) + Spacer() + if isBusy { + ProgressView().controlSize(.small) + } + } + .foregroundStyle(tint) + .contentShape(Rectangle()) + } } struct AboutSettings: View { diff --git a/localization/strings.json b/localization/strings.json index f8bed6c..4c826e4 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -3188,6 +3188,76 @@ "ru": "Освобождено {size}" } }, + "storage_delete_transfers_caption": { + "context": "Settings > Storage: caption under the destructive delete-all button.", + "translations": { + "en": "Clears your send and receive history and the app's cached share content. Received files on disk are kept.", + "fr": "Efface votre historique d'envois et de réceptions ainsi que le contenu de partage mis en cache par l'app. Les fichiers reçus sur le disque sont conservés.", + "es": "Borra tu historial de envíos y recepciones y el contenido compartido en caché de la app. Los archivos recibidos en el disco se conservan.", + "it": "Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall'app. I file ricevuti sul disco vengono mantenuti.", + "de": "Löscht deinen Sende- und Empfangsverlauf sowie die zwischengespeicherten Freigabeinhalte der App. Empfangene Dateien auf dem Datenträger bleiben erhalten.", + "pt": "Limpa o teu histórico de envios e receções e o conteúdo de partilha em cache da app. Os ficheiros recebidos no disco são mantidos.", + "pl": "Czyści historię wysyłania i odbierania oraz zapisane w pamięci podręcznej udostępniane treści. Odebrane pliki na dysku zostają zachowane.", + "nl": "Wist je verzend- en ontvangstgeschiedenis en de gecachte deelinhoud van de app. Ontvangen bestanden op schijf blijven behouden.", + "ru": "Очищает историю отправки и получения и кэшированное содержимое общих ресурсов. Полученные файлы на диске сохраняются." + } + }, + "storage_free_up_space_caption": { + "context": "Settings > Storage: caption under the free-up-space button.", + "translations": { + "en": "Removes temporary files and leftover trash from earlier transfers. Your transfers and received files are kept.", + "fr": "Supprime les fichiers temporaires et les résidus des transferts précédents. Vos transferts et fichiers reçus sont conservés.", + "es": "Elimina los archivos temporales y los restos de transferencias anteriores. Tus transferencias y archivos recibidos se conservan.", + "it": "Rimuove i file temporanei e i residui dei trasferimenti precedenti. I tuoi trasferimenti e i file ricevuti vengono mantenuti.", + "de": "Entfernt temporäre Dateien und Reste früherer Übertragungen. Deine Übertragungen und empfangenen Dateien bleiben erhalten.", + "pt": "Remove ficheiros temporários e resíduos de transferências anteriores. As tuas transferências e ficheiros recebidos são mantidos.", + "pl": "Usuwa pliki tymczasowe i pozostałości po wcześniejszych transferach. Twoje transfery i odebrane pliki zostają zachowane.", + "nl": "Verwijdert tijdelijke bestanden en resten van eerdere overdrachten. Je overdrachten en ontvangen bestanden blijven behouden.", + "ru": "Удаляет временные файлы и остатки прошлых передач. Ваши передачи и полученные файлы сохраняются." + } + }, + "storage_refresh": { + "context": "Settings > Storage: label for the button that recalculates usage.", + "translations": { + "en": "Refresh", + "fr": "Actualiser", + "es": "Actualizar", + "it": "Aggiorna", + "de": "Aktualisieren", + "pt": "Atualizar", + "pl": "Odśwież", + "nl": "Vernieuwen", + "ru": "Обновить" + } + }, + "storage_unavailable": { + "context": "Settings > Storage: shown when usage couldn't be calculated yet.", + "translations": { + "en": "Storage usage isn't available yet", + "fr": "L'utilisation du stockage n'est pas encore disponible", + "es": "El uso de almacenamiento aún no está disponible", + "it": "L'utilizzo dello spazio non è ancora disponibile", + "de": "Die Speichernutzung ist noch nicht verfügbar", + "pt": "A utilização do armazenamento ainda não está disponível", + "pl": "Wykorzystanie pamięci nie jest jeszcze dostępne", + "nl": "Opslaggebruik is nog niet beschikbaar", + "ru": "Данные об использовании хранилища пока недоступны" + } + }, + "storage_usage_header": { + "context": "Settings > Storage: header above the usage breakdown.", + "translations": { + "en": "On this device", + "fr": "Sur cet appareil", + "es": "En este dispositivo", + "it": "Su questo dispositivo", + "de": "Auf diesem Gerät", + "pt": "Neste dispositivo", + "pl": "Na tym urządzeniu", + "nl": "Op dit apparaat", + "ru": "На этом устройстве" + } + }, "storage_free_up_space": { "context": "Settings > Storage: button that clears temporary files and stray trash.", "translations": { diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index b85e8d5..32660c6 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -206,6 +206,11 @@ Wird bereinigt… Beende aktive Übertragungen, bevor du Speicher freigibst %1$s freigegeben + Löscht deinen Sende- und Empfangsverlauf sowie die zwischengespeicherten Freigabeinhalte der App. Empfangene Dateien auf dem Datenträger bleiben erhalten. + Entfernt temporäre Dateien und Reste früherer Übertragungen. Deine Übertragungen und empfangenen Dateien bleiben erhalten. + Aktualisieren + Die Speichernutzung ist noch nicht verfügbar + Auf diesem Gerät Speicher freigeben Alle Übertragungen löschen Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden. diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index a1dcb0e..551f8e1 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -206,6 +206,11 @@ Limpiando… Finaliza las transferencias activas antes de liberar espacio Se liberó %1$s + Borra tu historial de envíos y recepciones y el contenido compartido en caché de la app. Los archivos recibidos en el disco se conservan. + Elimina los archivos temporales y los restos de transferencias anteriores. Tus transferencias y archivos recibidos se conservan. + Actualizar + El uso de almacenamiento aún no está disponible + En este dispositivo Liberar espacio Eliminar todas las transferencias Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer. diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index a49c270..0e86280 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -206,6 +206,11 @@ Nettoyage… Terminez les transferts en cours avant de libérer de l\'espace %1$s libéré + Efface votre historique d\'envois et de réceptions ainsi que le contenu de partage mis en cache par l\'app. Les fichiers reçus sur le disque sont conservés. + Supprime les fichiers temporaires et les résidus des transferts précédents. Vos transferts et fichiers reçus sont conservés. + Actualiser + L\'utilisation du stockage n\'est pas encore disponible + Sur cet appareil Libérer de l\'espace Supprimer tous les transferts Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible. diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index bb26acb..4394347 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -206,6 +206,11 @@ Pulizia… Completa i trasferimenti attivi prima di liberare spazio Liberati %1$s + Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall\'app. I file ricevuti sul disco vengono mantenuti. + Rimuove i file temporanei e i residui dei trasferimenti precedenti. I tuoi trasferimenti e i file ricevuti vengono mantenuti. + Aggiorna + L\'utilizzo dello spazio non è ancora disponibile + Su questo dispositivo Libera spazio Elimina tutti i trasferimenti Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata. diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index 60d3791..58930d7 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -206,6 +206,11 @@ Opschonen… Voltooi actieve overdrachten voordat je ruimte vrijmaakt %1$s vrijgemaakt + Wist je verzend- en ontvangstgeschiedenis en de gecachte deelinhoud van de app. Ontvangen bestanden op schijf blijven behouden. + Verwijdert tijdelijke bestanden en resten van eerdere overdrachten. Je overdrachten en ontvangen bestanden blijven behouden. + Vernieuwen + Opslaggebruik is nog niet beschikbaar + Op dit apparaat Ruimte vrijmaken Alle overdrachten verwijderen Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt. diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index 9de29f4..391b3e4 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -206,6 +206,11 @@ Czyszczenie… Zakończ aktywne transfery przed zwolnieniem miejsca Zwolniono %1$s + Czyści historię wysyłania i odbierania oraz zapisane w pamięci podręcznej udostępniane treści. Odebrane pliki na dysku zostają zachowane. + Usuwa pliki tymczasowe i pozostałości po wcześniejszych transferach. Twoje transfery i odebrane pliki zostają zachowane. + Odśwież + Wykorzystanie pamięci nie jest jeszcze dostępne + Na tym urządzeniu Zwolnij miejsce Usuń wszystkie transfery Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć. diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index b6d0c35..54a990d 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -206,6 +206,11 @@ A limpar… Conclui as transferências ativas antes de libertar espaço Libertado %1$s + Limpa o teu histórico de envios e receções e o conteúdo de partilha em cache da app. Os ficheiros recebidos no disco são mantidos. + Remove ficheiros temporários e resíduos de transferências anteriores. As tuas transferências e ficheiros recebidos são mantidos. + Atualizar + A utilização do armazenamento ainda não está disponível + Neste dispositivo Libertar espaço Eliminar todas as transferências Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada. diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index f8894e5..8c9ed33 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -206,6 +206,11 @@ Очистка… Завершите активные передачи перед освобождением места Освобождено %1$s + Очищает историю отправки и получения и кэшированное содержимое общих ресурсов. Полученные файлы на диске сохраняются. + Удаляет временные файлы и остатки прошлых передач. Ваши передачи и полученные файлы сохраняются. + Обновить + Данные об использовании хранилища пока недоступны + На этом устройстве Освободить место Удалить все передачи Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить. diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 4daed58..4dc463a 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -206,6 +206,11 @@ Cleaning up… Finish active transfers before freeing up space Freed %1$s + Clears your send and receive history and the app\'s cached share content. Received files on disk are kept. + Removes temporary files and leftover trash from earlier transfers. Your transfers and received files are kept. + Refresh + Storage usage isn\'t available yet + On this device Free up space Delete all transfers This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone. From 6e2c8b4b2d4c3a096d15b767abd86ed1ac01c3fe Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:18:22 +0200 Subject: [PATCH 16/36] feat(apple): open the share panel right after creating a transfer After Start sharing succeeds, jump straight to the new transfer's share panel (QR code + delivery actions) instead of returning to the list and making the user drill in via the row and the share row. Refresh first so the transfer exists in state before selecting it; the share panel already handles the brief window before the ticket is ready. --- apple/VniDrop/Features/Send/SendModel.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apple/VniDrop/Features/Send/SendModel.swift b/apple/VniDrop/Features/Send/SendModel.swift index 325fe5c..18175dc 100644 --- a/apple/VniDrop/Features/Send/SendModel.swift +++ b/apple/VniDrop/Features/Send/SendModel.swift @@ -300,6 +300,13 @@ final class SendModel: ObservableObject { state.transferName = "" state.accessPolicy = .requireApproval state.isSharing = false + // Jump straight to the new transfer's share panel (QR + delivery) rather + // than dropping the user on the list to drill in manually. Refresh first + // so the transfer exists in state before it's selected. + _ = await repository.refresh() + state.selectedTransferId = share.transferId + state.detailPanel = .share + refreshReceivers(share.transferId) messages.show(UiMessage(text: .resource(L10n.Send.transferCreated), tone: .success)) case .failure(let error): state.isSharing = false From 4bd51106e819a8c61b99d70da450d1c0a7c64e94 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:41:37 +0200 Subject: [PATCH 17/36] feat(apple): cover the window while the core boots MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The core initializes asynchronously at launch, so for a moment the transfer lists look empty and the app feels stalled. Show a full-window overlay (centered spinner + "Starting…") as the top layer of the root stack while coreState.isInitialized is false; it fades out once the core is ready. --- apple/VniDrop/App/RootView.swift | 34 +++++++++++++++++++ localization/strings.json | 14 ++++++++ .../composeResources/values-de/strings.xml | 1 + .../composeResources/values-es/strings.xml | 1 + .../composeResources/values-fr/strings.xml | 1 + .../composeResources/values-it/strings.xml | 1 + .../composeResources/values-nl/strings.xml | 1 + .../composeResources/values-pl/strings.xml | 1 + .../composeResources/values-pt/strings.xml | 1 + .../composeResources/values-ru/strings.xml | 1 + .../composeResources/values/strings.xml | 1 + 11 files changed, 57 insertions(+) diff --git a/apple/VniDrop/App/RootView.swift b/apple/VniDrop/App/RootView.swift index 7abbc96..4954e07 100644 --- a/apple/VniDrop/App/RootView.swift +++ b/apple/VniDrop/App/RootView.swift @@ -63,6 +63,14 @@ struct RootView: View { onRefuse: approvals.refuse ) } + .overlay { + // A small, unobtrusive indicator while the core finishes its async + // startup — otherwise the lists look empty and the app feels stalled. + if !sendModel.coreState.isInitialized { + CoreStartingOverlay() + } + } + .animation(.easeInOut(duration: 0.25), value: sendModel.coreState.isInitialized) .vniDropTheme(isDark: isDark) .preferredColorScheme(appModel.themeMode.preferredColorScheme) .environment(\.vniColors, isDark ? .dark : .light) @@ -183,6 +191,32 @@ struct RootView: View { } } +/// A full-window cover with a centered spinner shown while the core is starting. +private struct CoreStartingOverlay: View { + var body: some View { + ZStack { + backgroundColor.ignoresSafeArea() + VStack(spacing: 16) { + ProgressView().controlSize(.large) + Text(String(localized: L10n.App.starting)) + .font(.headline) + .foregroundStyle(.secondary) + } + } + .transition(.opacity) + .accessibilityElement(children: .combine) + .accessibilityLabel(Text(String(localized: L10n.App.starting))) + } + + private var backgroundColor: Color { + #if os(iOS) + Color(uiColor: .systemBackground) + #else + Color(nsColor: .windowBackgroundColor) + #endif + } +} + #if os(iOS) import UIKit #else diff --git a/localization/strings.json b/localization/strings.json index 4c826e4..4a59818 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -321,6 +321,20 @@ "ru": "О приложении" } }, + "app_starting": { + "context": "Shown briefly at launch while the core is still starting up.", + "translations": { + "en": "Starting…", + "fr": "Démarrage…", + "es": "Iniciando…", + "it": "Avvio…", + "de": "Wird gestartet…", + "pt": "A iniciar…", + "pl": "Uruchamianie…", + "nl": "Bezig met starten…", + "ru": "Запуск…" + } + }, "appearance_auto_description": { "context": "Settings > Appearance: description for the System/auto option.", "translations": { diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index 32660c6..315627c 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -22,6 +22,7 @@ Datenschutz & Sicherheit Senden Sie Dateien direkt. Behalten Sie die Kontrolle darüber, wer sie empfängt. Über + Wird gestartet… Der hellen oder dunklen Darstellung dieses Geräts folgen. Dunkelmodus Hellmodus diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index 551f8e1..130422a 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -22,6 +22,7 @@ Privacidad y seguridad Envíe archivos directamente. Mantenga el control de quién los recibe. Acerca de + Iniciando… Seguir la apariencia clara u oscura de este dispositivo. Modo oscuro Modo claro diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 0e86280..ce9eb80 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -22,6 +22,7 @@ Confidentialité et sécurité Envoyez des fichiers directement. Gardez le contrôle de qui les reçoit. À propos + Démarrage… Suivre l’apparence claire ou sombre de cet appareil. Mode sombre Mode clair diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index 4394347..03645d8 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -22,6 +22,7 @@ Privacy e sicurezza Invii file direttamente. Mantenga il controllo su chi li riceve. Informazioni + Avvio… Segue l’aspetto chiaro o scuro di questo dispositivo. Modalità scura Modalità chiara diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index 58930d7..4674cec 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -22,6 +22,7 @@ Privacy en beveiliging Verstuur bestanden rechtstreeks. Houd controle over wie ze ontvangt. Over + Bezig met starten… De lichte of donkere weergave van dit apparaat volgen. Donkere modus Lichte modus diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index 391b3e4..237dabe 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -22,6 +22,7 @@ Prywatność i bezpieczeństwo Wysyłaj pliki bezpośrednio. Zachowaj kontrolę nad tym, kto je otrzymuje. Informacje + Uruchamianie… Dopasuj do jasnego lub ciemnego wyglądu tego urządzenia. Tryb ciemny Tryb jasny diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index 54a990d..b95839c 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -22,6 +22,7 @@ Privacidade e segurança Envie ficheiros diretamente. Mantenha o controlo sobre quem os recebe. Acerca de + A iniciar… Acompanhar o aspeto claro ou escuro deste dispositivo. Modo escuro Modo claro diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index 8c9ed33..8eb0899 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -22,6 +22,7 @@ Конфиденциальность и безопасность Отправляйте файлы напрямую. Сохраняйте контроль над тем, кто их получает. О приложении + Запуск… Следовать светлому или тёмному оформлению этого устройства. Тёмный режим Светлый режим diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 4dc463a..f8c1232 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -22,6 +22,7 @@ Privacy & security Send files directly. Stay in control of who receives them. About + Starting… Match this device’s light or dark appearance. Dark mode Light mode From 30dfabf8e703802e0f6c6dea418b74b137acc2ca Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 14:46:44 +0200 Subject: [PATCH 18/36] refactor(apple): move share to the toolbar and delete to the bottom Put a share icon in the transfer-details toolbar (opening the QR/share panel) in place of the delete button, drop the now-redundant Share row from the list, and move Delete transfer into the bottom section alongside Stop sharing. --- .../Features/Send/TransferDetailsView.swift | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index 71e57ad..da537d6 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -44,22 +44,19 @@ struct TransferDetailsView: View { count: pendingReceivers + completedReceivers, onTap: model.openReceivers ) - DetailDestination( - title: String(localized: L10n.Transfer.shareTitle), - description: String(localized: L10n.Transfer.shareDescription), - count: 0, - onTap: model.openShare - ) } - if isActiveShare { - Section { + Section { + if isActiveShare { Button(role: .destructive) { showStopConfirmation = true } label: { Label(String(localized: L10n.Send.stopSharing), systemSymbol: .stopCircle) } } + Button(role: .destructive, action: model.requestDeleteTransfer) { + Label(String(localized: L10n.Button.deleteTransfer), systemSymbol: .trash) + } } } .formStyle(.grouped) @@ -69,9 +66,10 @@ struct TransferDetailsView: View { #endif .toolbar { ToolbarItem(placement: .primaryAction) { - Button(role: .destructive, action: model.requestDeleteTransfer) { - Image(systemSymbol: .trash) + Button(action: model.openShare) { + Label(String(localized: L10n.Transfer.shareTitle), systemSymbol: .squareAndArrowUp) } + .help(String(localized: L10n.Transfer.shareTitle)) } } .confirmationDialog( From c7ebaee15bd53d06f2153c50bb775d0ed1e3bdd1 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:08:37 +0200 Subject: [PATCH 19/36] feat(apple): add context menus to Send and Receive rows Send rows get a context menu that acts inline without navigating: Share opens the share panel (QR + delivery) over the list via a dedicated sheet host, Stop sharing (active shares) and Delete transfer run in place, the latter through a new id-based SendModel.deleteTransfer and a list-level confirmation alert. Receive rows get a Delete action mirroring swipe-to-delete (handy on macOS). --- .../Features/Receive/ReceiveScreen.swift | 9 +++ apple/VniDrop/Features/Send/SendModel.swift | 25 +++++++++ apple/VniDrop/Features/Send/SendScreen.swift | 55 +++++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/apple/VniDrop/Features/Receive/ReceiveScreen.swift b/apple/VniDrop/Features/Receive/ReceiveScreen.swift index a49012f..314300c 100644 --- a/apple/VniDrop/Features/Receive/ReceiveScreen.swift +++ b/apple/VniDrop/Features/Receive/ReceiveScreen.swift @@ -79,6 +79,15 @@ struct ReceiveScreen: View { } } } + .contextMenu { + if transfer.status.isTerminalReceiveHistory { + Button(role: .destructive) { + model.requestDeleteHistoryItem(transfer.transferId) + } label: { + Label(String(localized: L10n.Button.deleteTransfer), systemSymbol: .trash) + } + } + } } } header: { Text(String(localized: L10n.Receive.historyTitle)) diff --git a/apple/VniDrop/Features/Send/SendModel.swift b/apple/VniDrop/Features/Send/SendModel.swift index 18175dc..eda9c95 100644 --- a/apple/VniDrop/Features/Send/SendModel.swift +++ b/apple/VniDrop/Features/Send/SendModel.swift @@ -228,6 +228,31 @@ final class SendModel: ObservableObject { } } + /// Deletes a transfer by id, independent of the detail selection — used by the + /// list context menu so it can act inline without navigating into the detail. + func deleteTransfer(id: UInt64) { + if state.isDeleting { return } + state.isDeleting = true + Task { + let result = await repository.delete(transferId: id) + switch result { + case .success: + filePreviewRepository.remove(transferId: id) + if state.selectedTransferId == id { + state.selectedTransferId = nil + state.detailPanel = nil + state.receiverHistory = [] + } + state.isDeleting = false + _ = await repository.refresh() + messages.tryShow(UiMessage(text: .resource(L10n.Transfer.deleted), tone: .success)) + case .failure(let error): + state.isDeleting = false + messages.error(error) + } + } + } + /// Cancels/refuses a single receiver by responding to its request negatively. /// Uses the core's `respondReceiverRequest` (no backend change); applies to /// receivers that are still pending or accepted. diff --git a/apple/VniDrop/Features/Send/SendScreen.swift b/apple/VniDrop/Features/Send/SendScreen.swift index ee8b55d..4d73bea 100644 --- a/apple/VniDrop/Features/Send/SendScreen.swift +++ b/apple/VniDrop/Features/Send/SendScreen.swift @@ -7,6 +7,11 @@ struct SendScreen: View { @ObservedObject var model: SendModel let windowClass: WindowClass + /// Transfer whose share panel is presented inline from the list context menu. + @State private var shareTarget: Transfer? + /// Transfer pending an inline (list-level) delete confirmation. + @State private var deleteTarget: Transfer? + private var outgoing: [Transfer] { model.coreState.transfers.filter { $0.direction == .send } } @@ -41,6 +46,18 @@ struct SendScreen: View { detailView(for: transfer) } } + // Attached inside the NavigationStack (a different sheet host than the + // composer drawer on the outer body, so the two don't clash). Opens the + // share panel over the list without navigating into the transfer detail. + .adaptiveDrawer( + isPresented: Binding(get: { shareTarget != nil }, set: { if !$0 { shareTarget = nil } }), + windowClass: windowClass, + onDismiss: { shareTarget = nil } + ) { + if let shareTarget { + TransferSharePanel(model: model, transfer: shareTarget) + } + } } .adaptiveDrawer( isPresented: Binding(get: { model.state.isComposerOpen }, set: { _ in }), @@ -49,8 +66,24 @@ struct SendScreen: View { ) { TransferComposer(model: model, windowClass: windowClass) } + .alert( + Text(String(localized: L10n.Transfer.deleteTitle)), + isPresented: Binding(get: { deleteTarget != nil }, set: { if !$0 { deleteTarget = nil } }) + ) { + Button(String(localized: L10n.Button.cancel), role: .cancel) { deleteTarget = nil } + Button(String(localized: L10n.Button.deleteTransfer), role: .destructive) { + if let target = deleteTarget { model.deleteTransfer(id: target.transferId) } + deleteTarget = nil + } + } message: { + if let target = deleteTarget { + Text(L10n.Transfer.deleteDescription( + transferName: target.transferName ?? String(localized: L10n.Send.newTransferTitle))) + } + } } + /// The pushed transfer details view, with its detail-panel sheet and delete /// alert attached here so they present from the detail's own context (presenting /// modals from the parent stack while a detail is pushed is unreliable on macOS). @@ -91,6 +124,28 @@ struct SendScreen: View { ) } .buttonStyle(.plain) + .contextMenu { + if transfer.ticket != nil { + Button { + shareTarget = transfer + } label: { + Label(String(localized: L10n.Transfer.shareTitle), systemSymbol: .squareAndArrowUp) + } + } + if transfer.status == .sharing { + Button(role: .destructive) { + model.stopSharing(transferId: transfer.transferId) + } label: { + Label(String(localized: L10n.Send.stopSharing), systemSymbol: .stopCircle) + } + } + Divider() + Button(role: .destructive) { + deleteTarget = transfer + } label: { + Label(String(localized: L10n.Button.deleteTransfer), systemSymbol: .trash) + } + } } } header: { Text(String(localized: L10n.Send.transfersTitle)) From 065d57e89689a1f3aa4d376a1856e5a075626f5a Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 15:14:21 +0200 Subject: [PATCH 20/36] refactor(apple): redesign the composer source buttons Replace the bare text links under Start sharing with an even row of bordered, icon-led buttons (Change files, Choose folder, plus Clear on wider layouts). Single-line labels keep them equal height, and a neutral tint keeps them quiet so the purple Start sharing reads as the primary action. --- .../Features/Send/TransferComposer.swift | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/apple/VniDrop/Features/Send/TransferComposer.swift b/apple/VniDrop/Features/Send/TransferComposer.swift index 1cbfff7..8dc4fd9 100644 --- a/apple/VniDrop/Features/Send/TransferComposer.swift +++ b/apple/VniDrop/Features/Send/TransferComposer.swift @@ -75,29 +75,41 @@ struct TransferComposer: View { } } - @ViewBuilder private var actions: some View { let shareTitle = state.isSharing ? String(localized: L10n.Button.sharingFile) : String(localized: L10n.Button.shareFile) - let shareButton = PrimaryButton( - title: shareTitle, action: model.createShare, - enabled: state.canCreateShare(coreInitialized: model.coreState.isInitialized) - ) - if windowClass == .phone { - VStack(spacing: 8) { - shareButton - QuietButton(title: String(localized: L10n.Button.changeFiles), action: model.selectFile, enabled: !state.isSharing) - QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder, enabled: !state.isSharing) - } - } else { - HStack(spacing: 8) { - shareButton.fixedSize() - QuietButton(title: String(localized: L10n.Button.changeFiles), action: model.selectFile, enabled: !state.isSharing) - QuietButton(title: String(localized: L10n.Button.chooseFolder), action: model.selectFolder, enabled: !state.isSharing) - QuietButton(title: String(localized: L10n.Button.clear), action: model.clearSelectedSource, enabled: !state.isSharing) + return VStack(spacing: 10) { + PrimaryButton( + title: shareTitle, action: model.createShare, + enabled: state.canCreateShare(coreInitialized: model.coreState.isInitialized) + ) + // Secondary source actions as an even row of bordered buttons rather than + // bare text links, so they read as controls and align with the primary. + HStack(spacing: 10) { + sourceButton(title: L10n.Button.changeFiles, symbol: .docBadgeArrowUp, action: model.selectFile) + sourceButton(title: L10n.Button.chooseFolder, symbol: .folder, action: model.selectFolder) + if windowClass != .phone { + sourceButton(title: L10n.Button.clear, symbol: .xmark, action: model.clearSelectedSource) + } } } } + + private func sourceButton( + title: String.LocalizationValue, symbol: SFSymbol, action: @escaping () -> Void + ) -> some View { + Button(action: action) { + Label(String(localized: title), systemSymbol: symbol) + .lineLimit(1) + .minimumScaleFactor(0.85) + .frame(maxWidth: .infinity) + .frame(minHeight: 20) + } + .buttonStyle(.bordered) + .controlSize(.large) + .tint(.secondary) + .disabled(state.isSharing) + } } private struct SelectedFileCard: View { From aab5f243ca4954797f1cc6c994c3e9a6bf158653 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 16:02:06 +0200 Subject: [PATCH 21/36] fix(apple): treat a completed receiver event as terminal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit progressForReceiver only labelled a receiver Completed when no progress/started events preceded the completion, so the normal progress→completed sequence fell through and rendered as Sending despite a .completed kind. Events are newest-first, so a completed latest event is always terminal — label it Completed. Fixes the failing ProgressDerivationTests.testReceiverCompletionAfterProgressIsTerminal. --- apple/VniDrop/Core/TransferProgress.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apple/VniDrop/Core/TransferProgress.swift b/apple/VniDrop/Core/TransferProgress.swift index 9c8fe3d..c8eebbb 100644 --- a/apple/VniDrop/Core/TransferProgress.swift +++ b/apple/VniDrop/Core/TransferProgress.swift @@ -82,7 +82,9 @@ func progressForReceiver( labelKey: L10n.Progress.interrupted, progress: nil, detail: nil ) } - if latestKind == .completed && !transferEvents.contains(where: { $0.eventKind == .progress || $0.eventKind == .started }) { + // Events are newest-first, so a completed latest event is terminal even when + // progress/started events precede it — it must show as Completed, not Sending. + if latestKind == .completed { return TransferProgress( transferId: transferId, phase: .transfer, kind: .completed, labelKey: L10n.Progress.completed, progress: 1, detail: nil From 9b15a388d8cad6250dcb6be39e4df43787bc844c Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:54:59 +0200 Subject: [PATCH 22/36] fix(apple): polish the merged Network settings Move the relay-mode picker's Network title into a Section header (the inline picker label rendered as a stray row on iOS) and hide the picker label. Use a verbatim prompt for the relay URL placeholder so macOS stops markdown-linkifying the URL-shaped text into a purple link. --- .../Features/Settings/SettingsSections.swift | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index 5a9696d..0e8605a 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -80,7 +80,7 @@ struct NetworkSettings: View { var body: some View { Section { Picker( - String(localized: "settings_network_title"), + "", selection: Binding(get: { model.state.relayMode }, set: { model.setRelayMode($0) }) ) { ForEach(RelayPreferenceMode.allCases, id: \.self) { mode in @@ -88,7 +88,10 @@ struct NetworkSettings: View { } } .pickerStyle(.inline) + .labelsHidden() .disabled(model.state.isApplyingRelayConfiguration) + } header: { + Text(String(localized: L10n.Settings.networkTitle)) } footer: { Text(LocalizedStringKey(relayModeDescriptionKey(model.state.relayMode))) } @@ -127,7 +130,7 @@ struct NetworkSettings: View { VStack(alignment: .leading, spacing: 6) { HStack { TextField( - "https://relay.example.com", + "", text: Binding( get: { model.state.relayURLs.indices.contains(index) @@ -135,8 +138,12 @@ struct NetworkSettings: View { : "" }, set: { model.setRelayURL($0, at: index) } - ) + ), + // `Text(verbatim:)` avoids macOS markdown-linkifying the + // URL-shaped placeholder into a purple link. + prompt: Text(verbatim: "https://relay.example.com") ) + .labelsHidden() #if os(iOS) .keyboardType(.URL) .textInputAutocapitalization(.never) From 30420052267d7a1404f6b88fde09f8af40d55082 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:15:36 +0200 Subject: [PATCH 23/36] refactor(apple): type the merged relay/network resources Convert master's raw-string localization keys and SF Symbols in the new relay/network code to typed accessors, matching this branch's typed-resources convention: relay mode labels/descriptions, NetworkSettings strings, the endpoint id and relay-validation messages (now typed L10n functions), and SF Symbols via SFSafeSymbols. Retype the model's relayApplyErrorKey from a raw String key to String.LocalizationValue so no loose key literals remain in the settings layer. --- .../Features/Send/TransferDetailsView.swift | 4 +- .../Features/Settings/SettingsModel.swift | 22 ++++---- .../Features/Settings/SettingsScreen.swift | 18 +++---- .../Features/Settings/SettingsSections.swift | 51 ++++++++----------- 4 files changed, 43 insertions(+), 52 deletions(-) diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index cc86990..b5b51e1 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -303,9 +303,9 @@ struct TransferSharePanel: View { image.interpolation(.none).resizable().scaledToFit().padding(14) } else { VStack(spacing: 10) { - Image(systemName: "qrcode") + Image(systemSymbol: .qrcode) .font(.system(size: 36, weight: .medium)) - Text(LocalizedStringKey("transfer_qr_unavailable")) + Text(String(localized: L10n.Transfer.qrUnavailable)) .font(VniType.bodySmall) .multilineTextAlignment(.center) } diff --git a/apple/VniDrop/Features/Settings/SettingsModel.swift b/apple/VniDrop/Features/Settings/SettingsModel.swift index 308a363..b5d3c3f 100644 --- a/apple/VniDrop/Features/Settings/SettingsModel.swift +++ b/apple/VniDrop/Features/Settings/SettingsModel.swift @@ -52,7 +52,7 @@ struct SettingsState: Equatable { var isApplyingRelayConfiguration = false var hasActiveNetworkWork = false var endpointId: String? - var relayApplyErrorKey: String? + var relayApplyErrorKey: String.LocalizationValue? var deviceInfo: DeviceInfo? var appVersion = "" var isLoadingDeviceInfo = false @@ -169,7 +169,7 @@ final class SettingsModel: ObservableObject { || coreState.transfers.contains(where: { $0.status.isActiveTransfer }) self.state.hasActiveNetworkWork = hasActiveWork self.state.endpointId = coreState.status?.endpointId - if !hasActiveWork && self.state.relayApplyErrorKey == "relay_apply_active_transfers" { + if !hasActiveWork && self.state.relayApplyErrorKey == L10n.Relay.applyActiveTransfers { self.state.relayApplyErrorKey = nil } } @@ -296,8 +296,8 @@ final class SettingsModel: ObservableObject { || coreState.transfers.contains(where: { $0.status.isActiveTransfer }) guard !hasActiveWork else { state.hasActiveNetworkWork = true - state.relayApplyErrorKey = "relay_apply_active_transfers" - messages.show(UiMessage(text: .resource("relay_apply_active_transfers"), tone: .warning)) + state.relayApplyErrorKey = L10n.Relay.applyActiveTransfers + messages.show(UiMessage(text: .resource(L10n.Relay.applyActiveTransfers), tone: .warning)) return } @@ -316,16 +316,16 @@ final class SettingsModel: ObservableObject { preferences.setRelayConfiguration(configuration) state.isApplyingRelayConfiguration = false state.relayConfigurationIsDirty = false - messages.show(UiMessage(text: .resource("relay_settings_applied"), tone: .success)) + messages.show(UiMessage(text: .resource(L10n.Relay.settingsApplied), tone: .success)) case .failure(let error): if let lifecycleError = error as? CoreNetworkLifecycleError { state.isApplyingRelayConfiguration = false switch lifecycleError { case .activeNetworkWork: state.hasActiveNetworkWork = true - state.relayApplyErrorKey = "relay_apply_active_transfers" + state.relayApplyErrorKey = L10n.Relay.applyActiveTransfers case .transitionInProgress: - state.relayApplyErrorKey = "relay_apply_failed" + state.relayApplyErrorKey = L10n.Relay.applyFailed } return } @@ -335,11 +335,11 @@ final class SettingsModel: ObservableObject { ) state.isApplyingRelayConfiguration = false if case .success = rollbackResult { - state.relayApplyErrorKey = "relay_apply_failed" - messages.show(UiMessage(text: .resource("relay_apply_failed"), tone: .error)) + state.relayApplyErrorKey = L10n.Relay.applyFailed + messages.show(UiMessage(text: .resource(L10n.Relay.applyFailed), tone: .error)) } else { - state.relayApplyErrorKey = "relay_restore_failed" - messages.show(UiMessage(text: .resource("relay_restore_failed"), tone: .error)) + state.relayApplyErrorKey = L10n.Relay.restoreFailed + messages.show(UiMessage(text: .resource(L10n.Relay.restoreFailed), tone: .error)) } } } diff --git a/apple/VniDrop/Features/Settings/SettingsScreen.swift b/apple/VniDrop/Features/Settings/SettingsScreen.swift index 2fa7346..5b436fa 100644 --- a/apple/VniDrop/Features/Settings/SettingsScreen.swift +++ b/apple/VniDrop/Features/Settings/SettingsScreen.swift @@ -125,19 +125,19 @@ private struct SettingsSectionContent: View { func relayModeLabel(_ mode: RelayPreferenceMode) -> String { switch mode { - case .automatic: return String(localized: "relay_mode_automatic") - 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") + case .automatic: return String(localized: L10n.Relay.modeAutomatic) + case .strictCustom: return String(localized: L10n.Relay.modeCustom) + case .customWithDirectFallback: return String(localized: L10n.Relay.modeCustomDirectFallback) + case .localOnly: return String(localized: L10n.Relay.modeLocalOnly) } } -func relayModeDescriptionKey(_ mode: RelayPreferenceMode) -> String { +func relayModeDescription(_ mode: RelayPreferenceMode) -> String.LocalizationValue { 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" + case .automatic: return L10n.Relay.modeAutomaticDescription + case .strictCustom: return L10n.Relay.modeCustomDescription + case .customWithDirectFallback: return L10n.Relay.modeCustomDirectFallbackDescription + case .localOnly: return L10n.Relay.modeLocalOnlyDescription } } diff --git a/apple/VniDrop/Features/Settings/SettingsSections.swift b/apple/VniDrop/Features/Settings/SettingsSections.swift index 0e8605a..88f21af 100644 --- a/apple/VniDrop/Features/Settings/SettingsSections.swift +++ b/apple/VniDrop/Features/Settings/SettingsSections.swift @@ -93,22 +93,22 @@ struct NetworkSettings: View { } header: { Text(String(localized: L10n.Settings.networkTitle)) } footer: { - Text(LocalizedStringKey(relayModeDescriptionKey(model.state.relayMode))) + Text(String(localized: relayModeDescription(model.state.relayMode))) } Section { Label { - Text(LocalizedStringKey("relay_privacy_description")) + Text(String(localized: L10n.Relay.privacyDescription)) .fixedSize(horizontal: false, vertical: true) } icon: { - Image(systemName: "lock.shield") + Image(systemSymbol: .lockShield) } .foregroundStyle(.secondary) } if let endpointId = model.state.endpointId, !endpointId.isEmpty { Section { - Text(String(format: String(localized: "approval_endpoint_id"), endpointId)) + Text(L10n.Approval.endpointId(deviceId: endpointId)) .font(.footnote.monospaced()) .textSelection(.enabled) } @@ -118,10 +118,10 @@ struct NetworkSettings: View { Section { if model.state.relayMode == .strictCustom { Label { - Text(LocalizedStringKey("relay_strict_warning")) + Text(String(localized: L10n.Relay.strictWarning)) .fixedSize(horizontal: false, vertical: true) } icon: { - Image(systemName: "exclamationmark.shield.fill") + Image(systemSymbol: .exclamationmarkShieldFill) } .foregroundStyle(.orange) } @@ -154,10 +154,10 @@ struct NetworkSettings: View { Button(role: .destructive) { model.removeRelayURL(at: index) } label: { - Image(systemName: "minus.circle.fill") + Image(systemSymbol: .minusCircleFill) } .buttonStyle(.borderless) - .accessibilityLabel(Text(LocalizedStringKey("relay_remove_url"))) + .accessibilityLabel(Text(String(localized: L10n.Relay.removeUrl))) .disabled(model.state.isApplyingRelayConfiguration) } @@ -170,16 +170,16 @@ struct NetworkSettings: View { } Button(action: model.addRelayURL) { - Label(String(localized: "relay_add_url"), systemImage: "plus.circle") + Label(String(localized: L10n.Relay.addUrl), systemSymbol: .plusCircle) } .disabled( model.state.relayURLs.count >= RelayConfigurationValidator.maximumRelayCount || model.state.isApplyingRelayConfiguration ) } header: { - Text(LocalizedStringKey("relay_custom_urls_label")) + Text(String(localized: L10n.Relay.customUrlsLabel)) } footer: { - Text(LocalizedStringKey("relay_custom_urls_help")) + Text(String(localized: L10n.Relay.customUrlsHelp)) } } @@ -188,7 +188,7 @@ struct NetworkSettings: View { Label { Text(relayValidationMessage(error)) } icon: { - Image(systemName: "exclamationmark.triangle.fill") + Image(systemSymbol: .exclamationmarkTriangleFill) } .foregroundStyle(.red) } @@ -197,13 +197,9 @@ struct NetworkSettings: View { if model.state.hasActiveNetworkWork || model.state.relayApplyErrorKey != nil { Section { Label { - Text(LocalizedStringKey( - model.state.hasActiveNetworkWork - ? "relay_apply_active_transfers" - : model.state.relayApplyErrorKey ?? "relay_apply_failed" - )) + Text(String(localized: model.state.hasActiveNetworkWork ? L10n.Relay.applyActiveTransfers : (model.state.relayApplyErrorKey ?? L10n.Relay.applyFailed))) } icon: { - Image(systemName: "exclamationmark.triangle.fill") + Image(systemSymbol: .exclamationmarkTriangleFill) } .foregroundStyle(.red) } @@ -212,9 +208,7 @@ struct NetworkSettings: View { Section { Button(action: model.applyRelayConfiguration) { HStack { - Text(LocalizedStringKey( - model.state.isApplyingRelayConfiguration ? "relay_applying" : "relay_apply" - )) + Text(String(localized: model.state.isApplyingRelayConfiguration ? L10n.Relay.applying : L10n.Relay.apply)) if model.state.isApplyingRelayConfiguration { Spacer() ProgressView() @@ -227,7 +221,7 @@ struct NetworkSettings: View { || model.state.hasActiveNetworkWork ) } footer: { - Text(LocalizedStringKey("relay_apply_restart_description")) + Text(String(localized: L10n.Relay.applyRestartDescription)) } } } @@ -235,18 +229,15 @@ struct NetworkSettings: View { private func relayValidationMessage(_ error: RelayConfigurationValidationError) -> String { switch error { case .missingURL: - return String(localized: "relay_validation_missing_url") + return String(localized: L10n.Relay.validationMissingUrl) case .tooManyURLs: - return String( - format: String(localized: "relay_validation_too_many_urls"), - RelayConfigurationValidator.maximumRelayCount - ) + return L10n.Relay.validationTooManyUrls(maximum: RelayConfigurationValidator.maximumRelayCount) case .httpsRequired(let index): - return String(format: String(localized: "relay_validation_https_required"), index + 1) + return L10n.Relay.validationHttpsRequired(line: index + 1) case .invalidURL(let index): - return String(format: String(localized: "relay_validation_invalid_url"), index + 1) + return L10n.Relay.validationInvalidUrl(line: index + 1) case .duplicateURL(let index): - return String(format: String(localized: "relay_validation_duplicate_url"), index + 1) + return L10n.Relay.validationDuplicateUrl(line: index + 1) } } From d2924f7ce67af9588a763016f05c87e6888da9c3 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:32:15 +0200 Subject: [PATCH 24/36] fix(apple): focus the running instance on notification tap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a UNUserNotificationCenterDelegate didReceive handler so tapping a notification is handled inside the running app — activating and bringing the existing window forward — instead of falling through to default launch behavior, which on macOS can surface a second process. The approval/transfer UI is driven by core state, so activating the window reveals any pending approval. --- .../Core/LocalNotificationService.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apple/VniDrop/Core/LocalNotificationService.swift b/apple/VniDrop/Core/LocalNotificationService.swift index 4f7f11c..8b246e2 100644 --- a/apple/VniDrop/Core/LocalNotificationService.swift +++ b/apple/VniDrop/Core/LocalNotificationService.swift @@ -26,6 +26,27 @@ private final class NotificationPresenter: NSObject, UNUserNotificationCenterDel ) async -> UNNotificationPresentationOptions { [.banner, .sound, .list] } + + /// Handle a notification tap inside the running instance and bring the existing + /// window forward, rather than letting the default launch behavior surface (which + /// on macOS can spin up a second process). The approval/transfer UI is driven by + /// core state, so activating the window is enough to reveal a pending approval. + func userNotificationCenter( + _ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse + ) async { + #if os(macOS) + await MainActor.run { + NSApp.activate(ignoringOtherApps: true) + // Reopen/focus the single main window (activation triggers SwiftUI's + // reopen handling when it was closed). + for window in NSApp.windows where window.canBecomeMain { + window.makeKeyAndOrderFront(nil) + break + } + } + #endif + } } /// Local notification service backed by `UNUserNotificationCenter`. From b65bac021f56fc8740fa0ab4f64ba8a5caa42977 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:32:15 +0200 Subject: [PATCH 25/36] build(apple): enforce typed resources with SwiftLint Add a focused .swiftlint.yml (custom rules only, no default style noise) flagging raw String(localized:) / LocalizedStringKey / systemName|systemImage literals, and wire it as a required pre-build phase that fails the build if SwiftLint is missing (brew install swiftlint). The phase prepends the Homebrew bin dirs since Xcode runs scripts with a minimal PATH. Runs clean on the current tree (0 violations). --- apple/.swiftlint.yml | 27 +++++++++++++++++++++++++++ apple/project.yml | 15 +++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 apple/.swiftlint.yml diff --git a/apple/.swiftlint.yml b/apple/.swiftlint.yml new file mode 100644 index 0000000..1614011 --- /dev/null +++ b/apple/.swiftlint.yml @@ -0,0 +1,27 @@ +# Focused lint for the native app: enforce the typed-resources convention only +# (no default style rules, so this stays signal, not noise). +only_rules: + - custom_rules + +included: + - VniDrop + +excluded: + - VniDrop/Generated + +custom_rules: + raw_localized_string: + name: "Raw localized key" + regex: 'String\(localized:\s*"' + message: "Use a typed L10n.* accessor, not a raw key string." + severity: warning + raw_localized_string_key: + name: "Raw LocalizedStringKey" + regex: 'LocalizedStringKey\("' + message: "Use a typed L10n.* accessor instead of a raw key." + severity: warning + raw_sf_symbol: + name: "Raw SF Symbol" + regex: 'system(Name|Image):\s*"' + message: "Use SFSafeSymbols: Image(systemSymbol:) or systemSymbol:." + severity: warning diff --git a/apple/project.yml b/apple/project.yml index 843a363..806fc85 100644 --- a/apple/project.yml +++ b/apple/project.yml @@ -69,6 +69,21 @@ targets: - sdk: SystemConfiguration.framework - sdk: Security.framework - sdk: libresolv.tbd + preBuildScripts: + # Enforce the typed-resources convention (see .swiftlint.yml). Required: fails + # the build if SwiftLint is missing so the rules can't be silently bypassed. + - name: SwiftLint (typed resources) + basedOnDependencyAnalysis: false + script: | + # Xcode runs build phases with a minimal PATH that omits Homebrew, so add + # the common Homebrew bin dirs (Apple Silicon + Intel) before resolving it. + export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH" + if which swiftlint >/dev/null; then + swiftlint lint --config "${SRCROOT}/.swiftlint.yml" + else + echo "error: SwiftLint not installed — run 'brew install swiftlint'" + exit 1 + fi VniDropTests: type: bundle.unit-test From 35f06a0b6bd51a72039ad6f14bcc0a01fa39e21a Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:45:02 +0200 Subject: [PATCH 26/36] fix(apple): localize receiver failure reasons The receiver row showed the core's raw reason code (e.g. destination_exists), breaking the never-expose-raw-reason-blobs rule. Map the core reason codes to the existing L10n.Error.* messages via receiverReasonUiText, with a generic fallback so a raw code is never surfaced. --- .../Features/Send/TransferDetailsView.swift | 3 +- .../VniDrop/UI/Feedback/UserFacingError.swift | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index b5b51e1..534133a 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -251,7 +251,8 @@ private struct ReceiverRow: View { .foregroundStyle(receiver.status.statusColor(colors)) } if let reason = receiver.reason, !reason.isEmpty { - Text(reason).font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter) + Text(receiverReasonUiText(reason).resolved()) + .font(VniType.bodySmall).foregroundStyle(colors.foregroundLighter) } } .frame(maxWidth: .infinity, alignment: .leading) diff --git a/apple/VniDrop/UI/Feedback/UserFacingError.swift b/apple/VniDrop/UI/Feedback/UserFacingError.swift index 2bc2e9d..77cab8d 100644 --- a/apple/VniDrop/UI/Feedback/UserFacingError.swift +++ b/apple/VniDrop/UI/Feedback/UserFacingError.swift @@ -76,6 +76,39 @@ extension Error { } } +/// Maps a receiver delivery/refusal reason code to a user-facing message, never +/// surfacing the raw core code (e.g. `destination_exists`). Unknown codes fall back +/// to the substring hints, then a generic message. +func receiverReasonUiText(_ reason: String) -> UiText { + switch reason { + case "destination_exists": + return .resource(L10n.Error.destinationExists) + case "filesystem", "filesystem_permission_denied": + return .resource(L10n.Error.filesystem) + case "permission_denied", "approval-required", "approval-expired", + "unknown-transfer", "missing-endpoint-id", "invalid-receipt": + return .resource(L10n.Error.permission) + case "storage_full": + return .resource(L10n.Error.storageFull) + case "network": + return .resource(L10n.Error.network) + case "invalid_ticket": + return .resource(L10n.Error.invalidTicket) + case "transfer": + return .resource(L10n.Error.transfer) + case "repository", "repository-error": + return .resource(L10n.Error.repository) + case "invalid_input": + return .resource(L10n.Error.invalidInput) + case "initialization": + return .resource(L10n.Error.initialization) + case "cancelled", "internal": + return .resource(L10n.Error.generic) + default: + return reasonHints(reason) ?? .resource(L10n.Error.generic) + } +} + private func transferUiText(_ reason: String) -> UiText { let detail = reason.lowercased() if detail.contains("refused") || detail.contains("denied") || detail.contains("not approved") { From f513a6118e8d33a2f3f19b7a659e0c2da6002ee0 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:45:02 +0200 Subject: [PATCH 27/36] feat(apple): notify the sender when a receiver's delivery fails plannedReceiverNotifications only fired for completed receivers, so a failed delivery produced no notification. Add a receiverFailed kind wired through the planner, id, and deliver paths, with localized notifications_receiver_failed_* strings and a unit test. --- apple/Tests/TransferNotificationTests.swift | 7 +++ .../TransferNotificationCoordinator.swift | 20 +++++++-- localization/strings.json | 44 +++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/apple/Tests/TransferNotificationTests.swift b/apple/Tests/TransferNotificationTests.swift index e38e911..931461d 100644 --- a/apple/Tests/TransferNotificationTests.swift +++ b/apple/Tests/TransferNotificationTests.swift @@ -41,4 +41,11 @@ final class TransferNotificationTests: XCTestCase { let requests = [Fixtures.request(id: "a", requestedAt: 1, status: .completed)] XCTAssertTrue(plannedReceiverNotifications(requests, published: ["receiver-completed-a"]).isEmpty) } + + func testReceiverNotificationsFireForFailedReceivers() { + let requests = [Fixtures.request(id: "x", requestedAt: 1, status: .failed)] + let planned = plannedReceiverNotifications(requests, published: []) + XCTAssertEqual(planned.map(\.id), ["receiver-failed-x"]) + XCTAssertEqual(planned.first?.kind, .receiverFailed) + } } diff --git a/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift b/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift index 0b166fc..4c52fb7 100644 --- a/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift +++ b/apple/VniDrop/Features/Notifications/TransferNotificationCoordinator.swift @@ -7,6 +7,7 @@ enum TransferNotificationKind: Equatable { case receiveCompleted // An incoming transfer finished downloading. case receiveFailed // An incoming transfer failed. case receiverCompleted // A receiver finished downloading your shared transfer. + case receiverFailed // A receiver's download of your shared transfer failed. } /// A notification resolved from core state but not yet published. `transferName` @@ -39,11 +40,17 @@ func plannedTransferNotifications(_ transfers: [Transfer], published: Set) -> [PlannedNotification] { requests.compactMap { request in - guard request.status == .completed else { return nil } - let id = "receiver-completed-\(request.id)" + let kind: TransferNotificationKind + let idPrefix: String + switch request.status { + case .completed: kind = .receiverCompleted; idPrefix = "receiver-completed" + case .failed: kind = .receiverFailed; idPrefix = "receiver-failed" + default: return nil + } + let id = "\(idPrefix)-\(request.id)" guard !published.contains(id) else { return nil } return PlannedNotification( - id: id, kind: .receiverCompleted, + id: id, kind: kind, transferName: request.transferName, receiver: request.receiverName ?? request.receiverDeviceName ) @@ -56,6 +63,7 @@ private func transferNotificationId(_ kind: TransferNotificationKind, transferId case .receiveCompleted: return "receive-completed-\(transferId)" case .receiveFailed: return "receive-failed-\(transferId)" case .receiverCompleted: return "receiver-completed-\(transferId)" + case .receiverFailed: return "receiver-failed-\(transferId)" } } @@ -176,6 +184,12 @@ final class TransferNotificationCoordinator: ObservableObject { id: plan.id, title: String(localized: L10n.Notifications.receiverCompletedTitle), body: L10n.Notifications.receiverCompletedBody(receiver: receiver, transferName: name)) + case .receiverFailed: + let receiver = plan.receiver ?? String(localized: L10n.Approval.nearbyDevice) + notification = LocalNotification( + id: plan.id, + title: String(localized: L10n.Notifications.receiverFailedTitle), + body: L10n.Notifications.receiverFailedBody(receiver: receiver, transferName: name)) } if case .failure(let error) = await notifications.publish(notification) { messages.error(error) diff --git a/localization/strings.json b/localization/strings.json index be75e03..6fdd4cb 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -1948,6 +1948,50 @@ "ru": "Передача получена" } }, + "notifications_receiver_failed_body": { + "context": "Notification body shown to the sender when a receiver's download fails. {receiver} = receiver name, {transferName} = transfer name.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "receiver", + "type": "string" + }, + { + "name": "transferName", + "type": "string" + } + ], + "translations": { + "en": "{receiver} couldn't receive “{transferName}”", + "fr": "{receiver} n'a pas pu recevoir « {transferName} »", + "es": "{receiver} no pudo recibir «{transferName}»", + "it": "{receiver} non ha potuto ricevere “{transferName}”", + "de": "{receiver} konnte „{transferName}“ nicht empfangen", + "pt": "{receiver} não conseguiu receber “{transferName}”", + "pl": "{receiver} nie mógł odebrać „{transferName}”", + "nl": "{receiver} kon “{transferName}” niet ontvangen", + "ru": "{receiver} не удалось получить «{transferName}»" + } + }, + "notifications_receiver_failed_title": { + "context": "Notification title shown to the sender when a receiver's download fails.", + "targets": [ + "apple" + ], + "translations": { + "en": "Delivery failed", + "fr": "Échec de l'envoi", + "es": "Error en la entrega", + "it": "Consegna non riuscita", + "de": "Übertragung fehlgeschlagen", + "pt": "Falha na entrega", + "pl": "Dostarczenie nie powiodło się", + "nl": "Levering mislukt", + "ru": "Ошибка доставки" + } + }, "notifications_send_failed_body": { "context": "Notification body shown to the sender when a shared transfer fails. {transferName} = transfer name.", "targets": [ From 83ddf9f059a7a11e4cbcf5b0dda0f9800e29ffe7 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 18:52:26 +0200 Subject: [PATCH 28/36] ci(apple): install SwiftLint for the required lint build phase The VniDrop target's SwiftLint pre-build phase is required (fails if missing), so the Apple CI job must have SwiftLint available. Add a brew install step. --- .github/workflows/apple.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/apple.yml b/.github/workflows/apple.yml index 0871d58..415aebe 100644 --- a/.github/workflows/apple.yml +++ b/.github/workflows/apple.yml @@ -65,6 +65,10 @@ jobs: - name: Install XcodeGen run: brew install xcodegen + - name: Install SwiftLint + # Required by the VniDrop target's SwiftLint build phase (typed-resources rules). + run: brew install swiftlint + - name: Install Bun # The Apple l10n catalog (Localizable.xcstrings) and L10n.swift are # generated from localization/strings.json at build time, not tracked. From 7d3f1b9862c364e1a85235fda8b131abd8467c60 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:02:02 +0200 Subject: [PATCH 29/36] fix(l10n): add the transfer-cache-clear strings dropped in merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Master referenced storage_clear_transfer_cache(_description) and storage_transfer_cache_cleared from Kotlin but never added them to strings.json — they lived only in the committed Compose XML. Regenerating l10n from the merged strings.json dropped them, breaking the shared-kmp build. Add them (kmp target, all 9 languages, text carried over from master) so generation restores them. --- localization/strings.json | 51 +++++++++++++++++++ .../composeResources/values-de/strings.xml | 3 ++ .../composeResources/values-es/strings.xml | 3 ++ .../composeResources/values-fr/strings.xml | 3 ++ .../composeResources/values-it/strings.xml | 3 ++ .../composeResources/values-nl/strings.xml | 3 ++ .../composeResources/values-pl/strings.xml | 3 ++ .../composeResources/values-pt/strings.xml | 3 ++ .../composeResources/values-ru/strings.xml | 3 ++ .../composeResources/values/strings.xml | 3 ++ 10 files changed, 78 insertions(+) diff --git a/localization/strings.json b/localization/strings.json index 6fdd4cb..10af452 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -3662,6 +3662,40 @@ "ru": "Освобождено {size}" } }, + "storage_clear_transfer_cache": { + "context": "Settings > Storage: button that clears cached transfer content (KMP).", + "targets": [ + "kmp" + ], + "translations": { + "en": "Clear transfer cache", + "fr": "Vider le cache des transferts", + "es": "Borrar caché de transferencias", + "it": "Svuota cache trasferimenti", + "de": "Übertragungscache leeren", + "pt": "Limpar cache de transferências", + "pl": "Wyczyść pamięć podręczną transferów", + "nl": "Overdrachtscache wissen", + "ru": "Очистить кэш передач" + } + }, + "storage_clear_transfer_cache_description": { + "context": "Settings > Storage: description under the clear-transfer-cache button (KMP).", + "targets": [ + "kmp" + ], + "translations": { + "en": "Removes cached transfer content after briefly restarting VniDrop. Finish ongoing transfers and stop active shares first. Received files and transfer history are not deleted.", + "fr": "Supprime le contenu de transfert en cache qui n’est pas utilisé par une réception en cours ou un partage actif. Les fichiers reçus et l’historique ne sont pas supprimés.", + "es": "Elimina el contenido de transferencia en caché que no esté siendo utilizado por una recepción en curso o un recurso compartido activo. Los archivos recibidos y el historial no se eliminan.", + "it": "Rimuove il contenuto dei trasferimenti memorizzato nella cache che non è usato da una ricezione in corso o da una condivisione attiva. I file ricevuti e la cronologia non vengono eliminati.", + "de": "Entfernt zwischengespeicherte Übertragungsinhalte, die nicht von einem laufenden Empfang oder einer aktiven Freigabe verwendet werden. Empfangene Dateien und der Übertragungsverlauf werden nicht gelöscht.", + "pt": "Remove conteúdo de transferência em cache que não esteja a ser utilizado por uma receção em curso ou partilha ativa. Os ficheiros recebidos e o histórico não são eliminados.", + "pl": "Usuwa zawartość transferów z pamięci podręcznej, która nie jest używana przez trwające odbieranie ani aktywne udostępnianie. Odebrane pliki i historia nie są usuwane.", + "nl": "Verwijdert overdrachtsinhoud uit de cache die niet wordt gebruikt door een lopende ontvangst of actieve share. Ontvangen bestanden en de overdrachtsgeschiedenis worden niet verwijderd.", + "ru": "Удаляет кэшированное содержимое передач, которое не используется текущим приёмом или активной раздачей. Полученные файлы и история передач не удаляются." + } + }, "storage_delete_transfers_caption": { "context": "Settings > Storage: caption under the destructive delete-all button.", "translations": { @@ -3704,6 +3738,23 @@ "ru": "Обновить" } }, + "storage_transfer_cache_cleared": { + "context": "Settings > Storage: confirmation that the transfer cache was cleared (KMP).", + "targets": [ + "kmp" + ], + "translations": { + "en": "Transfer cache cleared", + "fr": "Cache des transferts vidé", + "es": "Caché de transferencias borrada", + "it": "Cache trasferimenti svuotata", + "de": "Übertragungscache geleert", + "pt": "Cache de transferências limpa", + "pl": "Wyczyszczono pamięć podręczną transferów", + "nl": "Overdrachtscache gewist", + "ru": "Кэш передач очищен" + } + }, "storage_unavailable": { "context": "Settings > Storage: shown when usage couldn't be calculated yet.", "translations": { diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index 4a85909..63f206c 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -235,9 +235,12 @@ Wird bereinigt… Beende aktive Übertragungen, bevor du Speicher freigibst %1$s freigegeben + Übertragungscache leeren + Entfernt zwischengespeicherte Übertragungsinhalte, die nicht von einem laufenden Empfang oder einer aktiven Freigabe verwendet werden. Empfangene Dateien und der Übertragungsverlauf werden nicht gelöscht. Löscht deinen Sende- und Empfangsverlauf sowie die zwischengespeicherten Freigabeinhalte der App. Empfangene Dateien auf dem Datenträger bleiben erhalten. Entfernt temporäre Dateien und Reste früherer Übertragungen. Deine Übertragungen und empfangenen Dateien bleiben erhalten. Aktualisieren + Übertragungscache geleert Die Speichernutzung ist noch nicht verfügbar Auf diesem Gerät Speicher freigeben diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index 7d1c285..943f86c 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -235,9 +235,12 @@ Limpiando… Finaliza las transferencias activas antes de liberar espacio Se liberó %1$s + Borrar caché de transferencias + Elimina el contenido de transferencia en caché que no esté siendo utilizado por una recepción en curso o un recurso compartido activo. Los archivos recibidos y el historial no se eliminan. Borra tu historial de envíos y recepciones y el contenido compartido en caché de la app. Los archivos recibidos en el disco se conservan. Elimina los archivos temporales y los restos de transferencias anteriores. Tus transferencias y archivos recibidos se conservan. Actualizar + Caché de transferencias borrada El uso de almacenamiento aún no está disponible En este dispositivo Liberar espacio diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 41de788..4b015b4 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -235,9 +235,12 @@ Nettoyage… Terminez les transferts en cours avant de libérer de l\'espace %1$s libéré + Vider le cache des transferts + Supprime le contenu de transfert en cache qui n’est pas utilisé par une réception en cours ou un partage actif. Les fichiers reçus et l’historique ne sont pas supprimés. Efface votre historique d\'envois et de réceptions ainsi que le contenu de partage mis en cache par l\'app. Les fichiers reçus sur le disque sont conservés. Supprime les fichiers temporaires et les résidus des transferts précédents. Vos transferts et fichiers reçus sont conservés. Actualiser + Cache des transferts vidé L\'utilisation du stockage n\'est pas encore disponible Sur cet appareil Libérer de l\'espace diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index 463dc47..a6d5a16 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -235,9 +235,12 @@ Pulizia… Completa i trasferimenti attivi prima di liberare spazio Liberati %1$s + Svuota cache trasferimenti + Rimuove il contenuto dei trasferimenti memorizzato nella cache che non è usato da una ricezione in corso o da una condivisione attiva. I file ricevuti e la cronologia non vengono eliminati. Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall\'app. I file ricevuti sul disco vengono mantenuti. Rimuove i file temporanei e i residui dei trasferimenti precedenti. I tuoi trasferimenti e i file ricevuti vengono mantenuti. Aggiorna + Cache trasferimenti svuotata L\'utilizzo dello spazio non è ancora disponibile Su questo dispositivo Libera spazio diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index e5728ab..4d1b3e1 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -235,9 +235,12 @@ Opschonen… Voltooi actieve overdrachten voordat je ruimte vrijmaakt %1$s vrijgemaakt + Overdrachtscache wissen + Verwijdert overdrachtsinhoud uit de cache die niet wordt gebruikt door een lopende ontvangst of actieve share. Ontvangen bestanden en de overdrachtsgeschiedenis worden niet verwijderd. Wist je verzend- en ontvangstgeschiedenis en de gecachte deelinhoud van de app. Ontvangen bestanden op schijf blijven behouden. Verwijdert tijdelijke bestanden en resten van eerdere overdrachten. Je overdrachten en ontvangen bestanden blijven behouden. Vernieuwen + Overdrachtscache gewist Opslaggebruik is nog niet beschikbaar Op dit apparaat Ruimte vrijmaken diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index 60d814c..88d4675 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -235,9 +235,12 @@ Czyszczenie… Zakończ aktywne transfery przed zwolnieniem miejsca Zwolniono %1$s + Wyczyść pamięć podręczną transferów + Usuwa zawartość transferów z pamięci podręcznej, która nie jest używana przez trwające odbieranie ani aktywne udostępnianie. Odebrane pliki i historia nie są usuwane. Czyści historię wysyłania i odbierania oraz zapisane w pamięci podręcznej udostępniane treści. Odebrane pliki na dysku zostają zachowane. Usuwa pliki tymczasowe i pozostałości po wcześniejszych transferach. Twoje transfery i odebrane pliki zostają zachowane. Odśwież + Wyczyszczono pamięć podręczną transferów Wykorzystanie pamięci nie jest jeszcze dostępne Na tym urządzeniu Zwolnij miejsce diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index 3ffe0b0..9838306 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -235,9 +235,12 @@ A limpar… Conclui as transferências ativas antes de libertar espaço Libertado %1$s + Limpar cache de transferências + Remove conteúdo de transferência em cache que não esteja a ser utilizado por uma receção em curso ou partilha ativa. Os ficheiros recebidos e o histórico não são eliminados. Limpa o teu histórico de envios e receções e o conteúdo de partilha em cache da app. Os ficheiros recebidos no disco são mantidos. Remove ficheiros temporários e resíduos de transferências anteriores. As tuas transferências e ficheiros recebidos são mantidos. Atualizar + Cache de transferências limpa A utilização do armazenamento ainda não está disponível Neste dispositivo Libertar espaço diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index b1f2727..621f9f0 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -235,9 +235,12 @@ Очистка… Завершите активные передачи перед освобождением места Освобождено %1$s + Очистить кэш передач + Удаляет кэшированное содержимое передач, которое не используется текущим приёмом или активной раздачей. Полученные файлы и история передач не удаляются. Очищает историю отправки и получения и кэшированное содержимое общих ресурсов. Полученные файлы на диске сохраняются. Удаляет временные файлы и остатки прошлых передач. Ваши передачи и полученные файлы сохраняются. Обновить + Кэш передач очищен Данные об использовании хранилища пока недоступны На этом устройстве Освободить место diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index bd6f807..8730f32 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -235,9 +235,12 @@ Cleaning up… Finish active transfers before freeing up space Freed %1$s + Clear transfer cache + Removes cached transfer content after briefly restarting VniDrop. Finish ongoing transfers and stop active shares first. Received files and transfer history are not deleted. Clears your send and receive history and the app\'s cached share content. Received files on disk are kept. Removes temporary files and leftover trash from earlier transfers. Your transfers and received files are kept. Refresh + Transfer cache cleared Storage usage isn\'t available yet On this device Free up space From 98da43b122930d0025a0d5b7718f9f41bb8a0130 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:02:02 +0200 Subject: [PATCH 30/36] docs: make strings.json the documented source of truth for l10n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record in AGENTS.md that localization/strings.json is the single source of truth and the KMP XML + Apple xcstrings/L10n.swift are generated by the loc CLI and must never be hand-edited — a key present only in a generated file is dropped on the next regeneration (which is how the transfer-cache strings were lost in the merge). --- AGENTS.md | 11 +++++++++++ shared/AGENTS.md | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b09f4a9..dab6e35 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,15 @@ Domain docs (reference, do not paste into PRs): 8. **Every bug fix includes a regression test** at the lowest layer that catches it. 9. After code changes, run the **relevant** checks in [Build and test](#build-and-test) and fix failures before finishing. +10. **`localization/strings.json` is the single source of truth for all localized + strings.** The KMP Compose resources (`shared/src/commonMain/composeResources/ + values*/strings.xml`) and the Apple catalog + accessors + (`apple/VniDrop/Resources/Localizable.xcstrings`, `apple/VniDrop/Generated/ + L10n.swift`) are **generated** by the loc CLI (`cd localization && bun run + src/cli.ts generate`) — never hand-edit them. To add/change a string: edit + `strings.json` (set `targets` to `kmp`, `apple`, or omit for both), then + regenerate. A key referenced in code but only present in a generated file will + be silently dropped the next time generation runs. --- @@ -291,6 +300,8 @@ branch from updated `master`. - Flaky multi-minute sleeps in tests - Unsigned commits when signing is required - Force-push or secret commits without explicit user direction +- Hand-editing generated localization files (`values*/strings.xml`, + `Localizable.xcstrings`, `L10n.swift`) instead of `localization/strings.json` --- diff --git a/shared/AGENTS.md b/shared/AGENTS.md index 6add30a..ab70c02 100644 --- a/shared/AGENTS.md +++ b/shared/AGENTS.md @@ -32,7 +32,7 @@ lists, animation, accessibility: | Architecture | Keep **MVVM-style** ViewModels: immutable `*State`, `StateFlow`, **named methods**. Do not force MVI `onEvent` sealed hierarchies unless asked. | | Structure | Feature packages under `com.vnidrop.app.feature.*`; thin route/wiring + screen/composables. | | Theme | Only `LocalVniDropColors` / `VniDropThemeTokens` (`ui/theme/VniDropTheme.kt`). Brand primary light ≈ `#A855F7` (HSL 271, 91%, 65%). | -| Strings | CMP composeResources / `Res.string.*` — not Android `R` in `commonMain`. | +| Strings | CMP composeResources / `Res.string.*` — not Android `R` in `commonMain`. `values*/strings.xml` are **generated** from `localization/strings.json` (source of truth) via the loc CLI — add/edit keys there, never in the XML. | | DI | Follow existing `AppGraph` construction; no unprompted Hilt/Koin migration. | | Platform | `androidMain` / `jvmMain` for pickers, SAF, NFC/QR, and desktop integration. | | Dependencies | Before adding Jetpack/AndroidX to `commonMain`, verify multiplatform artifacts for all targets. | From b66cb8c1f145002657b047ec626070adae736978 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:28:52 +0200 Subject: [PATCH 31/36] fix(l10n): restore storage_clearing_transfer_cache dropped in merge Another key master referenced from Kotlin but kept only in the generated Compose XML, so regeneration dropped it. Verified exhaustively this time: every Res.string.* reference in shared/src/commonMain/kotlin now resolves against the regenerated values/strings.xml, so no further keys are missing. --- localization/strings.json | 17 +++++++++++++++++ .../composeResources/values-de/strings.xml | 1 + .../composeResources/values-es/strings.xml | 1 + .../composeResources/values-fr/strings.xml | 1 + .../composeResources/values-it/strings.xml | 1 + .../composeResources/values-nl/strings.xml | 1 + .../composeResources/values-pl/strings.xml | 1 + .../composeResources/values-pt/strings.xml | 1 + .../composeResources/values-ru/strings.xml | 1 + .../composeResources/values/strings.xml | 1 + 10 files changed, 26 insertions(+) diff --git a/localization/strings.json b/localization/strings.json index 10af452..9b0c084 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -3696,6 +3696,23 @@ "ru": "Удаляет кэшированное содержимое передач, которое не используется текущим приёмом или активной раздачей. Полученные файлы и история передач не удаляются." } }, + "storage_clearing_transfer_cache": { + "context": "Settings > Storage: clear-transfer-cache button while clearing is in progress (KMP).", + "targets": [ + "kmp" + ], + "translations": { + "en": "Clearing cache…", + "fr": "Vidage du cache…", + "es": "Borrando caché…", + "it": "Svuotamento cache…", + "de": "Cache wird geleert…", + "pt": "A limpar cache…", + "pl": "Czyszczenie pamięci podręcznej…", + "nl": "Cache wissen…", + "ru": "Очистка кэша…" + } + }, "storage_delete_transfers_caption": { "context": "Settings > Storage: caption under the destructive delete-all button.", "translations": { diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index 63f206c..a1a464e 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -237,6 +237,7 @@ %1$s freigegeben Übertragungscache leeren Entfernt zwischengespeicherte Übertragungsinhalte, die nicht von einem laufenden Empfang oder einer aktiven Freigabe verwendet werden. Empfangene Dateien und der Übertragungsverlauf werden nicht gelöscht. + Cache wird geleert… Löscht deinen Sende- und Empfangsverlauf sowie die zwischengespeicherten Freigabeinhalte der App. Empfangene Dateien auf dem Datenträger bleiben erhalten. Entfernt temporäre Dateien und Reste früherer Übertragungen. Deine Übertragungen und empfangenen Dateien bleiben erhalten. Aktualisieren diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index 943f86c..2e3c80e 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -237,6 +237,7 @@ Se liberó %1$s Borrar caché de transferencias Elimina el contenido de transferencia en caché que no esté siendo utilizado por una recepción en curso o un recurso compartido activo. Los archivos recibidos y el historial no se eliminan. + Borrando caché… Borra tu historial de envíos y recepciones y el contenido compartido en caché de la app. Los archivos recibidos en el disco se conservan. Elimina los archivos temporales y los restos de transferencias anteriores. Tus transferencias y archivos recibidos se conservan. Actualizar diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 4b015b4..1583d2d 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -237,6 +237,7 @@ %1$s libéré Vider le cache des transferts Supprime le contenu de transfert en cache qui n’est pas utilisé par une réception en cours ou un partage actif. Les fichiers reçus et l’historique ne sont pas supprimés. + Vidage du cache… Efface votre historique d\'envois et de réceptions ainsi que le contenu de partage mis en cache par l\'app. Les fichiers reçus sur le disque sont conservés. Supprime les fichiers temporaires et les résidus des transferts précédents. Vos transferts et fichiers reçus sont conservés. Actualiser diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index a6d5a16..af85d35 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -237,6 +237,7 @@ Liberati %1$s Svuota cache trasferimenti Rimuove il contenuto dei trasferimenti memorizzato nella cache che non è usato da una ricezione in corso o da una condivisione attiva. I file ricevuti e la cronologia non vengono eliminati. + Svuotamento cache… Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall\'app. I file ricevuti sul disco vengono mantenuti. Rimuove i file temporanei e i residui dei trasferimenti precedenti. I tuoi trasferimenti e i file ricevuti vengono mantenuti. Aggiorna diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index 4d1b3e1..93fe872 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -237,6 +237,7 @@ %1$s vrijgemaakt Overdrachtscache wissen Verwijdert overdrachtsinhoud uit de cache die niet wordt gebruikt door een lopende ontvangst of actieve share. Ontvangen bestanden en de overdrachtsgeschiedenis worden niet verwijderd. + Cache wissen… Wist je verzend- en ontvangstgeschiedenis en de gecachte deelinhoud van de app. Ontvangen bestanden op schijf blijven behouden. Verwijdert tijdelijke bestanden en resten van eerdere overdrachten. Je overdrachten en ontvangen bestanden blijven behouden. Vernieuwen diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index 88d4675..aa4c09f 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -237,6 +237,7 @@ Zwolniono %1$s Wyczyść pamięć podręczną transferów Usuwa zawartość transferów z pamięci podręcznej, która nie jest używana przez trwające odbieranie ani aktywne udostępnianie. Odebrane pliki i historia nie są usuwane. + Czyszczenie pamięci podręcznej… Czyści historię wysyłania i odbierania oraz zapisane w pamięci podręcznej udostępniane treści. Odebrane pliki na dysku zostają zachowane. Usuwa pliki tymczasowe i pozostałości po wcześniejszych transferach. Twoje transfery i odebrane pliki zostają zachowane. Odśwież diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index 9838306..1c17822 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -237,6 +237,7 @@ Libertado %1$s Limpar cache de transferências Remove conteúdo de transferência em cache que não esteja a ser utilizado por uma receção em curso ou partilha ativa. Os ficheiros recebidos e o histórico não são eliminados. + A limpar cache… Limpa o teu histórico de envios e receções e o conteúdo de partilha em cache da app. Os ficheiros recebidos no disco são mantidos. Remove ficheiros temporários e resíduos de transferências anteriores. As tuas transferências e ficheiros recebidos são mantidos. Atualizar diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index 621f9f0..08e9f83 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -237,6 +237,7 @@ Освобождено %1$s Очистить кэш передач Удаляет кэшированное содержимое передач, которое не используется текущим приёмом или активной раздачей. Полученные файлы и история передач не удаляются. + Очистка кэша… Очищает историю отправки и получения и кэшированное содержимое общих ресурсов. Полученные файлы на диске сохраняются. Удаляет временные файлы и остатки прошлых передач. Ваши передачи и полученные файлы сохраняются. Обновить diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index 8730f32..ed6c6dd 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -237,6 +237,7 @@ Freed %1$s Clear transfer cache Removes cached transfer content after briefly restarting VniDrop. Finish ongoing transfers and stop active shares first. Received files and transfer history are not deleted. + Clearing cache… Clears your send and receive history and the app\'s cached share content. Received files on disk are kept. Removes temporary files and leftover trash from earlier transfers. Your transfers and received files are kept. Refresh From 3abd4d0cfd54fe928270dc5f0349cbe4076c6822 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:42:40 +0200 Subject: [PATCH 32/36] build(apple): flag raw string literals in SwiftUI initializers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The typed-resource rules missed a bare string literal passed as the leading arg of a view initializer (e.g. Label("send_stop_sharing", …)), which is an implicit LocalizedStringKey. Add a rule covering Text/Label/Button/Section/Picker/etc. (empty labels allowed). Fixes the two dynamic-content Text sites it surfaced by switching them to Text(verbatim:). --- apple/.swiftlint.yml | 7 +++++++ apple/VniDrop/Features/Send/TransferDetailsView.swift | 2 +- apple/VniDrop/UI/Components/Components.swift | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apple/.swiftlint.yml b/apple/.swiftlint.yml index 1614011..b9a3f8e 100644 --- a/apple/.swiftlint.yml +++ b/apple/.swiftlint.yml @@ -25,3 +25,10 @@ custom_rules: regex: 'system(Name|Image):\s*"' message: "Use SFSafeSymbols: Image(systemSymbol:) or systemSymbol:." severity: warning + raw_swiftui_string_literal: + name: "Raw SwiftUI string" + # A non-empty string literal as the leading arg of a view initializer is an + # implicit LocalizedStringKey. Empty labels (e.g. Picker("", …)) are allowed. + regex: '\b(Text|Label|Button|Toggle|Link|NavigationLink|Section|Picker|Stepper|TextField|SecureField|DisclosureGroup|Menu|GroupBox)\("[^"]' + message: "Pass a typed L10n.* accessor (or Text(verbatim:)), not a raw string literal." + severity: warning diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index 534133a..b0358fc 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -112,7 +112,7 @@ private struct DetailDestination: View { } Spacer() if count > 0 { - Text("\(count)") + Text(verbatim: "\(count)") .font(.footnote) .foregroundStyle(.secondary) } diff --git a/apple/VniDrop/UI/Components/Components.swift b/apple/VniDrop/UI/Components/Components.swift index eb0dc4e..4fb2c0f 100644 --- a/apple/VniDrop/UI/Components/Components.swift +++ b/apple/VniDrop/UI/Components/Components.swift @@ -42,7 +42,7 @@ struct ProgressRow: View { label.font(.subheadline).lineLimit(1) Spacer() if let progress { - Text("\(Int(progress * 100))%").font(.caption).foregroundStyle(.secondary) + Text(verbatim: "\(Int(progress * 100))%").font(.caption).foregroundStyle(.secondary) } } if let detail { From 319af6f2dedb0486f0bdd77126a2a165404a17e5 Mon Sep 17 00:00:00 2001 From: cdricms <36056008+cdricms@users.noreply.github.com> Date: Fri, 24 Jul 2026 19:58:29 +0200 Subject: [PATCH 33/36] fix(l10n): align notification/storage descriptions with KMP behavior The merge kept this branch's reworded notifications_description and storage_delete_transfers_description over master's, but the merged KMP code is master's, so its FoundationComposeTest assertions (and the shipped KMP copy) expect master's wording. Restore both to master's committed text (pulling the storage one from master's XML, since master's own strings.json was stale for it). Verified the two failing KMP Compose tests pass locally. --- localization/strings.json | 36 +++++++++---------- .../composeResources/values-de/strings.xml | 4 +-- .../composeResources/values-es/strings.xml | 4 +-- .../composeResources/values-fr/strings.xml | 4 +-- .../composeResources/values-it/strings.xml | 4 +-- .../composeResources/values-nl/strings.xml | 4 +-- .../composeResources/values-pl/strings.xml | 4 +-- .../composeResources/values-pt/strings.xml | 4 +-- .../composeResources/values-ru/strings.xml | 4 +-- .../composeResources/values/strings.xml | 4 +-- 10 files changed, 36 insertions(+), 36 deletions(-) diff --git a/localization/strings.json b/localization/strings.json index 9b0c084..072ecce 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -1771,15 +1771,15 @@ "notifications_description": { "context": "Settings > Notifications: explanation of what notifications are used for.", "translations": { - "en": "Get notified about transfer activity while VniDrop is in the background.", - "fr": "Soyez averti de l’activité des transferts lorsque VniDrop est en arrière-plan.", - "es": "Reciba avisos sobre la actividad de las transferencias cuando VniDrop está en segundo plano.", - "it": "Ricevi avvisi sull’attività dei trasferimenti quando VniDrop è in background.", - "de": "Werden Sie über Übertragungsaktivitäten benachrichtigt, während VniDrop im Hintergrund läuft.", - "pt": "Seja notificado sobre a atividade das transferências enquanto o VniDrop está em segundo plano.", - "pl": "Otrzymuj powiadomienia o aktywności transferów, gdy VniDrop działa w tle.", - "nl": "Ontvang meldingen over overdrachtsactiviteit terwijl VniDrop op de achtergrond draait.", - "ru": "Получайте уведомления об активности передач, пока VniDrop работает в фоне." + "en": "Get notified about new receive requests while VniDrop is in the background.", + "fr": "Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan.", + "es": "Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano.", + "it": "Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background.", + "de": "Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft.", + "pt": "Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano.", + "pl": "Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle.", + "nl": "Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait.", + "ru": "Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне." } }, "notifications_enabled_message": { @@ -3831,15 +3831,15 @@ "storage_delete_transfers_description": { "context": "Settings > Storage: confirmation body for deleting all transfer records.", "translations": { - "en": "This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone.", - "fr": "Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible.", - "es": "Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer.", - "it": "Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata.", - "de": "Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden.", - "pt": "Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada.", - "pl": "Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć.", - "nl": "Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt.", - "ru": "Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить." + "en": "This clears all sent and received transfer records from your history and immediately reclaims unused transfer cache. Ongoing transfers and received files are not deleted. This can’t be undone.", + "fr": "Cela efface tous les transferts envoyés et reçus de l’historique et libère immédiatement le cache inutilisé. Les transferts en cours et les fichiers reçus ne sont pas supprimés. Cette action est irréversible.", + "es": "Esto borra del historial todos los registros de transferencias enviadas y recibidas y libera inmediatamente la caché de transferencia no utilizada. Las transferencias en curso y los archivos recibidos no se eliminan. Esto no se puede deshacer.", + "it": "Elimina dalla cronologia tutti i trasferimenti inviati e ricevuti e libera immediatamente la cache inutilizzata. I trasferimenti in corso e i file ricevuti non vengono eliminati. Questa azione non può essere annullata.", + "de": "Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht und nicht benötigter Übertragungscache sofort freigegeben. Laufende Übertragungen und empfangene Dateien werden nicht gelöscht. Dies kann nicht rückgängig gemacht werden.", + "pt": "Isto elimina do histórico todas as transferências enviadas e recebidas e liberta imediatamente a cache não utilizada. As transferências em curso e os ficheiros recebidos não são eliminados. Esta ação não pode ser anulada.", + "pl": "Usuwa z historii wszystkie wysłane i odebrane transfery oraz natychmiast zwalnia nieużywaną pamięć podręczną. Trwające transfery i odebrane pliki nie są usuwane. Tej operacji nie można cofnąć.", + "nl": "Hiermee worden alle verzonden en ontvangen overdrachten uit de geschiedenis gewist en wordt ongebruikte overdrachtscache direct vrijgemaakt. Lopende overdrachten en ontvangen bestanden worden niet verwijderd. Dit kan niet ongedaan worden gemaakt.", + "ru": "Это удалит из истории все отправленные и полученные передачи и немедленно освободит неиспользуемый кэш. Текущие передачи и полученные файлы не удаляются. Это действие нельзя отменить." } }, "storage_app_data": { diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index a1a464e..34946ab 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -118,7 +118,7 @@ Senden Einstellungen Netzwerk - Werden Sie über Übertragungsaktivitäten benachrichtigt, während VniDrop im Hintergrund läuft. + Werden Sie über neue Empfangsanfragen benachrichtigt, während VniDrop im Hintergrund läuft. Mitteilungen aktiviert. Mitteilungen erlauben Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren. @@ -246,7 +246,7 @@ Auf diesem Gerät Speicher freigeben Alle Übertragungen löschen - Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht. Ihre empfangenen Dateien werden nicht gelöscht. Nicht mehr benötigte zwischengespeicherte freigegebene Inhalte werden automatisch bereinigt; dies kann etwas dauern. Dies kann nicht rückgängig gemacht werden. + Dadurch werden alle Datensätze gesendeter und empfangener Übertragungen aus Ihrem Verlauf gelöscht und nicht benötigter Übertragungscache sofort freigegeben. Laufende Übertragungen und empfangene Dateien werden nicht gelöscht. Dies kann nicht rückgängig gemacht werden. App-Daten Wird gelöscht… Übertragungsdaten umfassen Ihren Verlauf und zwischengespeicherte Inhalte aktiver Freigaben. Nicht mehr benötigter Cache wird nach dem Löschen der Datensätze automatisch bereinigt; dies kann etwas dauern. Empfangene Dateien werden für diese Übersicht erfasst, aber hier niemals gelöscht. diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index 2e3c80e..a663eeb 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -118,7 +118,7 @@ Enviar Ajustes Red - Reciba avisos sobre la actividad de las transferencias cuando VniDrop está en segundo plano. + Reciba avisos sobre nuevas solicitudes de recepción cuando VniDrop está en segundo plano. Notificaciones activadas. Permitir notificaciones Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes. @@ -246,7 +246,7 @@ En este dispositivo Liberar espacio Eliminar todas las transferencias - Esto borra de su historial todos los registros de transferencias enviadas y recibidas. Sus archivos recibidos no se eliminan. El contenido compartido en caché que ya no se necesita se recupera automáticamente, lo que puede tardar un poco. Esto no se puede deshacer. + Esto borra del historial todos los registros de transferencias enviadas y recibidas y libera inmediatamente la caché de transferencia no utilizada. Las transferencias en curso y los archivos recibidos no se eliminan. Esto no se puede deshacer. Datos de la aplicación Eliminando… Los datos de transferencia incluyen su historial y el contenido en caché de los recursos compartidos activos. La caché innecesaria se recupera automáticamente tras eliminar los registros, lo que puede tardar un poco. Los archivos recibidos se registran para este resumen, pero nunca se eliminan aquí. diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 1583d2d..884f55f 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -118,7 +118,7 @@ Envoyer Réglages Réseau - Soyez averti de l’activité des transferts lorsque VniDrop est en arrière-plan. + Soyez averti des nouvelles demandes de réception lorsque VniDrop est en arrière-plan. Notifications activées. Autoriser les notifications Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages. @@ -246,7 +246,7 @@ Sur cet appareil Libérer de l\'espace Supprimer tous les transferts - Cela efface de votre historique tous les enregistrements de transferts envoyés et reçus. Vos fichiers reçus ne sont pas supprimés. Le contenu partagé mis en cache qui n’est plus nécessaire est récupéré automatiquement, ce qui peut prendre un peu de temps. Cette action est irréversible. + Cela efface tous les transferts envoyés et reçus de l’historique et libère immédiatement le cache inutilisé. Les transferts en cours et les fichiers reçus ne sont pas supprimés. Cette action est irréversible. Données de l’app Suppression… Les données de transfert comprennent votre historique et le contenu mis en cache pour les partages actifs. Le cache inutile est récupéré automatiquement après la suppression des enregistrements, ce qui peut prendre un peu de temps. Les fichiers reçus sont suivis pour ce récapitulatif, mais ne sont jamais supprimés ici. diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index af85d35..79c209e 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -118,7 +118,7 @@ Invia Impostazioni Rete - Ricevi avvisi sull’attività dei trasferimenti quando VniDrop è in background. + Ricevi avvisi sulle nuove richieste di ricezione quando VniDrop è in background. Notifiche attivate. Consenti le notifiche Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni. @@ -246,7 +246,7 @@ Su questo dispositivo Libera spazio Elimina tutti i trasferimenti - Questo cancella dalla cronologia tutti i record dei trasferimenti inviati e ricevuti. I file ricevuti non vengono eliminati. Il contenuto condiviso nella cache che non serve più viene recuperato automaticamente, operazione che può richiedere un po’ di tempo. Questa azione non può essere annullata. + Elimina dalla cronologia tutti i trasferimenti inviati e ricevuti e libera immediatamente la cache inutilizzata. I trasferimenti in corso e i file ricevuti non vengono eliminati. Questa azione non può essere annullata. Dati dell’app Eliminazione… I dati di trasferimento includono la cronologia e il contenuto nella cache per le condivisioni attive. La cache non necessaria viene recuperata automaticamente dopo la rimozione dei record, operazione che può richiedere un po’ di tempo. I file ricevuti vengono monitorati per questo riepilogo, ma non sono mai eliminati qui. diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index 93fe872..c97faf5 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -118,7 +118,7 @@ Versturen Instellingen Netwerk - Ontvang meldingen over overdrachtsactiviteit terwijl VniDrop op de achtergrond draait. + Ontvang meldingen over nieuwe ontvangstverzoeken terwijl VniDrop op de achtergrond draait. Meldingen ingeschakeld. Meldingen toestaan Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen. @@ -246,7 +246,7 @@ Op dit apparaat Ruimte vrijmaken Alle overdrachten verwijderen - Hiermee worden alle records van verzonden en ontvangen overdrachten uit uw geschiedenis gewist. Uw ontvangen bestanden worden niet verwijderd. Gedeelde inhoud in de cache die niet meer nodig is, wordt automatisch opgeruimd; dit kan enige tijd duren. Dit kan niet ongedaan worden gemaakt. + Hiermee worden alle verzonden en ontvangen overdrachten uit de geschiedenis gewist en wordt ongebruikte overdrachtscache direct vrijgemaakt. Lopende overdrachten en ontvangen bestanden worden niet verwijderd. Dit kan niet ongedaan worden gemaakt. Appgegevens Verwijderen… Overdrachtsgegevens omvatten uw geschiedenis en inhoud in de cache voor actieve shares. Onnodige cache wordt automatisch opgeruimd nadat overdrachtsrecords zijn verwijderd; dit kan enige tijd duren. Ontvangen bestanden worden voor dit overzicht bijgehouden, maar hier nooit verwijderd. diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index aa4c09f..33ae2cd 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -118,7 +118,7 @@ Wyślij Ustawienia Sieć - Otrzymuj powiadomienia o aktywności transferów, gdy VniDrop działa w tle. + Otrzymuj powiadomienia o nowych prośbach o odbiór, gdy VniDrop działa w tle. Powiadomienia włączone. Zezwól na powiadomienia Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach. @@ -246,7 +246,7 @@ Na tym urządzeniu Zwolnij miejsce Usuń wszystkie transfery - Spowoduje to usunięcie z historii wszystkich rekordów wysłanych i odebranych transferów. Odebrane pliki nie zostaną usunięte. Niepotrzebna już zawartość udostępniona w pamięci podręcznej jest odzyskiwana automatycznie, co może chwilę potrwać. Tej operacji nie można cofnąć. + Usuwa z historii wszystkie wysłane i odebrane transfery oraz natychmiast zwalnia nieużywaną pamięć podręczną. Trwające transfery i odebrane pliki nie są usuwane. Tej operacji nie można cofnąć. Dane aplikacji Usuwanie… Dane transferu obejmują historię oraz zawartość w pamięci podręcznej dla aktywnych udostępnień. Niepotrzebna pamięć podręczna jest odzyskiwana automatycznie po usunięciu rekordów, co może chwilę potrwać. Odebrane pliki są śledzone na potrzeby tego podsumowania, ale nigdy nie są tu usuwane. diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index 1c17822..8f7cd15 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -118,7 +118,7 @@ Enviar Definições Rede - Seja notificado sobre a atividade das transferências enquanto o VniDrop está em segundo plano. + Seja notificado sobre novos pedidos de receção enquanto o VniDrop está em segundo plano. Notificações ativadas. Permitir notificações As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições. @@ -246,7 +246,7 @@ Neste dispositivo Libertar espaço Eliminar todas as transferências - Isto elimina do histórico todos os registos de transferências enviadas e recebidas. Os ficheiros recebidos não são eliminados. O conteúdo partilhado em cache que já não é necessário é recuperado automaticamente, o que pode demorar algum tempo. Esta ação não pode ser anulada. + Isto elimina do histórico todas as transferências enviadas e recebidas e liberta imediatamente a cache não utilizada. As transferências em curso e os ficheiros recebidos não são eliminados. Esta ação não pode ser anulada. Dados da aplicação A eliminar… Os dados de transferência incluem o histórico e o conteúdo em cache das partilhas ativas. A cache desnecessária é recuperada automaticamente após a remoção dos registos, o que pode demorar algum tempo. Os ficheiros recebidos são acompanhados para este resumo, mas nunca são eliminados aqui. diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index 08e9f83..12bc011 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -118,7 +118,7 @@ Отправить Настройки Сеть - Получайте уведомления об активности передач, пока VniDrop работает в фоне. + Получайте уведомления о новых запросах на получение, пока VniDrop работает в фоне. Уведомления включены. Разрешить уведомления Уведомления отключены для VniDrop. Вы можете включить их в Настройках. @@ -246,7 +246,7 @@ На этом устройстве Освободить место Удалить все передачи - Это удалит из истории все записи об отправленных и полученных передачах. Полученные файлы не удаляются. Кэшированное общее содержимое, которое больше не требуется, освобождается автоматически; это может занять некоторое время. Это действие нельзя отменить. + Это удалит из истории все отправленные и полученные передачи и немедленно освободит неиспользуемый кэш. Текущие передачи и полученные файлы не удаляются. Это действие нельзя отменить. Данные приложения Удаление… Данные передачи включают историю и кэшированное содержимое активных раздач. Ненужный кэш освобождается автоматически после удаления записей; это может занять некоторое время. Полученные файлы учитываются в этой сводке, но никогда не удаляются здесь. diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index ed6c6dd..c3c27eb 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -118,7 +118,7 @@ Send Settings Network - Get notified about transfer activity while VniDrop is in the background. + Get notified about new receive requests while VniDrop is in the background. Notifications enabled. Allow notifications Notifications are turned off for VniDrop. You can enable them in Settings. @@ -246,7 +246,7 @@ On this device Free up space Delete all transfers - This clears all sent and received transfer records from your history. Your received files are not deleted. Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. This can’t be undone. + This clears all sent and received transfer records from your history and immediately reclaims unused transfer cache. Ongoing transfers and received files are not deleted. This can’t be undone. App data Deleting… Transfer data includes your history and cached content for active shares. Unneeded cache is reclaimed automatically after transfer records are removed, which may take a little time. Received files are tracked for this summary but are never deleted here. From c81cb7c8b60e0532a6805d9862e47c8c04ed16e8 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Fri, 24 Jul 2026 19:58:33 +0200 Subject: [PATCH 34/36] feat(shared): align non-Apple UX with Apple --- localization/strings.json | 44 ++-- .../app/core/FileSystemService.android.kt | 24 +++ .../composeResources/values-de/strings.xml | 11 + .../composeResources/values-es/strings.xml | 11 + .../composeResources/values-fr/strings.xml | 11 + .../composeResources/values-it/strings.xml | 11 + .../composeResources/values-nl/strings.xml | 11 + .../composeResources/values-pl/strings.xml | 11 + .../composeResources/values-pt/strings.xml | 11 + .../composeResources/values-ru/strings.xml | 11 + .../composeResources/values/strings.xml | 11 + .../commonMain/kotlin/com/vnidrop/app/App.kt | 38 ++++ .../kotlin/com/vnidrop/app/AppGraph.kt | 9 + .../com/vnidrop/app/core/FileSystemService.kt | 2 + .../app/feature/receive/ReceiveScreen.kt | 2 +- .../vnidrop/app/feature/send/SendCatalog.kt | 85 +++++++- .../com/vnidrop/app/feature/send/SendRoute.kt | 8 +- .../vnidrop/app/feature/send/SendScreen.kt | 12 +- .../vnidrop/app/feature/send/SendViewModel.kt | 28 ++- .../app/feature/send/TransferComposer.kt | 54 +++-- .../app/feature/send/TransferDetails.kt | 50 ++--- .../app/feature/settings/SettingsRoute.kt | 2 + .../app/feature/settings/SettingsScreen.kt | 10 + .../app/feature/settings/SettingsViewModel.kt | 47 ++++- .../app/feature/settings/StorageSettings.kt | 55 ++++- .../TransferNotificationCoordinator.kt | 190 ++++++++++++++++++ .../app/ui/feedback/UiMessageController.kt | 2 +- .../app/ui/feedback/VniDropSnackbarHost.kt | 2 +- .../com/vnidrop/app/feature/ViewModelsTest.kt | 62 ++++++ .../TransferNotificationCoordinatorTest.kt | 72 +++++++ .../kotlin/com/vnidrop/app/support/Fakes.kt | 13 +- .../vnidrop/app/core/FileSystemService.jvm.kt | 35 ++++ .../vnidrop/app/core/FileSystemServiceTest.kt | 30 +++ .../vnidrop/app/ui/FoundationComposeTest.kt | 18 +- 34 files changed, 902 insertions(+), 91 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinator.kt create mode 100644 shared/src/commonTest/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinatorTest.kt diff --git a/localization/strings.json b/localization/strings.json index 9b0c084..57fdf8f 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -828,6 +828,20 @@ "ru": "Назад" } }, + "button_more_actions": { + "context": "Accessibility label for a button that opens more actions for an item.", + "translations": { + "en": "More actions", + "fr": "Plus d’actions", + "es": "Más acciones", + "it": "Altre azioni", + "de": "Weitere Aktionen", + "pt": "Mais ações", + "pl": "Więcej działań", + "nl": "Meer acties", + "ru": "Другие действия" + } + }, "button_cancel": { "context": "Button: cancel the current action or dialog.", "translations": { @@ -1826,9 +1840,6 @@ }, "notifications_receive_completed_body": { "context": "Notification body shown when an incoming transfer finishes downloading. {transferName} = transfer name.", - "targets": [ - "apple" - ], "args": [ { "name": "transferName", @@ -1849,9 +1860,6 @@ }, "notifications_receive_completed_title": { "context": "Notification title shown when an incoming transfer finishes downloading.", - "targets": [ - "apple" - ], "translations": { "en": "Download complete", "fr": "Téléchargement terminé", @@ -1866,9 +1874,6 @@ }, "notifications_receive_failed_body": { "context": "Notification body shown when an incoming transfer fails. {transferName} = transfer name.", - "targets": [ - "apple" - ], "args": [ { "name": "transferName", @@ -1889,9 +1894,6 @@ }, "notifications_receive_failed_title": { "context": "Notification title shown when an incoming transfer fails.", - "targets": [ - "apple" - ], "translations": { "en": "Download failed", "fr": "Échec du téléchargement", @@ -1906,9 +1908,6 @@ }, "notifications_receiver_completed_body": { "context": "Notification body shown to the sender when a receiver finishes downloading a shared transfer. {receiver} = receiver name, {transferName} = transfer name.", - "targets": [ - "apple" - ], "args": [ { "name": "receiver", @@ -1933,9 +1932,6 @@ }, "notifications_receiver_completed_title": { "context": "Notification title shown to the sender when a receiver finishes downloading a shared transfer.", - "targets": [ - "apple" - ], "translations": { "en": "Transfer received", "fr": "Transfert reçu", @@ -1950,9 +1946,6 @@ }, "notifications_receiver_failed_body": { "context": "Notification body shown to the sender when a receiver's download fails. {receiver} = receiver name, {transferName} = transfer name.", - "targets": [ - "apple" - ], "args": [ { "name": "receiver", @@ -1977,9 +1970,6 @@ }, "notifications_receiver_failed_title": { "context": "Notification title shown to the sender when a receiver's download fails.", - "targets": [ - "apple" - ], "translations": { "en": "Delivery failed", "fr": "Échec de l'envoi", @@ -1994,9 +1984,6 @@ }, "notifications_send_failed_body": { "context": "Notification body shown to the sender when a shared transfer fails. {transferName} = transfer name.", - "targets": [ - "apple" - ], "args": [ { "name": "transferName", @@ -2017,9 +2004,6 @@ }, "notifications_send_failed_title": { "context": "Notification title shown to the sender when a shared transfer fails.", - "targets": [ - "apple" - ], "translations": { "en": "Sharing failed", "fr": "Échec du partage", diff --git a/shared/src/androidMain/kotlin/com/vnidrop/app/core/FileSystemService.android.kt b/shared/src/androidMain/kotlin/com/vnidrop/app/core/FileSystemService.android.kt index 9bb4d11..02d9d2c 100644 --- a/shared/src/androidMain/kotlin/com/vnidrop/app/core/FileSystemService.android.kt +++ b/shared/src/androidMain/kotlin/com/vnidrop/app/core/FileSystemService.android.kt @@ -105,6 +105,30 @@ private class AndroidFileSystemService( override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = directorySize(context.cacheDir) + override suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong { + var reclaimed = 0UL + context.cacheDir.listFiles().orEmpty().forEach { entry -> + val size = if (entry.isDirectory) directorySize(entry) else entry.length().coerceAtLeast(0L).toULong() + if (entry.deleteRecursively()) reclaimed += size + } + val appDataRoot = File(appDataDir) + val appDataIsOwned = runCatching { + val appDataPath = appDataRoot.canonicalPath + val filesPath = context.filesDir.canonicalPath + appDataPath == filesPath || appDataPath.startsWith(filesPath + File.separator) + }.getOrDefault(false) + if (appDataIsOwned) { + appDataRoot.walkTopDown() + .filter { it.isDirectory && it.name == ".Trash" } + .toList() + .forEach { trash -> + val size = directorySize(trash) + if (trash.deleteRecursively()) reclaimed += size + } + } + return reclaimed + } + override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = when (folder.kind) { ReceiveFolderKind.AndroidPublicDownloads -> AndroidMediaStoreDownloadsSink(context) diff --git a/shared/src/commonMain/composeResources/values-de/strings.xml b/shared/src/commonMain/composeResources/values-de/strings.xml index a1a464e..7ee3fb1 100644 --- a/shared/src/commonMain/composeResources/values-de/strings.xml +++ b/shared/src/commonMain/composeResources/values-de/strings.xml @@ -55,6 +55,7 @@ Was ist passiert? Genehmigen Zurück + Weitere Aktionen Abbrechen Abbrechen Dateien ändern @@ -122,6 +123,16 @@ Mitteilungen aktiviert. Mitteilungen erlauben Mitteilungen sind für VniDrop deaktiviert. Sie können sie in den Einstellungen aktivieren. + „%1$s“ wurde vollständig heruntergeladen. + Download abgeschlossen + „%1$s“ konnte nicht empfangen werden. + Download fehlgeschlagen + %1$s hat „%2$s“ vollständig empfangen. + Übertragung empfangen + %1$s konnte „%2$s“ nicht empfangen + Übertragung fehlgeschlagen + „%1$s“ konnte nicht geteilt werden. + Freigabe fehlgeschlagen Die Mitteilungseinstellungen konnten nicht geöffnet werden. Mitteilungen Mitteilungen sind auf diesem Gerät nicht verfügbar. diff --git a/shared/src/commonMain/composeResources/values-es/strings.xml b/shared/src/commonMain/composeResources/values-es/strings.xml index 2e3c80e..8d3ebfb 100644 --- a/shared/src/commonMain/composeResources/values-es/strings.xml +++ b/shared/src/commonMain/composeResources/values-es/strings.xml @@ -55,6 +55,7 @@ ¿Qué ocurrió? Aprobar Atrás + Más acciones Cancelar Cancelar Cambiar archivos @@ -122,6 +123,16 @@ Notificaciones activadas. Permitir notificaciones Las notificaciones están desactivadas para VniDrop. Puede activarlas en Ajustes. + «%1$s» terminó de descargarse. + Descarga completada + No se pudo recibir «%1$s». + Error en la descarga + %1$s terminó de recibir «%2$s». + Transferencia recibida + %1$s no pudo recibir «%2$s» + Error en la entrega + No se pudo compartir «%1$s». + Error al compartir No se pudieron abrir los ajustes de notificaciones. Notificaciones Las notificaciones no están disponibles en este dispositivo. diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index 1583d2d..ac3dc1a 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -55,6 +55,7 @@ Que s’est-il passé ? Approuver Retour + Plus d’actions Annuler Annuler Modifier les fichiers @@ -122,6 +123,16 @@ Notifications activées. Autoriser les notifications Les notifications sont désactivées pour VniDrop. Vous pouvez les activer dans les Réglages. + « %1$s » a fini de se télécharger. + Téléchargement terminé + « %1$s » n’a pas pu être reçu. + Échec du téléchargement + %1$s a fini de recevoir « %2$s ». + Transfert reçu + %1$s n\'a pas pu recevoir « %2$s » + Échec de l\'envoi + « %1$s » n’a pas pu être partagé. + Échec du partage Impossible d’ouvrir les réglages de notifications. Notifications Les notifications ne sont pas disponibles sur cet appareil. diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index af85d35..1ec70da 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -55,6 +55,7 @@ Cosa è accaduto? Approva Indietro + Altre azioni Annulla Annulla Cambia file @@ -122,6 +123,16 @@ Notifiche attivate. Consenti le notifiche Le notifiche sono disattivate per VniDrop. Può attivarle in Impostazioni. + «%1$s» è stato scaricato. + Download completato + Impossibile ricevere «%1$s». + Download non riuscito + %1$s ha finito di ricevere «%2$s». + Trasferimento ricevuto + %1$s non ha potuto ricevere “%2$s” + Consegna non riuscita + Impossibile condividere «%1$s». + Condivisione non riuscita Impossibile aprire le impostazioni delle notifiche. Notifiche Le notifiche non sono disponibili su questo dispositivo. diff --git a/shared/src/commonMain/composeResources/values-nl/strings.xml b/shared/src/commonMain/composeResources/values-nl/strings.xml index 93fe872..7033c9c 100644 --- a/shared/src/commonMain/composeResources/values-nl/strings.xml +++ b/shared/src/commonMain/composeResources/values-nl/strings.xml @@ -55,6 +55,7 @@ Wat is er gebeurd? Goedkeuren Terug + Meer acties Annuleren Annuleren Bestanden wijzigen @@ -122,6 +123,16 @@ Meldingen ingeschakeld. Meldingen toestaan Meldingen zijn uitgeschakeld voor VniDrop. U kunt ze inschakelen in Instellingen. + ‘%1$s’ is volledig gedownload. + Download voltooid + ‘%1$s’ kon niet worden ontvangen. + Download mislukt + %1$s heeft ‘%2$s’ volledig ontvangen. + Overdracht ontvangen + %1$s kon “%2$s” niet ontvangen + Levering mislukt + ‘%1$s’ kon niet worden gedeeld. + Delen mislukt De meldingsinstellingen konden niet worden geopend. Meldingen Meldingen zijn niet beschikbaar op dit apparaat. diff --git a/shared/src/commonMain/composeResources/values-pl/strings.xml b/shared/src/commonMain/composeResources/values-pl/strings.xml index aa4c09f..c91ac35 100644 --- a/shared/src/commonMain/composeResources/values-pl/strings.xml +++ b/shared/src/commonMain/composeResources/values-pl/strings.xml @@ -55,6 +55,7 @@ Co się stało? Zatwierdź Wstecz + Więcej działań Anuluj Anuluj Zmień pliki @@ -122,6 +123,16 @@ Powiadomienia włączone. Zezwól na powiadomienia Powiadomienia są wyłączone dla VniDrop. Możesz je włączyć w Ustawieniach. + Zakończono pobieranie „%1$s”. + Pobieranie zakończone + Nie udało się odebrać „%1$s”. + Pobieranie nie powiodło się + %1$s zakończył odbieranie „%2$s”. + Transfer odebrany + %1$s nie mógł odebrać „%2$s” + Dostarczenie nie powiodło się + Nie udało się udostępnić „%1$s”. + Udostępnianie nie powiodło się Nie udało się otworzyć ustawień powiadomień. Powiadomienia Powiadomienia nie są dostępne na tym urządzeniu. diff --git a/shared/src/commonMain/composeResources/values-pt/strings.xml b/shared/src/commonMain/composeResources/values-pt/strings.xml index 1c17822..78e529b 100644 --- a/shared/src/commonMain/composeResources/values-pt/strings.xml +++ b/shared/src/commonMain/composeResources/values-pt/strings.xml @@ -55,6 +55,7 @@ O que aconteceu? Aprovar Voltar + Mais ações Cancelar Cancelar Alterar ficheiros @@ -122,6 +123,16 @@ Notificações ativadas. Permitir notificações As notificações estão desativadas para o VniDrop. Pode ativá-las nas Definições. + «%1$s» concluiu a transferência. + Transferência concluída + Não foi possível receber «%1$s». + Falha na transferência + %1$s terminou de receber «%2$s». + Transferência recebida + %1$s não conseguiu receber “%2$s” + Falha na entrega + Não foi possível partilhar «%1$s». + Falha na partilha Não foi possível abrir as definições de notificações. Notificações As notificações não estão disponíveis neste dispositivo. diff --git a/shared/src/commonMain/composeResources/values-ru/strings.xml b/shared/src/commonMain/composeResources/values-ru/strings.xml index 08e9f83..64cec59 100644 --- a/shared/src/commonMain/composeResources/values-ru/strings.xml +++ b/shared/src/commonMain/composeResources/values-ru/strings.xml @@ -55,6 +55,7 @@ Что произошло? Одобрить Назад + Другие действия Отмена Отмена Изменить файлы @@ -122,6 +123,16 @@ Уведомления включены. Разрешить уведомления Уведомления отключены для VniDrop. Вы можете включить их в Настройках. + «%1$s» завершил загрузку. + Загрузка завершена + Не удалось получить «%1$s». + Ошибка загрузки + %1$s завершил получение «%2$s». + Передача получена + %1$s не удалось получить «%2$s» + Ошибка доставки + Не удалось поделиться «%1$s». + Не удалось поделиться Не удалось открыть настройки уведомлений. Уведомления Уведомления недоступны на этом устройстве. diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index ed6c6dd..dcf3929 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -55,6 +55,7 @@ What happened? Approve Back + More actions Cancel Cancel Change files @@ -122,6 +123,16 @@ Notifications enabled. Allow notifications Notifications are turned off for VniDrop. You can enable them in Settings. + “%1$s” finished downloading. + Download complete + “%1$s” couldn’t be received. + Download failed + %1$s finished receiving “%2$s”. + Transfer received + %1$s couldn\'t receive “%2$s” + Delivery failed + “%1$s” couldn’t be shared. + Sharing failed Could not open notification settings. Notifications Notifications are not available on this device. diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt index 72ebbcc..260a3ce 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/App.kt @@ -6,6 +6,13 @@ import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect @@ -14,6 +21,8 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -49,6 +58,9 @@ import com.vnidrop.app.ui.theme.rememberResolvedDarkTheme import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeoutOrNull +import org.jetbrains.compose.resources.stringResource +import vnidrop.shared.generated.resources.Res +import vnidrop.shared.generated.resources.app_starting @Composable fun App( @@ -217,6 +229,32 @@ fun App( ) } windowChrome?.invoke() + val startingLabel = stringResource(Res.string.app_starting) + AnimatedVisibility( + visible = !sendCoreState.isInitialized, + enter = fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background(LocalVniDropColors.current.backgroundSurface100) + .semantics { contentDescription = startingLabel }, + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + CircularProgressIndicator() + Text( + startingLabel, + style = MaterialTheme.typography.titleMedium, + color = LocalVniDropColors.current.foregroundLighter, + ) + } + } + } } } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/AppGraph.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/AppGraph.kt index f9054ec..6f64179 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/AppGraph.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/AppGraph.kt @@ -8,6 +8,7 @@ import com.vnidrop.app.feature.approvals.ApprovalCoordinator import com.vnidrop.app.feature.send.AppFilePreviewRepository import com.vnidrop.app.feature.send.createPlatformPreviewStore import com.vnidrop.app.logging.AppLogger +import com.vnidrop.app.notifications.TransferNotificationCoordinator import com.vnidrop.app.platform.AppVisibility import com.vnidrop.app.preferences.AppPreferencesDefaults import com.vnidrop.app.preferences.AppPreferencesRepository @@ -62,6 +63,14 @@ class AppGraph( messages = messages, scope = applicationScope, ) + val transferNotificationCoordinator = TransferNotificationCoordinator( + repository = coreRepository, + preferencesRepository = preferencesRepository, + notifications = dependencies.localNotificationService, + visibility = visibility, + messages = messages, + scope = applicationScope, + ) init { AppLogger.initialize(dependencies.environment.defaultCoreDataDir) diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/core/FileSystemService.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/core/FileSystemService.kt index 50355d4..a36a4c1 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/core/FileSystemService.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/core/FileSystemService.kt @@ -41,6 +41,8 @@ interface FileSystemService { suspend fun validateReceiveFolder(folder: ReceiveFolder): FolderAccessStatus suspend fun inspectReceivedArtifacts(artifacts: List): ReceivedStorageInspection suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong + /** Reclaims only app-owned temporary files and returns the number of bytes removed. */ + suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? fun canRevealReceiveFolder(folder: ReceiveFolder): Boolean = false suspend fun revealReceiveFolder(folder: ReceiveFolder): Result = diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/receive/ReceiveScreen.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/receive/ReceiveScreen.kt index e7f7dc0..0135444 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/receive/ReceiveScreen.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/receive/ReceiveScreen.kt @@ -294,7 +294,7 @@ private fun InvitationReviewPanel( Text( when (error) { is UiText.Dynamic -> error.value - is UiText.Resource -> stringResource(error.resource) + is UiText.Resource -> stringResource(error.resource, *error.formatArgs.toTypedArray()) }, color = LocalVniDropColors.current.destructiveDefault, style = MaterialTheme.typography.bodySmall, diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt index 278b9b9..3b2243a 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt @@ -19,17 +19,25 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -58,12 +66,16 @@ import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.decodeToImageBitmap import vnidrop.shared.generated.resources.Res import vnidrop.shared.generated.resources.button_create_new_transfer +import vnidrop.shared.generated.resources.button_delete_transfer +import vnidrop.shared.generated.resources.button_more_actions import vnidrop.shared.generated.resources.send_empty_body import vnidrop.shared.generated.resources.send_empty_title import vnidrop.shared.generated.resources.send_new_transfer_description import vnidrop.shared.generated.resources.send_new_transfer_title import vnidrop.shared.generated.resources.send_title +import vnidrop.shared.generated.resources.send_stop_sharing import vnidrop.shared.generated.resources.send_transfers_title +import vnidrop.shared.generated.resources.transfer_share_title @Composable internal fun SendFloatingAction(onClick: () -> Unit, modifier: Modifier = Modifier) { @@ -86,6 +98,9 @@ internal fun TransferCatalog( windowClass: WindowClass, onOpenComposer: () -> Unit, onTransferSelected: (ULong) -> Unit, + onShare: (ULong) -> Unit = {}, + onStopSharing: (ULong) -> Unit = {}, + onDelete: (ULong) -> Unit = {}, ) { val usesFloatingAction = usesMobilePresentation(LocalUiPlatform.current, windowClass) LazyColumn( @@ -129,6 +144,9 @@ internal fun TransferCatalog( thumbnailBytes = transferThumbnails[transfer.transferId], progress = progress, onClick = { onTransferSelected(transfer.transferId) }, + onShare = { onShare(transfer.transferId) }, + onStopSharing = { onStopSharing(transfer.transferId) }, + onDelete = { onDelete(transfer.transferId) }, ) } } @@ -192,6 +210,9 @@ private fun TransferListItem( thumbnailBytes: ByteArray?, progress: TransferProgress?, onClick: () -> Unit, + onShare: () -> Unit, + onStopSharing: () -> Unit, + onDelete: () -> Unit, ) { val colors = LocalVniDropColors.current Surface(onClick = onClick, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(16.dp), color = colors.backgroundSurface200) { @@ -227,8 +248,68 @@ private fun TransferListItem( ProgressRow(label = progress.label, progress = progress.progress, detail = progress.detail) } } - Spacer(Modifier.width(8.dp)) - PlatformIcon(AppIcon.ChevronRight, contentDescription = null, tint = colors.foregroundLighter, modifier = Modifier.size(18.dp)) + TransferActionsMenu(transfer, onShare, onStopSharing, onDelete) + } + } +} + +@Composable +private fun TransferActionsMenu( + transfer: Transfer, + onShare: () -> Unit, + onStopSharing: () -> Unit, + onDelete: () -> Unit, +) { + var expanded by remember { mutableStateOf(false) } + val moreActionsLabel = stringResource(Res.string.button_more_actions) + Box { + IconButton( + onClick = { expanded = true }, + modifier = Modifier.semantics { + contentDescription = moreActionsLabel + }, + ) { + Text( + "⋮", + style = MaterialTheme.typography.headlineSmall, + color = LocalVniDropColors.current.foregroundLighter, + ) + } + DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { + if (transfer.ticket != null) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.transfer_share_title)) }, + onClick = { + expanded = false + onShare() + }, + leadingIcon = { PlatformIcon(AppIcon.Send, contentDescription = null) }, + ) + } + if (transfer.status == TransferStatus.Sharing) { + DropdownMenuItem( + text = { Text(stringResource(Res.string.send_stop_sharing)) }, + onClick = { + expanded = false + onStopSharing() + }, + leadingIcon = { PlatformIcon(AppIcon.Close, contentDescription = null) }, + ) + } + DropdownMenuItem( + text = { Text(stringResource(Res.string.button_delete_transfer)) }, + onClick = { + expanded = false + onDelete() + }, + leadingIcon = { + PlatformIcon( + AppIcon.Delete, + contentDescription = null, + tint = LocalVniDropColors.current.destructiveDefault, + ) + }, + ) } } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt index f8d2cab..0eae335 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendRoute.kt @@ -46,6 +46,11 @@ fun SendRoute( onAccessPolicyChanged = viewModel::setAccessPolicy, onCreateShare = viewModel::createShare, onTransferSelected = viewModel::openTransfer, + onShareTransfer = { transferId -> + viewModel.openTransfer(transferId) + viewModel.openShare() + }, + onStopSharing = viewModel::stopSharing, onCloseTransferDetails = viewModel::closeTransferDetails, onCopyTicket = viewModel::copyTicket, onActivity = viewModel::openActivity, @@ -53,7 +58,8 @@ fun SendRoute( onShare = viewModel::openShare, onCloseDetailPanel = viewModel::closeDetailPanel, onInvitationResult = viewModel::onInvitationResult, - onRequestDelete = viewModel::requestDeleteTransfer, + onRequestDelete = { viewModel.requestDeleteTransfer() }, + onRequestDeleteTransfer = { viewModel.requestDeleteTransfer(it) }, onDismissDelete = viewModel::dismissDeleteTransfer, onConfirmDelete = viewModel::confirmDeleteTransfer, ) diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt index b46a171..065e3c9 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendScreen.kt @@ -33,6 +33,8 @@ fun SendScreen( onAccessPolicyChanged: (ShareAccessPolicy) -> Unit, onCreateShare: () -> Unit, onTransferSelected: (ULong) -> Unit, + onShareTransfer: (ULong) -> Unit = {}, + onStopSharing: (ULong) -> Unit = {}, onCloseTransferDetails: () -> Unit, onCopyTicket: (String) -> Unit, onActivity: () -> Unit = {}, @@ -41,11 +43,13 @@ fun SendScreen( onCloseDetailPanel: () -> Unit = {}, onInvitationResult: (InvitationAction, Result) -> Unit = { _, _ -> }, onRequestDelete: () -> Unit = {}, + onRequestDeleteTransfer: (ULong) -> Unit = {}, onDismissDelete: () -> Unit = {}, onConfirmDelete: () -> Unit = {}, ) { val outgoingTransfers = coreState.transfers.filter { it.direction == TransferDirection.Send } val selectedTransfer = state.selectedTransferId?.let { id -> outgoingTransfers.firstOrNull { it.transferId == id } } + val deleteTarget = state.deleteTargetTransferId?.let { id -> outgoingTransfers.firstOrNull { it.transferId == id } } val qrCache = remember { mutableStateMapOf() } LaunchedEffect(outgoingTransfers.mapNotNull { it.ticket }) { qrCache.keys.retainAll(outgoingTransfers.mapNotNull { it.ticket }.toSet()) @@ -64,6 +68,7 @@ fun SendScreen( onActivity = onActivity, onReceivers = onReceivers, onShare = onShare, + onStopSharing = { onStopSharing(selectedTransfer.transferId) }, onDelete = onRequestDelete, ) } else { @@ -75,6 +80,9 @@ fun SendScreen( windowClass = windowClass, onOpenComposer = onOpenComposer, onTransferSelected = onTransferSelected, + onShare = onShareTransfer, + onStopSharing = onStopSharing, + onDelete = onRequestDeleteTransfer, ) } } @@ -123,10 +131,10 @@ fun SendScreen( } } - if (selectedTransfer != null && state.isDeleteConfirmationOpen) { + if (deleteTarget != null && state.isDeleteConfirmationOpen) { AdaptiveDrawer(windowClass = windowClass, onDismissRequest = onDismissDelete) { DeleteTransferPanel( - transferName = selectedTransfer.transferName, + transferName = deleteTarget.transferName, isDeleting = state.isDeleting, onCancel = onDismissDelete, onConfirm = onConfirmDelete, diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt index 8cfc7a1..93b81a6 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendViewModel.kt @@ -44,6 +44,7 @@ data class SendState( val receiversByTransfer: Map> = emptyMap(), val isLoadingReceivers: Boolean = false, val isDeleteConfirmationOpen: Boolean = false, + val deleteTargetTransferId: ULong? = null, val isDeleting: Boolean = false, ) { val selectedFile: PickedShareFile? get() = selectedFiles.singleOrNull() @@ -221,12 +222,19 @@ class SendViewModel( refreshReceivers(transferId) } fun closeDetailPanel() = _state.update { it.copy(detailPanel = null) } - fun requestDeleteTransfer() = _state.update { it.copy(isDeleteConfirmationOpen = true) } + fun requestDeleteTransfer(transferId: ULong? = null) = _state.update { + it.copy( + isDeleteConfirmationOpen = true, + deleteTargetTransferId = transferId ?: it.selectedTransferId, + ) + } fun dismissDeleteTransfer() { - if (!_state.value.isDeleting) _state.update { it.copy(isDeleteConfirmationOpen = false) } + if (!_state.value.isDeleting) { + _state.update { it.copy(isDeleteConfirmationOpen = false, deleteTargetTransferId = null) } + } } fun confirmDeleteTransfer() { - val transferId = _state.value.selectedTransferId ?: return + val transferId = _state.value.deleteTargetTransferId ?: return if (_state.value.isDeleting) return viewModelScope.launch { _state.update { it.copy(isDeleting = true) } @@ -239,6 +247,7 @@ class SendViewModel( detailPanel = null, receiverHistory = emptyList(), isDeleteConfirmationOpen = false, + deleteTargetTransferId = null, isDeleting = false, ) } @@ -285,6 +294,7 @@ class SendViewModel( onSuccess = { share -> current.selectedFiles.firstNotNullOfOrNull { it.thumbnailBytes } ?.let { filePreviewRepository.save(share.transferId, it) } + repository.refresh() _state.update { it.copy( isComposerOpen = false, @@ -292,8 +302,11 @@ class SendViewModel( transferName = "", accessPolicy = ShareAccessPolicy.RequireApproval, isSharing = false, + selectedTransferId = share.transferId, + detailPanel = TransferDetailPanel.Share, ) } + refreshReceivers(share.transferId) messages.show(UiMessage(UiText.Resource(Res.string.send_transfer_created), UiMessageTone.Success)) }, onFailure = { error -> @@ -304,6 +317,15 @@ class SendViewModel( } } + fun stopSharing(transferId: ULong) { + viewModelScope.launch { + repository.cancel(transferId).fold( + onSuccess = { repository.refresh() }, + onFailure = messages::error, + ) + } + } + private fun defaultTransferName(files: List): String = when { files.isEmpty() -> "" files.size == 1 && files.first().isDirectory -> files.first().displayName diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferComposer.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferComposer.kt index 6ff956d..bb74a62 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferComposer.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferComposer.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.RadioButton import androidx.compose.material3.Surface import androidx.compose.material3.Text @@ -174,22 +175,51 @@ private fun ReviewFileStep( style = MaterialTheme.typography.bodySmall, ) } - if (windowClass == WindowClass.Phone) { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - ShareButton(state, coreInitialized, onCreateShare, Modifier.fillMaxWidth()) - QuietButton(stringResource(Res.string.button_change_files), onClick = onSelectFile, modifier = Modifier.fillMaxWidth(), enabled = !state.isSharing) - QuietButton(stringResource(Res.string.button_choose_folder), onClick = onSelectFolder, modifier = Modifier.fillMaxWidth(), enabled = !state.isSharing) - } - } else { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - ShareButton(state, coreInitialized, onCreateShare) - QuietButton(stringResource(Res.string.button_change_files), onClick = onSelectFile, enabled = !state.isSharing) - QuietButton(stringResource(Res.string.button_choose_folder), onClick = onSelectFolder, enabled = !state.isSharing) - QuietButton(stringResource(Res.string.button_clear), onClick = onClearFile, enabled = !state.isSharing) + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + ShareButton(state, coreInitialized, onCreateShare, Modifier.fillMaxWidth()) + Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp)) { + SourceButton( + text = stringResource(Res.string.button_change_files), + icon = AppIcon.File, + onClick = onSelectFile, + modifier = Modifier.weight(1f), + enabled = !state.isSharing, + ) + SourceButton( + text = stringResource(Res.string.button_choose_folder), + icon = AppIcon.Folder, + onClick = onSelectFolder, + modifier = Modifier.weight(1f), + enabled = !state.isSharing, + ) + if (windowClass != WindowClass.Phone) { + SourceButton( + text = stringResource(Res.string.button_clear), + icon = AppIcon.Close, + onClick = onClearFile, + modifier = Modifier.weight(1f), + enabled = !state.isSharing, + ) + } } } } +@Composable +private fun SourceButton( + text: String, + icon: AppIcon, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean, +) { + OutlinedButton(onClick = onClick, modifier = modifier, enabled = enabled) { + PlatformIcon(icon, contentDescription = null, modifier = Modifier.size(18.dp)) + Spacer(Modifier.width(8.dp)) + Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) + } +} + @Composable private fun ShareButton(state: SendState, coreInitialized: Boolean, onCreateShare: () -> Unit, modifier: Modifier = Modifier) { PrimaryButton( diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt index 422db5d..e7334d0 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt @@ -84,6 +84,7 @@ internal fun TransferDetails( onActivity: () -> Unit, onReceivers: () -> Unit, onShare: () -> Unit, + onStopSharing: () -> Unit, onDelete: () -> Unit, ) { LazyColumn( @@ -100,8 +101,14 @@ internal fun TransferDetails( style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold, ) - IconButton(onClick = onDelete) { - PlatformIcon(AppIcon.Delete, stringResource(Res.string.button_delete_transfer), tint = LocalVniDropColors.current.destructiveDefault) + if (transfer.status in setOf(TransferStatus.Importing, TransferStatus.Sharing)) { + IconButton(onClick = onShare) { + PlatformIcon( + AppIcon.Send, + stringResource(Res.string.transfer_share_title), + tint = LocalVniDropColors.current.brandLink, + ) + } } } } @@ -130,32 +137,25 @@ internal fun TransferDetails( count = pendingReceivers + completedReceivers, onClick = onReceivers, ) - when (transfer.status) { - TransferStatus.Sharing -> { - HorizontalDivider(color = LocalVniDropColors.current.borderDefault) - DetailDestination( - title = stringResource(Res.string.transfer_share_title), - description = stringResource(Res.string.transfer_share_description), - onClick = onShare, - ) - } - TransferStatus.Importing -> { - HorizontalDivider(color = LocalVniDropColors.current.borderDefault) - DetailDestination( - title = stringResource(Res.string.transfer_share_title), - description = stringResource(Res.string.transfer_event_preparing), - ) - } - TransferStatus.Receiving, - TransferStatus.Done, - TransferStatus.Failed, - TransferStatus.Cancelled, - TransferStatus.Stopped, - -> Unit - } } } } + item { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + if (transfer.status == TransferStatus.Sharing) { + DestructiveButton( + stringResource(Res.string.send_stop_sharing), + onClick = onStopSharing, + modifier = Modifier.fillMaxWidth(), + ) + } + DestructiveButton( + stringResource(Res.string.button_delete_transfer), + onClick = onDelete, + modifier = Modifier.fillMaxWidth(), + ) + } + } } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsRoute.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsRoute.kt index 16bbdd3..1f9fd18 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsRoute.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsRoute.kt @@ -42,5 +42,7 @@ fun SettingsRoute(viewModel: SettingsViewModel, windowClass: WindowClass) { onSubmitBugReport = viewModel::submitBugReport, onDeleteAllTransfers = viewModel::deleteAllTransfers, onClearTransferCache = viewModel::clearTransferCache, + onFreeUpSpace = viewModel::freeUpSpace, + onRefreshStorage = viewModel::loadStorageUsage, ) } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt index 920f983..b303c13 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsScreen.kt @@ -32,6 +32,8 @@ fun SettingsScreen( onSubmitBugReport: () -> Unit, onDeleteAllTransfers: () -> Unit = {}, onClearTransferCache: () -> Unit = {}, + onFreeUpSpace: () -> Unit = {}, + onRefreshStorage: () -> Unit = {}, onRelayModeChanged: (RelayMode) -> Unit = {}, onRelayUrlChanged: (Int, String) -> Unit = { _, _ -> }, onAddRelayUrl: () -> Unit = {}, @@ -69,6 +71,8 @@ fun SettingsScreen( onSubmitBugReport = onSubmitBugReport, onDeleteAllTransfers = onDeleteAllTransfers, onClearTransferCache = onClearTransferCache, + onFreeUpSpace = onFreeUpSpace, + onRefreshStorage = onRefreshStorage, onRelayModeChanged = onRelayModeChanged, onRelayUrlChanged = onRelayUrlChanged, onAddRelayUrl = onAddRelayUrl, @@ -110,6 +114,8 @@ fun SettingsScreen( onSubmitBugReport = onSubmitBugReport, onDeleteAllTransfers = onDeleteAllTransfers, onClearTransferCache = onClearTransferCache, + onFreeUpSpace = onFreeUpSpace, + onRefreshStorage = onRefreshStorage, onRelayModeChanged = onRelayModeChanged, onRelayUrlChanged = onRelayUrlChanged, onAddRelayUrl = onAddRelayUrl, @@ -143,6 +149,8 @@ private fun SettingsSectionContent( onSubmitBugReport: () -> Unit, onDeleteAllTransfers: () -> Unit, onClearTransferCache: () -> Unit, + onFreeUpSpace: () -> Unit, + onRefreshStorage: () -> Unit, onRelayModeChanged: (RelayMode) -> Unit, onRelayUrlChanged: (Int, String) -> Unit, onAddRelayUrl: () -> Unit, @@ -169,6 +177,8 @@ private fun SettingsSectionContent( windowClass, onDeleteAllTransfers, onClearTransferCache, + onFreeUpSpace, + onRefreshStorage, onBack, showBack, ) diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt index 1bde443..f8ea529 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/SettingsViewModel.kt @@ -25,6 +25,7 @@ import com.vnidrop.app.ui.feedback.UiMessage import com.vnidrop.app.ui.feedback.UiMessageController import com.vnidrop.app.ui.feedback.UiMessageTone import com.vnidrop.app.ui.feedback.UiText +import com.vnidrop.app.ui.state.formatBytes import com.vnidrop.app.ui.theme.ThemeMode import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel @@ -51,6 +52,8 @@ import vnidrop.shared.generated.resources.notifications_unsupported import vnidrop.shared.generated.resources.relay_settings_applied import vnidrop.shared.generated.resources.storage_transfer_cache_cleared import vnidrop.shared.generated.resources.storage_transfers_deleted +import vnidrop.shared.generated.resources.storage_cleanup_busy +import vnidrop.shared.generated.resources.storage_cleanup_freed enum class SettingsSection { Overview, @@ -112,8 +115,10 @@ data class SettingsState( val bugLogPreviewBytes: Int = 0, val storage: StorageBreakdown? = null, val isCalculatingStorage: Boolean = false, + val storageLoadFailed: Boolean = false, val isDeletingTransfers: Boolean = false, val isClearingTransferCache: Boolean = false, + val isCleaningStorage: Boolean = false, ) { val hasRelaySettingsChanges: Boolean get() = relayMode != savedRelaySettings.mode || @@ -188,6 +193,14 @@ class SettingsViewModel( endpointId = coreState.status?.endpointId, ) } + if ( + coreState.isInitialized && + _state.value.selectedSection == SettingsSection.Storage && + _state.value.storage == null && + _state.value.storageLoadFailed + ) { + loadStorageUsage() + } } } refreshNotificationPermission() @@ -209,7 +222,7 @@ class SettingsViewModel( fun loadStorageUsage() { if (_state.value.isCalculatingStorage) return viewModelScope.launch { - _state.update { it.copy(isCalculatingStorage = true) } + _state.update { it.copy(isCalculatingStorage = true, storageLoadFailed = false) } try { val receiveFolder = _state.value.receiveFolder ?: fileSystemService.defaultReceiveFolder() val coreUsage = repository.storageUsage().getOrThrow() @@ -227,12 +240,42 @@ class SettingsViewModel( inaccessibleReceivedFileCount = received.inaccessibleCount, ), isCalculatingStorage = false, + storageLoadFailed = false, ) } } catch (error: CancellationException) { throw error } catch (error: Throwable) { - _state.update { it.copy(isCalculatingStorage = false) } + _state.update { it.copy(isCalculatingStorage = false, storageLoadFailed = true) } + messages.error(error) + } + } + } + + fun freeUpSpace() { + val current = _state.value + if (current.isCleaningStorage) return + if (current.hasActiveNetworkWork) { + messages.tryShow(UiMessage(UiText.Resource(Res.string.storage_cleanup_busy), UiMessageTone.Warning)) + return + } + viewModelScope.launch { + _state.update { it.copy(isCleaningStorage = true) } + try { + val receiveFolder = _state.value.receiveFolder ?: fileSystemService.defaultReceiveFolder() + val reclaimed = fileSystemService.reclaimTemporaryStorage(environment.defaultCoreDataDir, receiveFolder) + _state.update { it.copy(isCleaningStorage = false) } + loadStorageUsage() + messages.tryShow( + UiMessage( + UiText.Resource(Res.string.storage_cleanup_freed, formatArgs = listOf(formatBytes(reclaimed))), + UiMessageTone.Success, + ), + ) + } catch (error: CancellationException) { + throw error + } catch (error: Throwable) { + _state.update { it.copy(isCleaningStorage = false) } messages.error(error) } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt index d5d48fc..682cf13 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt @@ -36,9 +36,16 @@ import vnidrop.shared.generated.resources.storage_calculating import vnidrop.shared.generated.resources.storage_clear_transfer_cache import vnidrop.shared.generated.resources.storage_clear_transfer_cache_description import vnidrop.shared.generated.resources.storage_clearing_transfer_cache +import vnidrop.shared.generated.resources.storage_cleaning +import vnidrop.shared.generated.resources.storage_delete_transfers_caption import vnidrop.shared.generated.resources.storage_delete_transfers import vnidrop.shared.generated.resources.storage_delete_transfers_description import vnidrop.shared.generated.resources.storage_deleting +import vnidrop.shared.generated.resources.storage_free_up_space +import vnidrop.shared.generated.resources.storage_free_up_space_caption +import vnidrop.shared.generated.resources.storage_refresh +import vnidrop.shared.generated.resources.storage_unavailable +import vnidrop.shared.generated.resources.storage_usage_header import vnidrop.shared.generated.resources.storage_total import vnidrop.shared.generated.resources.storage_received_files import vnidrop.shared.generated.resources.storage_temporary @@ -52,6 +59,8 @@ internal fun StorageSettings( windowClass: WindowClass, onDeleteAllTransfers: () -> Unit, onClearTransferCache: () -> Unit, + onFreeUpSpace: () -> Unit, + onRefreshStorage: () -> Unit, onBack: () -> Unit, showBack: Boolean, ) { @@ -59,8 +68,30 @@ internal fun StorageSettings( var showClearCacheConfirmation by rememberSaveable { mutableStateOf(false) } Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { SettingsTopBar(stringResource(Res.string.storage_title), onBack, showBack) + Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + Text( + stringResource(Res.string.storage_usage_header), + modifier = Modifier.weight(1f), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + SecondaryButton( + stringResource(Res.string.storage_refresh), + onClick = onRefreshStorage, + enabled = !state.isCalculatingStorage && + !state.isCleaningStorage && + !state.isDeletingTransfers && + !state.isClearingTransferCache, + ) + } val storage = state.storage - if (storage == null || state.isCalculatingStorage) { + if (storage == null && state.storageLoadFailed && !state.isCalculatingStorage) { + SecondaryButton( + stringResource(Res.string.storage_unavailable), + onClick = onRefreshStorage, + modifier = Modifier.fillMaxWidth(), + ) + } else if (storage == null || state.isCalculatingStorage) { SettingsGroup { StorageRow( title = stringResource(Res.string.storage_calculating), @@ -84,6 +115,23 @@ internal fun StorageSettings( ) } } + SecondaryButton( + text = stringResource( + if (state.isCleaningStorage) Res.string.storage_cleaning else Res.string.storage_free_up_space, + ), + onClick = onFreeUpSpace, + modifier = Modifier.fillMaxWidth(), + enabled = !state.isCleaningStorage && + !state.isDeletingTransfers && + !state.isClearingTransferCache && + !state.isCalculatingStorage && + !state.hasActiveNetworkWork, + ) + Text( + stringResource(Res.string.storage_free_up_space_caption), + style = MaterialTheme.typography.bodySmall, + color = LocalVniDropColors.current.foregroundLighter, + ) SecondaryButton( text = stringResource( if (state.isClearingTransferCache) { @@ -107,6 +155,11 @@ internal fun StorageSettings( ) { Text(stringResource(if (state.isDeletingTransfers) Res.string.storage_deleting else Res.string.storage_delete_transfers)) } + Text( + stringResource(Res.string.storage_delete_transfers_caption), + style = MaterialTheme.typography.bodySmall, + color = LocalVniDropColors.current.foregroundLighter, + ) Text( stringResource(Res.string.storage_footer), style = MaterialTheme.typography.bodySmall, diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinator.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinator.kt new file mode 100644 index 0000000..992a69c --- /dev/null +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinator.kt @@ -0,0 +1,190 @@ +package com.vnidrop.app.notifications + +import com.vnidrop.app.core.CoreGateway +import com.vnidrop.app.core.CoreSignal +import com.vnidrop.app.core.ReceiverDeliveryStatus +import com.vnidrop.app.core.ReceiverRequestModel +import com.vnidrop.app.core.Transfer +import com.vnidrop.app.core.TransferDirection +import com.vnidrop.app.core.TransferStatus +import com.vnidrop.app.platform.AppVisibility +import com.vnidrop.app.preferences.PreferencesRepository +import com.vnidrop.app.ui.feedback.UiMessageController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.getString +import vnidrop.shared.generated.resources.Res +import vnidrop.shared.generated.resources.approval_nearby_device +import vnidrop.shared.generated.resources.notifications_receive_completed_body +import vnidrop.shared.generated.resources.notifications_receive_completed_title +import vnidrop.shared.generated.resources.notifications_receive_failed_body +import vnidrop.shared.generated.resources.notifications_receive_failed_title +import vnidrop.shared.generated.resources.notifications_receiver_completed_body +import vnidrop.shared.generated.resources.notifications_receiver_completed_title +import vnidrop.shared.generated.resources.notifications_receiver_failed_body +import vnidrop.shared.generated.resources.notifications_receiver_failed_title +import vnidrop.shared.generated.resources.notifications_send_failed_body +import vnidrop.shared.generated.resources.notifications_send_failed_title +import vnidrop.shared.generated.resources.receive_unknown_transfer + +internal enum class TransferNotificationKind { + SendFailed, + ReceiveCompleted, + ReceiveFailed, + ReceiverCompleted, + ReceiverFailed, +} + +internal data class PlannedTransferNotification( + val id: String, + val kind: TransferNotificationKind, + val transferName: String?, + val receiver: String? = null, +) + +internal fun plannedTransferNotifications( + transfers: List, + published: Set, +): List = transfers.mapNotNull { transfer -> + val kind = when { + transfer.direction == TransferDirection.Send && transfer.status == TransferStatus.Failed -> + TransferNotificationKind.SendFailed + transfer.direction == TransferDirection.Receive && transfer.status == TransferStatus.Done -> + TransferNotificationKind.ReceiveCompleted + transfer.direction == TransferDirection.Receive && transfer.status == TransferStatus.Failed -> + TransferNotificationKind.ReceiveFailed + else -> return@mapNotNull null + } + val id = "${kind.idPrefix}-${transfer.transferId}" + PlannedTransferNotification(id, kind, transfer.transferName).takeUnless { id in published } +} + +internal fun plannedReceiverNotifications( + requests: List, + published: Set, +): List = requests.mapNotNull { request -> + val kind = when (request.status) { + ReceiverDeliveryStatus.Completed -> TransferNotificationKind.ReceiverCompleted + ReceiverDeliveryStatus.Failed -> TransferNotificationKind.ReceiverFailed + else -> return@mapNotNull null + } + val id = "${kind.idPrefix}-${request.id}" + PlannedTransferNotification( + id = id, + kind = kind, + transferName = request.transferName, + receiver = request.receiverName ?: request.receiverDeviceName, + ).takeUnless { id in published } +} + +class TransferNotificationCoordinator( + private val repository: CoreGateway, + private val preferencesRepository: PreferencesRepository, + private val notifications: LocalNotificationService, + private val visibility: AppVisibility, + private val messages: UiMessageController, + private val scope: CoroutineScope, +) { + private val published = mutableSetOf() + private var transfersPrimed = false + private var notificationsEnabled = false + + init { + scope.launch { + preferencesRepository.preferences.collectLatest { preferences -> + notificationsEnabled = preferences.notificationsEnabled + } + } + scope.launch { + repository.state.collect { core -> + if (core.isInitialized) syncTransfers(core.transfers) + } + } + scope.launch { + repository.signals.collect { signal -> + when (signal) { + is CoreSignal.ReceiverHistoryChanged -> syncReceivers(signal.transferId) + is CoreSignal.TransfersChanged -> syncReceivers(signal.transferId) + is CoreSignal.ApprovalChanged -> Unit + } + } + } + } + + private suspend fun syncTransfers(transfers: List) { + val planned = plannedTransferNotifications(transfers, published) + if (!transfersPrimed) { + transfersPrimed = true + published += planned.map(PlannedTransferNotification::id) + return + } + planned.forEach { deliver(it) } + } + + private suspend fun syncReceivers(transferId: ULong) { + val isOutgoing = repository.state.value.transfers.any { + it.transferId == transferId && it.direction == TransferDirection.Send + } + if (!isOutgoing) return + repository.receiverRequests(transferId).fold( + onSuccess = { requests -> + plannedReceiverNotifications(requests, published).forEach { deliver(it) } + }, + onFailure = messages::error, + ) + } + + private suspend fun deliver(plan: PlannedTransferNotification) { + published += plan.id + if ( + !notificationsEnabled || + visibility.isForeground.value || + notifications.permission.value != NotificationPermission.Granted + ) return + val transferName = plan.transferName ?: getString(Res.string.receive_unknown_transfer) + val notification = when (plan.kind) { + TransferNotificationKind.SendFailed -> LocalNotification( + plan.id, + getString(Res.string.notifications_send_failed_title), + getString(Res.string.notifications_send_failed_body, transferName), + ) + TransferNotificationKind.ReceiveCompleted -> LocalNotification( + plan.id, + getString(Res.string.notifications_receive_completed_title), + getString(Res.string.notifications_receive_completed_body, transferName), + ) + TransferNotificationKind.ReceiveFailed -> LocalNotification( + plan.id, + getString(Res.string.notifications_receive_failed_title), + getString(Res.string.notifications_receive_failed_body, transferName), + ) + TransferNotificationKind.ReceiverCompleted -> { + val receiver = plan.receiver ?: getString(Res.string.approval_nearby_device) + LocalNotification( + plan.id, + getString(Res.string.notifications_receiver_completed_title), + getString(Res.string.notifications_receiver_completed_body, receiver, transferName), + ) + } + TransferNotificationKind.ReceiverFailed -> { + val receiver = plan.receiver ?: getString(Res.string.approval_nearby_device) + LocalNotification( + plan.id, + getString(Res.string.notifications_receiver_failed_title), + getString(Res.string.notifications_receiver_failed_body, receiver, transferName), + ) + } + } + notifications.publish(notification).onFailure(messages::error) + } +} + +private val TransferNotificationKind.idPrefix: String + get() = when (this) { + TransferNotificationKind.SendFailed -> "send-failed" + TransferNotificationKind.ReceiveCompleted -> "receive-completed" + TransferNotificationKind.ReceiveFailed -> "receive-failed" + TransferNotificationKind.ReceiverCompleted -> "receiver-completed" + TransferNotificationKind.ReceiverFailed -> "receiver-failed" + } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/UiMessageController.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/UiMessageController.kt index b77dfa8..fee8974 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/UiMessageController.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/UiMessageController.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.receiveAsFlow import org.jetbrains.compose.resources.StringResource sealed interface UiText { - data class Resource(val resource: StringResource) : UiText + data class Resource(val resource: StringResource, val formatArgs: List = emptyList()) : UiText data class Dynamic(val value: String) : UiText } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/VniDropSnackbarHost.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/VniDropSnackbarHost.kt index 7e8ce53..f5b3488 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/VniDropSnackbarHost.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/feedback/VniDropSnackbarHost.kt @@ -138,5 +138,5 @@ private fun DismissButton(onClick: () -> Unit) { private suspend fun UiText.resolve(): String = when (this) { is UiText.Dynamic -> value - is UiText.Resource -> getString(resource) + is UiText.Resource -> getString(resource, *formatArgs.toTypedArray()) } diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt index 7d9535a..2cd18a5 100644 --- a/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/feature/ViewModelsTest.kt @@ -5,6 +5,7 @@ import com.vnidrop.app.PlatformEnvironment import com.vnidrop.app.core.CoreState import com.vnidrop.app.core.CoreStatus import com.vnidrop.app.core.CoreSignal +import com.vnidrop.app.core.CoreStorageUsageModel import com.vnidrop.app.core.PickedShareFile import com.vnidrop.app.core.ReceiveFolder import com.vnidrop.app.core.ReceiveFolderKind @@ -26,6 +27,7 @@ import com.vnidrop.app.feature.app.AppViewModel import com.vnidrop.app.feature.receive.ReceiveHistoryDeleteTarget import com.vnidrop.app.feature.receive.ReceiveViewModel import com.vnidrop.app.feature.send.SendViewModel +import com.vnidrop.app.feature.send.TransferDetailPanel import com.vnidrop.app.feature.settings.SettingsSection import com.vnidrop.app.feature.settings.RelaySettingsApplyError import com.vnidrop.app.feature.settings.RelaySettingsInputError @@ -168,6 +170,43 @@ class ViewModelsTest { assertEquals(0, core.clearTransferCacheCount) } + @Test + fun settingsFreesOnlyPlatformOwnedTemporaryStorage() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + val fileSystem = FakeFileSystemService(folder).apply { + reclaimedTemporaryBytes = 12_000UL + } + val viewModel = settingsViewModel(fileSystem = fileSystem) + advanceUntilIdle() + + viewModel.freeUpSpace() + advanceUntilIdle() + + assertEquals(1, fileSystem.reclaimTemporaryStorageCount) + assertFalse(viewModel.state.value.isCleaningStorage) + } + + @Test + fun settingsRetriesStorageAfterCoreFinishesStarting() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + val core = FakeCoreGateway().apply { + storageUsageResult = Result.failure(IllegalStateException("not initialized")) + } + val viewModel = settingsViewModel(repository = core) + advanceUntilIdle() + + viewModel.selectSection(SettingsSection.Storage) + advanceUntilIdle() + assertTrue(viewModel.state.value.storageLoadFailed) + + core.storageUsageResult = Result.success(CoreStorageUsageModel(25UL, 10UL, 5UL, 0UL, 0UL)) + core.mutableState.value = core.mutableState.value.copy(isInitialized = true) + advanceUntilIdle() + + assertFalse(viewModel.state.value.storageLoadFailed) + assertEquals(25UL, viewModel.state.value.storage?.transferCacheBytes) + } + @Test fun settingsDeleteAllTransfersImmediatelyClearsUnusedCache() = runTest { Dispatchers.setMain(StandardTestDispatcher(testScheduler)) @@ -533,10 +572,33 @@ class ViewModelsTest { assertEquals(null, viewModel.state.value.selectedFile) assertEquals(ShareAccessPolicy.AnyoneWithTransfer, core.lastShareAccessPolicy) assertEquals(7UL, core.state.value.transfers.first().transferId) + assertEquals(7UL, viewModel.state.value.selectedTransferId) + assertEquals(TransferDetailPanel.Share, viewModel.state.value.detailPanel) assertContentEquals(thumbnail, previews.previews.value.getValue(7UL)) assertEquals(listOf(selected), fileSystem.discardedPickedFiles) } + @Test + fun sendViewModelStopsSharingFromCatalogAction() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + val core = FakeCoreGateway().apply { + mutableState.value = CoreState(isInitialized = true, transfers = listOf(sentTransfer(7UL))) + } + val viewModel = SendViewModel( + core, + FakeFileSystemService(folder), + preferences(), + FakeFilePreviewRepository(), + UiMessageController(), + ) + advanceUntilIdle() + + viewModel.stopSharing(7UL) + advanceUntilIdle() + + assertEquals(listOf(7UL), core.cancelledTransfers) + } + @Test fun sendComposerStaysOpenWhenShareCreationFails() = runTest { Dispatchers.setMain(StandardTestDispatcher(testScheduler)) diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinatorTest.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinatorTest.kt new file mode 100644 index 0000000..66f7cbb --- /dev/null +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/notifications/TransferNotificationCoordinatorTest.kt @@ -0,0 +1,72 @@ +package com.vnidrop.app.notifications + +import com.vnidrop.app.core.ReceiverDeliveryStatus +import com.vnidrop.app.core.ReceiverRequestModel +import com.vnidrop.app.core.ShareAccessPolicy +import com.vnidrop.app.core.Transfer +import com.vnidrop.app.core.TransferDirection +import com.vnidrop.app.core.TransferStatus +import kotlin.test.Test +import kotlin.test.assertEquals + +class TransferNotificationCoordinatorTest { + @Test + fun plansOnlyNewTerminalTransferOutcomes() { + val transfers = listOf( + transfer(1UL, TransferDirection.Receive, TransferStatus.Done), + transfer(2UL, TransferDirection.Receive, TransferStatus.Failed), + transfer(3UL, TransferDirection.Send, TransferStatus.Failed), + transfer(4UL, TransferDirection.Send, TransferStatus.Sharing), + ) + + assertEquals( + listOf("receive-completed-1", "send-failed-3"), + plannedTransferNotifications(transfers, setOf("receive-failed-2")).map { it.id }, + ) + } + + @Test + fun plansCompletedAndFailedReceiverOutcomesOnce() { + val requests = listOf( + request("completed", ReceiverDeliveryStatus.Completed), + request("failed", ReceiverDeliveryStatus.Failed), + request("accepted", ReceiverDeliveryStatus.Accepted), + ) + + assertEquals( + listOf("receiver-failed-failed"), + plannedReceiverNotifications(requests, setOf("receiver-completed-completed")).map { it.id }, + ) + } + + private fun transfer(id: ULong, direction: TransferDirection, status: TransferStatus) = Transfer( + localId = "local-$id", + transferId = id, + direction = direction, + status = status, + peerId = null, + transferName = "Transfer $id", + contentHash = null, + fileCount = 1UL, + totalSize = 10UL, + ticket = null, + accessPolicy = ShareAccessPolicy.RequireApproval, + createdAt = 1L, + updatedAt = 1L, + ) + + private fun request(id: String, status: ReceiverDeliveryStatus) = ReceiverRequestModel( + id = id, + transferId = 1UL, + remoteEndpointId = "peer-$id", + transferName = "Transfer", + receiverName = "Receiver", + receiverDeviceName = null, + appVersion = "1.0", + status = status, + reason = null, + requestedAt = 1L, + respondedAt = 2L, + completedAt = 3L, + ) +} diff --git a/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt b/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt index 4e09b09..a912d6d 100644 --- a/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt +++ b/shared/src/commonTest/kotlin/com/vnidrop/app/support/Fakes.kt @@ -48,6 +48,9 @@ class FakeCoreGateway : CoreGateway { private var receiveGate: CompletableDeferred? = null var deleteResult: Result = Result.success(Unit) var clearTransferCacheResult: Result = Result.success(0UL) + var storageUsageResult: Result = Result.success( + CoreStorageUsageModel(0UL, 0UL, 0UL, 0UL, 0UL), + ) var clearReceiveHistoryResult: Result = Result.success(0UL) val deletedTransfers = mutableListOf() val cancelledTransfers = mutableListOf() @@ -153,9 +156,7 @@ class FakeCoreGateway : CoreGateway { awaitReceiveIfNeeded() return receiveResult } - override suspend fun storageUsage(): Result = Result.success( - CoreStorageUsageModel(0UL, 0UL, 0UL, 0UL, 0UL), - ) + override suspend fun storageUsage(): Result = storageUsageResult override suspend fun clearTransferCache(): Result { clearTransferCacheCount += 1 return clearTransferCacheResult @@ -248,6 +249,8 @@ class FakeFileSystemService( var supportsCustomFolders = true var effectiveFolder: ReceiveFolder? = null var canRevealFolder = false + var reclaimedTemporaryBytes = 0UL + var reclaimTemporaryStorageCount = 0 var revealFolderResult: Result = Result.success(Unit) val revealedFolders = mutableListOf() val discardedPickedFiles = mutableListOf() @@ -259,6 +262,10 @@ class FakeFileSystemService( override suspend fun inspectReceivedArtifacts(artifacts: List) = ReceivedStorageInspection(artifacts.fold(0UL) { total, item -> total + item.logicalSize }, artifacts.size, 0, 0) override suspend fun temporaryUsage(receiveFolder: ReceiveFolder): ULong = 0UL + override suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong { + reclaimTemporaryStorageCount += 1 + return reclaimedTemporaryBytes + } override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null override fun canRevealReceiveFolder(folder: ReceiveFolder) = canRevealFolder override suspend fun revealReceiveFolder(folder: ReceiveFolder): Result { diff --git a/shared/src/jvmMain/kotlin/com/vnidrop/app/core/FileSystemService.jvm.kt b/shared/src/jvmMain/kotlin/com/vnidrop/app/core/FileSystemService.jvm.kt index c566035..a6d17e1 100644 --- a/shared/src/jvmMain/kotlin/com/vnidrop/app/core/FileSystemService.jvm.kt +++ b/shared/src/jvmMain/kotlin/com/vnidrop/app/core/FileSystemService.jvm.kt @@ -48,6 +48,9 @@ private class JvmFileSystemService : FileSystemService { return desktopTemporaryUsage(receiveFolder) } + override suspend fun reclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong = + desktopReclaimTemporaryStorage(appDataDir, receiveFolder) + override fun createReceiveOutputSink(folder: ReceiveFolder): ReceiveOutputSinkV2? = null override suspend fun sharePickedFiles( @@ -88,3 +91,35 @@ internal fun desktopTemporaryUsage(receiveFolder: ReceiveFolder): ULong { } }.getOrDefault(0UL) } + +internal fun desktopReclaimTemporaryStorage(appDataDir: String, receiveFolder: ReceiveFolder): ULong { + var reclaimed = 0UL + if (receiveFolder.kind == ReceiveFolderKind.FileSystemPath) { + val receiveRoot = File(receiveFolder.value) + receiveRoot.walkTopDown() + .filter { file -> + file.isFile && + file.name.startsWith(".") && + file.name.contains(".vnidrop-") && + file.name.endsWith(".part") + } + .toList() + .forEach { file -> + val size = file.length().coerceAtLeast(0L).toULong() + if (file.delete()) reclaimed += size + } + } + val appDataRoot = File(appDataDir) + if (appDataRoot.isDirectory) { + appDataRoot.walkTopDown() + .filter { it.isDirectory && it.name == ".Trash" } + .toList() + .forEach { trash -> + val size = trash.walkTopDown() + .filter(File::isFile) + .fold(0UL) { total, file -> total + file.length().coerceAtLeast(0L).toULong() } + if (trash.deleteRecursively()) reclaimed += size + } + } + return reclaimed +} diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/FileSystemServiceTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/FileSystemServiceTest.kt index b8909f7..ae3a97d 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/core/FileSystemServiceTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/core/FileSystemServiceTest.kt @@ -5,6 +5,8 @@ import kotlin.io.path.createDirectories import kotlin.io.path.createTempDirectory import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class FileSystemServiceTest { @Test @@ -26,4 +28,32 @@ class FileSystemServiceTest { root.toFile().deleteRecursively() } } + + @Test + fun desktopReclaimTemporaryStorageKeepsUserFiles() { + val root = createTempDirectory("vnidrop-storage-cleanup") + try { + val receive = root.resolve("receive").createDirectories() + val appData = root.resolve("app-data").createDirectories() + val partial = receive.resolve(".photo.jpg.vnidrop-test.part") + val received = receive.resolve("photo.jpg") + val trash = appData.resolve("nested/.Trash").createDirectories() + Files.write(partial, ByteArray(7)) + Files.write(received, ByteArray(13)) + Files.write(trash.resolve("stale.bin"), ByteArray(11)) + + assertEquals( + 18UL, + desktopReclaimTemporaryStorage( + appData.toString(), + ReceiveFolder(ReceiveFolderKind.FileSystemPath, receive.toString(), "Test"), + ), + ) + assertFalse(Files.exists(partial)) + assertFalse(Files.exists(trash)) + assertTrue(Files.exists(received)) + } finally { + root.toFile().deleteRecursively() + } + } } diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt index bbf7060..6ab14cf 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt @@ -111,7 +111,7 @@ class FoundationComposeTest { } } onNodeWithText("Notifications").performClick() - onNodeWithText("Get notified about new receive requests while VniDrop is in the background.").assertIsDisplayed() + onNodeWithText("Get notified about transfer activity while VniDrop is in the background.").assertIsDisplayed() } @Test @@ -212,10 +212,11 @@ class FoundationComposeTest { runOnIdle { assertTrue(cacheClearRequested) } onNodeWithText("Delete all transfers").performClick() - onNodeWithText( - "This clears all sent and received transfer records from your history and immediately reclaims unused transfer cache. " + - "Ongoing transfers and received files are not deleted. This can’t be undone.", - ).assertIsDisplayed() + onNodeWithText( + "This clears all sent and received transfer records from your history. Your received files are not deleted. " + + "Cached shared content that is no longer needed is reclaimed automatically, which may take a little time. " + + "This can’t be undone.", + ).assertIsDisplayed() runOnIdle { assertFalse(deleteRequested) } onNodeWithTag("confirm-delete-all-transfers").performClick() @@ -636,10 +637,13 @@ class FoundationComposeTest { } } - onNodeWithText("Share").assertIsDisplayed() + onNodeWithContentDescription("Share").assertIsDisplayed() onAllNodesWithText("Scan with VniDrop to receive this transfer").assertCountEquals(0) - onNode(hasText("Share") and hasClickAction()).performClick() + onNodeWithContentDescription("Share").performClick() runOnIdle { assertEquals(com.vnidrop.app.feature.send.TransferDetailPanel.Share, state.value.detailPanel) } + waitUntil(timeoutMillis = 5_000) { + onAllNodesWithText("Scan with VniDrop to receive this transfer").fetchSemanticsNodes().isNotEmpty() + } onNodeWithText("Scan with VniDrop to receive this transfer").assertIsDisplayed() onNodeWithText("Save .vnd file").assertIsDisplayed() onNodeWithContentDescription("Close").assertIsDisplayed() From 81e84b14f826632f5546207814854afcec0bbf25 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Fri, 24 Jul 2026 20:24:23 +0200 Subject: [PATCH 35/36] fix(shared): align action icons and storage refresh --- localization/strings.json | 6 +-- .../drawable/icon_fluent_more_vertical.xml | 5 ++ .../drawable/icon_fluent_share.xml | 5 ++ .../drawable/icon_fluent_sparkles.xml | 5 ++ .../drawable/icon_fluent_stop_circle.xml | 5 ++ .../drawable/icon_lucide_more_vertical.xml | 7 +++ .../drawable/icon_lucide_share.xml | 9 ++++ .../drawable/icon_lucide_sparkles.xml | 8 +++ .../drawable/icon_lucide_stop_circle.xml | 6 +++ .../drawable/icon_material_more_vertical.xml | 5 ++ .../drawable/icon_material_share.xml | 5 ++ .../drawable/icon_material_sparkles.xml | 5 ++ .../drawable/icon_material_stop_circle.xml | 5 ++ .../composeResources/values-fr/strings.xml | 2 +- .../composeResources/values-it/strings.xml | 2 +- .../composeResources/values/strings.xml | 2 +- .../vnidrop/app/feature/send/SendCatalog.kt | 12 ++--- .../app/feature/send/TransferDetails.kt | 8 ++- .../app/feature/settings/StorageSettings.kt | 53 +++++++++++++------ .../com/vnidrop/app/ui/components/Buttons.kt | 38 +++++++++++-- .../com/vnidrop/app/ui/icons/PlatformIcons.kt | 12 +++++ .../vnidrop/app/ui/FoundationComposeTest.kt | 43 +++++++++++++++ shared/tools/import_platform_icons.py | 4 ++ 23 files changed, 220 insertions(+), 32 deletions(-) create mode 100644 shared/src/commonMain/composeResources/drawable/icon_fluent_more_vertical.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_fluent_share.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_fluent_sparkles.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_fluent_stop_circle.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_lucide_more_vertical.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_lucide_share.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_lucide_sparkles.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_lucide_stop_circle.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_material_more_vertical.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_material_share.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_material_sparkles.xml create mode 100644 shared/src/commonMain/composeResources/drawable/icon_material_stop_circle.xml diff --git a/localization/strings.json b/localization/strings.json index 57fdf8f..d3db23e 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -3700,10 +3700,10 @@ "storage_delete_transfers_caption": { "context": "Settings > Storage: caption under the destructive delete-all button.", "translations": { - "en": "Clears your send and receive history and the app's cached share content. Received files on disk are kept.", - "fr": "Efface votre historique d'envois et de réceptions ainsi que le contenu de partage mis en cache par l'app. Les fichiers reçus sur le disque sont conservés.", + "en": "Clears your send and receive history and the app’s cached share content. Received files on disk are kept.", + "fr": "Efface votre historique d’envois et de réceptions ainsi que le contenu de partage mis en cache par l’app. Les fichiers reçus sur le disque sont conservés.", "es": "Borra tu historial de envíos y recepciones y el contenido compartido en caché de la app. Los archivos recibidos en el disco se conservan.", - "it": "Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall'app. I file ricevuti sul disco vengono mantenuti.", + "it": "Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall’app. I file ricevuti sul disco vengono mantenuti.", "de": "Löscht deinen Sende- und Empfangsverlauf sowie die zwischengespeicherten Freigabeinhalte der App. Empfangene Dateien auf dem Datenträger bleiben erhalten.", "pt": "Limpa o teu histórico de envios e receções e o conteúdo de partilha em cache da app. Os ficheiros recebidos no disco são mantidos.", "pl": "Czyści historię wysyłania i odbierania oraz zapisane w pamięci podręcznej udostępniane treści. Odebrane pliki na dysku zostają zachowane.", diff --git a/shared/src/commonMain/composeResources/drawable/icon_fluent_more_vertical.xml b/shared/src/commonMain/composeResources/drawable/icon_fluent_more_vertical.xml new file mode 100644 index 0000000..b61b8b9 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_fluent_more_vertical.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_fluent_share.xml b/shared/src/commonMain/composeResources/drawable/icon_fluent_share.xml new file mode 100644 index 0000000..06524e7 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_fluent_share.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_fluent_sparkles.xml b/shared/src/commonMain/composeResources/drawable/icon_fluent_sparkles.xml new file mode 100644 index 0000000..9119ff9 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_fluent_sparkles.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_fluent_stop_circle.xml b/shared/src/commonMain/composeResources/drawable/icon_fluent_stop_circle.xml new file mode 100644 index 0000000..f49022d --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_fluent_stop_circle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_lucide_more_vertical.xml b/shared/src/commonMain/composeResources/drawable/icon_lucide_more_vertical.xml new file mode 100644 index 0000000..8b1de96 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_lucide_more_vertical.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_lucide_share.xml b/shared/src/commonMain/composeResources/drawable/icon_lucide_share.xml new file mode 100644 index 0000000..d56f3d9 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_lucide_share.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_lucide_sparkles.xml b/shared/src/commonMain/composeResources/drawable/icon_lucide_sparkles.xml new file mode 100644 index 0000000..f50080a --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_lucide_sparkles.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_lucide_stop_circle.xml b/shared/src/commonMain/composeResources/drawable/icon_lucide_stop_circle.xml new file mode 100644 index 0000000..611df28 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_lucide_stop_circle.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_material_more_vertical.xml b/shared/src/commonMain/composeResources/drawable/icon_material_more_vertical.xml new file mode 100644 index 0000000..dcd9c8a --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_material_more_vertical.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_material_share.xml b/shared/src/commonMain/composeResources/drawable/icon_material_share.xml new file mode 100644 index 0000000..913817b --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_material_share.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_material_sparkles.xml b/shared/src/commonMain/composeResources/drawable/icon_material_sparkles.xml new file mode 100644 index 0000000..00d1712 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_material_sparkles.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/drawable/icon_material_stop_circle.xml b/shared/src/commonMain/composeResources/drawable/icon_material_stop_circle.xml new file mode 100644 index 0000000..ab13130 --- /dev/null +++ b/shared/src/commonMain/composeResources/drawable/icon_material_stop_circle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/shared/src/commonMain/composeResources/values-fr/strings.xml b/shared/src/commonMain/composeResources/values-fr/strings.xml index ac3dc1a..912d76e 100644 --- a/shared/src/commonMain/composeResources/values-fr/strings.xml +++ b/shared/src/commonMain/composeResources/values-fr/strings.xml @@ -249,7 +249,7 @@ Vider le cache des transferts Supprime le contenu de transfert en cache qui n’est pas utilisé par une réception en cours ou un partage actif. Les fichiers reçus et l’historique ne sont pas supprimés. Vidage du cache… - Efface votre historique d\'envois et de réceptions ainsi que le contenu de partage mis en cache par l\'app. Les fichiers reçus sur le disque sont conservés. + Efface votre historique d’envois et de réceptions ainsi que le contenu de partage mis en cache par l’app. Les fichiers reçus sur le disque sont conservés. Supprime les fichiers temporaires et les résidus des transferts précédents. Vos transferts et fichiers reçus sont conservés. Actualiser Cache des transferts vidé diff --git a/shared/src/commonMain/composeResources/values-it/strings.xml b/shared/src/commonMain/composeResources/values-it/strings.xml index 1ec70da..53a698a 100644 --- a/shared/src/commonMain/composeResources/values-it/strings.xml +++ b/shared/src/commonMain/composeResources/values-it/strings.xml @@ -249,7 +249,7 @@ Svuota cache trasferimenti Rimuove il contenuto dei trasferimenti memorizzato nella cache che non è usato da una ricezione in corso o da una condivisione attiva. I file ricevuti e la cronologia non vengono eliminati. Svuotamento cache… - Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall\'app. I file ricevuti sul disco vengono mantenuti. + Cancella la cronologia di invii e ricezioni e i contenuti di condivisione memorizzati dall’app. I file ricevuti sul disco vengono mantenuti. Rimuove i file temporanei e i residui dei trasferimenti precedenti. I tuoi trasferimenti e i file ricevuti vengono mantenuti. Aggiorna Cache trasferimenti svuotata diff --git a/shared/src/commonMain/composeResources/values/strings.xml b/shared/src/commonMain/composeResources/values/strings.xml index dcf3929..5a68fa7 100644 --- a/shared/src/commonMain/composeResources/values/strings.xml +++ b/shared/src/commonMain/composeResources/values/strings.xml @@ -249,7 +249,7 @@ Clear transfer cache Removes cached transfer content after briefly restarting VniDrop. Finish ongoing transfers and stop active shares first. Received files and transfer history are not deleted. Clearing cache… - Clears your send and receive history and the app\'s cached share content. Received files on disk are kept. + Clears your send and receive history and the app’s cached share content. Received files on disk are kept. Removes temporary files and leftover trash from earlier transfers. Your transfers and received files are kept. Refresh Transfer cache cleared diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt index 3b2243a..601f048 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/SendCatalog.kt @@ -269,10 +269,10 @@ private fun TransferActionsMenu( contentDescription = moreActionsLabel }, ) { - Text( - "⋮", - style = MaterialTheme.typography.headlineSmall, - color = LocalVniDropColors.current.foregroundLighter, + PlatformIcon( + AppIcon.MoreVertical, + contentDescription = null, + tint = LocalVniDropColors.current.foregroundLighter, ) } DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { @@ -283,7 +283,7 @@ private fun TransferActionsMenu( expanded = false onShare() }, - leadingIcon = { PlatformIcon(AppIcon.Send, contentDescription = null) }, + leadingIcon = { PlatformIcon(AppIcon.Share, contentDescription = null) }, ) } if (transfer.status == TransferStatus.Sharing) { @@ -293,7 +293,7 @@ private fun TransferActionsMenu( expanded = false onStopSharing() }, - leadingIcon = { PlatformIcon(AppIcon.Close, contentDescription = null) }, + leadingIcon = { PlatformIcon(AppIcon.StopCircle, contentDescription = null) }, ) } DropdownMenuItem( diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt index e7334d0..1917c55 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/send/TransferDetails.kt @@ -104,7 +104,7 @@ internal fun TransferDetails( if (transfer.status in setOf(TransferStatus.Importing, TransferStatus.Sharing)) { IconButton(onClick = onShare) { PlatformIcon( - AppIcon.Send, + AppIcon.Share, stringResource(Res.string.transfer_share_title), tint = LocalVniDropColors.current.brandLink, ) @@ -147,12 +147,18 @@ internal fun TransferDetails( stringResource(Res.string.send_stop_sharing), onClick = onStopSharing, modifier = Modifier.fillMaxWidth(), + leadingIcon = { + PlatformIcon(AppIcon.StopCircle, null, modifier = Modifier.size(18.dp)) + }, ) } DestructiveButton( stringResource(Res.string.button_delete_transfer), onClick = onDelete, modifier = Modifier.fillMaxWidth(), + leadingIcon = { + PlatformIcon(AppIcon.Delete, null, modifier = Modifier.size(18.dp)) + }, ) } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt index 682cf13..36442f4 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/feature/settings/StorageSettings.kt @@ -7,9 +7,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,6 +24,8 @@ import androidx.compose.ui.unit.dp import com.vnidrop.app.ui.components.AdaptiveDrawer import com.vnidrop.app.ui.components.DestructiveButton import com.vnidrop.app.ui.components.SecondaryButton +import com.vnidrop.app.ui.icons.AppIcon +import com.vnidrop.app.ui.icons.PlatformIcon import com.vnidrop.app.ui.state.formatBytes import com.vnidrop.app.ui.state.WindowClass import com.vnidrop.app.ui.theme.LocalVniDropColors @@ -75,14 +76,18 @@ internal fun StorageSettings( style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, ) - SecondaryButton( - stringResource(Res.string.storage_refresh), - onClick = onRefreshStorage, - enabled = !state.isCalculatingStorage && - !state.isCleaningStorage && - !state.isDeletingTransfers && - !state.isClearingTransferCache, - ) + if (state.isCalculatingStorage) { + CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp) + } else { + IconButton( + onClick = onRefreshStorage, + enabled = !state.isCleaningStorage && + !state.isDeletingTransfers && + !state.isClearingTransferCache, + ) { + PlatformIcon(AppIcon.Sync, stringResource(Res.string.storage_refresh)) + } + } } val storage = state.storage if (storage == null && state.storageLoadFailed && !state.isCalculatingStorage) { @@ -90,8 +95,11 @@ internal fun StorageSettings( stringResource(Res.string.storage_unavailable), onClick = onRefreshStorage, modifier = Modifier.fillMaxWidth(), + leadingIcon = { + PlatformIcon(AppIcon.Sync, null, modifier = Modifier.size(18.dp)) + }, ) - } else if (storage == null || state.isCalculatingStorage) { + } else if (storage == null) { SettingsGroup { StorageRow( title = stringResource(Res.string.storage_calculating), @@ -126,6 +134,13 @@ internal fun StorageSettings( !state.isClearingTransferCache && !state.isCalculatingStorage && !state.hasActiveNetworkWork, + leadingIcon = { + if (state.isCleaningStorage) { + CircularProgressIndicator(Modifier.size(18.dp), strokeWidth = 2.dp) + } else { + PlatformIcon(AppIcon.Sparkles, null, modifier = Modifier.size(18.dp)) + } + }, ) Text( stringResource(Res.string.storage_free_up_space_caption), @@ -145,16 +160,22 @@ internal fun StorageSettings( !state.isClearingTransferCache && !state.isCalculatingStorage && !state.hasActiveNetworkWork, + leadingIcon = { + PlatformIcon(AppIcon.Storage, null, modifier = Modifier.size(18.dp)) + }, ) - Button( + DestructiveButton( + text = stringResource( + if (state.isDeletingTransfers) Res.string.storage_deleting else Res.string.storage_delete_transfers, + ), onClick = { showDeleteConfirmation = true }, enabled = !state.isDeletingTransfers && !state.isClearingTransferCache && !state.isCalculatingStorage, - colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.error), - ) { - Text(stringResource(if (state.isDeletingTransfers) Res.string.storage_deleting else Res.string.storage_delete_transfers)) - } + leadingIcon = { + PlatformIcon(AppIcon.Delete, null, modifier = Modifier.size(18.dp)) + }, + ) Text( stringResource(Res.string.storage_delete_transfers_caption), style = MaterialTheme.typography.bodySmall, diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/components/Buttons.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/components/Buttons.kt index 78e8a79..2f5fc4f 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/components/Buttons.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/components/Buttons.kt @@ -1,6 +1,8 @@ package com.vnidrop.app.ui.components import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -17,7 +19,13 @@ import com.vnidrop.app.ui.platform.LocalUiPlatform import com.vnidrop.app.ui.theme.LocalVniDropColors @Composable -fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) { +fun PrimaryButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: @Composable (() -> Unit)? = null, +) { val desktop = LocalUiPlatform.current.isDesktop Button( onClick = onClick, @@ -26,12 +34,22 @@ fun PrimaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifi shape = RoundedCornerShape(if (desktop) 6.dp else 8.dp), colors = ButtonDefaults.buttonColors(containerColor = LocalVniDropColors.current.brandButton, contentColor = Color.White), ) { + leadingIcon?.let { + it() + Spacer(Modifier.width(8.dp)) + } Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) } } @Composable -fun SecondaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) { +fun SecondaryButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: @Composable (() -> Unit)? = null, +) { val desktop = LocalUiPlatform.current.isDesktop OutlinedButton( onClick = onClick, @@ -39,6 +57,10 @@ fun SecondaryButton(text: String, onClick: () -> Unit, modifier: Modifier = Modi modifier = modifier.heightIn(min = if (desktop) 36.dp else 44.dp), shape = RoundedCornerShape(if (desktop) 6.dp else 8.dp), ) { + leadingIcon?.let { + it() + Spacer(Modifier.width(8.dp)) + } Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) } } @@ -65,7 +87,13 @@ fun DestructiveQuietButton(text: String, onClick: () -> Unit, modifier: Modifier } @Composable -fun DestructiveButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) { +fun DestructiveButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: @Composable (() -> Unit)? = null, +) { val desktop = LocalUiPlatform.current.isDesktop Button( onClick = onClick, @@ -77,6 +105,10 @@ fun DestructiveButton(text: String, onClick: () -> Unit, modifier: Modifier = Mo contentColor = Color.White, ), ) { + leadingIcon?.let { + it() + Spacer(Modifier.width(8.dp)) + } Text(text, maxLines = 1, overflow = TextOverflow.Ellipsis) } } diff --git a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/icons/PlatformIcons.kt b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/icons/PlatformIcons.kt index bd7e6a0..909041e 100644 --- a/shared/src/commonMain/kotlin/com/vnidrop/app/ui/icons/PlatformIcons.kt +++ b/shared/src/commonMain/kotlin/com/vnidrop/app/ui/icons/PlatformIcons.kt @@ -39,12 +39,19 @@ internal enum class AppIcon( Info(Res.drawable.icon_material_info, Res.drawable.icon_fluent_info, Res.drawable.icon_lucide_info), Lock(Res.drawable.icon_material_lock, Res.drawable.icon_fluent_lock, Res.drawable.icon_lucide_lock), Megaphone(Res.drawable.icon_material_megaphone, Res.drawable.icon_fluent_megaphone, Res.drawable.icon_lucide_megaphone), + MoreVertical( + Res.drawable.icon_material_more_vertical, + Res.drawable.icon_fluent_more_vertical, + Res.drawable.icon_lucide_more_vertical, + ), Moon(Res.drawable.icon_material_moon, Res.drawable.icon_fluent_moon, Res.drawable.icon_lucide_moon), Nfc(Res.drawable.icon_material_nfc, Res.drawable.icon_fluent_nfc, Res.drawable.icon_lucide_nfc), QrCode(Res.drawable.icon_material_qr_code, Res.drawable.icon_fluent_qr_code, Res.drawable.icon_lucide_qr_code), Radio(Res.drawable.icon_material_radio, Res.drawable.icon_fluent_radio, Res.drawable.icon_lucide_radio), Scan(Res.drawable.icon_material_scan, Res.drawable.icon_fluent_scan, Res.drawable.icon_lucide_scan), Send(Res.drawable.icon_material_send, Res.drawable.icon_fluent_send, Res.drawable.icon_lucide_send), + Share(Res.drawable.icon_material_share, Res.drawable.icon_fluent_share, Res.drawable.icon_lucide_share), + Sparkles(Res.drawable.icon_material_sparkles, Res.drawable.icon_fluent_sparkles, Res.drawable.icon_lucide_sparkles), Settings(Res.drawable.icon_material_settings, Res.drawable.icon_fluent_settings, Res.drawable.icon_lucide_settings), Shield(Res.drawable.icon_material_shield, Res.drawable.icon_fluent_shield, Res.drawable.icon_lucide_shield), ShieldCheck( @@ -52,6 +59,11 @@ internal enum class AppIcon( Res.drawable.icon_fluent_shield_check, Res.drawable.icon_lucide_shield_check, ), + StopCircle( + Res.drawable.icon_material_stop_circle, + Res.drawable.icon_fluent_stop_circle, + Res.drawable.icon_lucide_stop_circle, + ), Storage(Res.drawable.icon_material_storage, Res.drawable.icon_fluent_storage, Res.drawable.icon_lucide_storage), Sun(Res.drawable.icon_material_sun, Res.drawable.icon_fluent_sun, Res.drawable.icon_lucide_sun), Sync(Res.drawable.icon_material_sync, Res.drawable.icon_fluent_sync, Res.drawable.icon_lucide_sync), diff --git a/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt b/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt index 6ab14cf..d0571bd 100644 --- a/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt +++ b/shared/src/jvmTest/kotlin/com/vnidrop/app/ui/FoundationComposeTest.kt @@ -40,6 +40,7 @@ import com.vnidrop.app.feature.receive.ReceiveState import com.vnidrop.app.feature.settings.SettingsScreen import com.vnidrop.app.feature.settings.SettingsSection import com.vnidrop.app.feature.settings.SettingsState +import com.vnidrop.app.feature.settings.StorageBreakdown import com.vnidrop.app.feature.settings.SettingsOverview import com.vnidrop.app.feature.send.SendScreen import com.vnidrop.app.feature.send.SendState @@ -223,6 +224,48 @@ class FoundationComposeTest { runOnIdle { assertTrue(deleteRequested) } } + @Test + fun storageKeepsCurrentUsageVisibleWhileRefreshing() = runComposeUiTest { + setContent { + VniDropTheme(isDarkTheme = false) { + SettingsScreen( + state = SettingsState( + selectedSection = SettingsSection.Storage, + isCalculatingStorage = true, + storage = StorageBreakdown( + transferCacheBytes = 1UL, + appDataBytes = 2UL, + temporaryBytes = 3UL, + receivedBytes = 4UL, + receivedFileCount = 1, + missingReceivedFileCount = 0, + inaccessibleReceivedFileCount = 0, + ), + ), + windowClass = WindowClass.Desktop, + onSectionSelected = {}, + onUsernameChanged = {}, + onThemeModeChanged = {}, + onChooseFolder = {}, + onResetFolder = {}, + onNotificationsChanged = {}, + onOpenNotificationSettings = {}, + onDiagnosticsChanged = {}, + onBugWhatChanged = {}, + onBugExpectedChanged = {}, + onBugStepsChanged = {}, + onBugContactChanged = {}, + onBugIncludeLogsChanged = {}, + onSubmitBugReport = {}, + ) + } + } + + onNodeWithText("Received files").assertIsDisplayed() + onNodeWithText("Transfer data").assertIsDisplayed() + onAllNodesWithText("Calculating storage usage…").assertCountEquals(0) + } + @Test fun aboutSettingsShowsTheSharedProductAndPrivacyContent() = runComposeUiTest { setContent { diff --git a/shared/tools/import_platform_icons.py b/shared/tools/import_platform_icons.py index 4dc0978..3633f87 100644 --- a/shared/tools/import_platform_icons.py +++ b/shared/tools/import_platform_icons.py @@ -48,15 +48,19 @@ ICONS = ( IconSource("info", "info", "Info", "info", "info"), IconSource("lock", "lock", "Lock Closed", "lock_closed", "lock"), IconSource("megaphone", "campaign", "Megaphone", "megaphone", "megaphone"), + IconSource("more_vertical", "more_vert", "More Vertical", "more_vertical", "ellipsis-vertical"), IconSource("moon", "dark_mode", "Weather Moon", "weather_moon", "moon"), IconSource("nfc", "nfc", "Tap Double", "tap_double", "nfc"), IconSource("qr_code", "qr_code_scanner", "QR Code", "qr_code", "qr-code"), IconSource("radio", "cell_tower", "Cellular Data 1", "cellular_data_1", "radio-tower"), IconSource("scan", "document_scanner", "Scan Type", "scan_type", "scan-line"), IconSource("send", "send", "Send", "send", "send"), + IconSource("share", "share", "Share", "share", "share-2"), + IconSource("sparkles", "auto_awesome", "Sparkle", "sparkle", "sparkles"), IconSource("settings", "settings", "Settings", "settings", "settings"), IconSource("shield", "shield", "Shield", "shield", "shield"), IconSource("shield_check", "verified_user", "Shield Checkmark", "shield_checkmark", "shield-check"), + IconSource("stop_circle", "stop_circle", "Stop", "stop", "circle-stop"), IconSource("storage", "database", "Database", "database", "database"), IconSource("sun", "light_mode", "Weather Sunny", "weather_sunny", "sun"), IconSource("sync", "sync", "Arrow Sync", "arrow_sync", "refresh-cw"), From b8e8dd86445759b9efd5c0d97daa433950a3390a Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Fri, 24 Jul 2026 21:05:37 +0200 Subject: [PATCH 36/36] test(core): wait for delivery event visibility --- crates/vnidrop/tests/approval.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/vnidrop/tests/approval.rs b/crates/vnidrop/tests/approval.rs index a09253b..726fa4d 100644 --- a/crates/vnidrop/tests/approval.rs +++ b/crates/vnidrop/tests/approval.rs @@ -73,14 +73,23 @@ fn public_share_receives_without_sender_approval() { assert_eq!(deliveries[0].receiver_name.as_deref(), Some("Receiver")); assert_eq!(deliveries[0].status, "completed"); assert!(deliveries[0].completed_at.is_some()); - assert!( - sender.sink.events().iter().any(|event| { + // The repository commit becomes visible just before the receipt handler + // emits its event, so completion and sink observation are not atomic. + let started = Instant::now(); + loop { + if sender.sink.events().iter().any(|event| { event.phase == "delivery" && event.kind == "receiver-completed" && event.transfer_id == Some(share.transfer_id) - }), - "delivery receipts must emit a delivery phase event for UI live updates" - ); + }) { + break; + } + assert!( + started.elapsed() < Duration::from_secs(5), + "delivery receipts must emit a delivery phase event for UI live updates" + ); + std::thread::sleep(Duration::from_millis(10)); + } } #[test]