Summary
The regular-string String Catalog fold treats each (key, locale) cell's stored English as the record of what its translation was made for. If an explicit-key string's English is reworded in place (the key stays, only value: changes), xcstringstool sync silently keeps the old translations — now stale for the new text — and the fold can't tell a translation of the old English from a current one.
We've decided localization keys are immutable: rewording requires a new key. generate_strings_catalog now hard-fails on a reworded key (CatalogHelper.reworded_keys → enforce_immutable_source_keys in fastlane/lanes/localization_catalog.rb).
The gap this issue tracks
That check compares freshly-extracted English against the persisted catalog, which works for the local manual lane but not in CI: Localizable.xcstrings is gitignored and generated transiently, so CI has no prior English to diff against. A reword can therefore merge without CI catching it.
Proposal
Add a CI lint on the committed English .strings — the one cross-run baseline we do keep (it's the GlotPress source of truth): fail when an existing key's value changes (same key, different English).
- A key-as-source reword shows up in the
.strings diff as a remove + add → passes (that key genuinely changed).
- An explicit-key reword shows up as same-key / changed-value → fails, with a message to mint a new key.
This is what makes immutable keys enforced on every PR, not only where the catalog persists.
Escape hatch
Hard-fail everywhere for now, no exceptions. If the friction on cosmetic edits (typo fixes) proves burdensome, add an opt-in ack later — a Reword: commit trailer, or a PR-comment + review gate — that flips reconcile from fail to its keep-and-flag (needs_review) path. The placeholder-format gate runs regardless, so an ack could only ever let through prose drift, never a runtime crash.
References
- Enforcement today:
CatalogHelper.reworded_keys (fastlane/lanes/catalog_helper.rb), enforce_immutable_source_keys (fastlane/lanes/localization_catalog.rb).
- Rationale + the
human ?? AI ?? English contract and immutable-key note: docs/localization-pipeline.md.
Summary
The regular-string String Catalog fold treats each
(key, locale)cell's stored English as the record of what its translation was made for. If an explicit-key string's English is reworded in place (the key stays, onlyvalue:changes),xcstringstool syncsilently keeps the old translations — now stale for the new text — and the fold can't tell a translation of the old English from a current one.We've decided localization keys are immutable: rewording requires a new key.
generate_strings_catalognow hard-fails on a reworded key (CatalogHelper.reworded_keys→enforce_immutable_source_keysinfastlane/lanes/localization_catalog.rb).The gap this issue tracks
That check compares freshly-extracted English against the persisted catalog, which works for the local manual lane but not in CI:
Localizable.xcstringsis gitignored and generated transiently, so CI has no prior English to diff against. A reword can therefore merge without CI catching it.Proposal
Add a CI lint on the committed English
.strings— the one cross-run baseline we do keep (it's the GlotPress source of truth): fail when an existing key's value changes (same key, different English)..stringsdiff as a remove + add → passes (that key genuinely changed).This is what makes immutable keys enforced on every PR, not only where the catalog persists.
Escape hatch
Hard-fail everywhere for now, no exceptions. If the friction on cosmetic edits (typo fixes) proves burdensome, add an opt-in ack later — a
Reword:commit trailer, or a PR-comment + review gate — that flips reconcile from fail to its keep-and-flag (needs_review) path. The placeholder-format gate runs regardless, so an ack could only ever let through prose drift, never a runtime crash.References
CatalogHelper.reworded_keys(fastlane/lanes/catalog_helper.rb),enforce_immutable_source_keys(fastlane/lanes/localization_catalog.rb).human ?? AI ?? Englishcontract and immutable-key note:docs/localization-pipeline.md.