App internationalization: seven languages, 573 keys, gated - #95
Merged
Conversation
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
app.js UI copy (74 keys: composer, thread chrome, headings, tooltips,
the attachment placeholder, link-status pill) moves behind pmString/
pmFormat, with composed fragments rebuilt as {placeholder} templates
and singular/plural variants as separate keys. PageStrings resolves
the table via Bundle.main and page(payload:) stamps it on every
non-preview page. make-app.sh assembles loc/*.lproj into the app and
QL appex with CFBundleLocalizations declared.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
KeyboardShortcuts display strings (titles, categories, scope notes, spoken/reserved names — 86), the lastError/lastNotice channel (44, interpolations become format keys, plural composites split into explicit singular/plural), NSAlert/panel/menu/export sinks (26), the review-thread location labels, and the .help() concatenation traps (rejoined literals localize via LocalizedStringKey; label+hint composites wrap the label). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Inventories SwiftUI literal keys, String(localized:) sites, and the PageStrings table (507 keys; interpolations rendered as format keys); verifies PageStrings covers every app.js pmString/pmFormat key; diffs each locale's Localizable.strings for missing/orphan keys and mismatched format specifiers. --write-inventory emits loc/_inventory.json for the translation step. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
The wrap pass had left 14 String(localized:)+"tail" concatenations rendering half-translated, six Text literal concatenations selecting the verbatim overload, and several unlocalizable surfaces the translation agents flagged: sidebar section titles, the ternary Show/Hide menu items, appearance/width picker labels, and the shortcut-refusal frame — all folded or wrapped. check-strings' Swift literal scanning is now interpolation-aware (nested quotes inside \(...) truncated keys and hid the real runtime keys like "Commit to %@" from the inventory), and one plural-inside- interpolation site split into explicit forms. Inventory: 527 keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Opus fan-out per locale against the extracted inventory, plus the delta pass after the source repairs. Apple's own menu vocabulary per language (Ablage / Fichier / ファイル / 访达…), GitHub-domain terms carried from the site corpus, format specifiers verified by the gate. UI names the app still renders in English stay English inside translated prose so instructions match the screen. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Int interpolations render as %lld in the runtime lookup key, not %@ —
every count key was silently falling back to English (caught live: a
Japanese launch translated string-interpolated frames but left '3
Markdown files changed' in English). The checker now infers %lld for
count-typed expressions, the seven .strings migrated mechanically
(translations keep their positions), and bare-number badge keys are
excluded as non-language. Also: reviewer/verdict frames and timeline
verbs localized (RelativeDateTimeFormatter output slots into per-
language templates), blame relative labels format through the system
formatter (same buckets, native units/plurals), the file-position and
review-control counters split singular/plural, conversation counts
and thread-badge tooltips become whole {n} templates, and the
screenshot generator passes -AppleLocale with -AppleLanguages so
captures carry native date/number formats. 551 keys, gate green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Live per-locale captures caught the stragglers: the review-decision
and checks chips, the files-changed summary (split singular/plural),
Resolve/Unresolve/Resolved in the rendered page, and the APIError
frame + demo-mode message that surface in alerts. The checker now
extracts .alert/.confirmationDialog/.navigationTitle titles too
("Something went wrong" had never entered the inventory). Generator
delivers the demo Location twice — the document open can race the
launch and get dropped, which sent one scene into the demo-network
alert instead of the folder tree.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
The alert-title batch and cockpit stragglers translated; six more misclassified Int keys (summary line, words·min pill, PR overview help, APIError status) migrated to %lld across all seven files after an agent compiled a verification confirming the runtime lookup shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Folder delivery via open -a silently routes to Finder when the bundle is unregistered — and the standing cleanup rule unregisters dist after every trial, which is why the demo Location vanished from scenes after the dark-mode wave. The generator now registers at start and unregisters on exit, keeping the no-residue rule intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
PRStatus.label was a bare computed String (the chip stayed English in every locale) and Link titles never entered the extraction — both caught by Josh on the Japanese screenshot. Link joins the checker's call list; six new keys authored across the seven files (GitHub's own localized state vocabulary). 579 keys, gate green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
Settings → General gains a Language picker (System + the eight languages, each naming itself) that writes AppleLanguages into the app's own defaults domain — the same mechanism as the system's per-app Language & Region override — with a takes-effect-on-reopen note and a settings deep-link anchor. A systematic sweep of the two string classes the gate cannot see (bare literals in computed display vars, ternary titles) caught and localized the connection-status label, the checks detail line, the commit sheet's git-status words, the credential-source names, and six ternary sites. 603 keys, seven locales, gate green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
The takes-effect note now appears only when the selection differs from the language the process launched with, alongside a Relaunch Now button (detached-shell quit-and-reopen, verified live). 604 keys. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6
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.
Spec:
docs/specs/app-i18n.md. The app joins the site in seven languages: Simplified Chinese, Japanese, French, German, Dutch, Spanish, Brazilian Portuguese — 603 localizable keys, translated and gate-verified, plus an in-app Language setting (Settings → General: System + the eight languages, each naming itself, applied via AppleLanguages in the app domain — the same mechanism as the system per-app override).Architecture
loc/<locale>.lproj/Localizable.strings, hand-authored, deliberately OUTSIDESources/(SwiftPM would bury them in the resource bundle whereBundle.maincan't see them).make-app.shassembles them into the app and the Quick Look appex and declaresCFBundleLocalizations; ~10 lines of build change, no Xcode anywhere.pmString/pmFormat, composed fragments rebuilt as whole{placeholder}templates with explicit singular/plural variants, resolved Swift-side viaBundle.mainand stamped on every non-preview page.scripts/check-strings.pygatesmake test: inventories SwiftUI literals (including.alert/.confirmationDialog/.navigationTitletitles),String(localized:)sites, and the page-strings table; diffs all seven.stringsfiles; validates format specifiers. A missing translation now fails the build instead of silently rendering English.Hard-won correctness details
%lldkeys at runtime, not%@— verified by a compiled test after a live Japanese launch showed count strings stubbornly English while string-keyed frames translated. The checker infers%lldfor count-typed expressions and the seven files were migrated mechanically, translations keeping their positions.dateStyleformatters; the blame gutter's relative labels now format throughRelativeDateTimeFormatter(identical bucketing, native units and plural rules); word counts via.formatted(); every English frame around them (reviewer tooltips, verdict lines, "N of M", "N reviews · N comments") became localized keys — with per-language template shapes (ja wraps relative times in 「(…)」 because "今に承認しました" is broken Japanese).--langnow pairs-AppleLanguageswith-AppleLocale(language alone leaves US date formats), and registers the dist bundle with Launch Services per run — unregistered bundles silently routeopen -afolder delivery to Finder, which cost one scene its demo Location.Verified
make test: 617 tests +strings OK: 603 keys; locales checked: de, es, fr, ja, nl, pt-BR, zh-Hans.Japanese PR cockpit:
German folder browsing:
Chinese, dark mode, margin notes:
Standing tax
Every release that adds user-facing strings owes seven translations before
make testgoes green — the gate makes forgetting impossible, and the opus delta-translation pattern makes paying it minutes.🤖 Generated with Claude Code
https://claude.ai/code/session_01JcXC4FCipx7zTVNRVbTjq6