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] 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": {