SuperAPI AI Directory

translate-multilingual

General & other

View repository

Updated 10-15 19:50

About this skill

Translate a Rosey Site with AI

Two translation systems can run on the same site, and this skill covers both:

Part What it holds Section
Rosey locale JSON (rosey/locales/{code}.json) Shared UI and any page text tagged with data-rosey — nav, footer, headings, buttons, breadcrumbs, and (for non-split pages) whole page bodies Part 1
Content collection files (blog_fr/, blog_de/ …) Split-by-directory body content + per-post frontmatter (title, description, alt text) that the SSG renders natively per locale Part 2

Most sites only need Part 1. Only reach for Part 2 if the project actually has per-locale content directories (split-by-directory, set up in the make-site-multilingual skill, Phase 8). A split-by-directory page needs both: its body comes from a content collection file (Part 2), its shared UI from the locale JSON (Part 1).

This skill is written for AI coding agents, but the process works with any AI tool that reads and writes JSON.

Why Rosey files are ideal for AI

Rosey locale files are flat JSON with a predictable three-field structure per entry. That gives an agent three properties that make translation efficient and safe:

  1. Incremental — new entries from write-locales have value set to the source original. Comparing value to original instantly identifies what's untranslated; already-translated entries are left untouched. No diffing, no external state, no tracking database.
  2. Deterministic / idempotent — running the same pass twice produces the same output. No re-translation of existing work, reviewable git diff.
  3. Context-rich — keys encode where text appears (nav:about, index:hero:title, blog:recent-posts), which disambiguates short strings ("More", "Back", "Home") without a screenshot.

The data format is the state management: read a JSON file, find entries where value === original, translate them, write the file.


Part 1: Translate Rosey Locale JSON Files

Prerequisites

  • Locale files exist at rosey/locales/{code}.json (generated by write-locales).
  • The user has said which locale(s) to translate and into which language(s).
  • base.json is up to date — run npx rosey generate + npx rosey-cloudcannon-connector write-locales if unsure.
  • Source-language copy edits are finished. Changing source text after translating marks every translation of it out of date — correct behaviour, but pure churn. Settle the English copy first, in both directions.

Locale file format

Each file is a flat JSON object keyed by Rosey translation keys:

{
  "hero:title": {
    "original": "Welcome to Sendit",
    "value": "Welcome to 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 Source text when the translation was last acknowledged
value The translation (or the source original if untranslated)
_base_original Current source text from base.json, updated by write-locales each build

Classification follows from these three fields:

  • Untranslated — value === original and original === _base_original. Needs full translation.
  • Stale — original !== _base_original. Source changed since the translation was acknowledged. Translate from _base_original, then set original = _base_original.
  • Current — original === _base_original and value !== original. Already translated; leave it alone.

Workflow overview

This skill ships helper scripts that do the mechanical work (classification, translation memory, merging, HTML validation), leaving only the actual translation to the AI. Three phases:

  1. Prepare — a script classifies entries and writes a slim task file.
  2. Translate — the AI reads the task file and fills in translations.
  3. Merge — a script merges translations back into the full locale file.

Locate the scripts before running them. The commands below assume the skills were copied to skills/ in the project root. Depending on the install route they may be under .agents/skills/, .cursor/skills/, or a plugin directory outside the project — adjust the path to wherever this skill's scripts/ sits. If they aren't present at all, use the Manual Fallback at the end of Part 1.

Phase 1.1: Prepare

node skills/translate-multilingual/scripts/prepare-translation.mjs --locale fr

The script:

  • Classifies every entry as untranslated / stale / current (skip)
  • Builds a translation memory from already-translated entries and auto-applies exact matches (e.g. "Recent Posts" appearing 4×, translated once, applied everywhere) — writing those straight back to the locale file
  • Picks tone/register examples from existing translations
  • Writes a slim task file to rosey/locales/.translation-task-{code}.json
  • Reports counts (total, auto-applied, needs AI, stale)

Flags: --locale <code> (required), --source <dir> (default rosey), --output <path>, --examples <n> (default 5).

Phase 1.2: Translate

Read the task file. It contains:

  • _meta.tone_examples — already-translated entries showing the project's tone/register. Match this style.
  • untranslated — entries needing full translation. Each has original; add a value.
  • stale — entries where the source changed. Each has old_original, new_original, old_value; add a value based on new_original.

Translation rules

Use key namespaces for context. nav:about = an About nav link; index:hero:title = a hero page title; footer:copyright = footer copyright. For short/ambiguous strings ("More", "Back", "Home") the namespace tells you how it should read.

Preserve HTML structure. Locale values are always HTML (the SSG converted Markdown to HTML at build time). Keep every tag, attribute, and nesting; translate only the text nodes.

{
  "original": "<p>A starting point for <strong>developers</strong> looking to build a website.</p>",
  "value": "<p>Un point de départ pour les <strong>développeurs</strong> cherchant à créer un site web.</p>"
}
  • Keep all tags in place (<p>, <strong>, <a href="...">, <br>).
  • Preserve attributes exactly (especially href, class, id).
  • Keep &nbsp; and other entities where they appear.

Translate in namespace-grouped batches, not one entry at a time in isolation — group all nav:* together, all index:hero:* together, etc. This gives natural context and consistent results.

Match tone and register using _meta.tone_examples: formal vs informal address (vous/tu, Sie/du, usted/tú), and terminology consistency (if "Sign up" is already "S'inscrire", don't switch to "Créer un compte").

Intentional non-translations. Brand names ("CloudCannon"), product names ("Rosey"), proper nouns, technical terms, URLs, email addresses, and code snippets stay identical — set value equal to original. Signals: the key contains brand/name/logo/copyright, or the value is a single proper noun/trademark/URL/code. When in doubt, translate — a reviewer can revert one entry more easily than spot a missing translation.

Whitespace. write-locales preserves whitespace from the HTML (entries may be " Blog "). Preserve the same leading/trailing whitespace — Rosey substitutes the value directly into HTML and altered whitespace can break rendering.

After translating, write the task file back to the same path with value fields added.

Phase 1.3: Merge

node skills/translate-multilingual/scripts/merge-translation.mjs --locale fr

The script:

  • Merges value fields from the task file into the full locale file
  • For stale entries, sets original = _base_original (clears the stale flag)
  • Validates HTML — checks tag names/counts match and <a> href values are preserved; logs warnings for mismatches but still writes
  • Sorts keys alphabetically, 2-space indent, trailing newline
  • Deletes the task file after a successful merge

Flags: --locale <code> (required), --source <dir> (default rosey), --input <path>, --dry-run.

If the script reports HTML warnings, review those entries and fix broken tags before committing.

Part 1 checklist

  • Run prepare-translation.mjs; review the counts
  • Read the task file; check tone examples
  • Translate all untranslated entries (add value)
  • Translate all stale entries (add value based on new_original)
  • Preserve HTML tags, attributes, whitespace
  • Match tone/register
  • Write the task file back; run merge-translation.mjs; check warnings
  • Review the git diff of the locale file

Manual fallback (Part 1)

If the helper scripts aren't available, the AI reads and writes the full locale file directly:

  1. Classify every entry as untranslated / stale / current using the three-field rules above. Report counts (e.g. "42 untranslated, 3 stale, 107 current — translating 45 entries to French").
  2. Translate applying the same rules as Phase 1.2. Match tone/register of existing translations; translate in namespace-grouped batches. For stale entries, set both value (new translation) and original = _base_original.
  3. Write back:
    • JSON.stringify(data, null, 2) (2-space indent), keys sorted alphabetically (matching write-locales output)
    • Preserve all three fields on every entry — never drop _base_original
    • Don't add or remove keys — only modify value (and original for stale entries)
    • End with a trailing newline

Part 2: Translate Split-by-Directory Content Collections

Only needed if the project has per-locale content directories (e.g. src/content/blog_fr/, src/content/blog_de/). These are MDX/MD files with YAML frontmatter that the SSG renders natively per locale.

The clean boundary with Part 1

On a split-by-directory page, the two systems never touch the same element:

  • Content collection files (Part 2): body content and post-specific frontmatter (title, heading, description, image alt). Rendered natively from the locale file. These have no data-rosey attributes — adding data-rosey to content already translated via a collection would create two systems fighting over one element.
  • Rosey locale JSON (Part 1): shared UI in layout/component templates — header, footer, nav, breadcrumbs, sidebar headings like "Recent Posts". These use data-rosey because they're shared across pages and aren't in any post's frontmatter.

Rule of thumb: frontmatter or body → content collection (Part 2); shared layout/component UI → Rosey (Part 1). Fully translating a split-by-directory page needs both parts.

Prerequisites

  • Per-locale content directories exist (src/content/blog_fr/, src/content/blog_de/).
  • A source-language directory with the same slugs exists (src/content/blog/).
  • The user has said which locale(s) to translate.

Workflow overview

Same prepare → translate → merge shape as Part 1, with content-specific scripts. If the scripts aren't available, use the Manual Fallback at the end of Part 2.

Phase 2.1: Prepare

First identify the source and locale directories: look for {collection}_{locale}/ dirs in the content dir (usually src/content/), and check cloudcannon.config.yml for per-locale collections or a locale config file (often src/lib/locales.ts).

Then, per locale collection:

node skills/translate-multilingual/scripts/prepare-content-translation.mjs \
  --source-dir src/content/blog \
  --locale-dir src/content/blog_fr \
  --locale fr

The script:

  • Compares each locale file against the source file with the same filename
  • Marks files untranslated (frontmatter text + body still match source) vs already translated (skip)

MUST finish source-post edits before translating. This classification is binary — there is no stale state and no equivalent of Part 1's _base_original. Edit a source post after its locale copy is translated and the copy still differs from source, so it stays classified "already translated" and is silently skipped on every later run, with nothing flagging it. Remedy: re-translate that file explicitly, or diff it against its source file to find what changed. See make-site-multilingual/troubleshooting.md.

  • Extracts translatable frontmatter fields (title, headings, descriptions, alt text) by dot-notation path
  • Skips structural fields (dates, image paths, tags, booleans, CMS metadata, URLs)
  • Writes a task manifest to src/content/.translation-task-{code}-content.json

Flags: --source-dir <dir> (required), --locale-dir <dir> (required), --locale <code> (required), --output <path>.

Phase 2.2: Translate

For each file with "status": "untranslated" in the manifest:

Frontmatter — mirror translatable_frontmatter into a translated_frontmatter object with the same keys and translated values:

{
  "translatable_frontmatter": {
    "title": "Visual Translation Editing with the RCC",
    "post_hero.heading": "Visual Translation Editing with the RCC",
    "seo.page_description": "How the Rosey CloudCannon Connector enables..."
  },
  "translated_frontmatter": {
    "title": "Édition visuelle des traductions avec le RCC",
    "post_hero.heading": "Édition visuelle des traductions avec le RCC",
    "seo.page_description": "Comment le Rosey CloudCannon Connector permet..."
  }
}

Translate: title, headings (*.heading, *.subheading), descriptions (*.description, *.page_description), image alt text (*_alt, *.image_alt, *.featured_image_alt), and any other human-readable text. The script already excluded CMS metadata (_schema, _name, _uuid), dates, image paths, tags, author names, URLs, booleans.

Body — if the manifest includes body, add translated_body with the translated markdown:

{
  "body": "The Rosey CloudCannon Connector (RCC) is a client-side script...",
  "translated_body": "Le Rosey CloudCannon Connector (RCC) est un script côté client..."
}

Body rules:

  • Preserve markdown formatting (bold, italic, links, lists, blockquotes)
  • Keep link URLs unchanged — translate only link text, not href
  • Keep code blocks in the source language (code examples, CLI commands, HTML snippets)
  • Preserve MDX components — keep component syntax, translate only text inside
  • Keep technical terms and product names (Rosey, CloudCannon, Bookshop)

Match tone/register with existing translations (Rosey locale JSON or other translated content) — same formal/informal and terminology consistency as Part 1.

Write the manifest back with translated_frontmatter / translated_body added.

Phase 2.3: Merge

node skills/translate-multilingual/scripts/merge-content-translation.mjs \
  --input src/content/.translation-task-fr-content.json

The script patches translated frontmatter into the YAML (preserving structural fields and formatting), replaces body content, validates frontmatter integrity, and deletes the manifest after a successful merge. Flags: --input <path> (required), --dry-run. Review any fields it warns it couldn't patch.

Part 2 checklist

  • Identify source and locale content directories
  • Run prepare-content-translation.mjs per locale collection; review counts
  • Translate all translatable_frontmatter (add translated_frontmatter)
  • Translate body (add translated_body)
  • Preserve markdown formatting, code blocks, link URLs, MDX components
  • Match tone/register
  • Write the manifest back; run merge-content-translation.mjs; check warnings
  • Review the git diff

Manual fallback (Part 2)

  1. Detect collections — find {collection}_{locale}/ dirs, a locale config file, or per-locale entries in cloudcannon.config.yml; the source collection is the one with no locale suffix.
  2. Classify files — compare each locale file against the source file with the same slug. Untranslated = frontmatter text + body identical to source. Report counts.
  3. Translate each untranslated file. Frontmatter: translate title/headings/descriptions/alt text; leave _schema/_name/_uuid, dates, image paths, tags, author names, URLs, booleans. Body: translate prose/headings/list items; preserve formatting, code blocks, link URLs, MDX components.
  4. Write back in place — preserve exact YAML structure (field order, nesting, indentation), the file extension (.mdx/.md), any MDX imports, and a trailing newline.

Edge cases

  • Tags/categories as slugs (rosey, visual-editing) — used for URLs/filtering; do not translate.
  • Code blocks (fenced ```) — stay in the source language.
  • Brand names in alt text — translate the alt text but keep brand names.
  • Files with no source equivalent — skip.
  • Partially translated files — translate only the untranslated parts.

Learnings and Gotchas

This section is a living document. When you discover new patterns, issues, or improvements while translating, ask the user before appending them here. See the repo README's Key conventions → Living documents.