Skip to content

Adopt the client's translation pipeline: glossaries, MQM review, and a fail-closed write #29

Description

@emooreatx

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

  1. Lift localize.py's mirror list into config; point it at dictionaries/ + chrome/
  2. Copy the 29 glossaries over as-is
  3. 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
  4. Port i18n-lane.yml for CI, running fail-closed
  5. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions