Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
df69675
feat(i18n): guarantee link/identifier integrity and check typography
tym83 Jul 24, 2026
97dd5fc
docs(i18n): turn the per-language style guides into real contracts
tym83 Jul 24, 2026
c2ba45e
fix(i18n): resolve the number-formatting contradiction in the transla…
tym83 Jul 24, 2026
41255d0
fix(i18n): mask raw HTML and never regenerate hand-localized pages
tym83 Jul 24, 2026
a9af5f3
fix(i18n): never nest masking placeholders and unwind nesting on restore
lexfrei Jul 31, 2026
48f87fc
fix(i18n): stop the deterministic checks contradicting the style guides
lexfrei Jul 31, 2026
8d1f8c1
fix(i18n): refuse to overwrite a hand-localized page with machine output
lexfrei Jul 31, 2026
9a5a6f8
docs(i18n): document the deterministic checks stage and the typograph…
lexfrei Jul 31, 2026
d91a70a
ci(i18n): run the typography lint as an advisory step
lexfrei Jul 31, 2026
435ef0b
fix(i18n): stop flagging the style guides' own canonical examples
lexfrei Jul 31, 2026
d5d898d
fix(i18n): keep reporting hand-localized drift on an empty worklist
lexfrei Jul 31, 2026
dab8ee1
fix(i18n): keep the hand-localized drift signal alive across CI tooling
lexfrei Jul 31, 2026
c86b7d2
fix(i18n): treat the markdown image marker as syntax, not prose
lexfrei Jul 31, 2026
c70b20d
fix(i18n): exit 0 from update-digests when no stamped translations exist
lexfrei Jul 31, 2026
72fc0ae
fix(i18n): keep link-title quotes out of the prose checks
lexfrei Jul 31, 2026
345f998
fix(i18n): warn instead of claiming success when a page has no digest…
lexfrei Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/i18n-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ jobs:
run: |
python3 -m pip install --quiet pyyaml
python3 hack/i18n/test_i18n.py
- name: Typography lint on published translations
# Advisory: reports per-language typography drift (hand edits, pages
# translated before a rule existed) without failing the build. Flip to
# --strict per language once its backlog is clean.
run: python3 hack/i18n/lint_translations.py
70 changes: 57 additions & 13 deletions hack/check-i18n.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,17 @@
#
# Subcommands:
# check (default) Run every guard; exit non-zero on any gap.
# update-digests Recompute and rewrite `source_digest` in every translated
# page from its English source. Run this after editing an
# English source or after adding/refreshing a translation.
# Hand-localized pages (l10n: transcreate) get a ::warning::
# instead of an error when they drift: the pipeline never
# regenerates them, so drift is a report for a human, not a
# build failure.
# update-digests [file...]
# Recompute and rewrite `source_digest` in translated pages
# from their English source. Run after editing an English
# source or after adding/refreshing a translation. Without
# arguments, hand-localized pages are SKIPPED (a wholesale
# re-stamp would silence their drift report); pass the file
# explicitly to re-stamp one you refreshed by hand.
#
# Guards run by `check`:
# 1. i18n key parity — every i18n/<lang>.toml must define exactly the same
Expand Down Expand Up @@ -67,6 +75,14 @@ translated_digest_files() {
| sort
}

# A page a human localized by hand (l10n: transcreate). The pipeline never
# regenerates these, so a stale digest here is a drift REPORT, not a build
# failure — and re-stamping it wholesale would silence the report while the
# drift persists.
is_transcreate() {
grep -m1 -E '^l10n:' "$1" 2>/dev/null | grep -qE '^l10n:[[:space:]]*"?transcreate"?[[:space:]]*$'
}

# Map content/<lang>/<rel> -> content/en/<rel>
en_source_for() {
local f="$1"
Expand Down Expand Up @@ -129,28 +145,55 @@ check_digest_freshness() {
actual="$(grep -m1 -E '^source_digest:' "$f" | sed -E 's/.*sha256:([0-9a-fA-F]+).*/\1/')"
checked=$((checked + 1))
if [ "$actual" != "$expected" ]; then
rc=1
echo "::error::stale translation: $f"
echo " English source: $src"
echo " recorded digest: sha256:${actual}"
echo " current digest: sha256:${expected}"
echo " fix: refresh the translation, then run 'hack/check-i18n.sh update-digests'"
if is_transcreate "$f"; then
# Advisory by design: a drifted transcreation is refreshed when a
# human decides to, and must not block unrelated PRs meanwhile.
echo "::warning::hand-localized page drifted from its English source: $f"
echo " refresh the transcreation by hand, then re-stamp it with:"
echo " hack/check-i18n.sh update-digests $f"
else
rc=1
echo "::error::stale translation: $f"
echo " English source: $src"
echo " recorded digest: sha256:${actual}"
echo " current digest: sha256:${expected}"
echo " fix: refresh the translation, then run 'hack/check-i18n.sh update-digests'"
fi
fi
done < <(translated_digest_files)
[ "$rc" -eq 0 ] && echo "translation freshness: OK ($checked translated pages match their English source)"
return "$rc"
}

update_digests() {
# With explicit files, re-stamp exactly those — the escape hatch for a
# transcreation a human just refreshed. With no arguments, re-stamp every
# machine-translated page but SKIP hand-localized ones: their stale digest
# is the only signal that the transcreation drifted, and a wholesale
# re-stamp would silence it while the drift persists.
local f
while IFS= read -r f; do
{
# `|| true`: with zero stamped translations the underlying grep exits 1,
# which under `set -euo pipefail` would silently kill the whole script.
if [ "$#" -gt 0 ]; then printf '%s\n' "$@"; else translated_digest_files || true; fi
} | while IFS= read -r f; do
[ -z "$f" ] && continue
if [ "$#" -eq 0 ] && is_transcreate "$f"; then
echo "skip $f — hand-localized (l10n: transcreate); refresh it by hand, then: hack/check-i18n.sh update-digests $f"
continue
fi
local src expected tmp
src="$(en_source_for "$f")"
if [ ! -f "$src" ]; then
echo "::warning::skip $f — English source missing: $src"
continue
fi
# The awk below only REWRITES an existing line; without this check a page
# missing the field would pass through untouched yet be reported "updated".
if ! grep -qE '^source_digest:' "$f"; then
echo "::warning::skip $f — no source_digest line in the front matter; add one, then re-run"
continue
fi
expected="$(sha256 "$src")"
tmp="$(mktemp)"
awk -v d="sha256:${expected}" '
Expand All @@ -159,7 +202,7 @@ update_digests() {
' "$f" > "$tmp"
mv "$tmp" "$f"
echo "updated $f -> sha256:${expected}"
done < <(translated_digest_files)
done
}

cmd="${1:-check}"
Expand All @@ -171,10 +214,11 @@ case "$cmd" in
exit "$rc"
;;
update-digests)
update_digests
shift
update_digests "$@"
;;
*)
echo "usage: $0 [check|update-digests]" >&2
echo "usage: $0 [check|update-digests [file...]]" >&2
exit 2
;;
esac
7 changes: 6 additions & 1 deletion hack/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ content/en/** ──▶ worklist.py (missing | stale, via source_digest)
back-translate ─▶ compare vs original (meaning-drift check)
deterministic checks (version/flag/do-not-translate integrity,
per-language typography — lib.py, no model calls)
review ×2 (native speakers of the target language)
• technical editor → fluency / register / terminology
• Cozystack maintainer → technical correctness vs source
Expand Down Expand Up @@ -163,7 +167,8 @@ succeeds. A page that reproducibly fails is a signal to translate it by hand.
| `prompts/review-maintainer.md` | native Cozystack-maintainer reviewer (technical correctness) |
| `prompts/revise.md` | revise a translation against reviewer findings |
| `style-guides/<lang>.md` | per-language tone/register conventions |
| `lib.py` | shared helpers (config, discovery, digest, front matter, protect/restore) |
| `lib.py` | shared helpers (config, discovery, digest, front matter, protect/restore, integrity + typography checks) |
| `lint_translations.py` | typography lint for already-published translations (advisory; `--strict` to gate) |
| `worklist.py` | diff detector |
| `translate.py` | translate + review-gate driver; also removes orphaned translations |
| `run-daily.sh` | daily runner (subscription, until limit, commit + PR) |
Expand Down
Loading
Loading