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

@@ -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)
}