feat(i18n): localize UI strings via String Catalog (en/ja/zh-Hans/zh-Hant/ko/vi) - #132
Merged
Conversation
Adds Localizable.xcstrings (source of truth, 77 keys) covering every
menu item, settings tab, alert, notification, and NSOpenPanel string
across en (base) / ja / zh-Hans / zh-Hant / ko / vi, translated to
match current macOS System Settings terminology where an equivalent
exists (e.g. "Launch at Login" -> ja "ログイン時に開く", matching
System Settings > General > Login Items, not a literal translation).
Every call site goes through a new L(_:) helper (Localization.swift)
instead of Text("...")/NSLocalizedString directly, because SPM never
merges a target's resources into Bundle.main — a bundle-less lookup
would silently return the raw key in a real .app. L(_:) resolves
against Bundle.module and returns a plain String, so SwiftUI displays
it verbatim via the StringProtocol overloads of Text/Toggle/Button/
Picker/Label/Section/Link instead of re-resolving a LocalizedStringKey
against Bundle.main a second time.
Build-time gotcha found and worked around (documented in CLAUDE.md):
swift build/swift test do NOT compile .xcstrings into usable
localization data — only Xcode's build system does that. Confirmed
empirically: a raw .xcstrings copied into the resource bundle resolved
to the key on every locale. Localizable.xcstrings is now pre-compiled
with xcstringstool compile into committed Resources/<locale>.lproj/
Localizable.strings, which SPM's resource processing and Foundation's
Bundle localization both understand natively under swift build —
confirmed working end to end, including inside a real signed .app
produced by scripts/package.sh.
scripts/package.sh also never copied SPM's <Package>_<Target>.bundle
into Contents/Resources/ (harmless before this PR, since there were no
declared resources yet) — fixed, or every localized string would have
silently fallen back to its raw key in production while still passing
swift test.
New tests (LocalizationCatalogTests): every catalog key resolves to a
non-empty value in the compiled table for every locale; no compiled
table has stale keys beyond the source catalog; committed compiled
tables match a fresh xcstringstool compile of the source (drift
detection, skips gracefully if xcstringstool is unavailable); L(_:)
resolves a known key to a real value (not the raw key).
Ported from fix/smappservice-test-isolation (PR #134): this branch's AppSettingsTests.swift still had the pre-fix version, which calls the real SMAppService.mainApp.register() and re-registered the xctest test-runner as a login item after this branch's own test runs. See PR #134 for the full incident writeup.
Member
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Localizes every user-facing string (menu bar items, Settings tabs, alerts, update notification, NSOpenPanel) into a String Catalog (
Localizable.xcstrings, 77 keys) covering en (base) / ja / zh-Hans / zh-Hant / ko / vi.Design
L(_:)helper (Sources/CmdIMESwift/Localization.swift) instead ofText("...")/NSLocalizedStringdirectly. SPM never merges a target's resources intoBundle.main, so a bundle-less lookup would silently return the raw key in a real.app.L(_:)resolves againstBundle.moduleand returns a plainString; SwiftUI call sites use theStringProtocoloverloads ofText/Toggle/Button/Picker/Label/Section/Link(not theLocalizedStringKeyones) so the already-resolved string displays verbatim instead of being re-resolved a second time.英数/かなkey-name presets are intentionally identical across all 6 locales — they name a physical/system key label, not prose.Build-pipeline gotcha found and fixed (documented in CLAUDE.md)
swift build/swift testdo not compile.xcstringsinto usable localization data — that's an Xcode-only build phase. Confirmed empirically: a raw.xcstringscopied into the resource bundle resolved to the literal key on every locale, in every test run.Fix:
Localizable.xcstringsis pre-compiled withxcstringstool compileinto committedResources/<locale>.lproj/Localizable.strings, which SPM's.process()resource rule and Foundation'sBundlelocalization both understand natively underswift build. Confirmed working end-to-end, including inside a real signed.appbuilt viascripts/package.sh(verifiedContents/Resources/CmdIMESwift_CmdIMESwift.bundle/ja.lproj/Localizable.stringsresolvesgeneral.launchAtLogin→ログイン時に開く).scripts/package.shalso never copied SPM's<Package>_<Target>.bundleintoContents/Resources/— harmless before this PR (noresources:existed yet), but would have silently shipped zero translations in production whileswift teststayed green. Fixed by adding adittostep.Tests (
LocalizationCatalogTests.swift)xcstringstool compileof the source (drift detection; skips gracefully ifxcstringstoolis unavailable in the runner).L(_:)resolves a known key to a real translated value, not the raw key.Acceptance
Also built and code-signed a real
.applocally viaCMDIME_BUILD_MODE=local ./scripts/package.shand confirmed the resource bundle + all 6.lprojtables land insideContents/Resources/.Not yet done
manifest.tomlversion intentionally not bumped (release timing is an owner decision).Scope note for the companion PR
feat/multilang-input-source(input-source generalization, same base) also touchesSettings/ShortcutsSettingsView.swift. Both PRs are based onmainindependently, so merging both will need a small textual conflict resolved in that file — the string keys added here for the preset/action labels and the new "Switch to Input Source" submenu strings from that PR don't collide in meaning, just in diff position.