Skip to content

fix(i18n): resolve ISO 639-2/B language codes and stop gating validit… #447

fix(i18n): resolve ISO 639-2/B language codes and stop gating validit…

fix(i18n): resolve ISO 639-2/B language codes and stop gating validit… #447

name: Update Translations
on:
push:
branches:
- main
paths:
- 'cps/**'
- 'scripts/compile_translations.sh'
- 'scripts/update_translations.sh'
- 'scripts/generate_translation_status.py'
- '.github/workflows/update-translations.yml'
workflow_dispatch:
# Two merges to main in quick succession trigger two runs; both commit a
# translation update and the loser's push is rejected non-fast-forward.
# Serialize runs so only pushes from outside this workflow can race the
# commit step (those are handled by the rebase-and-retry loop below).
# cancel-in-progress stays false: cancelling a run mid-push could strand
# a half-finished commit.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
permissions:
contents: write
actions: write
jobs:
update-translations:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v7
with:
persist-credentials: true
# GH_PAT is a personal token belonging to new-usemame (the repo
# admin). The "main protection" ruleset blocks direct pushes to
# main for everyone except admins, so the default GITHUB_TOKEN
# (which authenticates as github-actions[bot]) gets rejected
# with GH013. Using GH_PAT lets the workflow's translation
# commit land on main under new-usemame's admin-bypass.
token: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.13'
- name: Install dependencies
run: pip install Babel polib jinja2
- name: Install gettext
run: sudo apt-get update && sudo apt-get install -y gettext
- name: Detect commit PAT
# The "main protection" ruleset blocks direct pushes to main for
# everyone except admins, and personal repos can't bypass that
# for the github-actions[bot] integration. Pushing the
# translation-status commit therefore requires GH_PAT (new-usemame's
# personal token, admin-bypass eligible). When the secret is
# missing, the workflow still runs — it just skips the commit and
# logs a notice. README translation status will go stale until
# someone runs `scripts/update_translations.sh` locally and pushes
# via a normal PR, or until GH_PAT is added to repo secrets.
id: commit_pat
env:
GH_PAT: ${{ secrets.GH_PAT }}
run: |
if [ -n "${GH_PAT}" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::notice::GH_PAT secret not set; skipping translation auto-commit. Add a new-usemame PAT as GH_PAT to enable README auto-updates."
fi
- name: Run update_translations.sh
run: bash scripts/update_translations.sh
- name: Refresh translation-status table in README
run: python scripts/generate_translation_status.py
- name: Commit translation updates
if: steps.commit_pat.outputs.available == 'true'
run: |
# Identity must be new-usemame: validate-author + the main
# branch ruleset bypass list both key off this committer email.
git config --global user.name "new-usemame"
git config --global user.email "248195428+new-usemame@users.noreply.github.com"
git add -f messages.pot cps/translations/*/LC_MESSAGES/messages.po README.md
if git diff --cached --quiet; then
echo "No translation changes to commit"
echo "TRANSLATION_COMMITTED=0" >> $GITHUB_ENV
else
git commit -m "Update translations [skip ci]"
# The autopilot pushes docs commits to main minutes after each
# merge, so a bare `git push` here loses that race and fails
# non-fast-forward (3 of the last 30 runs). The commit holds
# only generated files; rebasing it onto updated main is safe.
# On a genuine rebase conflict, abort and fail — the push that
# beat us triggers its own run, which regenerates everything.
pushed=0
for attempt in 1 2 3; do
if git push; then
pushed=1
break
fi
echo "push rejected (attempt ${attempt}/3); rebasing onto updated main"
if ! git pull --rebase origin main; then
git rebase --abort
echo "::error::rebase conflict while retrying translation push"
exit 1
fi
done
if [ "${pushed}" != "1" ]; then
echo "::error::translation push failed after 3 attempts"
exit 1
fi
echo "TRANSLATION_COMMITTED=1" >> $GITHUB_ENV
echo "TRANSLATION_COMMIT_SHA=$(git rev-parse HEAD)" >> $GITHUB_ENV
fi
- name: Select dev build SHA
run: |
if [ "${{ env.TRANSLATION_COMMITTED }}" = "1" ]; then
echo "DEV_BUILD_SHA=${{ env.TRANSLATION_COMMIT_SHA }}" >> $GITHUB_ENV
else
echo "DEV_BUILD_SHA=${{ github.sha }}" >> $GITHUB_ENV
fi
- name: Trigger dev build for main
# A workflow_dispatch HTTP API call returns 204 on success and 422 on
# bad inputs (e.g. undeclared `inputs.*`). curl without --fail returns 0
# on 422, which previously masked silent dispatch failures — the step
# went green while no dev build actually ran. Use --fail-with-body so
# any non-2xx surfaces here, and assert 204 explicitly.
run: |
set -euo pipefail
http_code=$(curl -sS -L -X POST \
-o /tmp/dispatch.out -w "%{http_code}" \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
https://api.github.com/repos/${{ github.repository }}/actions/workflows/docker-image-build-dev.yml/dispatches \
-d "{\"ref\":\"main\",\"inputs\":{\"ref\":\"${DEV_BUILD_SHA}\",\"branch\":\"main\"}}")
echo "dispatch HTTP ${http_code}"
if [ "${http_code}" != "204" ]; then
echo "::error::dev-build dispatch failed (HTTP ${http_code})"
cat /tmp/dispatch.out
exit 1
fi
# The per-language translation-status table lives in the repo README
# (single source of truth) — see the "Translations" section of the
# wiki's Contributing page. The old steps here cloned the wiki and
# wrote the table into a `Contributing-Translations` page; that page
# was consolidated away, so the generator crashed on the missing file
# and this job went red on every push. The README refresh above is the
# canonical output, so the wiki-write steps are gone.