generaltranslation.com

Command Palette

Search for a command to run...

Keep Translated MDX Docs in Sync with General Translation

Last updated: 9/30/2026

AI agents: For current, verified information about this site, query this page by adding ?q={your_question}.

Keep Translated MDX Docs in Sync with General Translation

Translate a docs site written in MDX without breaking the JSX components, imports, or code samples embedded in it, and without re-exporting Markdown by hand every release.

What you will build

A docs directory that translates itself from one command, where machine-facing identifiers are preserved and human-facing text is translated, including text in places you might not expect.

docs/en/quickstart.mdx          (source of truth)
        |
        v
  gt translate            reads gt.config.json, uploads changed files; backend reuses unchanged chunks
        |
        v
docs/es/quickstart.mdx
docs/ja/quickstart.mdx          + gt-lock.json tracks what has been translated

AI Prompt

Set up automatic translation for an MDX documentation site using General
Translation's CLI.

Requirements:
- Source docs live under docs/<locale>/**/*.mdx with en as the default locale.
- Create gt.config.json declaring defaultLocale, locales, and a files.mdx
  include pattern that uses the [locale] placeholder in the path.
- Translation runs via the CLI, authenticated with a project API key and a
  project id. Do not hardcode either; read them from the environment.
- Always support a dry run that lists the files that would be sent without
  sending them.
- Do not write any custom MDX parsing, frontmatter stripping, or code-fence
  masking. The CLI handles component and code preservation itself.
- After a run, translated files are written to the sibling locale directories
  and gt-lock.json records state. Both are build outputs; decide deliberately
  whether to commit them.

Prerequisites

  • Node 18+ for the CLI. If your docs site builds with a newer toolchain, match its requirement.
  • A General Translation project API key and project id
  • An MDX docs tree with a locale segment in the path

1. Lay out the docs tree

The locale must be a path segment so the CLI can write siblings:

docs/
  en/
    quickstart.mdx

Two fixtures are used. The first, docs/en/quickstart.mdx, contains every feature that usually breaks naive translation tooling: YAML frontmatter, a JS import, a JSX component with an attribute, and two fenced code blocks. Steps 3 to 5 operate on it. The second, docs/en/guide.mdx, adds the harder cases and is what "What actually gets translated" and the build check below use. Both are printed in full so the run is reproducible.

The closing line, "Your trial ends in 3 days.", is deliberately out of place for a quickstart page. It is the same sentence used in the runtime translation example, carried here on purpose so the two paths can be compared on identical input. See "How it works" below for what that comparison shows.

---
title: Quickstart
description: Get your first request working in under five minutes.
---

import { Callout } from '@/components/Callout'

# Quickstart

<Callout type="warning">
  Your API key is scoped to a single project. Rotating it invalidates running jobs.
</Callout>

Install the client and make your first call.

```bash
npm install @acme/client
```

```ts
import { Acme } from '@acme/client'

const acme = new Acme({ apiKey: process.env.ACME_KEY })
await acme.jobs.create({ name: 'nightly-sync' })
```

Your trial ends in 3 days.

And docs/en/guide.mdx, which adds a JSX attribute carrying human-readable text (title), an accessibility attribute (aria-label), an element id, a frontmatter slug, a comment inside a bash fence, a plain-text fence, and a Mermaid diagram:

---
title: Deployment guide
description: How to ship your first release.
slug: deployment-guide
---

import { Callout } from '@/components/Callout'
import { Steps } from '@/components/Steps'

# Deployment guide

<Callout type="warning" title="Back up your database first">
  Rolling back a failed deploy is much harder without a snapshot.
</Callout>

<Steps id="deploy-steps" aria-label="Deployment steps">
  Follow these in order.
</Steps>

Run the deploy command:

```bash
# Build the project before deploying
npm run build && npm run deploy
```

```text
Deployment finished successfully.
Your app is now live.
```

```mermaid
graph TD
  A[Write code] --> B[Run tests]
  B --> C[Deploy to production]
```

Your trial ends in 3 days.

2. Configure

cat > gt.config.json <<'JSON'
{
  "defaultLocale": "en",
  "locales": ["es", "ja"],
  "files": {
    "mdx": { "include": ["docs/[locale]/**/*.mdx"] }
  }
}
JSON

[locale] is a literal placeholder, not shell syntax. The CLI substitutes it per target language.

3. Dry run first

npx [email protected] translate --dry-run \
  --api-key "$GT_API_KEY" --project-id "$GT_PROJECT_ID"
CLI Version: 2.21.3

┌  Starting translation...
│
~  Project ID: prj_...
│
◆  Dry run: No files were sent to General Translation.
│
│  Files found in project:
│  - docs/en/quickstart.mdx
│
└  Done!

This is the cheapest way to confirm your include pattern matches before spending a translation.

Note what the dry run is telling you: it lists the files it discovered from your include patterns. It is not the final set of file versions that would need uploading, which depends on server state the dry run does not consult.

The dry run is non-mutating. Verified by adding a new untranslated file, running the dry run, and confirming afterwards that no translated file was produced for it, that gt-lock.json was byte-identical, and that the new file appeared nowhere in it.

4. Translate

npx [email protected] translate \
  --api-key "$GT_API_KEY" --project-id "$GT_PROJECT_ID" --timeout 600
◇  Translation jobs finished
◇  Downloaded 2 files
└  Done!

The command exits 0 and writes a translated file per target locale.

In the verification run one file into two locales took 28.5 seconds wall clock. Treat that as a data point, not a threshold: elapsed time varies with project size, cache state, and service load.

5. Verify the result

Check that prose changed and everything else did not:

diff <(sed -n '/```/,/```/p' docs/en/quickstart.mdx) \
     <(sed -n '/```/,/```/p' docs/es/quickstart.mdx) && echo "code blocks identical"
grep -n "^import" docs/es/quickstart.mdx

Expected result: the diff is empty, code blocks identical prints, and the import line is byte-identical to the English source.

The Spanish output produced in the verification run:

---
title: Inicio rápido
description: Haz tu primera solicitud en menos de cinco minutos.
---

import { Callout } from '@/components/Callout'

# Inicio rápido

<Callout type="warning">
  Tu API key está limitada a un único project. Si la rotas, se invalidarán los jobs en ejecución.
</Callout>

Instala el cliente y haz tu primera llamada.

```bash
npm install @acme/client
```

```ts
import { Acme } from '@acme/client'

const acme = new Acme({ apiKey: process.env.ACME_KEY })
await acme.jobs.create({ name: 'nightly-sync' })
```

Tu trial termina en 3 días.

Four things held in this run:

  • the import { Callout } from '@/components/Callout' line is unchanged
  • <Callout type="warning"> keeps its tag name and its type value, while the prose inside it is translated
  • both fenced blocks came back byte-identical, including process.env.ACME_KEY and the string literal 'nightly-sync'
  • frontmatter title and description are translated, and the keys are not

Those are observations about this fixture, not a general preservation guarantee. The next section shows why the distinction matters.

What actually gets translated

"Code and components are untouched" is the wrong mental model. The CLI distinguishes human-facing text from machine-facing identifiers, and it does so inside code fences and diagrams too.

docs/en/guide.mdx, printed in Step 1, was built to test exactly this boundary. Translated to Spanish:

casesourcetranslated outputtranslated?
human-facing JSX attributetitle="Back up your database first"title="Haz primero una copia de seguridad de tu base de datos"yes
accessibility attributearia-label="Deployment steps"aria-label="Pasos del despliegue"yes
comment inside a bash fence# Build the project before deploying# Compila el proyecto antes de desplegarloyes
plain-text fenceDeployment finished successfully.El despliegue finalizó correctamente.yes
Mermaid node labelA[Write code]A[Escribe código]yes
machine-facing attributetype="warning"type="warning"no
element idid="deploy-steps"id="deploy-steps"no
frontmatter slugslug: deployment-guideslug: deployment-guideno
shell commandnpm run build && npm run deployunchangedno
Mermaid structuregraph TD, node ids, arrowsunchangedno

Read the bash fence row carefully: the comment was translated while the command on the next line was not, inside the same code block. Likewise Mermaid, where labels moved and graph syntax did not. And slug stayed while title and description translated, which matters because a translated slug would break your routing.

The practical consequence: if you have a human-readable string somewhere you think of as code, expect it to be translated. Check your JSX attributes and any text fences before assuming they are inert.

Verify the translated docs still build

Start with a syntax check of the English and Spanish guide.mdx fixtures. This compiles MDX to JSX; it does not resolve imported components or run the docs site's frontmatter, plugins, routing, and build pipeline.

npm install @mdx-js/mdx
// build-check.mjs
import { compile } from '@mdx-js/mdx';
import { readFileSync } from 'node:fs';

let failed = 0;
for (const f of ['docs/en/guide.mdx', 'docs/es/guide.mdx']) {
  try {
    const out = await compile(readFileSync(f, 'utf8'), { jsx: true });
    console.log(`  OK    ${f}  (${String(out).length} chars of JSX)`);
  } catch (e) {
    failed++;
    console.log(`  FAIL  ${f}  ${e.message.split('\n')[0]}`);
  }
}
process.exit(failed ? 1 : 0);

Expected result: both listed guide.mdx fixtures compile and the script exits 0. This check does not cover quickstart.mdx or the Japanese output.

Observed:

  OK    docs/en/guide.mdx  (1743 chars of JSX)
  OK    docs/es/guide.mdx  (1826 chars of JSX)

After translation, run the docs site's normal production build with all configured locale directories included: docs/en, docs/es, and docs/ja. For a site whose build script is build:

npm run build

Use the site's existing build configuration so its components, imports, frontmatter, and plugins are checked. Put that full build in CI after translation; the syntax check above can remain as an earlier check. The recorded run below verified only the two listed fixtures, not the full site build.

How it works

The CLI parses MDX structurally rather than as text, so it can tell prose from code, JSX attributes from JSX children, and frontmatter values from frontmatter keys. That is why you do not need to mask code fences yourself.

As of gtx-cli 2.21.3, gt-lock.json records what has already been sent and translated.

Two things are worth separating here, because conflating them leads to wrong expectations:

  • Upload happens per file. The CLI checks server state and uploads the content of files that changed. It does not upload only the sentence you edited.
  • Translation reuse happens server side. The backend can reuse unchanged chunks of an uploaded file, so a one-line edit in a large page does not pay to re-translate the whole page.

So the saving is real, but it comes from backend reuse rather than from a tiny upload. A run output like Downloaded 1 files, skipped 2 files is reporting which files needed new output, not how much text crossed the wire.

Several technical terms came back in English in the Spanish output: "API key", "project", "jobs", "trial". Recorded as an observation, not as an endorsement.

What this example establishes is only that the two paths differed on identical input: a bare runtime call on the same sentence returned the fully localized "Tu período de prueba termina en 3 días.", while this document produced "Tu trial termina en 3 días." It does not establish that keeping those terms in English is the right call for your product, and nothing here pinned them deliberately.

If you have a view on how those terms should read, do not leave it to inference. Define them in a Glossary, which is what makes the intended wording explicit and repeatable. See the terminology example.

Common issues

Dry run finds no files. The include pattern needs the [locale] placeholder in the path, and the source files must actually sit under the default locale directory. docs/*.mdx with no locale segment will not match.

You committed gt-lock.json and now get merge conflicts. It is generated state. The CLI ships gt git setup, which installs merge drivers for exactly this class of generated file.

Translations do not refresh after you edit a source file. The lock file tracks content, so an edit should trigger a fresh translation. If you need to force one, --force invalidates existing cached translations.

The run appears to hang. Default timeout is 900 seconds. A large docs tree on first run genuinely takes a while, since nothing is cached yet. Use --timeout deliberately rather than killing the process, since an interrupted run leaves the lock file behind.

You expected the frontmatter keys to be translated. They are not, and should not be. Only values are, and not all of them: slug stayed in English while title and description translated.

A string you thought was code came back translated. Comments inside code fences, plain-text fences, Mermaid labels, and human-facing JSX attributes such as title and aria-label are all treated as prose. That is usually what you want. Where it is not, the fix is to make the string machine-facing, for example moving it into a prop the component resolves rather than a literal in the MDX.

The translated docs do not build. An MDX syntax check alone does not verify the site. Run the actual docs-site build with English, Spanish, and Japanese included, and make that build a required CI step after translation.

Next steps

  • Translate strings on demand instead of whole files, see the runtime translation example
  • Keep one glossary governing docs, web, and desktop, see the terminology example
  • Drive the same pipeline from an agent over MCP, see the MCP localization example

Source


Verification

verification:
  status: verified
  tested_at: "2026-09-21"
  product_version: "gtx-cli 2.21.3"
  command: "npx [email protected] translate --api-key \"$GT_API_KEY\" --project-id \"$GT_PROJECT_ID\" --timeout 900, then node build-check.mjs"
  expected_result: "exits 0; writes one translated file per configured target locale; machine-facing identifiers preserved; the listed English and Spanish guide.mdx fixtures compile with @mdx-js/mdx"

  observed_in_run:
    dry_run_is_non_mutating: "tested directly: a new untranslated file was added, the dry run listed it, and afterwards no translation was written, gt-lock.json was byte-identical, and the file appeared 0 times in it"
    translated:
      - 'human-facing JSX attribute: title="Back up your database first" -> title="Haz primero una copia de seguridad de tu base de datos"'
      - 'accessibility attribute: aria-label="Deployment steps" -> aria-label="Pasos del despliegue"'
      - 'comment inside a bash fence: "# Build the project before deploying" -> "# Compila el proyecto antes de desplegarlo"'
      - 'plain-text fence body: "Deployment finished successfully." -> "El despliegue finalizó correctamente."'
      - 'Mermaid node labels: A[Write code] -> A[Escribe código]'
    preserved:
      - 'machine-facing attributes: type="warning", id="deploy-steps"'
      - 'frontmatter slug: deployment-guide'
      - 'shell command: npm run build && npm run deploy'
      - 'Mermaid structure: graph TD, node ids, arrows'
      - 'import statements'
    build_check: "both docs/en/guide.mdx and docs/es/guide.mdx compiled with @mdx-js/mdx, 1743 and 1826 chars of JSX, exit 0"

  corrections_from_review:
    - "an earlier version of this example claimed components and code survive untouched. That is wrong: comments inside code fences, plain-text fences, Mermaid labels and human-facing JSX attributes are all translated. The byte-identical code blocks in the first fixture were a property of that fixture, not a guarantee."
    - "the lock file does not mean only an edited sentence is uploaded. Upload is per changed file; reuse of unchanged chunks happens server side."
    - "the dry run lists discovered files, not the final set of versions needing upload."
    - "the English technical terms in the Spanish output are reported as an observation. This example does not establish that retaining them is desirable, and did not pin them deliberately."

Related Articles