LocalizationKit: deferred strings, drift script, WhereUI + WhereCore#40
Open
kyleve wants to merge 18 commits into
Open
LocalizationKit: deferred strings, drift script, WhereUI + WhereCore#40kyleve wants to merge 18 commits into
kyleve wants to merge 18 commits into
Conversation
Adds the deferred LocalizedString value type and LocalizationConfig to StuffCore (Foundation-only shared slot), wiring StuffCore into the WhereUI target and WhereUITests bundle. Groundwork for the LocalizedStrings pattern; no behavior change yet. Closes plan to-do: infra. Co-authored-by: Cursor <cursoragent@cursor.com>
Static Text.localized(_:_:) factory that resolves a deferred LocalizedString at the point of display, giving call sites a single resolution seam (and the future home for an Environment-driven locale override). Closes plan to-do: text-helper. Co-authored-by: Cursor <cursoragent@cursor.com>
Rewrites the WhereUI Strings enum as LocalizedStrings: nested enums grouped by the existing catalog sections, each member returning a deferred LocalizedString built from a literal String(localized:defaultValue:bundle:.module, locale:) call so the English source lives in Swift and stays discoverable by both Xcode and the upcoming ./localize script. Call sites now resolve at the point of display via Text.localized(_:) or .localized(); tests assert against .localized(). Closes plan to-do: migrate-whereui. Co-authored-by: Cursor <cursoragent@cursor.com>
Closes plan to-do "script". `./localize` (build-free Ruby, like sync-agents) extracts each module's LocalizedStrings.swift String(localized:defaultValue:) calls and reconciles the sibling Localizable.xcstrings: add missing keys, prune orphans, update simple English defaults, and preserve plural variations, parameterized keys, and non-English translations untouched. Writes Xcode's exact pretty-printed JSON (2-space indent, case-insensitively sorted keys, no trailing newline) so an in-sync catalog round-trips with no diff. Modes: default (write), --lint (CI), --git-add (pre-commit). Co-authored-by: Cursor <cursoragent@cursor.com>
Closes plan to-do "precommit". The pre-commit hook runs ./localize --git-add (after the SwiftFormat re-stage, before sync-agents) so reconciled catalogs are written and re-staged on every commit. The CI format job runs ./localize --lint to fail the build on any catalog that has drifted from its Swift source. Co-authored-by: Cursor <cursoragent@cursor.com>
Closes plan to-do "docs". Root AGENTS.md gains a "Keeping localization in sync" section (the localize script, its add/prune/update/preserve rules, and the pre-commit + CI wiring) and lists ./localize among the root dev scripts. StuffCore README/AGENTS document LocalizedString / LocalizationConfig as the first real shared API, and Where/AGENTS.md documents the WhereUI LocalizedStrings convention and Text.localized helper. Regenerated CLAUDE.md files via ./sync-agents (gitignored). Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a `var localized` convenience that calls `localized()` with default arguments, so call sites that don't override the locale can drop the empty `()`. Migrates the WhereUI call sites and tests to `.localized`, and updates the StuffCore / WhereUI docs to match. `localized(_:)` stays for locale overrides. Co-authored-by: Cursor <cursoragent@cursor.com>
Introduces LocalizedString.module(_:_:) in WhereUI, which bakes in
bundle:.module and the locale plumbing so each simple catalog string is a
one-line `.module("key", "default")`. The key stays a StaticString literal so
the plural-aware String(localized:) overload, Xcode extraction, and ./localize
all keep reading keys statically. Members that compose a nested
.localized(config) or branch on a count keep the explicit closure form.
Teaches ./localize to parse the .module("key", value) form alongside the raw
String(localized:defaultValue:) calls, and updates the root/StuffCore/WhereUI
docs. The catalog is unchanged (the rewrite extracts identical keys/values).
Co-authored-by: Cursor <cursoragent@cursor.com>
Composed LocalizedStrings members built their default from the resolution config (a nested .localized(config)), so they couldn't use the value-taking .module(_:_:) and dropped to a raw String(localized:) closure. Add a .module overload taking a config -> LocalizationValue builder so every member stays on .module, and teach ./localize to read the default literal out of that trailing closure. Co-authored-by: Cursor <cursoragent@cursor.com>
Mark the LocalizedString builder @sendable and conform to Sendable so strings can be cached and cross isolation boundaries (e.g. widget timelines). That lets the 125 parameter-less LocalizedStrings members collapse from computed `static var { .module(...) }` to one-line cached `static let`s. Add View overloads of navigationTitle/accessibilityLabel/accessibilityHint that take a LocalizedString (mirroring Text.localized), and migrate the call sites so they pass the LocalizedString directly instead of resolving with a trailing .localized. Also fix the now-stale LocalizedStrings header doc and refresh the StuffCore/Where module docs. Co-authored-by: Cursor <cursoragent@cursor.com>
Add LocalizedString+DotSyntax.swift: one accessor per LocalizedStrings nested enum (top-level on LocalizedString, nested on their parent), each returning the enum metatype so `.category.member` chains resolve to the existing static let / static func members with no duplicated strings. Replace the Text.localized(_:) static with a Text(localized:) initializer and migrate the LocalizedString-typed call sites (Text plus the navigationTitle/accessibilityLabel/accessibilityHint View overloads) to dot syntax, e.g. Text(localized: .primary.emptyDescription). Plain-String sites keep LocalizedStrings.<Category>.<member>.localized. Co-authored-by: Cursor <cursoragent@cursor.com>
Create Shared/LocalizationKit with LocalizedString, LocalizationConfig, the generic .catalog(_:_:bundle:) factory, Text(localized:), and View modifier overloads. Move LocalizedStringTests here; wire LocalizationKitTests in Project.swift. StuffCore reverts to the version placeholder. Co-authored-by: Cursor <cursoragent@cursor.com>
Swap WhereUI's StuffCore dependency for LocalizationKit; rewrite the WhereUI .module factory to delegate to .catalog(..., bundle: .module). Update imports across WhereUI sources and tests; wire LocalizationKit in WhereUITests extraPackageProducts. Co-authored-by: Cursor <cursoragent@cursor.com>
Add WhereCore LocalizedStrings.swift and a thin .module wrapper; convert Region.localizedName, notification schedulers, DailySummaryReconciler, and BackupService to resolve via LocalizedString. Add LocalizationKit as a WhereCore dependency; reconcile the WhereCore catalog via ./localize. Co-authored-by: Cursor <cursoragent@cursor.com>
Update root AGENTS.md (LocalizationKit in targets table, factory split in keeping-localization-in-sync, WhereWidgets follow-up) and Where/AGENTS.md (WhereUI + WhereCore localization sections, LocalizationKit links). Co-authored-by: Cursor <cursoragent@cursor.com>
kyleve
commented
Jun 27, 2026
| /// read the locale from. Prefer them over `someString.localized` at call | ||
| /// sites. | ||
| public func navigationTitle( | ||
| _ title: LocalizedString, |
Owner
Author
There was a problem hiding this comment.
Let's change this to match .navigationTitle(localized: ...) like Text(localized: ...).
kyleve
commented
Jun 27, 2026
| } | ||
|
|
||
| /// Set the accessibility label from a deferred ``LocalizedString``. | ||
| public func accessibilityLabel( |
Owner
Author
There was a problem hiding this comment.
Let's change this to match .navigationTitle(localized: ...) like Text(localized: ...).
kyleve
commented
Jun 27, 2026
| } | ||
|
|
||
| /// Set the accessibility hint from a deferred ``LocalizedString``. | ||
| public func accessibilityHint( |
Owner
Author
There was a problem hiding this comment.
Let's change this to match .navigationTitle(localized: ...) like Text(localized: ...).
kyleve
commented
Jun 27, 2026
| /// the repo's `./localize` script can statically find every key. Adding a | ||
| /// new region case is intentionally a compile error here until you add a | ||
| /// matching ``LocalizedStrings/Region`` member and catalog entry. | ||
| public var localizedName: String { |
Owner
Author
There was a problem hiding this comment.
Change this to be a func localizedName(with config: LocalizationConfig? = nil) that's passed through to each string.
Add LocalizedStrings.swift and a thin .module wrapper to LifecycleKit (failure.launch.* in LifecycleFailureView) and WhereWidgets (gallery picker copy). Replace WidgetStrings with LocalizedStrings; move the WhereWidgets catalog to Sources/Resources so ./localize can discover it. Wire LocalizationKit into both targets and update module docs. Co-authored-by: Cursor <cursoragent@cursor.com>
Abort extraction when the same catalog key appears twice in one LocalizedStrings.swift, with context for both call sites, instead of silently keeping the last occurrence. Co-authored-by: Cursor <cursoragent@cursor.com>
Bring in the Primary tab year calendar (PresenceCalendar, CalendarView, session calendar plumbing) from main, resolve conflicts against the LocalizationKit migration, and port new strings to LocalizedStrings. Includes Xcode-extracted catalog comments and reconciled xcstrings entries. Co-authored-by: Cursor <cursoragent@cursor.com>
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
Makes Swift source the single source of truth for user-facing strings and adds a build-free
./localizescript that reconciles.xcstringscatalogs on every commit. Framework tooling lives in a new LocalizationKit module; WhereUI and WhereCore each keep their ownLocalizedStrings.swiftcatalog plus a thin.modulewrapper.LocalizationKit(new):LocalizedString,LocalizationConfig, generic.catalog(_:_:bundle:)factory (+ config-aware closure overload),Text(localized:), andViewoverloads fornavigationTitle/accessibilityLabel/accessibilityHint. Extracted from StuffCore/WhereUI; StuffCore reverts to a version placeholder.LocalizedStringsconvention: nested enums whose members return deferredLocalizedStringvalues; parameter-less members are cachedstatic lets (Sendable). WhereUI uses leading-dot syntax at SwiftUI call sites (e.g.Text(localized: .primary.emptyDescription)); plain-String sites keepLocalizedStrings.<Category>.<member>.localized../localize: Ruby script (no Tuist/build) that auto-discovers everyLocalizedStrings.swiftand reconciles the siblingLocalizable.xcstrings— add missing keys, prune orphans, update simple English defaults, preserve plurals/translations. Wired into pre-commit (--git-add) and CI (--lint).WhereCore/Sources/LocalizedStrings.swift(including plural/parameterized summary strings).Region.localizedNamestaysStringfor unchanged call sites.AGENTS.md,Where/AGENTS.md, and per-module README/AGENTS for LocalizationKit and StuffCore.Out of scope (follow-ups)
WidgetStrings.swift) still uses rawString(localized:).Test plan
./localize --lint(WhereUI + WhereCore catalogs)./swiftformat --linttuist test LocalizationKitTeststuist test StuffCoreTeststuist test WhereUITeststuist test WhereCoreTestsmacos-26