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

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