Locale Archives & Bundles
The Translations page establishes that the co-located {locale}.json files are the runtime source of truth. This page covers the transport and inactive-archive layer that sits alongside them: a versioned bundle format that lets you hand English strings to an external translator, accept a translated bundle back, and keep curated locales on disk without an AI pass.
The bundle system is reeman-only - there are no MCP tools for it. Drive it from the CLI or the interactive menu.
The locales-archive/ Directory
Every archived locale lives as a single file at locales-archive/<locale>.json. There is one file per locale, not a tree of per-namespace files - the whole locale's translations are collected into one bundle. A bundle is a JSON object:
{
"format": "reepolee-translations-v1",
"source_locale": "en-us",
"target_locale": "de-de",
"files": {
"apps/main/home/locales/en-us.json": {
"source_hash": "<sha256 of the sorted canonical English file>",
"translations": { "ui": { "title": "Startseite" } }
},
"platform/auth/login/locales/en-us.json": {
"source_hash": "...",
"translations": { "actions": { "submit": "Anmelden" } }
}
}
}
The top-level files map is keyed by repository-relative paths to the English source files. The source_hash is the SHA-256 of the sorted canonical English file at export time; on import it is checked against the current English sources so a stale bundle is rejected rather than silently merged. The translations object holds the translated leaves.
Bundles are inactive until installed: a bundle in locales-archive/de-de.json does not serve to visitors and is not loaded at startup. Installing it (see install-locale) writes the translations back to the co-located live files and registers the locale in config/supported_locales.ts.
Export an English Bundle
export-translation-bundle reads every active en-us.json file and writes one bundle containing the English leaves as the translation source for an external service:
bun reeman export-translation-bundle translation-bundle-en-us.json
bun reeman export-translation-bundle fr-fr-bundle.json --target-locale fr-fr
The output file is the transport artifact you hand to a translator. --target-locale tags the bundle's target_locale (e.g. fr-fr) so the receiving end knows which locale it is destined for; the exported leaves remain English either way. When --target-locale is omitted the bundle's target_locale is null and must be filled in (or a --target-locale supplied) before import will accept it - import rejects a bundle whose target_locale is missing or is en-us.
Only leaf values should be translated; the file structure and key paths must be preserved. The source_hash on each entry lets the importer detect drift: if the English source files changed since export, the import fails with a path-level mismatch instead of merging against a stale source.
Import a Translated Bundle
import-translation-bundle validates a translated bundle against the current English sources and archives it:
bun reeman import-translation-bundle fr-fr-bundle.json
bun reeman import-translation-bundle fr-fr-bundle.json --install
bun reeman import-translation-bundle fr-fr-bundle.json --install --activate
Validation, before anything is written:
- the
formatisreepolee-translations-v1andsource_localeisen-us; target_localeis set and is noten-us;- every file path in the bundle matches a current English source file (paths are normalized, so either
…/en-us.jsonor…/fr-fr.jsonendings resolve to the same source); - the bundle has no duplicate or unknown paths;
- each
translationsobject is a plain object of string leaves (no nested non-leaf objects, no non-string values); - every translated key exists in the English source, keeps the same leaf/object shape, and preserves placeholders (
{name},{count}) - a translation that drops or renames a placeholder is rejected.
On success the bundle is archived as locales-archive/<target_locale>.json. If an archive for that locale already exists, the new translations are merged in with apply_translations (incoming values win) and omitted files or keys are kept - import never deletes translations from an existing archive.
--install also restores the archived locale to the co-located live files (the {locale}.json files the runtime reads), registers it in config/supported_locales.ts, and pings the running server to reload. --activate (implies --install) additionally adds the locale to active_locales so it is served to visitors immediately. Without --install, the bundle is archived but stays inactive.
Install an Archived Locale
Once a locale has a curated bundle in locales-archive/, install-locale restores it to the live tree without any AI call - the archived translations are the curated source:
bun reeman install-locale de-de
bun reeman install-locale de-de --activate
It reads locales-archive/<locale>.json, writes the translations back to the matching {locale}.json live files, adds the code to locales (and to active_locales with --activate) and its display name to locale_names in config/supported_locales.ts, and notifies the running server. The locale is installed as inactive by default - it loads but no picker offers it until you activate it.
The interactive form lists the archived locales found in locales-archive/ and prompts for the code, so you don't have to remember what's available:
bun reeman
# pick "Install archived locale" → choose the code → confirm activation
This is the path for locales that ship curated translations (the framework's own bundled languages) or that you previously archived after an external translation pass. It is distinct from add-locale, which copies the default-locale files as placeholders and optionally runs an AI translation pass.
Migrate a Legacy Archive Tree
Older versions stored the archive as a mirror of the live tree (locales-archive/routes/…, locales-archive/routes_reeman/…, etc.). The current format is one bundle per locale at the archive root. migrate-translation-archive converts the legacy tree in place:
bun reeman migrate-translation-archive
It walks the legacy roots (locales, routes, routes_reeman, routes_reeqa under locales-archive/), collects the locale codes, and writes one locales-archive/<locale>.json bundle per locale using sync_lang_to_en to align each archived file with the current English structure. After the bundles are written, the legacy subdirectories are removed. Run this once after upgrading; it is idempotent.
The Full Round-Trip
The bundle system exists so an external translator (human or service) can work on a single transport file without touching the repository's live files:
- Export the English sources:
bun reeman export-translation-bundle nl-nl-bundle.json --target-locale nl-nl. - Translate only the leaf values in the bundle, preserving keys, structure, and placeholders.
- Import the translated bundle:
bun reeman import-translation-bundle nl-nl-bundle.json --install --activate. - The locale is now live and served. The archived bundle at
locales-archive/nl-nl.jsonis the curated record; re-runninginstall-locale nl-nlrestores it at any time.
If English sources change after step 1, the import at step 3 fails the hash/path check - re-export, carry the new/changed keys across, and import again. Existing archived translations for unchanged keys are preserved by the merge.
Where This Sits
| Concern | Owner |
|---|---|
| Runtime source of truth | the co-located {locale}.json live files (Translations) |
| AI-assisted gap filling | add-locale, sync-translations --translate (Dynamic Translations) |
| Curated / external translation transport | the locales-archive/ bundle system (this page) |
| Installing a curated locale to live files | install-locale (this page) |
The three layers are independent: you can AI-translate with sync-translations and never touch bundles, or you can run a curated external-translation pipeline and never call the AI provider. Both land in the same co-located {locale}.json files the runtime reads.