Skip to content

Adopt String Catalog symbol generation across the app#124

Open
kyleve wants to merge 8 commits into
mainfrom
cursor/string-catalog-symbols
Open

Adopt String Catalog symbol generation across the app#124
kyleve wants to merge 8 commits into
mainfrom
cursor/string-catalog-symbols

Conversation

@kyleve

@kyleve kyleve commented Jul 23, 2026

Copy link
Copy Markdown
Owner

What

Redo of the closed #79: adopt Xcode's built-in String Catalog symbol generation across the app and make every string reference type-safe, on top of current main. The hand-maintained string facades (Strings.swift ~2,455 lines, ShareStrings, WidgetStrings) are gone; user-facing copy now resolves through generated LocalizedStringResource symbols, so a typo'd or removed key is a compile error instead of a runtime fallback.

This is a fresh redo rather than a rebase of #79: main reorganized WhereUI so heavily since then (≈100 files moved/added — Secondary/Elsewhere/, new Manual/, Regions/, Year/, etc.) that a merge produced 26 conflicts and none of #79's per-file conversions survived. See #79 for the original discussion.

Stacked on #123 (the Xcode 27 CI move). Symbol generation for SwiftPM package targets is only reliable on Xcode 27, which is why that lands first; this PR targets cursor/ci-xcode-27 and will retarget to main once #123 merges.

How it works

  • STRING_CATALOG_GENERATE_SYMBOLS = YES is set project-wide in Project.swift for the Tuist-native app/extension targets. The SwiftPM library targets (WhereUI, WhereCore, LifecycleKit) generate symbols automatically from the toolchain.
  • A manual catalog key some.key generates a .someKey symbol on LocalizedStringResource, referenced directly: Text(.tabYear), String(localized: .commonOk).
  • Composition/plural/switch/formatting logic lives in small helpers that compose the symbols — new WhereFormat (WhereUI) and the rewritten IntentStrings (WhereIntents) — not a raw-key facade.

Commits (one per module, bisectable)

  1. Groundwork — enable the build setting.
  2. LifecycleKitLifecycleFailureView uses .failureLaunchTitle / .failureLaunchRetry.
  3. WhereCore — reminder/summary/data-issue notification + backup-error strings; the two String(format:) compositions collapse into type-safe symbol calls (output unchanged).
  4. WhereShareExtension / WhereWidgets — drop ShareStrings / WidgetStrings for the extensions' own generated symbols; normalize their catalogs stalemanual.
  5. WhereUI — delete the 2,455-line Strings facade; convert ~412 call sites across 52 files; add WhereFormat for the 45 logic-bearing strings; normalize the catalog (drop 2 extraction artifacts, all keys → manual, materialize 20 keys that only existed via defaultValue); replace StringsTests with WhereFormatTests.
  6. WhereIntents — rewrite IntentStrings to compose generated symbols (API unchanged, call sites untouched); normalize the 23 dialog keys → manual.
  7. Docs — update the localization guidance to the symbol workflow.

Not converted (by design)

  • RegionKit — region names are data-driven now (regions.json localizationKey, resolved dynamically), a documented trade-off in its AGENTS.md. Adopt Xcode String Catalog symbol generation across the app #79 converted an enum switch that no longer exists; converting it back would fight the current architecture.
  • App Intents static metadata (intent/parameter/entity titles) stays literal — the framework requires compile-time-constant literals and extracts them itself.

Verification

  • ./swiftformat --lint clean.
  • Full Stuff-iOS-Tests scheme green (all bundles) under Xcode 27 on iPhone 17 / iOS 27.0.
  • All 361 WhereUI catalog keys generate symbols (verified against the generated symbols file); every former Strings./ShareStrings./WidgetStrings. call site is converted (0 residual references).

kyleve added 8 commits July 22, 2026 21:08
Turn on Xcode's type-safe String Catalog codegen so catalog-backed targets
generate LocalizedStringResource symbols — the first step of redoing the
symbol-generation migration (supersedes #79) on current main.

- Project.swift: set STRING_CATALOG_GENERATE_SYMBOLS=YES in the project base
  settings so the Tuist-native app/extension targets (Where, WhereWidgets,
  WhereShareExtension) generate symbols. The SwiftPM package targets
  (WhereUI, WhereCore, RegionKit, LifecycleKit) generate symbols
  automatically from the toolchain and need no setting.

No behavior change; existing String(localized:) call sites are untouched.
Per-module catalog normalization + call-site conversion follow.
LifecycleFailureView reads the generated .failureLaunchTitle / .failureLaunchRetry
LocalizedStringResource symbols instead of raw "failure.launch.*" keys with
bundle: .module. Both keys are already manual in the module catalog, so the
symbols generate; a removed/renamed key becomes a compile error.
Reminder / daily-summary / data-issue-alert notification copy and the two
BackupError descriptions now resolve through generated LocalizedStringResource
symbols instead of raw keys with bundle: .module. The two String(format:)
compositions (summary.notification.regionDays '%1$@ in %2$@', and
backup.error.unsupportedFormatVersion '%lld') collapse into type-safe symbol
calls (.summaryNotificationRegionDays(count, region.localizedName),
.backupErrorUnsupportedFormatVersion(version)); plurals become symbol calls
carrying the count (.summaryNotificationDayCount, .dataIssuesNotificationBody).
Output is unchanged. All keys are already manual in the module catalog.
Delete the ShareStrings facade; ShareEvidenceView references the extension's
generated LocalizedStringResource symbols directly (Text/Label/Button take the
bare symbol; other call sites use String(localized: .symbol)). The
attachment-header singular/plural picks between the two catalog keys inline.
Normalize the catalog keys from stale -> manual so the symbols generate.
Delete the WidgetStrings facade; the two widgets' gallery name/description
resolve through the extension's generated symbols via String(localized:).
Normalize the catalog keys from stale -> manual so the symbols generate.
Delete the 2,455-line hand-maintained Strings facade. Simple call sites now
resolve through Xcode's generated LocalizedStringResource symbols directly
(String(localized: .symbol)); the composition/plural/switch cases move to a new
WhereFormat helper that composes those symbols. A removed or renamed catalog key
is now a compile error instead of a runtime fallback.

- Add WhereFormat.swift for the 45 logic-bearing strings (years formatted
  grouping-free, plurals, enum switches, composed accessibility labels, and the
  non-catalog coordinate/measurement formatters).
- Convert ~412 call sites across 52 files from Strings.* to
  String(localized: .symbol) / WhereFormat.*.
- Normalize the WhereUI catalog so every key generates a symbol: drop two
  extraction artifacts (empty key, "%lld"), set all keys to manual, and
  materialize 20 keys that previously existed only via defaultValue (settings
  search keywords, group headers, etc.). The large catalog diff is a re-sort +
  extractionState normalization, not content changes.
- Replace StringsTests with WhereFormatTests covering the WhereFormat logic.

RegionKit is intentionally untouched: its region names are data-driven via
regions.json localizationKeys (a documented trade-off), not an enum switch.
Rewrite IntentStrings to compose the module's generated LocalizedStringResource
symbols instead of raw "dialog.*" keys with inline defaultValues; its public
API is unchanged, so the intents' call sites are untouched. Normalize the 23
dialog keys the helper uses from (none) -> manual so the symbols generate; the
App Intents static-metadata keys (framework-extracted literals) stay as-is.
Update the localization guidance to the generated-symbol workflow (add a manual
catalog key, reference its .symbol; compose logic in WhereFormat / IntentStrings)
and drop references to the deleted Strings/ShareStrings/WidgetStrings facades
across the feature + module AGENTS/READMEs, MODULE_AUDIT, and TODOs (the
code-gen-the-strings item is now done).
@kyleve
kyleve force-pushed the cursor/string-catalog-symbols branch from c644fca to d1c9fd5 Compare July 23, 2026 04:08
@kyleve
kyleve changed the base branch from cursor/ci-xcode-27 to main July 23, 2026 04:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant