write-locales CLI
The package includes a CLI tool that generates and updates locale JSON files from Rosey's base.json. New keys are added with their original text as the default value, existing translations are preserved, and keys that no longer exist in base.json are removed.
#Usage
npx rosey-cloudcannon-connector write-locales [options]
| Flag | Description |
|---|---|
-s, --source <dir> |
Rosey directory containing base.json (default: the parent of the config's locales directory, else rosey) |
-l, --locales <codes> |
Comma-separated locale codes (e.g. fr,de,es); falls back to the config's languages, else auto-detects from existing files |
-d, --dest <dir> |
Build output directory; writes a locale manifest to {dest}/_rcc/locales.json (default: the config's source) |
--keep-unused |
Preserve locale keys that are no longer in base.json instead of removing them. Used when remapping translations onto new keys — see Migrating from v1 |
-h, --help |
Show help |
Values fall back through ROSEY_* environment variables and then your rosey.{yml,yaml,json} config file, so CLI flags > env > config file — the same precedence Rosey itself uses. --dest is the one value with no default of its own: the command exits with an error if it can't resolve one from --dest, ROSEY_SOURCE or source: in your Rosey config.
How it works
- Reads
{source}/base.json(generated byrosey generate) - For each locale, reads or creates
{source}/locales/{code}.json - Adds new keys with
{ original, value, _base_original }—valuedefaults to the original text - Updates
_base_originalon all existing entries to the current source text (for stale detection) - Rewrites an
originalthat says the same thing as the current source in a different serialization, so locale files converge on one form (reported asN healed; see Serialization) - Removes keys that no longer exist in
base.json - Writes
{dest}/_rcc/locales.json— a JSON manifest the connector fetches at runtime (see Manifest format) - Validates that
cloudcannon.config.ymlhas matchingdata_configentries and warns about missing ones
Auto-detection of locales
If --locales is omitted, the CLI scans {source}/locales/ for existing .json files and uses their filenames as locale codes. After the initial setup (where you specify locales explicitly), subsequent runs just work.
# First run — explicitly specify locales to create the files
npx rosey-cloudcannon-connector write-locales --locales fr,de --dest dist
# Subsequent runs — auto-detects fr.json and de.json
npx rosey-cloudcannon-connector write-locales --dest dist
Programmatic API
You can call write-locales from Node.js instead of the CLI:
import { writeLocales } from "rosey-cloudcannon-connector/write-locales";
await writeLocales({
roseyDir: "rosey",
locales: ["fr", "de"],
dest: "dist",
});
| Option | Type | Description |
|---|---|---|
roseyDir |
string |
Rosey directory (default: "rosey") |
locales |
string[] |
Locale codes; auto-detected if omitted |
dest |
string |
(required) Build output directory |
keepUnused |
boolean |
Preserve keys not in base.json instead of deleting them (default: false) |
Unlike the CLI, the programmatic API does no env or config-file fallback — dest must be passed explicitly.
Locale file format
Each locale JSON file is a flat object keyed by Rosey translation keys:
{
"hero:title": {
"original": "Welcome to Sendit",
"value": "Bienvenue chez Sendit",
"_base_original": "Welcome to Sendit"
},
"hero:subtitle": {
"original": "The best email platform",
"value": "La meilleure plateforme email",
"_base_original": "The best email platform"
}
}
| Field | Description |
|---|---|
original |
The source text when the translation was last acknowledged or edited. May be rewritten to an equivalent serialization on build — never to different content |
value |
The translated text (HTML) |
_base_original |
The current source text from base.json, updated at build time. Required for stale translation detection |
original and value are standard Rosey locale fields. _base_original is the only field added by the connector — it powers stale detection by storing what the source text currently says, so the connector can compare it to original (what the source said when the translation was last reviewed).
Postbuild pipeline
The typical .cloudcannon/postbuild script runs four commands in sequence:
#!/usr/bin/env bash
# 1. Scan the built HTML for data-rosey elements, generate base.json
npx rosey generate --source dist
# 2. Sync locale files with base.json, write the _rcc manifest
npx rosey-cloudcannon-connector write-locales --source rosey --dest dist
# 3. Build the translated site (Rosey reads locale files and generates
# translated copies of every page)
mv ./dist ./_untranslated_site
npx rosey build --source _untranslated_site --dest dist --default-language en --default-language-at-root --exclusions "\.(html?)$"
Why mv?
Rosey reads from a source directory and writes to a dest directory. Since both are dist, we rename the original to _untranslated_site first so Rosey has a clean source to read from and a fresh dest to write to.
Why --exclusions?
Rosey's default exclusion regex (\.(html?|json)$) prevents JSON files from being copied through as assets. The override \.(html?)$ lets JSON files like _rcc/locales.json and _cloudcannon/info.json flow through to the final output without manual cp steps.
Manifest format
The manifest at {dest}/_rcc/locales.json is a JSON object:
{
"locales": ["fr", "de"]
}
| Key | Required | Description |
|---|---|---|
locales |
Yes | Array of locale codes |
Using your own script instead of write-locales
write-locales is optional. Its job is to get data from Rosey's base.json into locale files that the connector can edit. You can replace it with your own script, use it alongside other tools (e.g. pulling translations from an external translation service), or run additional processing after it finishes.
Since everything runs in a CloudCannon build hook, your postbuild script can do whatever it needs: pull data from an API, merge external translations, then hand off to Rosey.
If you roll your own workflow, here's what the connector expects:
- A flat JSON object for each locale, keyed by Rosey translation keys (matching the keys in
base.json) - Each entry must have
originalandvaluefields — these are standard Rosey locale fields (see the Rosey docs) - Each entry should have a
_base_originalfield if you want stale translation detection. This is the only field not native to Rosey — it stores the current source text so the connector can detect when the original has changed since the translation was last reviewed. Without it, stale detection is skipped for that entry. - A locale manifest at
{dest}/_rcc/locales.json— a JSON object with alocalesarray (see Manifest format above):{ "locales": ["fr", "de"] } - Matching
data_configentries incloudcannon.config.ymlfollowing thelocales_{code}naming convention
Note that only the locale files and the manifest are write-locales' job. Getting the browser client into your build output is a separate command, install-client, so replacing write-locales doesn't affect it — keep that line in your postbuild either way. It writes to the same _rcc directory but neither command touches the other's output. See SSG Setup.
You could also use write-locales as your baseline and run your own middleware on top — for example, calling an external translation API to fill in empty value fields before handing off to Rosey.
For detailed integration patterns — pipeline insertion points, machine translation examples, TMS sync, CI-driven workflows — see External Integrations.
For AI-powered translation of locale files (using an AI coding agent or custom script), see AI-Powered Translation.