> ## Documentation Index
> Fetch the complete documentation index at: https://help.scribe-mail.com/llms.txt
> Use this file to discover all available pages before exploring further.

# I18n

# Translation pipeline notes

How the 12 target locales are produced, and the one rule that has to live
outside this repository.

## Where translation is configured

| Thing                                           | Where it lives                    | Who owns it                               |
| ----------------------------------------------- | --------------------------------- | ----------------------------------------- |
| Source locale, target locales, file globs       | `i18n.json`                       | this repo                                 |
| Lock file (what has already been translated)    | `i18n.lock`                       | this repo, written by the CLI             |
| When translation runs                           | `.github/workflows/translate.yml` | this repo                                 |
| **Glossary, brand voices, rules, model chains** | **the Lingo.dev engine**          | **the Lingo.dev platform, not this repo** |

That last row is the important one. `npx lingo.dev@latest run` with
`LINGO_API_KEY` set routes every string through our **localization engine** on
the Lingo.dev platform. The engine applies its layers in order:

1. **Glossary** (highest): exact term mappings
2. **Rules**: locale-scoped linguistic conventions
3. **Brand voice**: tone, formality, register
4. **Model chain**: which model handles each locale pair, with fallbacks
5. **AI reviewers**: asynchronous quality checks

None of these are expressible in `i18n.json`. The config schema (version 1.10)
has no field for instructions, glossaries or brand voice; its only prompt-ish
field is `provider.prompt`, which applies solely to a bring-your-own-key
provider (`openai`, `anthropic`, `google`, `ollama`, `openrouter`, `mistral`).
Adding a `provider` block would take us **off** the engine and lose the
glossary, brand voices and translation memory along with it. Do not do that to
get at the prompt.

## The no-dashes rule

House style bans em dashes (U+2014) and en dashes (U+2013) everywhere. English
is clean, but the engine reintroduces dashes while translating: as of
2026-09-16 a sweep removed 1543 of them across 545 files in all 12 locales.

Sweeping the locale files fixes today's output only. The locale directories are
generated, so the next time the English source of a string changes, the engine
retranslates it and can put the dash straight back. **The durable fix is a rule
on the engine.**

Add it as a **rule** (the "locale-scoped linguistic conventions" layer), applied
to every target locale, not as a glossary entry, because a glossary maps terms and
this is a punctuation convention. Suggested wording:

> Never use an em dash (U+2014, —) or an en dash (U+2013, –) in any output.
> This applies to body text, headings, link text, image alt text, table cells
> and YAML frontmatter values alike. Where the source or a natural translation
> would use one, rephrase using the target language's own conventions: a comma,
> a colon, parentheses, a conjunction, or two sentences. Never substitute an
> ASCII hyphen (-) for a dash. Numeric and date ranges are spelled out with the
> target language's word for "to" rather than a dash.

### How to apply it

The engine is configured through the **Lingo.dev MCP server**, which can create
and update rules, glossaries and brand voices conversationally, or through the
Lingo.dev dashboard. The MCP server is not wired into this repository's tooling,
so this is a one-time manual step by someone with access to the Scribe
organization on Lingo.dev. Ask the assistant to "add a rule to every locale on
the Scribe engine: never emit em dashes or en dashes, rephrase instead", and
paste the wording above.

Caveat worth keeping in mind: an engine rule steers a language model, it does
not hard-guarantee a character never appears. That is why the repo also keeps a
checker.

## The guard

`scripts/check-dashes.mjs` runs last in `translate.yml`, with `if: always()`.

* **`en/**` and the other hand-written pages: hard failure.** English is
  authored by people, so a dash there is a real style bug with an owner.
* **The 12 locale directories: report only.** Blocking would be a deadlock. The
  commit step in `translate.yml` only runs when the preceding steps succeed, so
  failing on a generated file means the refreshed translations are never
  committed, and since the dashes come back from the engine, that failure
  repeats every run and freezes all 12 locales on an older build. Locale hits
  become GitHub warning annotations instead.

Run `node scripts/check-dashes.mjs --strict` locally to fail on locales too,
which is what you want when doing a deliberate sweep.

`scripts/check-mdx.mjs` additionally parses each page's YAML frontmatter. The
MDX compiler treats the `---` block as opaque, so a page with unreadable
frontmatter compiles fine here and then fails the Mintlify build. The specific
trap when removing dashes: replacing a dash with `": "` inside an *unquoted*
plain scalar turns `description: Install in one click: do this` into a mapping
value nested in a mapping value, which is a YAML error. Use a comma, or quote
the whole scalar.

## Before committing anything that touches translations

```bash theme={null}
npm ci --prefix scripts --no-audit --no-fund && node scripts/check-mdx.mjs
```

Mintlify rejects an entire deploy on one bad page, and a failed build strands
that commit's files permanently, because later builds only carry their own diff.
