Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .cursor/rules/localization-workflow.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
description: Localization workflow and Lingo guardrails
alwaysApply: true
---

# Localization Workflow

- Treat `en/` as the canonical source locale and keep identical file paths/filenames across all locales.
- Keep all locale navigation in `docs.json` under `navigation.languages`; do not reintroduce `navigation.global` or top-level `navbar`.
- Keep SEO metatags in English unless explicitly requested otherwise.
- Do not translate code snippets, inline code, URLs, route slugs, or API identifiers.
- Shared media lives under repo-root `assets/`. Locale pages are nested under `en/` or `es/` (typically `locale/<section>/<page>.mdx`), so relative `src` paths must account for that depth (for example `../../assets/...`). `scripts/localize-internal-links.mjs` rewrites markdown `](/...)` and JSX `href="/..."` only: for whichever **`locale` is being processed**, bare paths get that prefix, and paths prefixed with **any other** locale from `i18n.json` are re-prefixed to that same active locale; it does not change media `src` or snippet imports.
- `scripts/localize-component-imports.mjs` rewrites MDX import paths that reference `<locale>/components/*.jsx` so each locale page imports its own locale components (for whichever locale is being processed).
- Shared MDX snippets live under repo-root `snippets/` (not per-locale). Import with relative paths from each page; do not add shared MDX snippets to Lingo `i18n.json` buckets unless you intend to translate them.
- Locale-aware React components that can contain text live under `[locale]/components/*.jsx` and are manually maintained per locale (do not add them to Lingo `i18n.json` buckets; do not keep these in shared `snippets/`).
- Keep a **single** OpenAPI document at repo-root `openapi.yml` (not under `en/` or `es/`). Endpoint and webhook MDX must use Mintlify’s form `openapi: openapi.yml <method> <path>` or `openapi: openapi.yml webhook <eventKey>`. Do not translate the `openapi:` line.
- English **nav titles** for those pages come from OpenAPI **`summary`** via `scripts/sync-openapi-titles.mjs` (`npm run translate:sync-openapi-titles`). It runs at the start of `translate:generate` and on the translate-on-main workflow. Lingo translates the `title` field for target locales; the script only fills a missing target `title` with English (bootstrap)—it does not overwrite existing translations.
- Mintlify custom heading IDs use markdown `## Title {#slug}` (see Mintlify docs). Slugs are aligned from English via `scripts/sync-heading-anchors.mjs`. Never translate or alter `{#…}`; sync runs after Lingo in PIT/CI. Do not merge heading-count drift between `en/` and target locales without fixing structure first.
- Treat `lingo/brand-voice.md` as the source of truth for **brand voice** (tone and style) only. Do not add structural or tooling rules there (for example Mintlify `{#slug}` handling); those stay in this rule, README, and optionally in the Lingo engine **Instructions** field—not brand voice.
- Do not add script-based brand voice sync; sync brand voice on demand via Lingo MCP tools from chat.
- Treat `lingo/glossary.csv` as the glossary source of truth; use MCP sync with canonical key `sourceLocale|targetLocale|type|sourceText`.
- For glossary sync, always do dry-run first, show create/update/delete counts, then apply on confirmation.
- When adding locales, update both `docs.json` language blocks and `i18n.json` `locale.targets`.
1 change: 1 addition & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Lingo.dev configuration
# Copy this file to .env and replace placeholders with real values.

# Lingo.dev API key (used by CLI and CI workflows)
LINGO_API_KEY=lingo_sk_your_api_key_here

# Lingo.dev localization engine id
LINGO_ENGINE_ID=eng_your_engine_id_here
81 changes: 81 additions & 0 deletions .github/workflows/translate-on-main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Translate on main

on:
push:
branches:
- main

permissions:
contents: write
pull-requests: write

jobs:
translate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Inject Lingo engine id into i18n config
env:
LINGO_ENGINE_ID: ${{ secrets.LINGO_ENGINE_ID }}
run: |
if [ -z "${LINGO_ENGINE_ID}" ]; then
echo "LINGO_ENGINE_ID secret is required."
exit 1
fi
node -e 'const fs=require("fs");const p="i18n.json";const j=JSON.parse(fs.readFileSync(p,"utf8"));j.engineId=process.env.LINGO_ENGINE_ID;fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n");'

- name: Sync API and webhook titles from OpenAPI
run: node scripts/sync-openapi-titles.mjs

- name: Run Lingo translation
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
run: |
locales=$(node -e 'const fs=require("fs");const j=JSON.parse(fs.readFileSync("i18n.json","utf8"));console.log((j.locale?.targets||[]).join(" "));')
if [ -z "$locales" ]; then
echo "No target locales configured in i18n.json locale.targets"
exit 1
fi
for locale in $locales; do
echo "Running Lingo translation for $locale"
npx lingo.dev@latest run --target-locale "$locale"
done

- name: Translate docs.json language-specific navigation
env:
LINGO_ENGINE_ID: ${{ secrets.LINGO_ENGINE_ID }}
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
run: |
locales=$(node -e 'const fs=require("fs");const j=JSON.parse(fs.readFileSync("i18n.json","utf8"));console.log((j.locale?.targets||[]).join(" "));')
for locale in $locales; do
echo "Translating docs.json labels for $locale"
node scripts/translate-docs-json.mjs --target "$locale"
echo "Localizing internal links for $locale"
node scripts/localize-internal-links.mjs --target "$locale"
echo "Localizing component imports for $locale"
node scripts/localize-component-imports.mjs --target "$locale"
done

- name: Sync Mintlify heading anchors
run: node scripts/sync-heading-anchors.mjs

- name: Create translation PR
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(i18n): update translations from main"
title: "chore(i18n): update translations from main"
body: "Automated translation update from main branch push."
branch: "chore/i18n/auto-translations"
delete-branch: true
73 changes: 73 additions & 0 deletions .github/workflows/validate-translations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Validate translations

on:
pull_request:
paths:
- "docs.json"
- "i18n.json"
- "openapi.yml"
- "package.json"
- "en/**"
- "es/**"
- ".github/workflows/translate-on-main.yml"
- ".github/workflows/validate-translations.yml"
- "scripts/*.mjs"

jobs:
validate:
if: ${{ !contains(fromJson('["translations-setup"]'), github.head_ref) }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Install dependencies
run: npm ci

- name: Inject Lingo engine id into i18n config
env:
LINGO_ENGINE_ID: ${{ secrets.LINGO_ENGINE_ID }}
run: |
if [ -z "${LINGO_ENGINE_ID}" ]; then
echo "LINGO_ENGINE_ID is not set, skipping engine injection."
else
node -e 'const fs=require("fs");const p="i18n.json";const j=JSON.parse(fs.readFileSync(p,"utf8"));j.engineId=process.env.LINGO_ENGINE_ID;fs.writeFileSync(p,JSON.stringify(j,null,2)+"\n");'
fi

- name: Validate translation parity and frontmatter
run: npm run translate:validate

- name: Ensure internal links are locale-localized
run: npm run translate:localize-links:check

- name: Ensure component imports are locale-localized
run: npm run translate:localize-component-imports:check

- name: Ensure heading anchors match English slugs
run: npm run translate:sync-anchors:check

- name: Ensure API and webhook titles match OpenAPI summaries
run: npm run translate:sync-openapi-titles:check

- name: Mintlify validate
run: npx mint@latest validate

- name: Check broken links
run: npx mint@latest broken-links --check-anchors --check-snippets

- name: Translation freshness gate
env:
LINGO_API_KEY: ${{ secrets.LINGO_API_KEY }}
LINGO_ENGINE_ID: ${{ secrets.LINGO_ENGINE_ID }}
run: |
if [ -z "${LINGO_API_KEY}" ] || [ -z "${LINGO_ENGINE_ID}" ]; then
echo "LINGO_API_KEY or LINGO_ENGINE_ID is not set, skipping frozen translation gate."
else
npx lingo.dev@latest run --frozen
fi
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.DS_Store
.DS_Store
.env
7 changes: 7 additions & 0 deletions .vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ mdx = md
BasedOnStyles = Vale
Vale.Terms = NO # Enforces really harsh capitalization rules, keep off

# Exclude Spanish locale pages from spellchecking.
# Vale may evaluate paths with leading directories, so match both forms.
[es/**/*.mdx]
Vale.Spelling = NO
[**/es/**/*.mdx]
Vale.Spelling = NO

# `import ...`, `export ...`
# `<Component ... />`
# `<Component>...</Component>`
Expand Down
Loading