Skip to content

Commit f984659

Browse files
committed
Refactor i18n, translation, and MDX config for multi-language support
Reworks translation file handling in the CLI to use a dot-suffix convention and dynamic language configuration from docs.site.json. Adds a translations utility for UI strings, updates layouts to use dynamic translations, and improves the root proxy to redirect based on Accept-Language. Adds remark-directive and admonition plugins to MDX config for enhanced markdown support. Cleans up MDX component injection and updates dependencies accordingly.
1 parent 7013b25 commit f984659

File tree

14 files changed

+298
-54
lines changed

14 files changed

+298
-54
lines changed

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
"fumadocs-core": "16.4.7",
1919
"fumadocs-mdx": "14.2.5",
2020
"fumadocs-ui": "16.4.7",
21-
"lucide-react": "^0.562.0",
22-
"next": "16.1.2",
2321
"react": "^19.2.3",
2422
"react-dom": "^19.2.3",
2523
"server-only": "^0.0.1",

packages/cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pnpm objectdocs build ./content/docs
3939

4040
### Translate Documentation
4141

42-
The `translate` command reads English documentation from `content/docs` and generates Chinese translations in `content/docs-cn`.
42+
The `translate` command reads English documentation from `content/docs/*.mdx` and generates Chinese translations as `*.cn.mdx` files in the same directory using the dot parser convention.
4343

4444
**Prerequisites:**
4545
You must set the following environment variables (in `.env` or your shell):

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: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,70 @@ import fs from 'node:fs';
22
import path from 'node:path';
33
import OpenAI from 'openai';
44

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+
}
59+
60+
/**
61+
* Check if a file has a language suffix
62+
* @param {string} filePath - The file path to check
63+
* @returns {boolean} - True if file has a language suffix
64+
*/
65+
function hasLanguageSuffix(filePath) {
66+
return LANGUAGE_SUFFIXES.some(suffix => filePath.endsWith(suffix));
67+
}
68+
569
export function getAllMdxFiles(dir) {
670
let results = [];
771
if (!fs.existsSync(dir)) return results;
@@ -13,7 +77,8 @@ export function getAllMdxFiles(dir) {
1377
if (stat && stat.isDirectory()) {
1478
results = results.concat(getAllMdxFiles(file));
1579
} else {
16-
if (file.endsWith('.mdx')) {
80+
// Only include .mdx files that don't have language suffix
81+
if (file.endsWith('.mdx') && !hasLanguageSuffix(file)) {
1782
results.push(path.relative(process.cwd(), file));
1883
}
1984
}
@@ -22,19 +87,23 @@ export function getAllMdxFiles(dir) {
2287
}
2388

2489
export function resolveTranslatedFilePath(enFilePath) {
25-
// Strategy: content/docs/path/to/file.mdx -> content/docs-cn/path/to/file.mdx
26-
const docsRoot = path.join(process.cwd(), 'content/docs');
27-
28-
// If input path is relative, make it absolute first to check
90+
// Strategy: Use dot parser convention
91+
// content/docs/path/to/file.mdx -> content/docs/path/to/file.{targetLang}.mdx
92+
// Target language is determined from docs.site.json configuration
93+
// Skip files that already have language suffix
2994
const absPath = path.resolve(enFilePath);
3095

31-
if (!absPath.startsWith(docsRoot)) {
32-
// Fallback or specific logic if file is not in content/docs
33-
return enFilePath.replace('content/docs', 'content/docs-cn');
96+
// Skip if already has a language suffix
97+
if (hasLanguageSuffix(absPath)) {
98+
return absPath;
3499
}
35-
36-
const relativePath = path.relative(docsRoot, enFilePath);
37-
return path.join(process.cwd(), 'content/docs-cn', relativePath);
100+
101+
// Replace .mdx with target language suffix
102+
if (absPath.endsWith('.mdx')) {
103+
return absPath.replace(/\.mdx$/, TARGET_LANGUAGE_SUFFIX);
104+
}
105+
106+
return absPath;
38107
}
39108

40109
export async function translateContent(content, openai, model) {

packages/site/app/[lang]/docs/[[...slug]]/page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@ import { DocsPage, DocsBody } from 'fumadocs-ui/page';
44
import { notFound } from 'next/navigation';
55
import { siteConfig } from '@/lib/site-config';
66
import defaultComponents from 'fumadocs-ui/mdx';
7-
import { Steps, Step } from 'fumadocs-ui/components/steps';
8-
import { Card, Cards } from 'fumadocs-ui/components/card';
97
import { Callout } from 'fumadocs-ui/components/callout';
8+
import { Card, Cards } from 'fumadocs-ui/components/card';
9+
import { Steps, Step } from 'fumadocs-ui/components/steps';
10+
11+
const components = {
12+
...defaultComponents,
13+
Callout,
14+
Card,
15+
Cards,
16+
Steps,
17+
Step,
18+
};
1019

1120
interface PageProps {
1221
params: Promise<{
@@ -43,7 +52,7 @@ export default async function Page({ params }: PageProps) {
4352
} : undefined}
4453
>
4554
<DocsBody>
46-
<MDX components={{ ...defaultComponents, Steps, Step, Card, Cards, Callout }} />
55+
<MDX components={components} />
4756
</DocsBody>
4857
</DocsPage>
4958
);

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/app/page.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { redirect } from 'next/navigation';
2+
3+
/**
4+
* Root page - redirects are handled by proxy.ts middleware
5+
* This page should never actually render as the middleware intercepts and redirects
6+
* But Next.js requires a page component for the route to be recognized
7+
*/
8+
export default function RootPage() {
9+
// Fallback redirect if middleware didn't handle it
10+
redirect('/en/docs');
11+
}

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+
}

packages/site/mdx-components.tsx

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import defaultComponents from 'fumadocs-ui/mdx';
22
import type { MDXComponents } from 'mdx/types';
33
import { Steps, Step } from 'fumadocs-ui/components/steps';
4-
import { Card, Cards } from 'fumadocs-ui/components/card';
5-
import { Callout } from 'fumadocs-ui/components/callout';
64

75
export function useMDXComponents(components: MDXComponents): MDXComponents {
86
return {
97
...defaultComponents,
108
Steps,
119
Step,
12-
Card,
13-
Cards,
14-
Callout,
1510
...components,
1611
};
1712
}

0 commit comments

Comments
 (0)