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
19 changes: 16 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,21 @@ You can use `npm run watch:api` to watch for changes in the API documentation du

## Adding a new language

1. Add the language to `export_languages` in [`src/content/crowdin.yml`](src/content/crowdin.yml) and sync so Crowdin writes translated MDX and UI JSON (paths come from the `files` section). Add ignore rules for the new language in the `files` section.
Each locale has two identifiers that are easy to confuse:

2. In [`src/utils/i18n.ts`](src/utils/i18n.ts), add the locale to `starlightLocales` (same key as the translated docs folder; see [Starlight i18n](https://starlight.astro.build/guides/i18n/)), import the new JSON from [`src/content/i18n/`](src/content/i18n/), and wire it into `sidebarLabel`’s `translations`.
- **Crowdin language code** — what Crowdin uses to export translations (e.g. `zh-CN`, `pt-PT`, `tr`). See the [Crowdin language codes](https://crowdin.com/page/language-codes).
- **Folder / URL segment** — the short locale key used in content paths (`/docs/zh/…`) and the published URL. We keep it short, dropping the region where it's unambiguous (`zh-CN` → `zh`, `pt-PT` → `pt`).

New keys in English [`en.json`](src/content/i18n/en.json) need matching entries in [`src/content/config.ts`](src/content/config.ts).
The steps below use Chinese Simplified (`zh-CN` → `zh`) as a worked example:

1. **[`src/content/crowdin.yml`](src/content/crowdin.yml)** — add the Crowdin code to `export_languages` (`zh-CN`), map it to the folder name under `languages_mapping` (`zh-CN: zh`), and add ignore rules for the folder to **both** the `/docs` and `/includes` `files` blocks (`/docs/zh/**/*.mdx`, `/includes/zh/**/*.mdx`). Then sync so Crowdin writes the translated MDX and the UI strings JSON (paths come from the `files` section).

2. **[`src/content/i18n/<folder>.json`](src/content/i18n/)** — the sync above creates this UI-strings file (e.g. `zh.json`), mirroring [`en.json`](src/content/i18n/en.json). Make sure it exists before the next step (the import will fail otherwise).

3. **[`src/utils/i18n.ts`](src/utils/i18n.ts)** — import the new JSON, then add the locale to `starlightLocales` keyed by the **folder name**, with the native `label` and the full BCP-47 `lang` (e.g. `'zh': { label: '简体中文', lang: 'zh-CN' }`; see [Starlight i18n](https://starlight.astro.build/guides/i18n/)), and wire it into `sidebarLabel`’s `translations`.

`starlightLocales` is the single source of truth: the language switcher, locale routing, fallback handling, and tests all derive from it, so no further wiring is needed. Do **not** add per-locale routes to [`vercel.json`](vercel.json).

> **Note!**
>
> New UI string keys added to English [`en.json`](src/content/i18n/en.json) also need a matching entry in the i18n schema in [`src/content/config.ts`](src/content/config.ts).
5 changes: 5 additions & 0 deletions src/content/crowdin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export_languages:
- pt-PT
- es-ES
- tr
- zh-CN

files:
- source: /docs/**/*.mdx
Expand All @@ -22,13 +23,15 @@ files:
pt-PT: pt
es-ES: es
tr: tr
zh-CN: zh
ignore:
- /docs/de/**/*.mdx
- /docs/da/**/*.mdx
- /docs/fr/**/*.mdx
- /docs/pt/**/*.mdx
- /docs/es/**/*.mdx
- /docs/tr/**/*.mdx
- /docs/zh/**/*.mdx

- source: /i18n/en.json
translation: /i18n/%locale%.json
Expand All @@ -41,6 +44,7 @@ files:
pt-PT: pt-PT
es-ES: es-ES
tr: tr-TR
zh-CN: zh-CN

- source: /includes/*.mdx
translation: /includes/%locale%/%original_file_name%
Expand All @@ -53,3 +57,4 @@ files:
- /includes/pt/**/*.mdx
- /includes/es/**/*.mdx
- /includes/tr/**/*.mdx
- /includes/zh/**/*.mdx
73 changes: 73 additions & 0 deletions src/content/i18n/zh-CN.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
{
"copyContent": "复制内容",
"editLink.loading": "加载中...",
"editLink.copied": "已复制!",
"editLink.error": "错误!",
"feedback.question": "此页面对您有帮助吗?",
"feedback.placeholder": "告诉我们更多...",
"feedback.submit": "提交",
"feedback.success": "感谢您的反馈!",
"footer.status": "状态",
"footer.terms": "服务条款",
"footer.privacy": "隐私政策",
"footer.cookies": "Cookie 声明",
"footer.security": "安全",
"footer.llms": "面向 LLM 的文档",
"hero.crowdinHelp": "Crowdin 帮助",
"hero.enterpriseHelp": "Enterprise 帮助",
"hero.developerPortal": "开发者门户",
"languages.errorLoading": "加载语言时出错",
"formats.alt": "格式图标",
"formats.supported": "支持的格式",
"formats.viewOnStore": "在商店中查看",
"repo.official": "官方",
"repo.viewInstall": "查看并安装",
"social.applePodcast": "Apple 播客",
"social.spotifyPodcast": "Spotify 播客",
"modal.header": "标题",
"translateInCrowdin": "在 Crowdin 中翻译",
"sidebar": {
"gettingStarted": "入门",
"account": "账户",
"translationProcess": "翻译流程",
"projectManagement": "项目管理",
"sources": "源文件",
"translations": "翻译",
"projectSettings": "项目设置",
"teamManagement": "团队管理",
"integrations": "集成",
"localizationResources": "本地化资源",
"onlineEditor": "在线编辑器",
"tasks": "任务",
"reports": "报告",
"billingAndPayments": "账单与付款",
"organizationManagement": "组织管理",
"workflows": "工作流",
"organizationSettings": "组织设置",
"crowdinApps": "Crowdin 应用",
"publishing": "发布",
"modules": "模块",
"uiModules": "UI 模块",
"aiModules": "AI 模块",
"fileProcessingModules": "文件处理模块",
"other": "其他",
"capabilities": "功能",
"api": "API",
"overview": "概览",
"crowdinApiFileBased": "Crowdin API(基于文件)",
"crowdinApiStringBased": "Crowdin API(基于字符串)",
"enterpriseApiFileBased": "Enterprise API(基于文件)",
"enterpriseApiStringBased": "Enterprise API(基于字符串)",
"devTools": "开发工具",
"consoleClientCli": "控制台客户端 (CLI)",
"githubAction": "GitHub Action",
"visualStudioCodePlugin": "Visual Studio Code 插件",
"androidStudioPlugin": "Android Studio 插件",
"androidSdk": "Android SDK",
"iosSdk": "iOS SDK",
"websiteJsSdk": "网站 JS SDK",
"flutterSdk": "Flutter SDK",
"security": "安全",
"guides": "指南"
}
}
3 changes: 2 additions & 1 deletion src/utils/docsSlug.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it, expect } from 'vitest';
import { slugFromPath, getBaseDocId, localizeDocId } from './docsSlug';
import { starlightLocales } from './i18n';

const LOCALES = ['de', 'da', 'fr', 'pt', 'es', 'tr'];
const LOCALES = Object.keys(starlightLocales).filter(locale => locale !== 'root');

describe('slugFromPath', () => {
it('strips the file extension', () => {
Expand Down
3 changes: 3 additions & 0 deletions src/utils/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fr from '../content/i18n/fr.json';
import pt from '../content/i18n/pt-PT.json';
import es from '../content/i18n/es-ES.json';
import tr from '../content/i18n/tr-TR.json';
import zh from '../content/i18n/zh-CN.json';

export const starlightLocales = {
root: { label: 'English', lang: 'en' },
Expand All @@ -14,6 +15,7 @@ export const starlightLocales = {
'pt': { label: 'Português (Portugal)', lang: 'pt-PT' },
'es': { label: 'Español', lang: 'es-ES' },
'tr': { label: 'Türkçe', lang: 'tr-TR' },
'zh': { label: '简体中文', lang: 'zh-CN' },
} as const;

export type SidebarTranslationKey = keyof typeof en.sidebar;
Expand All @@ -28,6 +30,7 @@ export function sidebarLabel(key: SidebarTranslationKey) {
'pt-PT': pt.sidebar[key],
'es-ES': es.sidebar[key],
'tr-TR': tr.sidebar[key],
'zh-CN': zh.sidebar[key],
},
};
}
Loading