Files
Tony Chang 10c14f0c12 Docs i18n: 简中 / 繁中 / Español (Mintlify) (#1693)
* Translate docs into zh-CN, zh-TW, es (Mintlify i18n)

- docs.json navigation restructured into navigation.languages (en default
  + zh / zh-TW / es), group/tab labels translated per locale, page paths
  prefixed with the locale dir
- 79 English pages × 3 locales = 237 translated .mdx under docs/{zh,zh-TW,es}/,
  mirroring the English tree (scripts/build-docs-langs.py regenerates the nav)
- Preserved verbatim: code fences, MDX component/prop names, imports, URLs,
  API/SQL/env names, brand names, openapi API Reference (shared), and the
  data-for-agents blocks (English on purpose)
- Adds scripts/check-docs-i18n-parity.sh and documents the whole flow in the
  insforge-dev docs skill (DOCS_I18N.md, mirrored to .claude/.codex)

Verified: parity check green (79×3), code-fence counts byte-match source on
spot checks, docs.json valid, zh-TW uses Taiwan vocabulary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* Fix kotlin-sdk-installation snippet: translate body, drop bogus frontmatter

A sub-delegated translation had left this snippet's prose English and added
frontmatter the source snippet doesn't have. Body now translated in all
three locales; structure matches source (no frontmatter); code fences
byte-match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* Address docs review: re-translate truncated pages, translate .md deploy guides, harden tooling

Fixes John-bot's two Critical findings on #1693:
- Content loss: re-translated 11 pages the haiku pass had truncated
  (partnership was 720 lines/68 fences cut to 81 lines/0; also mcp-setup,
  oauth-server, rest/database, realtime, razorpay, sites, webscraper,
  nuxt/vue/svelte). Redone with a stronger model; each verified at exact
  fence parity and >=85% line parity vs source. Also caught hallucinated
  Partner Program content and literal placeholder lines from the first pass.
- Dead nav links: translated the 5 .md deployment guides (aws, azure, gcp,
  containarium, security-guide) into all 3 locales; nav now prefixes them
  to real files.
- Tooling (findings 3/4): parity check now validates against docs.json nav
  (covers .md, catches dangling links); build-docs-langs.py is idempotent
  (derives tabs from the en entry, skips prefixing pages with no translated
  file) and its label map is externalized.

Verified: nav parity green (78 pages x 4 langs resolve), strict fence+line
audit clean across all nav pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* Fix docs CI: prettier-format locale files, exclude them from Vale spellcheck

- Ran prettier --write over all translated .md/.mdx + docs.json (lint-and-format
  was failing on formatting); fence parity re-verified intact.
- .vale.ini: disable Vale.Spelling for zh/zh-TW/es trees — the English speller
  flags every translated word as a misspelling (that was the vale-spellcheck
  failure). Localized prose is validated by native review instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* Force-add the .agents/.claude DOCS_I18N.md skill mirrors

They're gitignored (global ~/.gitignore ignores .claude; the .agents copy
was missed), so only the .codex copy had committed — sync-skills --check
failed in CI seeing the canonical .agents copy absent. Existing skill files
are force-added the same way. All three mirrors now tracked and in sync.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* Broaden Vale spellcheck exclusion to repo-root-prefixed locale globs

The hosted Mintlify vale-spellcheck kept failing though the docs/-relative
disable works locally — likely runs vale from the repo root, where zh/**
doesn't match docs/zh/**. Add docs/{zh,zh-TW,es}/** variants so the
Vale.Spelling=NO disable applies regardless of working directory.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* Address docs re-review: drop dead snippet copies, strengthen parity, utf-8

- Dropped docs/{zh,zh-TW,es}/snippets/* — pages import snippets by absolute
  path (/snippets/x.mdx → always English), so translated copies were dead
  files. Snippet includes stay English (documented in DOCS_I18N.md).
- check-docs-i18n-parity.sh now truly verifies coverage: every nav path
  resolves, every non-en nav entry is locale-prefixed (catches English
  fallback), and every English nav page has all 3 locale translations.
- build-docs-langs.py reads/writes docs.json with explicit encoding='utf-8'
  (safe under LC_ALL=C) and uses with-open handles.

Locale codes (zh / zh-TW) kept: Mintlify's internationalization guide lists
zh, zh-Hans, zh-CN for Simplified and zh-Hant, zh-TW for Traditional as
accepted — both our codes are valid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* Rename Traditional Chinese locale zh-TW -> zh-Hant (Mintlify schema requirement)

Ran Mintlify's own CLI (`mint dev`) to settle the locale-code question John-bot
raised: the docs.json schema enum is en|cn|zh|zh-Hans|zh-Hant|es|... — `zh` is
valid for Simplified, but `zh-TW` is REJECTED; Traditional Chinese must be
`zh-Hant`. (The Mintlify web i18n guide that listed zh-TW as accepted is wrong;
the CLI schema is authoritative. John-bot was right to flag it.)

- Renamed docs/zh-TW/ -> docs/zh-Hant/ (80 files) via git mv
- Updated docs.json nav, docs-langs-labels.json, build-docs-langs.py LOCALES,
  check-docs-i18n-parity.sh, .vale.ini globs, and DOCS_I18N.md
- `mint dev` now passes config validation (previously errored on zh-TW enum);
  parity green; broken-links clean in locale trees

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

* docs skill: fix stray zh-TW->zh-Hant in fan-out note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011pR7GMy3FRotoNgKZHSF2Q

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-15 16:06:17 +08:00

63 lines
2.2 KiB
Python

#!/usr/bin/env python3
"""Regenerate docs.json navigation.languages from the English (default) tree.
Idempotent: reads tabs from navigation.tabs if present (first run) or from the
en/default language entry (subsequent runs), then rebuilds all locale entries.
"""
import json, copy, os
DOCS = os.path.join(os.path.dirname(__file__), '..', 'docs')
CFG = os.path.join(DOCS, 'docs.json')
LOCALES = ['zh', 'zh-Hant', 'es']
d = json.load(open(CFG, encoding='utf-8'))
nav = d['navigation']
if 'tabs' in nav:
base_tabs = nav['tabs']
glob = nav.get('global')
else:
langs = nav['languages']
en = next(l for l in langs if l.get('default') or l['language'] == 'en')
# strip the en/ prefix back off (en uses bare paths already, so just copy)
base_tabs = copy.deepcopy(en['tabs'])
glob = nav.get('global')
LABELS = json.load(open(os.path.join(os.path.dirname(__file__), 'docs-langs-labels.json'), encoding='utf-8'))
def resolves(path):
p = os.path.join(DOCS, path)
return os.path.exists(p + '.mdx') or os.path.exists(p + '.md')
def transform(node, loc):
if isinstance(node, str):
prefixed = f"{loc}/{node}"
# Skip prefixing a page that has no translated file yet (avoid dead nav
# links); fall back to the English page so the entry still resolves.
return prefixed if resolves(prefixed) else node
if isinstance(node, list):
return [transform(x, loc) for x in node]
if isinstance(node, dict):
out = {}
for k, v in node.items():
if k in ('tab', 'group'):
out[k] = LABELS[loc].get(v, v)
elif k in ('pages', 'groups'):
out[k] = [transform(x, loc) for x in v]
else:
out[k] = copy.deepcopy(v)
return out
return copy.deepcopy(node)
languages = [{'language': 'en', 'default': True, 'tabs': copy.deepcopy(base_tabs)}]
for loc in LOCALES:
languages.append({'language': loc, 'tabs': [transform(t, loc) for t in base_tabs]})
nav_out = {'languages': languages}
if glob is not None:
nav_out['global'] = glob
d['navigation'] = nav_out
with open(CFG, 'w', encoding='utf-8') as fh:
json.dump(d, fh, ensure_ascii=False, indent=2)
fh.write('\n')
print(f"docs.json rebuilt: {len(languages)} languages")