ci(i18n): fix the two guards that keep the crowdin sync failing

Two independent guards were failing the workflow on healthy input.

Retention read "exported value equals the English source" as a lost translation, but exports are
built with skipUntranslatedStrings, so Crowdin omits a key entirely once it stops carrying a
translation. Anything present in the export is by definition translated, and the equality check only
ever fired on correct work: Spanish "Error", "Feb" and "Cookie", French "Description", Portuguese
"Readwise". Retention now compares presence alone, which is what absence already meant. The same
inference lived in a legacyComplete branch that would have silently stopped protecting the 2,318
keys whose translation legitimately equals the source once any locale reached full translation.

Scheduled runs had no way out of that. The only escape hatch is allowed_translation_losses, a
workflow_dispatch input that is never persisted, so every schedule rediscovered the same losses and
failed until someone dispatched the run by hand.

The slot count rule then only runs for the keys it is given, and the sync passed none, so its
rejection filter was weaker than the locale validation running on its output one step later. An
invalid ja message survived the filter, reached the catalog, and failed the next step instead of
being dropped and reported. Move the source scan into locale-source-keys.mjs so the sync and
validate-locales.mjs derive the same keys.
This commit is contained in:
neon
2026-08-21 15:36:12 +00:00
parent 93e7c7b6b7
commit 3b9bae7525
5 changed files with 201 additions and 64 deletions
+2 -2
View File
@@ -99,7 +99,7 @@ After an English source change reaches `main`:
2. Translators update target strings in Crowdin.
3. The scheduled or manually dispatched `Crowdin Translation Sync` workflow verifies that Crowdin's source keys match `en.json`.
4. The workflow requests only translated strings, removes the empty values Crowdin emits for untranslated nested JSON entries, and validates the resulting sparse catalogs.
5. Before writing, the workflow rejects exports that omit an existing translation or replace one with English source text.
5. Before writing, the workflow rejects exports that omit an existing translation.
6. Only after validation and retention checks pass, the workflow updates `l10n_main` and opens a pull request to `main`.
7. CI verifies that the pull request changes only the twenty-four target catalogs and runs the normal client checks.
8. A maintainer reviews and squash-merges the pull request.
@@ -109,7 +109,7 @@ Deleting that branch is required, not tidiness. It guarantees that every later e
Translation pull requests retain the configured `i18n(client)` title and commit format. The `i18n` commit type produces a patch release and an Internationalization release-note section. `CROWDIN_PR_TOKEN` must contain a fine-grained GitHub token with repository contents and pull-request write access. A separate token is required because pull requests created with the workflow's default `GITHUB_TOKEN` do not trigger normal pull-request workflows.
The retention check distinguishes the initial complete legacy catalogs from later sparse catalogs. In a complete legacy catalog, it protects values that differ from English and ignores copied English fallbacks. Once a catalog is sparse, every existing key is treated as an intentional translation, including technical terms that legitimately match English.
The retention check compares presence, never message content. Exports request only translated strings, so Crowdin omits a key as soon as it stops carrying a translation, and every key already present in a target catalog must come back in the export. A translation that legitimately matches the English source, such as `Error` in Spanish or a product name left untranslated, is a real translation and never counts as a loss. Reading it as one would reject the routine corrections that translators make to short labels, abbreviations, and loanwords.
An intentional removal requires a manual workflow dispatch with its exact `locale:message.key` value in `allowed_translation_losses`. Separate multiple acknowledgements with commas. Unknown, misspelled, duplicate, and unused acknowledgements fail the run, so this input cannot act as a broad bypass. Scheduled runs never acknowledge translation loss automatically.