diff --git a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift index 65f1646..ee80794 100644 --- a/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift +++ b/apple/VniDrop/Features/Receive/ReceiveInvitationActions.swift @@ -111,7 +111,10 @@ struct InvitationReviewPanel: View { let metadata = inspection.metadata VStack(alignment: .leading, spacing: 8) { Text(metadata.transferName).font(VniType.bodyLarge).lineLimit(2) - Text("\(metadata.fileCount) \(String(localized: L10n.Metadata.files).lowercased()) · \(formatBytes(metadata.totalSize))") + Text(L10n.Format.separatedTriple( + first: "\(metadata.fileCount)", + second: String(localized: L10n.Metadata.files).lowercased(), + third: formatBytes(metadata.totalSize))) .foregroundStyle(colors.foregroundLighter) } .padding(16) diff --git a/apple/VniDrop/Features/Receive/ReceiveScreen.swift b/apple/VniDrop/Features/Receive/ReceiveScreen.swift index 1f2c705..a49012f 100644 --- a/apple/VniDrop/Features/Receive/ReceiveScreen.swift +++ b/apple/VniDrop/Features/Receive/ReceiveScreen.swift @@ -137,7 +137,7 @@ private struct ReceiveTransferRow: View { VStack(alignment: .leading, spacing: 3) { Text(transfer.transferName ?? String(localized: L10n.Receive.unknownTransfer)) .font(.body).lineLimit(1) - Text("\(formatBytes(transfer.totalSize)) · \(statusLabel(transfer.status))") + Text(L10n.Format.separatedPair(first: formatBytes(transfer.totalSize), second: statusLabel(transfer.status))) .font(.caption).foregroundStyle(.secondary) if transfer.status == .receiving, let progress { ProgressRow(labelKey: progress.labelKey, progress: progress.progress, detail: progress.detail) diff --git a/apple/VniDrop/Features/Send/SendScreen.swift b/apple/VniDrop/Features/Send/SendScreen.swift index 66e0c4d..ab9e3a4 100644 --- a/apple/VniDrop/Features/Send/SendScreen.swift +++ b/apple/VniDrop/Features/Send/SendScreen.swift @@ -160,7 +160,7 @@ private struct TransferListItem: View { Spacer() StatusPill(label: statusLabel(transfer.status), tone: transfer.status.pillTone) } - Text("\(formatBytes(transfer.totalSize)) · \(accessPolicyLabel(transfer.accessPolicy))") + Text(L10n.Format.separatedPair(first: formatBytes(transfer.totalSize), second: accessPolicyLabel(transfer.accessPolicy))) .font(.caption).foregroundStyle(.secondary).lineLimit(1) if let progress, transfer.status == .importing || transfer.status == .sharing { ProgressRow(labelKey: progress.labelKey, progress: progress.progress, detail: progress.detail, labelText: progress.label) diff --git a/apple/VniDrop/Features/Send/TransferDetailsView.swift b/apple/VniDrop/Features/Send/TransferDetailsView.swift index 90d0077..71e57ad 100644 --- a/apple/VniDrop/Features/Send/TransferDetailsView.swift +++ b/apple/VniDrop/Features/Send/TransferDetailsView.swift @@ -91,7 +91,9 @@ struct TransferDetailsView: View { private func receiversDescription(_ pending: Int, _ completed: Int) -> String { if pending > 0 && completed > 0 { - return "\(L10n.Transfer.receiversPending(count: pending)) · \(L10n.Transfer.receiversCompletedCount(count: completed))" + return L10n.Format.separatedPair( + first: L10n.Transfer.receiversPending(count: pending), + second: L10n.Transfer.receiversCompletedCount(count: completed)) } if pending > 0 { return L10n.Transfer.receiversPending(count: pending) } if completed > 0 { return L10n.Transfer.receiversCompletedCount(count: completed) } diff --git a/apple/VniDrop/Generated/L10n.swift b/apple/VniDrop/Generated/L10n.swift index de09ca8..e1532c4 100644 --- a/apple/VniDrop/Generated/L10n.swift +++ b/apple/VniDrop/Generated/L10n.swift @@ -514,6 +514,23 @@ enum L10n { /// - nl: Batterijniveau /// - ru: Уровень заряда static let levelTitle: String.LocalizationValue = "battery_level_title" + /// {level}% + /// + /// Key: `battery_level_value` + /// Context: Device information: battery charge formatted as a percentage. {level} = integer percent value. + /// + /// - en: {level}% + /// - fr: {level} % + /// - es: {level} % + /// - it: {level}% + /// - de: {level} % + /// - pt: {level}% + /// - pl: {level}% + /// - nl: {level}% + /// - ru: {level} % + static func levelValue(level: String) -> String { + String(format: String(localized: "battery_level_value"), level) + } } enum Bug { /// name@example.com @@ -1684,6 +1701,42 @@ enum L10n { /// - ru: Готово static let statusWritable: String.LocalizationValue = "folder_status_writable" } + enum Format { + /// {first} · {second} + /// + /// Key: `format_separated_pair` + /// Context: Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values. + /// + /// - en: {first} · {second} + /// - fr: {first} · {second} + /// - es: {first} · {second} + /// - it: {first} · {second} + /// - de: {first} · {second} + /// - pt: {first} · {second} + /// - pl: {first} · {second} + /// - nl: {first} · {second} + /// - ru: {first} · {second} + static func separatedPair(first: String, second: String) -> String { + String(format: String(localized: "format_separated_pair"), first, second) + } + /// {first} {second} · {third} + /// + /// Key: `format_separated_triple` + /// Context: Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values. + /// + /// - en: {first} {second} · {third} + /// - fr: {first} {second} · {third} + /// - es: {first} {second} · {third} + /// - it: {first} {second} · {third} + /// - de: {first} {second} · {third} + /// - pt: {first} {second} · {third} + /// - pl: {first} {second} · {third} + /// - nl: {first} {second} · {third} + /// - ru: {first} {second} · {third} + static func separatedTriple(first: String, second: String, third: String) -> String { + String(format: String(localized: "format_separated_triple"), first, second, third) + } + } enum Metadata { /// Files /// diff --git a/apple/VniDrop/Platform/AppDependencies+iOS.swift b/apple/VniDrop/Platform/AppDependencies+iOS.swift index 1c27c43..e4d5bd2 100644 --- a/apple/VniDrop/Platform/AppDependencies+iOS.swift +++ b/apple/VniDrop/Platform/AppDependencies+iOS.swift @@ -36,7 +36,7 @@ private struct IosDeviceInfoProvider: DeviceInfoProvider { device.isBatteryMonitoringEnabled = true defer { device.isBatteryMonitoringEnabled = wasMonitoring } let level = device.batteryLevel - return level >= 0 ? "\(Int(level * 100))%" : nil + return level >= 0 ? L10n.Battery.levelValue(level: "\(Int(level * 100))") : nil }() return DeviceInfo( deviceName: device.name, diff --git a/apple/VniDrop/Resources/Localizable.xcstrings b/apple/VniDrop/Resources/Localizable.xcstrings index 17af5c5..4b811be 100644 --- a/apple/VniDrop/Resources/Localizable.xcstrings +++ b/apple/VniDrop/Resources/Localizable.xcstrings @@ -1,134 +1,6 @@ { "sourceLanguage": "en", "strings": { - "%@": { - "comment": "Apple format passthrough placeholder (single value). Legacy literal key — rename to a semantic key.", - "extractionState": "manual" - }, - "%@ %@ · %@": { - "comment": "Apple format template composing three values with a middot separator (e.g. metadata rows). Legacy literal key — rename.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@ %2$@ · %3$@" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ %2$@ · %3$@" - } - } - } - }, - "%@ · %@": { - "comment": "Apple format template composing two values with a middot separator. Legacy literal key — rename.", - "extractionState": "manual", - "localizations": { - "de": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "%1$@ · %2$@" - } - }, - "es": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "fr": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "it": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "nl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "pl": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "pt": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - }, - "ru": { - "stringUnit": { - "state": "needs_review", - "value": "%1$@ · %2$@" - } - } - } - }, - "%@%%": { - "comment": "Apple format template appending a percent sign to a value (e.g. battery level). Legacy literal key — rename.", - "extractionState": "manual" - }, "about_bug_report": { "comment": "About screen: button/link that opens the bug report form.", "extractionState": "manual", @@ -2109,6 +1981,66 @@ } } }, + "battery_level_value": { + "comment": "Device information: battery charge formatted as a percentage. {level} = integer percent value.", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@%%" + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "nl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "pt": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@%%" + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %%" + } + } + } + }, "bug_report_contact_hint": { "comment": "Bug report form: placeholder text in the contact email field.", "extractionState": "manual", @@ -6729,6 +6661,126 @@ } } }, + "format_separated_pair": { + "comment": "Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values.", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ · %2$@" + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "nl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "pt": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ · %2$@" + } + } + } + }, + "format_separated_triple": { + "comment": "Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values.", + "extractionState": "manual", + "localizations": { + "de": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "%1$@ %2$@ · %3$@" + } + }, + "es": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "fr": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "it": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "nl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "pl": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "pt": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + }, + "ru": { + "stringUnit": { + "state": "needs_review", + "value": "%1$@ %2$@ · %3$@" + } + } + } + }, "metadata_files": { "comment": "Transfer metadata label: number of files.", "extractionState": "manual", diff --git a/localization/strings.json b/localization/strings.json index 6b46ecb..dec300d 100644 --- a/localization/strings.json +++ b/localization/strings.json @@ -13,76 +13,6 @@ "ru" ], "strings": { - "%@": { - "context": "Apple format passthrough placeholder (single value). Legacy literal key — rename to a semantic key.", - "targets": [ - "apple" - ] - }, - "%@ %@ · %@": { - "context": "Apple format template composing three values with a middot separator (e.g. metadata rows). Legacy literal key — rename.", - "targets": [ - "apple" - ], - "args": [ - { - "name": "arg1", - "type": "string" - }, - { - "name": "arg2", - "type": "string" - }, - { - "name": "arg3", - "type": "string" - } - ], - "translations": { - "en": "{arg1} {arg2} · {arg3}", - "fr": "{arg1} {arg2} · {arg3}", - "es": "{arg1} {arg2} · {arg3}", - "it": "{arg1} {arg2} · {arg3}", - "de": "{arg1} {arg2} · {arg3}", - "pt": "{arg1} {arg2} · {arg3}", - "pl": "{arg1} {arg2} · {arg3}", - "nl": "{arg1} {arg2} · {arg3}", - "ru": "{arg1} {arg2} · {arg3}" - } - }, - "%@ · %@": { - "context": "Apple format template composing two values with a middot separator. Legacy literal key — rename.", - "targets": [ - "apple" - ], - "args": [ - { - "name": "arg1", - "type": "string" - }, - { - "name": "arg2", - "type": "string" - } - ], - "translations": { - "en": "{arg1} · {arg2}", - "fr": "{arg1} · {arg2}", - "es": "{arg1} · {arg2}", - "it": "{arg1} · {arg2}", - "de": "{arg1} · {arg2}", - "pt": "{arg1} · {arg2}", - "pl": "{arg1} · {arg2}", - "nl": "{arg1} · {arg2}", - "ru": "{arg1} · {arg2}" - } - }, - "%@%%": { - "context": "Apple format template appending a percent sign to a value (e.g. battery level). Legacy literal key — rename.", - "targets": [ - "apple" - ] - }, "about_bug_report": { "context": "About screen: button/link that opens the bug report form.", "translations": { @@ -567,6 +497,29 @@ "ru": "Уровень заряда" } }, + "battery_level_value": { + "context": "Device information: battery charge formatted as a percentage. {level} = integer percent value.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "level", + "type": "string" + } + ], + "translations": { + "en": "{level}%", + "fr": "{level} %", + "es": "{level} %", + "it": "{level}%", + "de": "{level} %", + "pt": "{level}%", + "pl": "{level}%", + "nl": "{level}%", + "ru": "{level} %" + } + }, "bug_report_contact_hint": { "context": "Bug report form: placeholder text in the contact email field.", "translations": { @@ -1645,6 +1598,64 @@ "ru": "Готово" } }, + "format_separated_pair": { + "context": "Composes two already-localized values with a middot separator (e.g. size · status). {first} and {second} are the two values.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "first", + "type": "string" + }, + { + "name": "second", + "type": "string" + } + ], + "translations": { + "en": "{first} · {second}", + "fr": "{first} · {second}", + "es": "{first} · {second}", + "it": "{first} · {second}", + "de": "{first} · {second}", + "pt": "{first} · {second}", + "pl": "{first} · {second}", + "nl": "{first} · {second}", + "ru": "{first} · {second}" + } + }, + "format_separated_triple": { + "context": "Composes three already-localized values, the first two space-joined then a middot before the third (e.g. count files · size). {first}, {second}, {third} are the values.", + "targets": [ + "apple" + ], + "args": [ + { + "name": "first", + "type": "string" + }, + { + "name": "second", + "type": "string" + }, + { + "name": "third", + "type": "string" + } + ], + "translations": { + "en": "{first} {second} · {third}", + "fr": "{first} {second} · {third}", + "es": "{first} {second} · {third}", + "it": "{first} {second} · {third}", + "de": "{first} {second} · {third}", + "pt": "{first} {second} · {third}", + "pl": "{first} {second} · {third}", + "nl": "{first} {second} · {third}", + "ru": "{first} {second} · {third}" + } + }, "metadata_files": { "context": "Transfer metadata label: number of files.", "translations": {