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.
This commit is contained in:
2026-07-23 17:12:43 +02:00
parent 5939489432
commit ef42875ddb
28 changed files with 4465 additions and 395 deletions

View File

@@ -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<Void, Error>) {
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)