mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-17 20:32:25 +03:00
feat(i18n): new-key gate rejects __MISSING__ markers; skills translate new keys in parallel (#13996)
On 2026-09-16 eight feature PRs added 61 keys to src/i18n/messages/en.json and stamped `__MISSING__:<en>` into all 65 locales instead of translating. check-new-key-coverage accepted the marker as "the key reached the locale", so nothing blocked the PRs, and the blocking real-translation ratio gate then failed on the release tip for everybody (pt-BR 3.2 % > 2.5 % + 0.5). - scripts/i18n/check-new-key-coverage.mjs: a leaf whose value starts with `__MISSING__:` is judged exactly like an absent leaf; the FAIL message names the marker as the cause and prints the per-locale sync-ui-keys command and the parallel runner. Header/JSDoc updated. - tests/unit/i18n-new-key-coverage.test.ts: "a new key that only carries a __MISSING__ marker is flagged" (was the inverse case, which encoded the old contract); the other six cases unchanged and green. - scripts/i18n/translate-new-keys.sh (+ `npm run i18n:translate-new-keys`): committed, detached-safe runner — flock queue, N workers (default 5), 3 attempts per locale of `sync-ui-keys.mjs --translate-markers --batch-size=40`, per-locale logs/.exit + batch.log/batch.status/batch.rc/batch.pid under _artifacts/i18n-new-keys/, non-zero exit while any locale still carries a marker, refuses to start (exit 2, names the five OMNIROUTE_TRANSLATION_* vars) when the backend env is absent. Reads only the OMNIROUTE_TRANSLATION_* lines of the repo .env; kills nothing, matches nothing by name. - docs: QUALITY_GATES.md (gate table + check-new-key-coverage section) and I18N.md (gate table + "Translating the keys a branch adds" subsection). The implementation/port/merge skills reference the new shared snippet `.agents/skills/_shared/i18n-translate-new-keys.md` (skills repo, separate).
This commit is contained in:
committed by
GitHub
parent
ceafa55824
commit
d6f720bceb
@@ -144,7 +144,7 @@ Runs on every PR to `main`. Blocks merge on failure.
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
|
||||
| `check-ui-keys-coverage` (inline) | UI i18n key coverage is ≥ 65% | Yes |
|
||||
| `check-ui-value-drift` (inline) | A rewritten English **value** leaves no stale translation behind | Yes |
|
||||
| `check-new-key-coverage` (inline) | A **new** English key reaches every locale | Yes |
|
||||
| `check-new-key-coverage` (inline) | A **new** English key is translated in every locale — a `__MISSING__:` marker is rejected | Yes |
|
||||
| `check-translation-ratio` | Real-translation ratio per locale (identical-to-English / placeholder / missing leaves outside the allowlist) must not exceed `config/quality/i18n-translation-baseline.json` + slack | **Advisory** |
|
||||
|
||||
Needs `fetch-depth: 0` — the value-drift gate diffs `en.json` against the merge base.
|
||||
@@ -572,9 +572,19 @@ received them. `deepMergeFallback` substitutes English for an absent key, so the
|
||||
untranslated UI rather than blank UI — real, and silent by construction.
|
||||
|
||||
Like its sibling it is **diff-aware**, comparing English at the merge base against the working
|
||||
tree, so pre-existing gaps stay frozen and the gate needed no migration to turn on. Escape hatch:
|
||||
`__MISSING__:<english>` defers a translation while keeping the runtime correct. `vi` bans
|
||||
placeholders (`tests/unit/i18n-vi-completeness.test.ts`) and needs a real translation.
|
||||
tree, so pre-existing gaps stay frozen and the gate needed no migration to turn on.
|
||||
|
||||
**A `__MISSING__:<english>` marker does not satisfy it (since 2026-09-17).** It used to be the
|
||||
documented deferral — the runtime falls back to correct English — until eight feature PRs on
|
||||
2026-09-16 added 61 keys and stamped the marker into all 65 locales instead of translating: this
|
||||
gate accepted every one, nothing blocked the PRs, and the blocking real-translation ratio gate
|
||||
then failed on the release tip for everybody (pt-BR 3.2 % > 2.5 % + 0.5). A marker is now judged
|
||||
as an absent translation. Fix a red with
|
||||
`node scripts/i18n/sync-ui-keys.mjs --locale=<codes> --translate-markers --batch-size=40`, or
|
||||
all locales in parallel with `npm run i18n:translate-new-keys` (`scripts/i18n/translate-new-keys.sh`,
|
||||
detached-safe, refuses to start without the `OMNIROUTE_TRANSLATION_*` env). A key that must stay
|
||||
English (a pinned product/engine/flag name) belongs in `scripts/i18n/untranslatable-keys.json`,
|
||||
never behind a marker. `vi` bans markers outright (`tests/unit/i18n-vi-completeness.test.ts`).
|
||||
|
||||
#### `check-vitest-exclusions` — parked-test gate
|
||||
|
||||
|
||||
@@ -220,13 +220,36 @@ adapter and must not be edited by hand. The Google-Translate generator
|
||||
|
||||
Three gates guard the catalogs, and they see different things:
|
||||
|
||||
| Gate | Sees |
|
||||
| -------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `npm run i18n:check-ui-coverage` | ≥ 80 % of leaves translated per locale |
|
||||
| `npm run i18n:check-new-keys` | a key the PR adds to `en.json` reached every locale |
|
||||
| `npm run i18n:check-keys` | every locale carries exactly the key set of `en.json`, whatever the age |
|
||||
| `npm run i18n:check-keys:cli` | the CLI catalogs carry exactly the key set of `bin/cli/locales/en.json` |
|
||||
| `npm run i18n:check-ratio` | share of leaves still identical to English may only fall (ratchet) |
|
||||
| Gate | Sees |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `npm run i18n:check-ui-coverage` | ≥ 80 % of leaves translated per locale |
|
||||
| `npm run i18n:check-new-keys` | a key the PR adds to `en.json` is translated in every locale — a `__MISSING__:` marker counts as absent (since 2026-09-17) |
|
||||
| `npm run i18n:check-keys` | every locale carries exactly the key set of `en.json`, whatever the age |
|
||||
| `npm run i18n:check-keys:cli` | the CLI catalogs carry exactly the key set of `bin/cli/locales/en.json` |
|
||||
| `npm run i18n:check-ratio` | share of leaves still identical to English may only fall (ratchet) |
|
||||
|
||||
### Translating the keys a branch adds (parallel runner)
|
||||
|
||||
A branch that adds or changes keys in `en.json` must translate them before its PR opens — the
|
||||
new-key gate rejects `__MISSING__:` markers (on 2026-09-16 eight feature PRs shipped 61 keys as
|
||||
markers into all 65 locales and the ratio gate went red on the release tip for everybody).
|
||||
`scripts/i18n/translate-new-keys.sh` translates every locale in parallel and is safe to detach:
|
||||
|
||||
```bash
|
||||
# pinned-name guard first: keys a test pins to English go in scripts/i18n/untranslatable-keys.json
|
||||
rg -n 'assert\.equal\(.*messages\.[a-zA-Z.]+, "' tests/unit
|
||||
# needs OMNIROUTE_TRANSLATION_API_URL / _API_KEY / _MODEL in .env — refuses to start otherwise
|
||||
npm run i18n:translate-new-keys # all locales, 5 workers
|
||||
bash scripts/i18n/translate-new-keys.sh --locales=pt-BR,es # a subset
|
||||
bash scripts/i18n/translate-new-keys.sh --catalog=cli --workers=3 # bin/cli/locales
|
||||
# detached: the shell returns at once; poll _artifacts/i18n-new-keys/batch.rc (0 = done)
|
||||
nohup setsid bash scripts/i18n/translate-new-keys.sh > _artifacts/i18n-new-keys/runner.out 2>&1 &
|
||||
```
|
||||
|
||||
Each locale gets three attempts of `sync-ui-keys.mjs --translate-markers --batch-size=40`;
|
||||
per-locale logs and `<code>.exit` land in `_artifacts/i18n-new-keys/`, and the script exits
|
||||
non-zero while any selected locale still carries a marker. Then
|
||||
`npm run i18n:check-keys && npm run i18n:check-ratio && npm run i18n:check-new-keys`.
|
||||
|
||||
**After every merge of the base into a locale branch**, re-sync the locales the branch owns —
|
||||
the base keeps adding keys while a batch translates:
|
||||
|
||||
@@ -287,6 +287,7 @@
|
||||
"alibaba:sync-allowlist": "node --import tsx/esm scripts/ops/sync-alibaba-allowlist.mjs",
|
||||
"check:vitest-exclusions": "node scripts/check/check-vitest-exclusions.mjs",
|
||||
"i18n:check-new-keys": "node scripts/i18n/check-new-key-coverage.mjs",
|
||||
"i18n:translate-new-keys": "bash scripts/i18n/translate-new-keys.sh",
|
||||
"i18n:check-keys": "node scripts/i18n/check-key-completeness.mjs",
|
||||
"i18n:check-keys:cli": "node scripts/i18n/check-key-completeness.mjs --catalog=cli"
|
||||
},
|
||||
|
||||
@@ -23,13 +23,22 @@
|
||||
*
|
||||
* How this gate works: DIFF-AWARE, like its sibling. It compares the English catalog at the
|
||||
* merge base against the working tree; every key that is NEW in English must be present and
|
||||
* non-placeholder in every locale. Pre-existing gaps are deliberately frozen — this gate
|
||||
* judges only what the current change adds, so it can be turned on without a migration.
|
||||
* TRANSLATED in every locale. Pre-existing gaps are deliberately frozen — this gate judges
|
||||
* only what the current change adds, so it can be turned on without a migration.
|
||||
*
|
||||
* Escape hatch, same as the sibling: set the value to `__MISSING__:<english>` to make the
|
||||
* runtime fall back to correct English and queue the key for the translation pipeline.
|
||||
* NOTE that `vi` bans placeholders (tests/unit/i18n-vi-completeness.test.ts), so `vi` needs
|
||||
* a real translation.
|
||||
* A `__MISSING__:<english>` marker does NOT satisfy this gate (since 2026-09-17). It used to:
|
||||
* the marker was the documented deferral, because the runtime falls back to correct English.
|
||||
* Then on 2026-09-16 eight feature PRs added 61 keys to en.json and stamped the marker into
|
||||
* all 65 locales instead of translating; this gate accepted every one of them, nothing blocked
|
||||
* the PRs, and the real-translation ratio gate (`check-translation-ratio`, blocking) went red
|
||||
* on the release tip for everybody (pt-BR 3.2 % > 2.5 % + 0.5). A marker is an absent
|
||||
* translation wearing a runtime-safe coat, and it is judged as absent here. Translate:
|
||||
*
|
||||
* node scripts/i18n/sync-ui-keys.mjs --locale=<codes> --translate-markers --batch-size=40
|
||||
* bash scripts/i18n/translate-new-keys.sh # same thing, all locales in parallel
|
||||
*
|
||||
* Keys that must stay English (product/engine/flag names a test pins) go in
|
||||
* `scripts/i18n/untranslatable-keys.json`, never behind a marker.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/i18n/check-new-key-coverage.mjs # strict, exit 1
|
||||
@@ -68,8 +77,8 @@ export function flattenLeaves(node, prefix = "", out = {}) {
|
||||
/**
|
||||
* Pure core: which (key, locale) pairs are keys new in English that a locale never got?
|
||||
*
|
||||
* A `__MISSING__:` placeholder counts as satisfied — it is the documented, runtime-correct
|
||||
* way to defer a translation.
|
||||
* A `__MISSING__:` placeholder counts as ABSENT — it is not a translation, and accepting it
|
||||
* is what let the 2026-09-16 batch ship 61 untranslated keys into 65 locales.
|
||||
*
|
||||
* @param {object} args
|
||||
* @param {object} args.baseEn en.json at the base ref
|
||||
@@ -91,7 +100,7 @@ export function findUntranslatedNewKeys({ baseEn, headEn, headLocales }) {
|
||||
for (const key of newKeys) {
|
||||
const value = flat[key];
|
||||
const satisfied =
|
||||
typeof value === "string" && (value.trim() !== "" || value.startsWith(PLACEHOLDER_PREFIX));
|
||||
typeof value === "string" && value.trim() !== "" && !value.startsWith(PLACEHOLDER_PREFIX);
|
||||
if (!satisfied) gaps.push({ key, locale });
|
||||
}
|
||||
}
|
||||
@@ -186,14 +195,20 @@ function main() {
|
||||
}
|
||||
const label = opts.warn ? "WARN" : "FAIL";
|
||||
console.error(
|
||||
`\n[i18n-new-keys] ${label} — ${byKey.size} new English key(s) missing from some locales:`
|
||||
`\n[i18n-new-keys] ${label} — ${byKey.size} new English key(s) untranslated in some locales:`
|
||||
);
|
||||
for (const [key, locales] of byKey) {
|
||||
console.error(` ✗ ${key} — missing in ${locales.length}: ${locales.join(", ")}`);
|
||||
console.error(` ✗ ${key} — untranslated in ${locales.length}: ${locales.join(", ")}`);
|
||||
}
|
||||
const codes = [...new Set(gaps.map((g) => g.locale))].sort().join(",");
|
||||
console.error(
|
||||
"\n Translate them, or set `__MISSING__:<english>` to defer (the runtime then falls back\n" +
|
||||
" to English). `vi` bans placeholders — it needs a real translation."
|
||||
"\n A `__MISSING__:<english>` marker does not count — it is an absent translation.\n" +
|
||||
" Translate the keys (needs OMNIROUTE_TRANSLATION_API_URL/_API_KEY/_MODEL in .env):\n" +
|
||||
` node scripts/i18n/sync-ui-keys.mjs --locale=${codes} --translate-markers --batch-size=40\n` +
|
||||
" or, all locales in parallel (detached runner):\n" +
|
||||
" bash scripts/i18n/translate-new-keys.sh\n" +
|
||||
" A key that must stay English (a pinned product/engine/flag name) belongs in\n" +
|
||||
" scripts/i18n/untranslatable-keys.json."
|
||||
);
|
||||
if (!opts.warn) process.exit(1);
|
||||
}
|
||||
|
||||
259
scripts/i18n/translate-new-keys.sh
Executable file
259
scripts/i18n/translate-new-keys.sh
Executable file
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env bash
|
||||
# OmniRoute — translate the `__MISSING__:<en>` markers a branch adds, every locale in parallel.
|
||||
#
|
||||
# Why this exists: on 2026-09-16 eight feature PRs added 61 keys to `src/i18n/messages/en.json`
|
||||
# and stamped `__MISSING__:<en>` into all 65 locales instead of translating. The new-key gate
|
||||
# (`scripts/i18n/check-new-key-coverage.mjs`) now rejects markers, so a branch that adds keys
|
||||
# has to translate them before its PR opens — and translating 65 locales one after the other
|
||||
# is what makes people skip it. This runner does it in parallel and is safe to detach.
|
||||
#
|
||||
# What it does: N workers pop locale codes from a queue (`flock`-serialized) and each runs
|
||||
# node scripts/i18n/sync-ui-keys.mjs --catalog=<c> --locale=<code> --translate-markers --batch-size=40
|
||||
# with up to 3 attempts per locale. A locale is DONE only when the run exits 0 AND its catalog
|
||||
# carries no `__MISSING__` marker any more. Everything is written under `_artifacts/i18n-new-keys/`
|
||||
# (gitignored, disposable):
|
||||
# <code>.log the sync-ui-keys output of the last attempt
|
||||
# <code>.exit 0 on success, else the last exit code (1 when markers survived a rc=0 run)
|
||||
# batch.log one line per attempt + START/END markers
|
||||
# batch.status running | done | failed
|
||||
# batch.rc the script's final exit code (written last — poll this file)
|
||||
# batch.pid PID of this script (kill by PID, never `pkill -f`)
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/i18n/translate-new-keys.sh [--catalog=ui|cli] [--workers=N] [--locales=<csv>]
|
||||
# npm run i18n:translate-new-keys -- --locales=pt-BR,es
|
||||
# # detached (the session keeps working; poll _artifacts/i18n-new-keys/batch.rc):
|
||||
# mkdir -p _artifacts/i18n-new-keys
|
||||
# nohup setsid bash scripts/i18n/translate-new-keys.sh > _artifacts/i18n-new-keys/runner.out 2>&1 &
|
||||
#
|
||||
# --catalog=ui|cli ui = src/i18n/messages (default); cli = bin/cli/locales
|
||||
# --workers=N parallel locales (default 5)
|
||||
# --locales=<csv> subset of locale codes (default: every <code>.json in the catalog but en)
|
||||
#
|
||||
# Backend: the translation env block must be present — in the shell or in the repo `.env`
|
||||
# (the script reads only the OMNIROUTE_TRANSLATION_* lines of `.env`; already-set variables
|
||||
# win). Without it the script refuses to start and names the variables. It never leaves
|
||||
# markers silently: the exit code is non-zero while any selected locale still carries one.
|
||||
#
|
||||
# Exit codes: 0 all selected locales translated · 1 some locale still has markers / failed ·
|
||||
# 2 usage or environment error (nothing ran).
|
||||
set -u
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
|
||||
ART="$ROOT/_artifacts/i18n-new-keys"
|
||||
|
||||
CATALOG=ui
|
||||
WORKERS=5
|
||||
LOCALES=""
|
||||
RETRY_SLEEP=${OMNIROUTE_TRANSLATION_RETRY_SLEEP:-30}
|
||||
|
||||
usage() {
|
||||
sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--catalog=*) CATALOG="${arg#--catalog=}" ;;
|
||||
--workers=*) WORKERS="${arg#--workers=}" ;;
|
||||
--locales=*) LOCALES="${arg#--locales=}" ;;
|
||||
--locale=*) LOCALES="${arg#--locale=}" ;;
|
||||
-h | --help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[i18n-new-keys] unknown argument: $arg" >&2
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$CATALOG" in
|
||||
ui) DIR="$ROOT/src/i18n/messages" ;;
|
||||
cli) DIR="$ROOT/bin/cli/locales" ;;
|
||||
*)
|
||||
echo "[i18n-new-keys] --catalog must be ui or cli (got: $CATALOG)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$WORKERS" in
|
||||
'' | *[!0-9]* | 0)
|
||||
echo "[i18n-new-keys] --workers must be a positive integer (got: $WORKERS)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
# ----- translation backend env: shell first, then the OMNIROUTE_TRANSLATION_* lines of .env
|
||||
load_translation_env() {
|
||||
local env_file="$ROOT/.env" line key value
|
||||
[ -f "$env_file" ] || return 0
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
line="${line#"${line%%[![:space:]]*}"}" # ltrim
|
||||
line="${line#export }"
|
||||
case "$line" in
|
||||
OMNIROUTE_TRANSLATION_*=*) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
value="${value%"${value##*[![:space:]]}"}" # rtrim
|
||||
case "$value" in
|
||||
\"*\") value="${value#\"}" value="${value%\"}" ;;
|
||||
\'*\') value="${value#\'}" value="${value%\'}" ;;
|
||||
esac
|
||||
if [ -z "${!key:-}" ]; then
|
||||
export "$key=$value"
|
||||
fi
|
||||
done <"$env_file"
|
||||
}
|
||||
load_translation_env
|
||||
|
||||
REQUIRED_VARS="OMNIROUTE_TRANSLATION_API_URL OMNIROUTE_TRANSLATION_API_KEY OMNIROUTE_TRANSLATION_MODEL"
|
||||
OPTIONAL_VARS="OMNIROUTE_TRANSLATION_CONCURRENCY OMNIROUTE_TRANSLATION_TIMEOUT_MS"
|
||||
missing=""
|
||||
for v in $REQUIRED_VARS; do
|
||||
[ -n "${!v:-}" ] || missing="$missing $v"
|
||||
done
|
||||
if [ -n "$missing" ]; then
|
||||
{
|
||||
echo "[i18n-new-keys] REFUSING TO START — translation backend not configured."
|
||||
echo " Missing (required):$missing"
|
||||
echo " The full block (put it in $ROOT/.env or export it in the shell):"
|
||||
for v in $REQUIRED_VARS $OPTIONAL_VARS; do
|
||||
if [ -n "${!v:-}" ]; then echo " $v (set)"; else echo " $v (MISSING)"; fi
|
||||
done
|
||||
echo " Nothing was translated; the __MISSING__ markers are still in place and the"
|
||||
echo " new-key gate (npm run i18n:check-new-keys) will reject them."
|
||||
} >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ----- locale selection: on-disk catalogs minus en, or the explicit --locales subset
|
||||
selected=()
|
||||
if [ -n "$LOCALES" ]; then
|
||||
IFS=',' read -r -a requested <<<"$LOCALES"
|
||||
for code in "${requested[@]}"; do
|
||||
code="${code//[[:space:]]/}"
|
||||
[ -n "$code" ] || continue
|
||||
if [ "$code" = "en" ]; then
|
||||
echo "[i18n-new-keys] en is the source catalog, skipping it" >&2
|
||||
continue
|
||||
fi
|
||||
if [ ! -f "$DIR/$code.json" ]; then
|
||||
echo "[i18n-new-keys] unknown locale: $code ($DIR/$code.json does not exist)" >&2
|
||||
exit 2
|
||||
fi
|
||||
selected+=("$code")
|
||||
done
|
||||
else
|
||||
for f in "$DIR"/*.json; do
|
||||
code=$(basename "$f" .json)
|
||||
[ "$code" = "en" ] && continue
|
||||
selected+=("$code")
|
||||
done
|
||||
fi
|
||||
if [ "${#selected[@]}" -eq 0 ]; then
|
||||
echo "[i18n-new-keys] no locale selected" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ----- artifacts (fresh per run for the selected locales)
|
||||
mkdir -p "$ART"
|
||||
QUEUE="$ART/queue.txt"
|
||||
LOCK="$ART/queue.lock"
|
||||
: >"$QUEUE"
|
||||
for code in "${selected[@]}"; do
|
||||
echo "$code" >>"$QUEUE"
|
||||
rm -f "$ART/$code.exit"
|
||||
done
|
||||
rm -f "$ART/batch.rc"
|
||||
echo running >"$ART/batch.status"
|
||||
echo $$ >"$ART/batch.pid"
|
||||
|
||||
log() { echo "$*" >>"$ART/batch.log"; }
|
||||
|
||||
count_markers() {
|
||||
# grep -c prints 0 and exits 1 when nothing matches — the count is what we want.
|
||||
grep -c "__MISSING__" "$DIR/$1.json" 2>/dev/null || true
|
||||
}
|
||||
|
||||
next_locale() {
|
||||
# pop the first line of the queue atomically
|
||||
(
|
||||
flock 9
|
||||
local l
|
||||
l=$(head -n 1 "$QUEUE")
|
||||
[ -n "$l" ] && sed -i '1d' "$QUEUE"
|
||||
echo "$l"
|
||||
) 9>"$LOCK"
|
||||
}
|
||||
|
||||
worker() {
|
||||
local id=$1 code rc left t0 wall attempt
|
||||
while :; do
|
||||
code=$(next_locale)
|
||||
[ -z "$code" ] && break
|
||||
for attempt in 1 2 3; do
|
||||
log "[$code] w$id attempt $attempt start $(date -Is)"
|
||||
t0=$(date +%s)
|
||||
(
|
||||
cd "$ROOT" && node scripts/i18n/sync-ui-keys.mjs --catalog="$CATALOG" --locale="$code" \
|
||||
--translate-markers --batch-size=40
|
||||
) >"$ART/$code.log" 2>&1
|
||||
rc=$?
|
||||
wall=$(($(date +%s) - t0))
|
||||
left=$(count_markers "$code")
|
||||
log "[$code] w$id attempt $attempt exit=$rc wall=${wall}s markers_left=$left $(date -Is)"
|
||||
if [ "$rc" -eq 0 ] && [ "$left" -eq 0 ]; then
|
||||
echo 0 >"$ART/$code.exit"
|
||||
log "[$code] DONE"
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
[ "$rc" -eq 0 ] && rc=1
|
||||
echo "$rc" >"$ART/$code.exit"
|
||||
log "[$code] FAILED exit=$rc markers_left=$left"
|
||||
else
|
||||
sleep "$RETRY_SLEEP"
|
||||
fi
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
echo "[i18n-new-keys] catalog=$CATALOG locales=${#selected[@]} workers=$WORKERS artifacts=$ART"
|
||||
log "BATCH START catalog=$CATALOG workers=$WORKERS locales=${selected[*]} $(date -Is)"
|
||||
pids=()
|
||||
for i in $(seq 1 "$WORKERS"); do
|
||||
worker "$i" &
|
||||
pids+=("$!")
|
||||
done
|
||||
wait "${pids[@]}"
|
||||
|
||||
# ----- verdict: every selected locale must have exit 0 AND zero markers on disk
|
||||
failed=""
|
||||
for code in "${selected[@]}"; do
|
||||
ex=$(cat "$ART/$code.exit" 2>/dev/null || echo missing)
|
||||
left=$(count_markers "$code")
|
||||
if [ "$ex" != "0" ] || [ "$left" -ne 0 ]; then
|
||||
failed="$failed $code(exit=$ex,markers=$left)"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$failed" ]; then
|
||||
log "BATCH END failed:$failed $(date -Is)"
|
||||
echo failed >"$ART/batch.status"
|
||||
echo 1 >"$ART/batch.rc"
|
||||
echo "[i18n-new-keys] FAILED — markers still present or run failed for:$failed" >&2
|
||||
echo " logs: $ART/<code>.log — re-run with --locales=<csv> for just those." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "BATCH END ok $(date -Is)"
|
||||
echo done >"$ART/batch.status"
|
||||
echo 0 >"$ART/batch.rc"
|
||||
echo "[i18n-new-keys] DONE — ${#selected[@]} locale(s) translated, no __MISSING__ marker left."
|
||||
echo " Now: npm run i18n:check-keys && npm run i18n:check-ratio && npm run i18n:check-new-keys"
|
||||
exit 0
|
||||
@@ -34,13 +34,22 @@ test("a new key translated everywhere passes", () => {
|
||||
assert.deepEqual(gaps, []);
|
||||
});
|
||||
|
||||
test("a __MISSING__ placeholder satisfies the gate — it is the documented deferral", () => {
|
||||
/**
|
||||
* 2026-09-16: eight feature PRs added 61 keys to en.json and stamped `__MISSING__:<en>` into
|
||||
* all 65 locales instead of translating. This gate accepted the marker as "the key reached the
|
||||
* locale", nothing blocked the PRs, and the real-translation ratio gate then went red on the
|
||||
* release tip for everybody (pt-BR 3.2 % > 2.5 % + 0.5). A marker is an absent translation.
|
||||
*/
|
||||
test("a new key that only carries a __MISSING__ marker is flagged", () => {
|
||||
const gaps = findUntranslatedNewKeys({
|
||||
baseEn: en(),
|
||||
headEn: en({ fresh: "Fresh" }),
|
||||
headLocales: { pt: { ui: { existing: "Existente", fresh: "__MISSING__:Fresh" } } },
|
||||
headLocales: {
|
||||
pt: { ui: { existing: "Existente", fresh: "__MISSING__:Fresh" } },
|
||||
de: { ui: { existing: "Vorhanden", fresh: "Frisch" } },
|
||||
},
|
||||
});
|
||||
assert.deepEqual(gaps, []);
|
||||
assert.deepEqual(gaps, [{ key: "ui.fresh", locale: "pt" }]);
|
||||
});
|
||||
|
||||
test("an empty string does NOT satisfy the gate", () => {
|
||||
|
||||
Reference in New Issue
Block a user