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
49 changes: 41 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ This repository contains the documentation for:
## Features

- 🌍 **Multi-language Support**:
- Source: English (`content/docs/*.mdx`)
- Target: Chinese (`content/docs/*.cn.mdx`) - *Auto-translated via AI using dot parser*
- Configurable via `docs.site.json`
- Built-in support for: English, Chinese, Japanese, French, German, Spanish
- Extensible: Easy to add new languages
- Auto-translation via AI CLI using dot parser convention
- 📝 **MDX Content**: Interactive documentation with Type-safe components.
- 🛠️ **Automated Workflows**:
- AI Translation CLI (`packages/cli`)
Expand Down Expand Up @@ -76,12 +78,43 @@ The guide covers:

### Internationalization (i18n)

The default language is configured in `lib/i18n.ts` as `en`. If you change the default language, you must also update the redirect destination in `vercel.json` to match (currently `/en/docs`).
Language configuration is managed in `content/docs.site.json`:

```json
{
"i18n": {
"enabled": true,
"defaultLanguage": "en",
"languages": ["en", "cn"]
}
}
```

**Configurable Options:**
- `enabled`: Enable/disable i18n support
- `defaultLanguage`: The default language for the site (e.g., "en")
- `languages`: Array of supported language codes (e.g., ["en", "cn", "ja", "fr"])

**Supported Languages:**
The system includes built-in UI translations for:
- `en` - English
- `cn` - Chinese (Simplified) / 简体中文
- `ja` - Japanese / 日本語
- `fr` - French / Français
- `de` - German / Deutsch
- `es` - Spanish / Español

To add a new language:
1. Add the language code to the `languages` array in `docs.site.json`
2. If UI translations don't exist, add them to `packages/site/lib/translations.ts`
3. Create content files with the language suffix (e.g., `file.{lang}.mdx`)

**Important:** If you change the default language, you must also update the redirect destination in `vercel.json` to match (currently `/en/docs`).

### Content Structure

Content files are located in `content/docs/` and use language suffixes:
- `{filename}.en.mdx` - English content
- `{filename}.cn.mdx` - Chinese content
- `meta.en.json` - English navigation
- `meta.cn.json` - Chinese navigation
Content files are located in `content/docs/` and use language suffixes based on the `languages` configuration:
- `{filename}.{lang}.mdx` - Language-specific content (e.g., `index.en.mdx`, `index.cn.mdx`)
- `meta.{lang}.json` - Language-specific navigation (e.g., `meta.en.json`, `meta.cn.json`)

The CLI translate utility automatically generates language suffixes based on your configuration.
7 changes: 6 additions & 1 deletion packages/cli/src/commands/translate.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import fs from 'node:fs';
import path from 'node:path';
import OpenAI from 'openai';
import { getAllMdxFiles, resolveTranslatedFilePath, translateContent } from '../utils/translate.mjs';
import { getAllMdxFiles, resolveTranslatedFilePath, translateContent, getSiteConfig } from '../utils/translate.mjs';

export function registerTranslateCommand(cli) {
cli
Expand All @@ -22,6 +22,11 @@ export function registerTranslateCommand(cli) {
baseURL: OPENAI_BASE_URL,
});

// Get language configuration
const config = getSiteConfig();
console.log(`Translation target: ${config.defaultLanguage} -> ${config.targetLanguage}`);
console.log(`Configured languages: ${config.languages.join(', ')}\n`);

let targetFiles = [];

if (options.all) {
Expand Down
60 changes: 56 additions & 4 deletions packages/cli/src/utils/translate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,60 @@ import fs from 'node:fs';
import path from 'node:path';
import OpenAI from 'openai';

// Supported language suffixes for i18n
const LANGUAGE_SUFFIXES = ['.cn.mdx', '.en.mdx'];
const TARGET_LANGUAGE_SUFFIX = '.cn.mdx';
/**
* Load site configuration from docs.site.json
* @returns {object} - The site configuration
*/
function loadSiteConfig() {
const configPath = path.resolve(process.cwd(), 'content/docs.site.json');
Comment on lines +9 to +10
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config path is hardcoded to 'content/docs.site.json'. Consider making this configurable or using a constant to allow flexibility for different project structures.

Suggested change
function loadSiteConfig() {
const configPath = path.resolve(process.cwd(), 'content/docs.site.json');
const DEFAULT_SITE_CONFIG_PATH = path.resolve(
process.cwd(),
process.env.DOCS_SITE_CONFIG_PATH || 'content/docs.site.json'
);
function loadSiteConfig() {
const configPath = DEFAULT_SITE_CONFIG_PATH;

Copilot uses AI. Check for mistakes.

if (!fs.existsSync(configPath)) {
console.warn(`Warning: docs.site.json not found at ${configPath}, using defaults`);
// Fallback matches the default configuration in packages/site/lib/site-config.ts
return {
i18n: {
enabled: true,
defaultLanguage: 'en',
languages: ['en', 'cn']
}
};
}

try {
const configContent = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(configContent);
} catch (error) {
console.error('Error loading docs.site.json:', error);
throw error;
}
}

// Load configuration
const siteConfig = loadSiteConfig();
const languages = siteConfig.i18n?.languages || ['en', 'cn'];
const defaultLanguage = siteConfig.i18n?.defaultLanguage || 'en';

// Generate language suffixes dynamically from config
// e.g., ['en', 'cn'] -> ['.en.mdx', '.cn.mdx']
const LANGUAGE_SUFFIXES = languages.map(lang => `.${lang}.mdx`);

// Target language is the first non-default language
const targetLanguage = languages.find(lang => lang !== defaultLanguage) || languages[0];
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback to languages[0] when all languages equal defaultLanguage could result in translating to the same language. This edge case should either throw an error or be explicitly documented as acceptable behavior.

Suggested change
const targetLanguage = languages.find(lang => lang !== defaultLanguage) || languages[0];
const nonDefaultLanguages = languages.filter(lang => lang !== defaultLanguage);
if (nonDefaultLanguages.length === 0) {
throw new Error(
`Invalid i18n configuration: languages (${JSON.stringify(
languages
)}) must include at least one language different from defaultLanguage ("${defaultLanguage}").`
);
}
const targetLanguage = nonDefaultLanguages[0];

Copilot uses AI. Check for mistakes.
const TARGET_LANGUAGE_SUFFIX = `.${targetLanguage}.mdx`;

/**
* Get the current site configuration
* @returns {object} - Configuration object with languages info
*/
export function getSiteConfig() {
return {
languages,
defaultLanguage,
targetLanguage,
languageSuffixes: LANGUAGE_SUFFIXES,
targetLanguageSuffix: TARGET_LANGUAGE_SUFFIX,
};
}

/**
* Check if a file has a language suffix
Expand Down Expand Up @@ -37,7 +88,8 @@ export function getAllMdxFiles(dir) {

export function resolveTranslatedFilePath(enFilePath) {
// Strategy: Use dot parser convention
// content/docs/path/to/file.mdx -> content/docs/path/to/file.cn.mdx
// content/docs/path/to/file.mdx -> content/docs/path/to/file.{targetLang}.mdx
// Target language is determined from docs.site.json configuration
// Skip files that already have language suffix
const absPath = path.resolve(enFilePath);

Expand Down
17 changes: 2 additions & 15 deletions packages/site/app/[lang]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,11 @@ import 'fumadocs-ui/style.css';
import { RootProvider } from 'fumadocs-ui/provider/next';
import { defineI18nUI } from 'fumadocs-ui/i18n';
import { i18n } from '@/lib/i18n';
import { getTranslations } from '@/lib/translations';


const { provider } = defineI18nUI(i18n, {
translations: {
en: {
displayName: 'English',
},
cn: {
displayName: '简体中文',
toc: '目录',
search: '搜索文档',
lastUpdate: '最后更新于',
searchNoResult: '没有结果',
previousPage: '上一页',
nextPage: '下一页',
chooseLanguage: '选择语言',
},
},
translations: getTranslations(),
});

export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
Expand Down
10 changes: 2 additions & 8 deletions packages/site/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,11 @@ import 'fumadocs-ui/style.css';
import { RootProvider } from 'fumadocs-ui/provider/next';
import { defineI18nUI } from 'fumadocs-ui/i18n';
import { i18n } from '@/lib/i18n';
import { getTranslations } from '@/lib/translations';


const { provider } = defineI18nUI(i18n, {
translations: {
en: {
displayName: 'English',
},
cn: {
displayName: 'Chinese', 
},
},
translations: getTranslations(),
});

export default function Layout({ children }: { children: React.ReactNode }) {
Expand Down
102 changes: 102 additions & 0 deletions packages/site/lib/translations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { siteConfig } from './site-config';

/**
* Translation strings for different languages
* These are the UI strings used by fumadocs-ui
*/
interface LanguageTranslations {
displayName: string;
toc?: string;
search?: string;
lastUpdate?: string;
searchNoResult?: string;
previousPage?: string;
nextPage?: string;
chooseLanguage?: string;
}

/**
* Default translations for supported languages
* Add new language translations here as needed
*/
const defaultTranslations: Record<string, LanguageTranslations> = {
en: {
displayName: 'English',
},
cn: {
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The language code 'cn' is inconsistent with ISO 639-1 standard. The correct code for Chinese (Simplified) is 'zh-CN' or 'zh-Hans'. Consider using 'zh' or documenting this deviation from standard language codes.

Suggested change
cn: {
zh-CN: {

Copilot uses AI. Check for mistakes.
displayName: '简体中文',
toc: '目录',
search: '搜索文档',
lastUpdate: '最后更新于',
searchNoResult: '没有结果',
previousPage: '上一页',
nextPage: '下一页',
chooseLanguage: '选择语言',
},
ja: {
displayName: '日本語',
toc: '目次',
search: 'ドキュメントを検索',
lastUpdate: '最終更新',
searchNoResult: '結果がありません',
previousPage: '前のページ',
nextPage: '次のページ',
chooseLanguage: '言語を選択',
},
fr: {
displayName: 'Français',
toc: 'Table des matières',
search: 'Rechercher dans la documentation',
lastUpdate: 'Dernière mise à jour',
searchNoResult: 'Aucun résultat',
previousPage: 'Page précédente',
nextPage: 'Page suivante',
chooseLanguage: 'Choisir la langue',
},
de: {
displayName: 'Deutsch',
toc: 'Inhaltsverzeichnis',
search: 'Dokumentation durchsuchen',
lastUpdate: 'Zuletzt aktualisiert',
searchNoResult: 'Keine Ergebnisse',
previousPage: 'Vorherige Seite',
nextPage: 'Nächste Seite',
chooseLanguage: 'Sprache wählen',
},
es: {
displayName: 'Español',
toc: 'Tabla de contenidos',
search: 'Buscar documentación',
lastUpdate: 'Última actualización',
searchNoResult: 'Sin resultados',
previousPage: 'Página anterior',
nextPage: 'Página siguiente',
chooseLanguage: 'Elegir idioma',
},
};

/**
* Get translations for configured languages
* Returns only the translations for languages specified in docs.site.json
*/
export function getTranslations(): Record<string, LanguageTranslations> {
const configuredLanguages = siteConfig.i18n.languages;
const translations: Record<string, LanguageTranslations> = {};

for (const lang of configuredLanguages) {
if (defaultTranslations[lang]) {
translations[lang] = defaultTranslations[lang];
} else {
// If no translation exists for a configured language, provide a minimal fallback
// Only log warning in development
if (process.env.NODE_ENV === 'development') {
console.warn(`Warning: No translations found for language "${lang}". Using minimal fallback.`);
}
translations[lang] = {
displayName: lang.toUpperCase(),
};
}
}

return translations;
}
Loading