Skip to content

chore(i18n): make config/locales.tsv the one language list (#262) - #335

Merged
jeiel85 merged 2 commits into
mainfrom
claude/locale-manifest-262
Aug 24, 2026
Merged

chore(i18n): make config/locales.tsv the one language list (#262)#335
jeiel85 merged 2 commits into
mainfrom
claude/locale-manifest-262

Conversation

@jeiel85

@jeiel85 jeiel85 commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Four items from the v2.33.0 section of the hardening tracker (#262), all one shape: the language list was copied into seven places, so a new language could ship with most of its surfaces ungated.

The list

config/locales.tsv is now the one list. Four whitespace-separated columns — code, store, source, starter — with the filename rules written in its header. It is a plain table rather than JSON because the consumers are Kotlin, Gradle, PowerShell and a shell one-liner, and none of them should need a parser dependency to read eight rows.

Consumers updated to read it:

Was Now
ResourceParityTest — two hardcoded lists LocaleManifest.translated / .hasStarterNotes
UntranslatedStringTestLOCALES and a LANGUAGE_COINCIDENCES row per locale LocaleManifest.translatedCodes; an absent coincidence list now means no exemptions
noteLocales in app/build.gradle.kts reads the manifest
$StoreLocales ×2, four lists in verify-landing-versions.ps1 scripts/locales.ps1
six grep -q '^<xx-XX>$' lines in .gitlab-ci.yml one awk loop over the manifest

The gate

A list alone closes half of it — a declared language whose files are missing is still silent. scripts/verify-locales.ps1 compares the manifest against the files on disk in both directions, across app resources, starter notes, fastlane store directories, landing pages, privacy pages, READMEs and demo clips. It runs in build on every push and PR, and in GitLab's verify_release_docs.

It also checks the language counts written into AGENTS.md, docs/RELEASE.md and docs/assets/README.md, and compares AGENTS.md's (en · ko · …) list as a set. Prose counts are the one thing that cannot be derived from the list, which is why they had been stale through two additions.

The three items that fall out of it

  • Nothing checked that a landing language has a privacy page. They carry no version string, so verify-landing-versions.ps1 never walked them; a language added without its privacy translation gave two dead links per page. Removing docs/privacy.hr.html now fails the gate by name.
  • .gitlab-ci.yml still grepped for six locale blocks — missing zh-CN since Create strings.xml for Chinese localization #294 and hr-HR since Create Croatian (hr) translation files #329. The job is gated behind GITLAB_RELEASE_FROM_CI and nothing runs it, which is why the drift went unnoticed; it would have passed the wrong thing the moment anyone turned it on. The path is the documented fallback for GitHub being unavailable, so the list is derived rather than the job deleted.
  • verify-release-export.ps1 reported "여섯 로케일 블록" whatever the count was. The assertion iterated the real list and was correct; the OK line beneath it said "six" through two additions. It now prints the count and the locales it checked.

One thing the change turned up

The manifest is declared as an input of the unit test tasks. Without it, editing config/locales.tsv alone left testDebugUnitTest UP-TO-DATE, so the tests kept their previous verdict. CI checks out clean and would never have seen it; the person adding a language would have.

Verification

  • Dropped the hr row → 12 failures naming every hr file across every surface, plus all four count checks.
  • Declared a language with no files → every surface fails with the path it wanted.
  • Deleted docs/privacy.hr.html → the exact item-C case fails by name.
  • Both resource tests fail on the same drift (4/4) once the manifest is a task input, and pass again when it is restored.
  • ./gradlew test (all variants) green; verify-locales.ps1, verify-landing-versions.ps1, verify-release-notes.ps1 green; verify-release-export.ps1 green against the real v2.33.0 artifacts in D:\Build, now printing 8개 로케일 블록이 모두 있습니다: ko-KR, en-US, ….
  • The Gradle snippet was executed against the manifest to confirm it yields the same eight store tags in the same order as the list it replaces.

Tracker: #262 (v2.33.0 section, four of five items).

🤖 Generated with Claude Code

The language list lived in seven places — `ResourceParityTest` twice,
`UntranslatedStringTest` twice, `noteLocales` in `app/build.gradle.kts`,
`$StoreLocales` in two verify scripts, four lists in a third — plus counts in
prose. A language missing from one of them was not a review problem: the
surfaces it was missing from are exactly the surfaces nothing looks at, so it
shipped untested and green. Chinese (#294) and Croatian (#329) both arrived
that way, and both were caught by counting by hand.

`config/locales.tsv` is now the list, and every consumer reads it: both
resource tests through `LocaleManifest`, the store locales in the release
export, `verify-release-notes.ps1`, `verify-release-export.ps1` and
`verify-landing-versions.ps1` through `scripts/locales.ps1`, and the
release-notes assertion in `.gitlab-ci.yml` through one `awk` line. The format
is a four-column table for exactly that reason — Kotlin, Gradle, PowerShell and
a shell one-liner all read it without a dependency.

The list alone only closes half of it, so `scripts/verify-locales.ps1` compares
the manifest against the files on disk in both directions — a declared language
whose files are missing, and a file whose language is not declared — across app
resources, starter notes, fastlane store directories, landing pages, privacy
pages, READMEs and the demo clips. It runs in `build` on every push and pull
request, and in GitLab's `verify_release_docs`.

Three of the tracker's other items fall out of the same change:

- **Nothing checked that a landing language has a privacy page.** Privacy pages
  carry no version string, so `verify-landing-versions.ps1` never walked them,
  and a language added without its translation gave two dead links per page.
  The new gate walks them with everything else; deleting `privacy.hr.html`
  fails it by name.
- **`.gitlab-ci.yml` still grepped for six locale blocks.** It had been missing
  `zh-CN` since #294 and `hr-HR` since #329, unnoticed because the job is gated
  behind `GITLAB_RELEASE_FROM_CI` and nothing runs it — a gate that would pass
  the wrong thing the moment anyone turned it on. The path is kept (it is the
  documented fallback for GitHub being unavailable), so the list is derived
  rather than the job deleted.
- **`verify-release-export.ps1` reported "여섯 로케일 블록" whatever the count
  was.** The assertion was right and the OK line said "six" through two
  additions. It now prints the count and the locales it actually checked.

Prose counts are the one thing that cannot be derived, so the gate checks those
too: `AGENTS.md`, `docs/RELEASE.md` and `docs/assets/README.md` are read for
their language counts and compared with the manifest, and `AGENTS.md`'s
`(en · ko · …)` list is compared as a set.

The manifest is also declared as an input of the unit test tasks. Without that,
editing it alone left `testDebugUnitTest` UP-TO-DATE — CI checks out clean and
never saw it, but the person adding a language would have.

Verified by breaking it in both directions: dropping the `hr` row fails 12
checks naming every `hr` file, and declaring a language with no files fails
every surface it should. Both resource tests fail on the same drift once the
manifest is an input. `./gradlew test` (all variants), the three verify scripts
and `verify-release-export.ps1` against the real v2.33.0 artifacts in `D:\Build`
are green, and the Gradle snippet was run against the manifest to confirm it
yields the same eight store tags in the same order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e800ab423e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/verify-locales.ps1 Outdated
Write-Host "`n앱 리소스 (app/src/main/res)"
Test-Declared "strings.xml" @($locales | ForEach-Object { "app/src/main/res/$($_.ResDir)/strings.xml" })
# values-night 처럼 언어가 아닌 한정자는 두 글자 코드 모양이 아니라 저절로 빠진다.
Test-Strays "values-*" "app/src/main/res" '^values-([a-z]{2})(?:-r[A-Z]{2})?$' $codes -Directory

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject suffixed resource directories for the source locale

When a values-en directory is accidentally added, this check accepts it because $codes includes the source locale en, even though the manifest contract maps the source language exclusively to values/. Android will prefer values-en on English devices, while ResourceParityTest continues reading only values/, allowing stale or incomplete source resources to bypass the new bidirectional gate. Compare suffixed directories against non-source locale codes instead; the analogous raw-* check has the same source-locale hole.

AGENTS.md reference: AGENTS.md:L266-L270

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 318f4f0 — correct on both counts, including the raw-* hole you pointed at. The suffixed surfaces (values-*, raw-*, index.<code>.html, privacy.<code>.html, README.<code>.md) now compare against the translated codes only, and a source-language file in one of those places gets its own message instead of the misleading "not in the manifest" one. Verified by adding values-en and index.en.html: both fail by name.

Comment thread scripts/locales.ps1 Outdated
Comment on lines +71 to +74
$duplicates = @($locales | Group-Object Code | Where-Object { $_.Count -gt 1 })
if ($duplicates.Count -gt 0) {
throw "$path 에 중복된 code 가 있습니다: $(($duplicates | ForEach-Object { $_.Name }) -join ', ')"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject duplicate store locale tags

When a new manifest row accidentally reuses another row's store value, this parser accepts it because it checks only duplicate Code values. The locale verifier then satisfies both rows with the same existing fastlane directory, and the Gradle exporter writes the same <store> release-note block twice, so the new language can still ship without its own store metadata while every gate remains green. Validate Store uniqueness alongside Code uniqueness.

AGENTS.md reference: AGENTS.md:L266-L270

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 318f4f0 — the parser now checks Store uniqueness alongside Code. A row reusing ko-KR is refused before any surface is walked, so the shared-fastlane-directory pass you describe cannot happen. Verified by adding such a row.

Two holes in the new gate, both from the Codex review on #335.

**values-en would have passed.** The stray check compared against every
declared code, and the source language is declared, so a `values-en`
directory read as a known language rather than as a file in the wrong
place. Android prefers it on English devices while `ResourceParityTest`
keeps reading `values/`, so stale English resources would render with no
gate looking at them. The same hole was in `raw-*`, `index.<code>.html`,
`privacy.<code>.html` and `README.<code>.md`: the suffixed surfaces now
compare against the translated codes only, and a source-language file
there gets its own message rather than the misleading "not in the
manifest" one.

**A repeated store tag would have passed too.** Only `Code` was checked
for uniqueness, so two rows could share `ko-KR`: both would be satisfied
by the one existing fastlane directory, and the release-notes TXT would
carry the same block twice while the new language shipped with no store
metadata of its own.

Both verified by breaking them — `values-en` + `index.en.html` fail by
name, a duplicate `store` value is refused by the parser.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jeiel85
jeiel85 merged commit b8b0f45 into main Aug 24, 2026
5 checks passed
jeiel85 added a commit that referenced this pull request Aug 24, 2026
The repository rule is that a work unit lands in HISTORY.md, and the
v2.33.0 entry above still lists these five candidates as open — Codex
caught the omission on #336. One entry covers the pass rather than one
per PR, since #335 (the locale list and its gate) and #336 (the golden
and the seam it needed) are the same piece of work against the same
tracker section.

No CHANGELOG entry: nothing in either PR is user-visible, and the 69
byte-identical goldens are the evidence for the one production change.

Co-Authored-By: Claude Opus 5 <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.

1 participant