diff --git a/README.md b/README.md index 75f6410..e0be927 100644 --- a/README.md +++ b/README.md @@ -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`) @@ -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 \ No newline at end of file +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. \ No newline at end of file diff --git a/packages/cli/src/commands/translate.mjs b/packages/cli/src/commands/translate.mjs index 2518466..ce93726 100644 --- a/packages/cli/src/commands/translate.mjs +++ b/packages/cli/src/commands/translate.mjs @@ -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 @@ -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) { diff --git a/packages/cli/src/utils/translate.mjs b/packages/cli/src/utils/translate.mjs index 10e8e94..8ff7491 100644 --- a/packages/cli/src/utils/translate.mjs +++ b/packages/cli/src/utils/translate.mjs @@ -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'); + + 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]; +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 @@ -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); diff --git a/packages/site/app/[lang]/layout.tsx b/packages/site/app/[lang]/layout.tsx index c4224c8..a5078c8 100644 --- a/packages/site/app/[lang]/layout.tsx +++ b/packages/site/app/[lang]/layout.tsx @@ -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]'>) { diff --git a/packages/site/app/layout.tsx b/packages/site/app/layout.tsx index a23b363..cde7a9b 100644 --- a/packages/site/app/layout.tsx +++ b/packages/site/app/layout.tsx @@ -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 }) { diff --git a/packages/site/lib/translations.ts b/packages/site/lib/translations.ts new file mode 100644 index 0000000..fb661d6 --- /dev/null +++ b/packages/site/lib/translations.ts @@ -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 = { + en: { + displayName: 'English', + }, + cn: { + 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 { + const configuredLanguages = siteConfig.i18n.languages; + const translations: Record = {}; + + 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; +}