Skip to content

Emit a localized Android What's New title (#878) - #951

Merged
ryanbr merged 3 commits into
mainfrom
fix/changelog-gen-localized-title
Jul 30, 2026
Merged

Emit a localized Android What's New title (#878)#951
ryanbr merged 3 commits into
mainfrom
fix/changelog-gen-localized-title

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Closes #878, from @pipiche38's write-up — the diagnosis and the proposed key scheme are theirs.

The problem, reproduced before fixing

appchangelog-gen.py wrote the Kotlin title as a raw literal. Compose has no auto-extraction, so the
i18n gate rejects it — and because that gate audits the whole tree, one generated line red-checks
every open PR on code none of them touched. It happened on 9.2.0 and again on 9.2.1, cleared by hand
both times.

Confirmed rather than assumed: reverting 9.2.1's title to a literal gives

FAIL 1 NEW hardcoded literal(s): android/.../AppChangelog.kt:42

Only the title ever failed. Items are long-form prose the gate doesn't require extracting, and Swift is
unaffected because SwiftUI auto-extracts — so both keep their literals.

The fix

The generator now emits title = uiString(R.string.<key>) and writes the string itself, keyed by the
repo's existing scheme (first six slugged words + sha1(title)[:8]). Verified against the key already
shipping
for 9.2.1, and a test pins it to that live artifact — so a drift in the hashing or the
slugging fails, rather than quietly minting a key nothing translates.

Translations come from an optional whatsnew.title_locales block in the release notes front-matter.

The trade I want checked

A locale with no translation gets the English title, plus a named warning. The alternative — leaving
the key out of that locale — fails the very gate this exists to prevent. An English title on a German
card is visible and fixable; a red main after every release, silently, because release pushes use
GITHUB_TOKEN and trigger no CI, is neither. Say if you'd rather it hard-fail instead; it is one line.

Verified

Ran it end to end against a synthetic release file: correct key, correct uiString reference, strings
written to all six locale files, apostrophes escaped for aapt2, warning naming the three locales I left
out — and i18n_audit.py --ci passes on the result, which is the whole point. Reverted those artifacts;
this PR touches only the tooling.

14 new tests, all passing.

One thing this PR also fixes about #943

That PR's Tools/ step ran test_i18n_audit by name, so these 14 tests would have been added and
never run — the exact hole #943 was meant to close. Switched to discover, floor raised to 45.

ryanbr added 3 commits July 29, 2026 12:44
appchangelog-gen wrote the Kotlin title as a raw literal, which the i18n gate
rejects. Because that gate audits the whole tree, one generated line red-checked
every open PR on code none of them touched. It happened on 9.2.0 and again on
9.2.1, and both times someone cleared it by hand afterwards.

The generator now emits `title = uiString(R.string.<key>)` and writes the string
itself, using the repo's existing scheme: the first six slugged words plus
sha1(title)[:8]. Verified against the 9.2.1 key that is already shipping, and a
test pins it there so a drift in either the hashing or the slugging fails rather
than mints a key nothing translates.

Translations come from an optional `whatsnew.title_locales` block in the release
notes front-matter. A locale with no entry falls back to the English title and is
named in a warning. That trade is deliberate: leaving the key out of a locale
fails the same gate this exists to prevent, and an English title on a German card
is visible and fixable where a red main after every release is neither.

Swift is untouched — SwiftUI auto-extracts, so its literal title is correct.
Items stay literals on both platforms; only the title ever failed.

Reproduced the failure first (reverting 9.2.1's title to a literal gives "FAIL 1
NEW hardcoded literal at AppChangelog.kt:42") and confirmed generated output now
passes the audit end to end.

Also switched the Tools/ CI step from a named module to discover. It ran
test_i18n_audit by name, so this PR's 14 new tests would have been added and
never run — the exact hole #943 was meant to close.
CI caught this: the new tests import appchangelog-gen to exercise title_key and
esc_xml, and the module did `import yaml` at load, so on a clean runner with no
PyYAML the import failed and the suite errored. It passed locally only because
PyYAML happens to be installed here.

Moved the import into frontmatter(), the only function that needs it, with the
same failure message. The pure helpers now import on any Python. Proven by
running the suite with yaml blocked from sys.meta_path: 14 tests, 0 errors.

The alternative was pip-installing PyYAML in the workflow, as fork-release.yml
does. Not needing the dependency at all is better than installing it to test
functions that never touch it.

Note the count floor did its job here — 37 collected against a floor of 45 is
what turned an import error into a red step rather than a quiet pass.
Found by doing the obvious thing: generate, edit the headline, re-run.

apply() skipped an entry that already existed for that version, while
write_title_strings ran unconditionally. So an edited headline minted a NEW key
and wrote it into all six locale files, while the entry kept referencing the OLD
one. The card showed the previous headline, the new key sat orphaned, and nothing
failed — a silent wrong answer, which is worse than the red gate this PR is
fixing.

apply() now takes the platform's rendered title line and re-applies it to an
existing entry, on both platforms so Kotlin and Swift cannot drift apart. A
re-run with an unchanged headline is a no-op on the entry and still refreshes the
constant, as before.

Three regression tests. One of them was wrong on the first attempt — it asserted
the whole file was byte-identical, when apply() legitimately rewrites the version
constant every run. It now asserts the title line, which is the actual invariant.
@ryanbr
ryanbr merged commit b9a4126 into main Jul 30, 2026
3 checks passed
@ryanbr
ryanbr deleted the fix/changelog-gen-localized-title branch July 30, 2026 00:50
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.

appchangelog-gen.py should emit a localized Android What's New title — a raw literal fails i18n on every release and red-checks all open PRs

1 participant