Skip to content

chore(l10n): remove proven-orphan keys and relabel live stale keys in both catalogs - #1093

Merged
wei18 merged 1 commit into
mainfrom
chore/1086-l10n-stale-keys
Sep 15, 2026
Merged

wei18 merged 1 commit into
mainfrom
chore/1086-l10n-stale-keys

Conversation

@wei18

@wei18 wei18 commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

Both catalogs are now at extractionState: stale = 0. scan:l10n gates that count, so an unproven "orphan" label cannot come back.

  • Sudoku: deleted 14 keys, each proven unreferenced by the three-check proof below. Relabelled 37 live keys stale → manual.
  • Minesweeper: deleted nothing. Relabelled 5 keys stale → manual.
  • Proof change: 6 of the issue's 20 Group A keys turned out to be live and moved to Group B:
    • 2 are referenced through string interpolation: Elapsed time %@, Solved in %@.
    • 4 are built by ASCRegister, not app code: gc.minesweeper.leaderboard.{easy,medium,hard}.daily.title, iap.minesweeper.remove_ads.description. These 4 now carry a catalog comment saying they are read by ASCRegister and must not be deleted.
  • The fixture was regenerated only via mise run gen:l10n_fixture.
  • Both catalogs were edited by text splice (no JSON round-trip). A JSON verifier checks the result before and after.

Closes #1086
part of #1014 · follow-up of #1025 / #1085

Group A proof (run at 728ef191 = origin/main)

How each check was run:

  • check1: rg -F on the literal text around each format specifier or digit.
  • check2: family regexes: error\.\\\(, \\\(x\)\.(title|body|action)", gc\.minesweeper, leaderboard\.\\\(, \\\(x\)\.daily, "iap\.\\\(, \\\(x\)\.description".
    • Also localized.*\\\( in error/toast/Game Center/alert/banner paths: 0 hits.
    • Also an inventory of every non-literal lookup in App/ and Packages/: LocalizedStringKey(var), LocalizationValue(var), LocalizedStringResource(stringLiteral:), localizedString(forKey:).
    • That inventory resolves only to difficulty.rawValue.capitalized (Easy/Medium/Hard), Minesweeper nameKey (Beginner/Intermediate/Expert), and UserFacingError.messageKey = error.userFacing.<rawCode>.body (TelemetryKit/UserFacingError.swift:50).
  • check3: rg -F --no-ignore --hidden over the whole repo, excluding only .git/, .build/ and __Snapshots__/ (1445 files). Run both case-sensitive and -i. Self-definitions (the 2 catalogs and the fixture) are not counted.
key check 1 (interpolation) check 2 (dynamic construction) check 3 (whole repo, non-Swift) verdict
Elapsed time %@ HIT SudokuUI/Board/BoardView+AccessibilityHeader.swift:103 .accessibilityLabel("Elapsed time \(elapsedLabel)") n/a 0 → Group B (relabel)
Solved in %@ HIT SudokuUI/Completion/CompletionView.swift:53 Text("Solved in \(elapsedLabel)") n/a 0 → Group B (relabel)
gc.minesweeper.leaderboard.{easy,medium,hard}.daily.title (3) n/a HIT ASCRegister Config.swift:93 titleKey: "\(keyPrefix).\(difficulty).daily.title", with keyPrefix gc.minesweeper.leaderboard (:55) and difficulties easy/medium/hard (:88). main.swift expectedXCStringsKeys requires them for validate/plan/apply --xcstrings <catalog> --app minesweeper HIT ASCRegister/Strings/gc-strings.xcstrings.patch:75/86/97 (paste-in source) → Group B (relabel + comment)
iap.minesweeper.remove_ads.description n/a HIT Config+Types.swift:201 "iap.\(shortId).description", with shortId minesweeper.remove_ads (per ConfigConsistencyTests:165). Required by expectedXCStringsKeys. docs/app-store/metadata/minesweeper/iap/remove-ads.yaml:20 names the Minesweeper catalog as the source 0 literal → Group B (relabel + comment)
error.gameCenter.not_authenticated.{action,title}, error.persistence.icloud_unavailable.{action,body,title}, error.puzzleStore.generator_failed.{action,body,title} (8) n/a 0. There is no error.\( builder anywhere; the only builder produces error.userFacing.*. L10nTests.swift:53 (errorKeysFollowConvention) filters catalog keys by prefix, and the set stays non-empty because the 5 error.userFacing.*.body keys remain. not_authenticated and icloud_unavailable appear only in comments (L10nTests.swift:55-56) 0 (-F and -i) delete
3 puzzles today puzzles today: 16 hits, all docs/designs, docs/v1 mockup, docs/test-plans, DesignPreviewKit same hits same 16, all docs/design delete
Account / language n/a docs only (SettingsView.swift:1 comment "Account / Statistics / …" is different text) 3, docs delete
Global / friends n/a docs/meetings only 3, docs delete
Mixed difficulty pool n/a docs only 11, docs/design delete
Global n/a no rawValue→key path. Only exact literal: DesignPreviewKit LeaderboardView_Designs.swift:12 case-sensitive: 8, all docs/meetings/design. -i: 87, all .global scopes and prose delete
Completed n/a no rawValue→key path (LivePrivateCKGateway "completed" is a CloudKit query field) exact-literal forms ("Completed", 'Completed', >Completed<, : Completed): 0. Bare -F: 904, all identifiers and prose (isCompleted, seedCompletedDaily, GC achievement copy "Completed your first…") delete

DesignPreviewKit (docs/designs/code) is a standalone package with its own Package.swift. Nothing outside docs/ and meetings/ references it (rg: 0 hits).

Evidence

Splice verifier. It asserts that the removed set equals the list, and that every other entry is identical except extractionState; comment may additionally change only on the 4 ASC keys.

App/Sudoku/Resources/Localizable.xcstrings: blocks=192 delete=14 relabel=37 comment=0 keys 193->179 stale_after=0 lines 9277->8618 verified
App/Minesweeper/Resources/Localizable.xcstrings: blocks=167 delete=0 relabel=5 comment=4 keys 168->168 stale_after=0 lines 8086->8090 verified

Independent base-vs-head check. It compares parsed key sets and per-entry field diffs, git show origin/main:<path> against the working tree:

== App/Sudoku/Resources/Localizable.xcstrings
  keys 193 -> 179; removed 14; added 0; relabel stale->manual 37; comment added 0; other changes 0; top-level fields unchanged True
== App/Minesweeper/Resources/Localizable.xcstrings
  keys 168 -> 168; removed 0; added 0; relabel stale->manual 5; comment added 4; other changes 0; top-level fields unchanged True
fixture byte-identical to shipping Sudoku catalog

Heads-up: git diff shows +245 insertions because git re-anchors hunks after large block deletions. The parsed diff above is the source of truth.

Stale count and duplicate-key check:

App/Sudoku/Resources/Localizable.xcstrings stale=0 dup=[]
App/Minesweeper/Resources/Localizable.xcstrings stale=0 dup=[]

mise run scan:l10n:

L10n gate passed: all 2 catalogs 0 placeholders, 0 stale, all 7 locales complete.

Negative proof of the new stale check. One Minesweeper key was set back to stale, the gate ran, and the file was restored:

FAIL [Minesweeper]: 1 extractionState "stale" entr(y/ies) in …/App/Minesweeper/Resources/Localizable.xcstrings — delete (proven orphan) or relabel to "manual" (live)
L10n gate FAILED.
scan exit=1 (expect 1)
restored byte-identical
L10n gate passed: all 2 catalogs 0 placeholders, 0 stale, all 7 locales complete.
scan exit=0 (expect 0)

swift test --package-path Packages/SudokuKit --filter L10n:

􁁛  Test allUserFacingStringsHaveEnAndZhTW() passed after 0.024 seconds.
􁁛  Test all7LocalesPresent() passed after 0.028 seconds.
􁁛  Test errorKeysFollowConvention() passed after 0.029 seconds.
􁁛  Suite "Localizable.xcstrings — en + zh-Hant seed (Phase 9.4)" passed after 0.029 seconds.
􁁛  Test noUntranslatedMarkers() passed after 0.031 seconds.
􁁛  Suite "Localizable.xcstrings — all 7 locales (Phase 9.5)" passed after 0.031 seconds.
􁁛  Test run with 4 tests in 2 suites passed after 0.031 seconds.
swift test exit=0

Notes

  • The PM suggested file:line references for the ASC-key comments. The comments name symbols instead (Config.leaderboards(for: .minesweeper) titleKey, IAPProduct.descriptionKey), so they stay accurate when line numbers move.
  • Files not touched: Packages/AppMonetizationKit, Packages/GameAppKit, Project.swift, any Package.swift, TodayTabHost, LiveRouteFactory*.

🤖 Generated with Claude Code

… both catalogs

Sudoku catalog: delete 14 keys, each proven unreferenced by the three-check
proof (interpolated form, dynamic key construction, whole-repo non-Swift
consumers). Relabel 37 live keys from `extractionState: stale` to `manual`,
including `Elapsed time %@` and `Solved in %@`, which are referenced by
string interpolation.

Minesweeper catalog: relabel 5 keys to `manual`. Four of them
(`gc.minesweeper.leaderboard.*.daily.title`,
`iap.minesweeper.remove_ads.description`) are built by ASCRegister rather
than app code, so they now carry a do-not-delete comment.

Regenerate the SudokuAppCompositionTests fixture via `gen:l10n_fixture`.
`scan:l10n` now fails on any `stale` entry, so an unproven orphan label
cannot come back.

Closes #1086
Part of #1014

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@wei18

wei18 commented Sep 15, 2026

Copy link
Copy Markdown
Owner Author

PM review record: dual CR + fresh-context acceptance; merging at 821b4f90.

  • CR (sonnet), independent re-proof: all 14 deleted Sudoku keys re-checked three ways at origin/main (literal text before any specifier; inventory of every non-literal lookup, where the only runtime builder is error.userFacing.<rawCode>.body and none of its five codes produces a deleted key; whole-repo search with hits classified). 0 live hits. Parsed JSON diff: exactly 37 + 5 extractionState stale→manual and 4 comment additions on the ASC keys, no other field changed; 0 stale, 0 duplicate keys; fixture byte-identical. Gate: the stale pattern matches Xcode's actual "extractionState" : "stale" spacing with no false positives; run at head exit 0, one key flipped to stale exit 1, restored exit 0. SudokuKit --filter L10n 4/4.
  • Acceptance (haiku): ACCEPT on done-when 1–6. Two statements in that report were corrected by the PM from code before merge: the three-check table has 11 grouped rows covering all 20 original keys (not 10), and the Minesweeper daily-title prefix is gc.minesweeper.leaderboard (Config.swift:55; the report had quoted Sudoku's back-compat default at Config+Types.swift:42). expectedXCStringsKeys (main.swift:344-364) requires those 3 titles and the IAP description, so keeping them is correct.
  • The widened proof moved 6 of the 20 issue-listed orphans back to live: 2 interpolated (Elapsed time %@, Solved in %@), 4 built by ASCRegister. The 4 pipeline keys now carry catalog comments naming their reader.

@wei18
wei18 merged commit c2959d0 into main Sep 15, 2026
7 checks passed
@wei18
wei18 deleted the chore/1086-l10n-stale-keys branch September 15, 2026 10:38
wei18 added a commit that referenced this pull request Sep 15, 2026
Merge origin/main (c2959d0) into session/brisk-stork-isio. Main advanced
by two commits after the first #1080 merge (68f840a):
- aeb80f0 test(sudoku): freeze the session clock in the Ready-window test (#1092)
- c2959d0 chore(l10n): remove proven-orphan keys and relabel live stale keys in both catalogs (#1093)

A read-only `git merge-tree` dry run was clean. The five files main touched
(both app catalogs, the SudokuAppCompositionTests L10n fixture,
BoardLeaveOrPauseStateTests.swift, mise-tasks/scan/l10n) do not overlap with
any file this branch changes.

L10n check: this branch adds no localized string literals. None of the 14
Sudoku keys #1093 removed is referenced by branch-added code.

part of #1024

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

chore(l10n): remove remaining orphan keys and relabel mislabelled stale keys in both catalogs

1 participant