The site already mirrors our language set — src/i18n/config.ts says so outright ("Language set + native names mirror CIRISAgent's localization manifest so the site and the app agree"), and dictionaries/ and chrome/ carry the same 29 codes as the client bundles.
What is missing is everything that produces and defends those translations. .github/workflows/ has sonar.yml and nothing else.
CIRISClient has run that machinery for several releases now and it is repo-agnostic in principle. This is an offer to hand it over, plus an honest account of what ports cleanly and what does not.
What you would get
29 glossaries — 3,045 canonical term pairs. localization/glossaries/{code}_glossary.md. Each is a table of CIRIS terms of art with their agreed rendering per language, plus that language's standing prose rules (use the formal እርስዎ; never omit Yoruba's dot-below). These are the same terms the site uses — OBSERVE, DEFER, Wise Authority, accord holder — so the glossaries are not merely reusable here, they are the thing that keeps the site and the app from drifting apart in 29 languages at once.
Translation memory: ~3,853 keys × 29 locales of shipped strings. A model asked to translate "node" cold will pick something; the corpus already knows what we picked, and picked it consistently.
Three lanes — --lane translate | evaluate | repair:
- translate fills missing keys, glossary-first
- evaluate is MQM, not a vibe: reference-free span-level review over
{accuracy, fluency, terminology, style, locale} × {critical, major, minor}, scored by severity weight (10/5/1), emitting the error spans and deriving the score rather than emitting a number nobody can act on
- repair takes what evaluate rejected and fixes it
A fail-closed write. Only accepted values ever land:
withheld = set(rejected.get(lang, {})) | set(unrepaired)
writable = {k: v for k, v in values.items() if k not in withheld}
A rejected translation is not written. Not written-with-a-warning — not written.
Terminology fails at any severity. Critical and major reject for the obvious reasons; terminology joins them even as a minor, because the glossary makes it objective — the canonical term is written down, so "disagrees with the glossary" is a fact rather than a judgement, and letting it ship is how terminology consistency stops being true.
Why I think this is worth your time specifically
ACCORD_TERMINOLOGY_NOTES.md is a hand-maintained file tracking whether v6–v9 say "Section" or "Book", with a status column and ⚠️ markers. That is a glossary, kept by hand, in one language, with no enforcement — and it is the exact failure the terminology rule above turns into a build error. In 29 languages that file does not scale; the machinery already exists.
What ports cleanly, and what does not
Ports directly — src/i18n/dictionaries/*.json and src/i18n/chrome/*.json. Flat key-value bundles keyed off an English source are precisely what the pipeline is built for. Writes are position-preserving against en.json so the diff stays reviewable.
Does NOT port as-is — content/docs/**/*.mdx. The pipeline translates strings, not long-form prose with JSX in it. Machine-translating the Constitution is a different problem with a different risk profile, and I would not claim this tool solves it. Two honest options: keep long-form English-only for now, or run the evaluate lane over human translations of it — the MQM review and the glossary checks work fine on prose the tool did not write.
Needs generalising — the write path. insert() writes four byte-identical mirrors because that is the client's layout (android/desktop/ios/shared). Yours is two trees, not four. That is a config change, not a redesign, and it is the main piece of work in adopting this.
Suggested sequencing
- Lift
localize.py's mirror list into config; point it at dictionaries/ + chrome/
- Copy the 29 glossaries over as-is
- Run
--lane evaluate --check against what you already have — this tells you the current state before changing anything, and is the cheapest way to see whether this is worth continuing
- Port
i18n-lane.yml for CI, running fail-closed
- Decide the MDX question separately, on its own merits
Happy to do the porting rather than just hand you a link — say the word. The client-side lane is at localization/localize.py and .github/workflows/i18n-{translate,evaluate,repair,lane}.yml in CIRISAI/CIRISClient, and localization/TRANSLATION_GUIDE.md documents what the pipeline does and does not guarantee.
The site already mirrors our language set —
src/i18n/config.tssays so outright ("Language set + native names mirror CIRISAgent's localization manifest so the site and the app agree"), anddictionaries/andchrome/carry the same 29 codes as the client bundles.What is missing is everything that produces and defends those translations.
.github/workflows/hassonar.ymland nothing else.CIRISClient has run that machinery for several releases now and it is repo-agnostic in principle. This is an offer to hand it over, plus an honest account of what ports cleanly and what does not.
What you would get
29 glossaries — 3,045 canonical term pairs.
localization/glossaries/{code}_glossary.md. Each is a table of CIRIS terms of art with their agreed rendering per language, plus that language's standing prose rules (use the formal እርስዎ; never omit Yoruba's dot-below). These are the same terms the site uses — OBSERVE, DEFER, Wise Authority, accord holder — so the glossaries are not merely reusable here, they are the thing that keeps the site and the app from drifting apart in 29 languages at once.Translation memory: ~3,853 keys × 29 locales of shipped strings. A model asked to translate "node" cold will pick something; the corpus already knows what we picked, and picked it consistently.
Three lanes —
--lane translate | evaluate | repair:{accuracy, fluency, terminology, style, locale} × {critical, major, minor}, scored by severity weight (10/5/1), emitting the error spans and deriving the score rather than emitting a number nobody can act onA fail-closed write. Only accepted values ever land:
A rejected translation is not written. Not written-with-a-warning — not written.
Terminology fails at any severity. Critical and major reject for the obvious reasons; terminology joins them even as a minor, because the glossary makes it objective — the canonical term is written down, so "disagrees with the glossary" is a fact rather than a judgement, and letting it ship is how terminology consistency stops being true.
Why I think this is worth your time specifically
ACCORD_TERMINOLOGY_NOTES.mdis a hand-maintained file tracking whether v6–v9 say "Section" or "Book", with a status column andWhat ports cleanly, and what does not
Ports directly —
src/i18n/dictionaries/*.jsonandsrc/i18n/chrome/*.json. Flat key-value bundles keyed off an English source are precisely what the pipeline is built for. Writes are position-preserving againsten.jsonso the diff stays reviewable.Does NOT port as-is —
content/docs/**/*.mdx. The pipeline translates strings, not long-form prose with JSX in it. Machine-translating the Constitution is a different problem with a different risk profile, and I would not claim this tool solves it. Two honest options: keep long-form English-only for now, or run the evaluate lane over human translations of it — the MQM review and the glossary checks work fine on prose the tool did not write.Needs generalising — the write path.
insert()writes four byte-identical mirrors because that is the client's layout (android/desktop/ios/shared). Yours is two trees, not four. That is a config change, not a redesign, and it is the main piece of work in adopting this.Suggested sequencing
localize.py's mirror list into config; point it atdictionaries/+chrome/--lane evaluate --checkagainst what you already have — this tells you the current state before changing anything, and is the cheapest way to see whether this is worth continuingi18n-lane.ymlfor CI, running fail-closedHappy to do the porting rather than just hand you a link — say the word. The client-side lane is at
localization/localize.pyand.github/workflows/i18n-{translate,evaluate,repair,lane}.ymlin CIRISAI/CIRISClient, andlocalization/TRANSLATION_GUIDE.mddocuments what the pipeline does and does not guarantee.