Our documentation is built on top of Mintlify. We welcome contributions for fixing small issues such as typos and for adding new or updating existing content that you think would help others.
Install the Mintlify CLI to preview the documentation changes locally. To install, use the following command:
npm i -g mintlify
Run the following command at the root of your documentation (where docs.json is):
npm run dev
Our docs are written in MDX, which if you haven't used it before is a bit if markdown and React had a baby.
See the full guide from Mintlify on writing MDX and the components that are available for how best to contribute.
This repository uses Lingo.dev for static-content localization of docs pages and language-specific navigation labels in docs.json.
- Open your Cursor MCP config file:
~/.cursor/mcp.json
- Add a Lingo MCP server entry:
{
"mcpServers": {
"lingo": {
"url": "https://mcp.lingo.dev/account",
"headers": {
"x-api-key": "${env:LINGO_API_KEY}"
}
}
}
}- Ensure the API key is available to the Cursor app process as an environment variable:
LINGO_API_KEY
- Restart Cursor fully after updating
mcp.jsonor environment variables. - Verify by asking Cursor chat to list engines from the Lingo MCP server.
Notes:
- This repo ignores
.envvia.cursorignore, so Cursor agents should not read API keys from project files. - If Cursor was launched before env vars were set, restart Cursor from a shell where env vars are already exported.
lingo/glossary.csv: Terms that must stay fixed or use specific translations.lingo/brand-voice.md: Single brand voice used for all locales.scripts/translate-docs-json.mjs: Translates language-specificdocs.jsonnavigation labels directly in the source-of-truthdocs.json. Prefernpm run translate:docs-json -- --target <locale>.scripts/localize-internal-links.mjs: Rewrites internal absolute navigation links in each locale’s MDX for whatever locale is being processed (--target,--all, or default targets): bare paths get that locale’s prefix (e.g./platform/points→/es/platform/pointswhen processinges/), and any path already prefixed with another locale fromi18n.json(en,es, futurefr, etc.) is re-prefixed to the active locale (so/en/...or stale/es/...on afr/page become/fr/...). Longer codes are matched first (e.g.en-USbeforeen). It does not change relative image or videosrcpaths, MDX/JSX import paths, or shared MDX snippets; those must be correct in source so all locales stay aligned. Prefernpm run translate:localize-links --with--target,--all, or--all --check.scripts/localize-component-imports.mjs: Rewrites MDXimport ... from ".../<locale>/components/...jsx"paths so they match the active locale being processed (--target,--all, or default targets). This keeps locale pages importing locale-local components after PIT/CI translation.- Shared MDX snippets (
snippets/*.mdx): Reusable MDX blocks stay in repo-rootsnippets/(not per-locale). Import them with relative paths from each page (for example../../snippets/foo.mdxfromlocale/<section>/<page>.mdx, or more../segments for deeper pages). They are excluded from Lingo buckets ini18n.jsonso they stay English and identical everywhere. - Localized React components (
components/*.jsxfor default language,<locale>/components/*.jsxfor targets): UI components that can contain locale text are stored per locale (for examplecomponents/rate-limit-badge.jsx,es/components/rate-limit-badge.jsx) and manually maintained per locale (not translated by PIT/CI automation). - Media paths: Default-language pages live at repo root paths like
<section>/<page>.mdxand target locales live under<locale>/<section>/<page>.mdx; shared files sit in repo-rootassets/. Keep relative media paths correct for each file depth. openapi.yml: One OpenAPI 3.1 spec at the repository root (alongsidedocs.json). API and webhook pages reference it explicitly in frontmatter, for exampleopenapi: openapi.yml get /users/{id}oropenapi: openapi.yml webhook points.changed. Do not duplicate the YAML under locale folders; Lingo must not alteropenapi:lines.scripts/sync-openapi-titles.mjs: Copies each operation/webhooksummaryfromopenapi.ymlinto the English page’stitle:frontmatter (Mintlify’s default whentitleis omitted). Target locales get an Englishtitleonly if missing (bootstrap); runnpm run translate:generateso Lingo translates those titles. Runs automatically at the start oftranslate:generateand in translate-on-main before Lingo.scripts/sync-heading-anchors.mjs: Writes Mintlify custom heading IDs as## Title {#slug}markdown. Slugs match Mintlify’s auto rules from the English title so hashes like#pro-planstay stable across locales. Runnpm run translate:sync-anchorsafter bulk heading edits; translation pipelines run it automatically (see Heading anchors and Lingo below). The script can also migrate one-line<h2 id="slug">…</h2>left from older tooling back to{#slug}syntax.scripts/validate-glossary-csv.mjs: Validates glossary schema and duplicate canonical keys. Prefernpm run lingo:validate-glossary.
- Canonical behavior is enforced by
scripts/sync-heading-anchors.mjs(runs at the end oftranslate:generateand on the translate-on-main workflow). It rewrites default-language root files from current English titles and reapplies the same{#slug}suffixes to translated headings in document order. Hash links stay aligned even if Lingo alters titles or fragments. - Lingo: Keep
lingo/brand-voice.mdto tone and style only. For structural rules (for example Mintlify{#slug}on headings), optionally add a separate line in your Lingo engine Instructions field (not brand voice), such as: “Preserve{#…}heading fragments exactly—do not translate or alter the slug.” That only reduces churn; the script +translate:sync-anchors:checkare authoritative. - Strict MDX parsers (for example unconfigured
mdx-js) can treat{as JSX and error on{#slug}; Mintlify’smint dev/ deploy pipeline is expected to handle this documented syntax. If you see Acorn errors locally, update the Mintlify CLI (npm i -D mint@latestor globalmintlify) or check Mintlify support; do not switch to raw HTML headings unless Mintlify asks you to. - Do not fold this into
localize-internal-links.mjs: that script only handles absolute pathhrefs; heading anchors are a separate structural pass and must run after Lingo (and after link localization) so all three stay consistent.
Use npm run <script> -- <args> when a script needs CLI flags (the -- forwards arguments to the underlying command).
| Script | Purpose |
|---|---|
translate:generate |
Run Lingo for target locale(s), then docs.json labels, internal link localization, and heading-anchor sync. Requires .env with LINGO_* vars. |
translate:docs-json |
Translate docs.json nav for one --target locale. Requires .env. |
translate:localize-links |
Localize internal href paths; pass --target, --all, and/or --check. |
translate:localize-links:all |
Same as translate:localize-links -- --all. |
translate:localize-links:check |
CI-style check: all locales, no writes (--all --check). |
translate:localize-component-imports |
Localize MDX imports that reference <locale>/components/*.jsx to the active locale. |
translate:localize-component-imports:check |
CI-style check: fail if any MDX imports point at another locale’s components. |
translate:sync-anchors |
Apply English-derived {#slug} on default-language root MDX and target locales (see i18n.json locale.targets). |
translate:sync-anchors:check |
Fail if any MDX needs re-sync (CI). |
translate:sync-openapi-titles |
Set default-language API & webhook title: from openapi.yml summaries; bootstrap missing target title from English. |
translate:sync-openapi-titles:check |
CI: fail if any OpenAPI-backed page title differs from the spec. |
lingo:validate-glossary |
Validate lingo/glossary.csv. |
translate:validate |
MDX parity and frontmatter checks (source root -> target locales, e.g. es). No .env required. |
translate:verify |
lingo.dev run --frozen (freshness gate). |
mint:validate |
Mintlify validate. |
mint:broken-links |
Mintlify broken-links with anchor and snippet checks. |
- Configure
LINGO_ENGINE_IDas an environment variable/secret.- Local:
export LINGO_ENGINE_ID=eng_... - CI: set repository secret
LINGO_ENGINE_ID i18n.jsonintentionally omitsengineId; workflows/scripts injectengineIdfromLINGO_ENGINE_IDat runtime.
- Local:
- In the Lingo.dev engine:
- import/create glossary entries from
lingo/glossary.csv - sync wildcard brand voice from
lingo/brand-voice.mdvia Cursor chat + Lingo MCP (on demand)
- import/create glossary entries from
- Run:
npx lingo.dev@latest runto generate/update translationsnpm run translate:docs-json -- --target esto translate language-specific labels indocs.jsonnpm run translate:localize-links -- --target esto localize on-page internal links for that localenpm run lingo:validate-glossaryto validate glossary rows before MCP syncnpx lingo.dev@latest run --frozenornpm run translate:verifyto enforce no pending translation deltas
Brand voice sync workflow (manual via Cursor + MCP):
- Update
lingo/brand-voice.md. - In Cursor chat ask: "Sync
lingo/brand-voice.mdto Lingo brand voice for engine<engine_id>." - The agent will use MCP tools to find the engine brand voice id and update it.
Glossary sync workflow (manual via Cursor + MCP):
- Validate first:
npm run lingo:validate-glossary - Always run dry-run first, then apply
- Canonical key used for reconciliation:
sourceLocale|targetLocale|type|sourceText - Dry run prompt:
Dry-run glossary sync for engine <ENGINE_ID> using lingo/glossary.csv via Lingo MCP. Use canonical key sourceLocale|targetLocale|type|sourceText. Show counts and exact create/update/delete operations. Do not apply changes yet.
- Apply prompt:
Apply glossary sync for engine <ENGINE_ID> using lingo/glossary.csv via Lingo MCP. Use canonical key sourceLocale|targetLocale|type|sourceText. Perform create/update and prune missing entries (delete remote items not in CSV).
- Use prune only when you want remote glossary to match CSV exactly.
- Add the locale to
docs.json:- In
navigation.languages, add a new object withlanguage: "<locale>". - Copy the default-language structure (tabs/groups/pages) and prefix pages with
<locale>/.... - Add language-specific
anchorsandnavbarfields in that locale block.
- In
- Add the locale to
i18n.json:- In
locale.targets, append the locale code (for examplefrorde).
- In
- Create the language content directory:
- Mirror the root default-language structure under
<locale>/with the same filenames. - Example:
platform/overview.mdx->fr/platform/overview.mdx. - Keep MDX snippets shared in repo-root
snippets/(no per-locale snippet trees). - Keep React components locale-local under
<locale>/components/*.jsxand mirror updates manually across locales (not via Lingo automation).
- Mirror the root default-language structure under
- Configure Lingo.dev engine controls for the new locale:
- Add/update brand voice for that locale.
- Add locale-specific glossary entries if needed.
- Generate translations (requires
.envwithLINGO_API_KEYandLINGO_ENGINE_ID):- One locale:
npm run translate:generate -- --target <locale> - Multiple locales:
npm run translate:generate -- --target es,fr,de - All configured targets:
npm run translate:generate - Force full re-translation for a locale:
npm run translate:generate -- --target <locale> --force. This deletes locali18n.cacheif present, runslingo.dev purge --locale <target> --yes-really(all managed keys for that locale peri18n.json), thenlingo.dev run --target-locale <target> --force. Both purge andrun --forcematter: without--force, Lingo may skip every file (“from cache” in the summary) when the delta is empty;--forcepushes all keys through the engine. For narrower purges, uselingo.dev purgewith filters, then runlingo.dev run --force --target-locale …yourself.
- One locale:
- Validate before merge:
npm run translate:validatenpm run translate:localize-links:checknpm run translate:sync-anchors:checknpm run mint:validatenpm run mint:broken-links
Notes:
- The workflow
.github/workflows/translate-on-main.ymlautomatically reads locales fromi18n.jsonlocale.targets. - Keep English as source/default and preserve identical relative file paths across locales.
--forceontranslate:generatepurges all Lingo-managed content for that target locale, then re-runs translation—use only for recovery/backfill (for example, cache stuck after bootstrap). Do not use--forcein CI; CI should run delta translation based oni18n.lock.