Continuous localization¶
The aim is to replace the string freeze with checks that run on every pull
request. A change that hides a string from lupdate fails the build the day
it is written, a layout that cannot grow shows up in a pseudo-locale, and a
broken placeholder never reaches a release. Only the deterministic steps
gate; the model-based ones run on a schedule and inform people.
The workflow below assumes the layout from project configuration:
localizzy.toml at the repository root, sources in src/, extraction into
i18n/fresh/ and reviewed catalogs i18n/app_<code>.ts.
The pull-request gate¶
# .github/workflows/localization.yml
name: Localization
on:
pull_request:
paths: ["src/**", "i18n/**", "localizzy.toml"]
permissions:
contents: read
security-events: write
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Qt Linguist tools
run: sudo apt-get update && sudo apt-get install -y qttools5-dev-tools
- uses: astral-sh/setup-uv@v5
- name: Install localizzy
run: uv tool install vexy-localizzy
- run: localizzy doctor
- name: Scan the source
run: localizzy qt scan src --min-coverage 0.8 --format sarif --out scan.sarif
- name: Upload scan results
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: scan.sarif
- name: Extract
run: localizzy qt extract
- name: Pseudo-locale
run: |
localizzy pseudo i18n/fresh/app_en.ts i18n/fresh/app_xx.ts
localizzy qt release i18n/fresh/app_xx.ts
- name: Check the reviewed catalogs
run: |
status=0
for code in de fr; do
localizzy qa "i18n/app_${code}.ts" --plural-forms auto \
--format sarif --out "qa-${code}.sarif" || status=1
done
exit $status
- name: Upload QA results (de)
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: qa-de.sarif
category: qa-de
- name: Upload QA results (fr)
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: qa-fr.sarif
category: qa-fr
Install Qt explicitly: localizzy doctor reports what is missing and installs
nothing. The scan step exits 1 on any critical finding, as well as below the
coverage threshold, so the upload runs under if: always() and the findings
still appear as code-scanning alerts on the pull request. Pass the source
directory as a relative path, as here, so the alerts point at repository
paths. Code scanning on a private repository needs GitHub Advanced Security;
without it, drop the upload and keep scan.sarif as an artifact.
qa runs only on the reviewed catalogs. The freshly extracted ones fail by
design until every message has a target, and the pseudo catalog proves
nothing about language. Each QA result points at its catalog file, since a
message has no source line. Give every uploaded SARIF file its own
category, so the languages do not replace each other's alerts.
qt extract leaves the fresh catalogs in the working tree. Whether a job
commits them back, or a person runs project upgrade and
review locally, is a choice for each team; the gate itself writes nothing to
the repository.
Drafts for review¶
With an engine key in the repository secrets, a job can prepare the next upgrade for reviewers. One language per call; each call writes NEW, RETIRED and a report, all unfinished where a model filled them:
- name: Upgrade drafts
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
uv tool install --force 'vexy-localizzy[translation]'
for code in de fr; do
localizzy project upgrade "$code" --model MODEL || [ $? -eq 1 ]
done
- uses: actions/upload-artifact@v4
with:
name: upgrade-drafts
path: |
i18n/*.new.ts
i18n/upgrade/
i18n/retired/
Exit 1 means some messages are still pending, which is expected for drafts;
the files are written either way. The engine settings come from [translate]
in localizzy.toml. The drafts
go to people through the review workspace, never straight to the
reviewed catalogs.
The nightly judge¶
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch:
jobs:
judge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- run: uv tool install 'vexy-localizzy[llm]'
- name: MQM sample
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
for code in de fr; do
localizzy qa "i18n/app_${code}.ts" --plural-forms auto --layers judge \
--endpoint https://api.openai.com/v1 --model MODEL \
--judge-sample 0.1 --fail-on critical \
--format json --out "judge-${code}.json" || true
done
- uses: actions/upload-artifact@v4
with:
name: judge-reports
path: judge-*.json
The judge never gates. || true keeps one language's low scores from hiding
the others; the reports are for a person to read in the morning. The same job
is the place for a golden-set comparison when the model or
the prompt changes, and for an editorial review of a finished
catalog: its candidates file is an artifact for a person to read, and apply
stays a local step.
What stays out of CI¶
Compiling .qm files for a release belongs to the build that packages the
application: commit .ts, ignore .qm. Approval belongs to people. No step
here marks a message approved.