Skip to content

Commit 3985cd9

Browse files
authored
Merge pull request #44 from objectstack-ai/copilot/add-supported-languages-config
2 parents 9009d5b + b656a36 commit 3985cd9

File tree

6 files changed

+209
-36
lines changed

6 files changed

+209
-36
lines changed

README.md

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@ This repository contains the documentation for:
1313
## Features
1414

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

7779
### Internationalization (i18n)
7880

79-
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`).
81+
Language configuration is managed in `content/docs.site.json`:
82+
83+
```json
84+
{
85+
"i18n": {
86+
"enabled": true,
87+
"defaultLanguage": "en",
88+
"languages": ["en", "cn"]
89+
}
90+
}
91+
```
92+
93+
**Configurable Options:**
94+
- `enabled`: Enable/disable i18n support
95+
- `defaultLanguage`: The default language for the site (e.g., "en")
96+
- `languages`: Array of supported language codes (e.g., ["en", "cn", "ja", "fr"])
97+
98+
**Supported Languages:**
99+
The system includes built-in UI translations for:
100+
- `en` - English
101+
- `cn` - Chinese (Simplified) / 简体中文
102+
- `ja` - Japanese / 日本語
103+
- `fr` - French / Français
104+
- `de` - German / Deutsch
105+
- `es` - Spanish / Español
106+
107+
To add a new language:
108+
1. Add the language code to the `languages` array in `docs.site.json`
109+
2. If UI translations don't exist, add them to `packages/site/lib/translations.ts`
110+
3. Create content files with the language suffix (e.g., `file.{lang}.mdx`)
111+
112+
**Important:** If you change the default language, you must also update the redirect destination in `vercel.json` to match (currently `/en/docs`).
80113

81114
### Content Structure
82115

83-
Content files are located in `content/docs/` and use language suffixes:
84-
- `{filename}.en.mdx` - English content
85-
- `{filename}.cn.mdx` - Chinese content
86-
- `meta.en.json` - English navigation
87-
- `meta.cn.json` - Chinese navigation
116+
Content files are located in `content/docs/` and use language suffixes based on the `languages` configuration:
117+
- `{filename}.{lang}.mdx` - Language-specific content (e.g., `index.en.mdx`, `index.cn.mdx`)
118+
- `meta.{lang}.json` - Language-specific navigation (e.g., `meta.en.json`, `meta.cn.json`)
119+
120+
The CLI translate utility automatically generates language suffixes based on your configuration.

packages/cli/src/commands/translate.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import fs from 'node:fs';
22
import path from 'node:path';
33
import OpenAI from 'openai';
4-
import { getAllMdxFiles, resolveTranslatedFilePath, translateContent } from '../utils/translate.mjs';
4+
import { getAllMdxFiles, resolveTranslatedFilePath, translateContent, getSiteConfig } from '../utils/translate.mjs';
55

66
export function registerTranslateCommand(cli) {
77
cli
@@ -22,6 +22,11 @@ export function registerTranslateCommand(cli) {
2222
baseURL: OPENAI_BASE_URL,
2323
});
2424

25+
// Get language configuration
26+
const config = getSiteConfig();
27+
console.log(`Translation target: ${config.defaultLanguage} -> ${config.targetLanguage}`);
28+
console.log(`Configured languages: ${config.languages.join(', ')}\n`);
29+
2530
let targetFiles = [];
2631

2732
if (options.all) {

packages/cli/src/utils/translate.mjs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,60 @@ import fs from 'node:fs';
22
import path from 'node:path';
33
import OpenAI from 'openai';
44

5-
// Supported language suffixes for i18n
6-
const LANGUAGE_SUFFIXES = ['.cn.mdx', '.en.mdx'];
7-
const TARGET_LANGUAGE_SUFFIX = '.cn.mdx';
5+
/**
6+
* Load site configuration from docs.site.json
7+
* @returns {object} - The site configuration
8+
*/
9+
function loadSiteConfig() {
10+
const configPath = path.resolve(process.cwd(), 'content/docs.site.json');
11+
12+
if (!fs.existsSync(configPath)) {
13+
console.warn(`Warning: docs.site.json not found at ${configPath}, using defaults`);
14+
// Fallback matches the default configuration in packages/site/lib/site-config.ts
15+
return {
16+
i18n: {
17+
enabled: true,
18+
defaultLanguage: 'en',
19+
languages: ['en', 'cn']
20+
}
21+
};
22+
}
23+
24+
try {
25+
const configContent = fs.readFileSync(configPath, 'utf-8');
26+
return JSON.parse(configContent);
27+
} catch (error) {
28+
console.error('Error loading docs.site.json:', error);
29+
throw error;
30+
}
31+
}
32+
33+
// Load configuration
34+
const siteConfig = loadSiteConfig();
35+
const languages = siteConfig.i18n?.languages || ['en', 'cn'];
36+
const defaultLanguage = siteConfig.i18n?.defaultLanguage || 'en';
37+
38+
// Generate language suffixes dynamically from config
39+
// e.g., ['en', 'cn'] -> ['.en.mdx', '.cn.mdx']
40+
const LANGUAGE_SUFFIXES = languages.map(lang => `.${lang}.mdx`);
41+
42+
// Target language is the first non-default language
43+
const targetLanguage = languages.find(lang => lang !== defaultLanguage) || languages[0];
44+
const TARGET_LANGUAGE_SUFFIX = `.${targetLanguage}.mdx`;
45+
46+
/**
47+
* Get the current site configuration
48+
* @returns {object} - Configuration object with languages info
49+
*/
50+
export function getSiteConfig() {
51+
return {
52+
languages,
53+
defaultLanguage,
54+
targetLanguage,
55+
languageSuffixes: LANGUAGE_SUFFIXES,
56+
targetLanguageSuffix: TARGET_LANGUAGE_SUFFIX,
57+
};
58+
}
859

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

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

packages/site/app/[lang]/layout.tsx

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,11 @@ import 'fumadocs-ui/style.css';
22
import { RootProvider } from 'fumadocs-ui/provider/next';
33
import { defineI18nUI } from 'fumadocs-ui/i18n';
44
import { i18n } from '@/lib/i18n';
5+
import { getTranslations } from '@/lib/translations';
56

67

78
const { provider } = defineI18nUI(i18n, {
8-
translations: {
9-
en: {
10-
displayName: 'English',
11-
},
12-
cn: {
13-
displayName: '简体中文',
14-
toc: '目录',
15-
search: '搜索文档',
16-
lastUpdate: '最后更新于',
17-
searchNoResult: '没有结果',
18-
previousPage: '上一页',
19-
nextPage: '下一页',
20-
chooseLanguage: '选择语言',
21-
},
22-
},
9+
translations: getTranslations(),
2310
});
2411

2512
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {

packages/site/app/layout.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,11 @@ import 'fumadocs-ui/style.css';
22
import { RootProvider } from 'fumadocs-ui/provider/next';
33
import { defineI18nUI } from 'fumadocs-ui/i18n';
44
import { i18n } from '@/lib/i18n';
5+
import { getTranslations } from '@/lib/translations';
56

67

78
const { provider } = defineI18nUI(i18n, {
8-
translations: {
9-
en: {
10-
displayName: 'English',
11-
},
12-
cn: {
13-
displayName: 'Chinese', 
14-
},
15-
},
9+
translations: getTranslations(),
1610
});
1711

1812
export default function Layout({ children }: { children: React.ReactNode }) {

packages/site/lib/translations.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { siteConfig } from './site-config';
2+
3+
/**
4+
* Translation strings for different languages
5+
* These are the UI strings used by fumadocs-ui
6+
*/
7+
interface LanguageTranslations {
8+
displayName: string;
9+
toc?: string;
10+
search?: string;
11+
lastUpdate?: string;
12+
searchNoResult?: string;
13+
previousPage?: string;
14+
nextPage?: string;
15+
chooseLanguage?: string;
16+
}
17+
18+
/**
19+
* Default translations for supported languages
20+
* Add new language translations here as needed
21+
*/
22+
const defaultTranslations: Record<string, LanguageTranslations> = {
23+
en: {
24+
displayName: 'English',
25+
},
26+
cn: {
27+
displayName: '简体中文',
28+
toc: '目录',
29+
search: '搜索文档',
30+
lastUpdate: '最后更新于',
31+
searchNoResult: '没有结果',
32+
previousPage: '上一页',
33+
nextPage: '下一页',
34+
chooseLanguage: '选择语言',
35+
},
36+
ja: {
37+
displayName: '日本語',
38+
toc: '目次',
39+
search: 'ドキュメントを検索',
40+
lastUpdate: '最終更新',
41+
searchNoResult: '結果がありません',
42+
previousPage: '前のページ',
43+
nextPage: '次のページ',
44+
chooseLanguage: '言語を選択',
45+
},
46+
fr: {
47+
displayName: 'Français',
48+
toc: 'Table des matières',
49+
search: 'Rechercher dans la documentation',
50+
lastUpdate: 'Dernière mise à jour',
51+
searchNoResult: 'Aucun résultat',
52+
previousPage: 'Page précédente',
53+
nextPage: 'Page suivante',
54+
chooseLanguage: 'Choisir la langue',
55+
},
56+
de: {
57+
displayName: 'Deutsch',
58+
toc: 'Inhaltsverzeichnis',
59+
search: 'Dokumentation durchsuchen',
60+
lastUpdate: 'Zuletzt aktualisiert',
61+
searchNoResult: 'Keine Ergebnisse',
62+
previousPage: 'Vorherige Seite',
63+
nextPage: 'Nächste Seite',
64+
chooseLanguage: 'Sprache wählen',
65+
},
66+
es: {
67+
displayName: 'Español',
68+
toc: 'Tabla de contenidos',
69+
search: 'Buscar documentación',
70+
lastUpdate: 'Última actualización',
71+
searchNoResult: 'Sin resultados',
72+
previousPage: 'Página anterior',
73+
nextPage: 'Página siguiente',
74+
chooseLanguage: 'Elegir idioma',
75+
},
76+
};
77+
78+
/**
79+
* Get translations for configured languages
80+
* Returns only the translations for languages specified in docs.site.json
81+
*/
82+
export function getTranslations(): Record<string, LanguageTranslations> {
83+
const configuredLanguages = siteConfig.i18n.languages;
84+
const translations: Record<string, LanguageTranslations> = {};
85+
86+
for (const lang of configuredLanguages) {
87+
if (defaultTranslations[lang]) {
88+
translations[lang] = defaultTranslations[lang];
89+
} else {
90+
// If no translation exists for a configured language, provide a minimal fallback
91+
// Only log warning in development
92+
if (process.env.NODE_ENV === 'development') {
93+
console.warn(`Warning: No translations found for language "${lang}". Using minimal fallback.`);
94+
}
95+
translations[lang] = {
96+
displayName: lang.toUpperCase(),
97+
};
98+
}
99+
}
100+
101+
return translations;
102+
}

0 commit comments

Comments
 (0)