Skip to content

Commit a670da0

Browse files
committed
feat: add packages/translate
1 parent 1a271b9 commit a670da0

18 files changed

+4635
-22
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ node_modules
1010
coverage
1111
.next/
1212
out/
13+
dist/
1314
build
1415
*.tsbuildinfo
1516

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ repos:
1818
hooks:
1919
- id: local-check-types
2020
name: check-types
21-
entry: pnpm run check-types
21+
entry: pnpm run -r check-types
2222
language: system
2323
files: \.(ts|tsx)$
2424
pass_filenames: false

apps/docs/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"build:post": "tsx ./scripts/post-build.mts",
88
"dev": "next dev --turbo",
99
"start": "next start",
10-
"postinstall": "fumadocs-mdx"
10+
"postinstall": "fumadocs-mdx",
11+
"check-types": "tsc --noEmit"
1112
},
1213
"dependencies": {
1314
"@fumadocs/mdx-remote": "^1.3.0",

apps/docs/translation.config.mjs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
export default {
2+
langs: {
3+
'zh-Hans': {
4+
name: 'Simplified Chinese',
5+
// 翻译规则和指南
6+
guide: `
7+
- For technical terms that should not be fully translated, use the format: "中文翻译 (English term)"
8+
Example: "服务端渲染 (SSR)" instead of just "SSR" or just "服务端渲染"
9+
- Add a space between Chinese characters and English words/symbols to improve readability
10+
- Maintain consistent translations for common terms across the entire document
11+
`,
12+
// 常见技术术语翻译词典
13+
// 格式: 'English term': '中文翻译'
14+
terms: {},
15+
},
16+
'zh-Hant': {
17+
name: 'Traditional Chinese',
18+
// 翻譯規則和指南
19+
guide: `
20+
- For technical terms that should not be fully translated, use the format: "繁體中文翻譯 (English term)"
21+
Example: "伺服器渲染 (SSR)" instead of just "SSR" or just "伺服器渲染"
22+
- Add a space between Chinese characters and English words/symbols to improve readability
23+
- Maintain consistent translations for common terms across the entire document
24+
`,
25+
// 常見技術術語翻譯詞典
26+
// 格式: 'English term': '繁體中文翻譯'
27+
terms: {},
28+
},
29+
ja: {
30+
name: 'Japanese',
31+
guide: `
32+
- For technical terms that should not be fully translated, use the format: "日本語訳 (English term)"
33+
Example: "サーバーサイドレンダリング (SSR)" instead of just "SSR" or just "サーバーサイドレンダリング"
34+
- Maintain consistent translations for common terms across the entire document
35+
- Use katakana for foreign technical terms where appropriate
36+
`,
37+
terms: {},
38+
},
39+
es: {
40+
name: 'Spanish',
41+
guide: `
42+
- For technical terms that should not be fully translated, use the format: "Traducción en español (English term)"
43+
Example: "Renderizado del lado del servidor (SSR)" instead of just "SSR" or just "Renderizado del lado del servidor"
44+
- Maintain consistent translations for common terms across the entire document
45+
- Use formal "usted" form instead of informal "tú" for instructions
46+
`,
47+
terms: {},
48+
},
49+
de: {
50+
name: 'German',
51+
guide: `
52+
- For technical terms that should not be fully translated, use the format: "Deutsche Übersetzung (English term)"
53+
Example: "Server-seitiges Rendering (SSR)" instead of just "SSR" or just "Server-seitiges Rendering"
54+
- Maintain consistent translations for common terms across the entire document
55+
- Follow German capitalization rules for nouns
56+
`,
57+
terms: {},
58+
},
59+
fr: {
60+
name: 'French',
61+
guide: `
62+
- For technical terms that should not be fully translated, use the format: "Traduction française (English term)"
63+
Example: "Rendu côté serveur (SSR)" instead of just "SSR" or just "Rendu côté serveur"
64+
- Maintain consistent translations for common terms across the entire document
65+
- Use proper French punctuation with spaces before certain punctuation marks
66+
`,
67+
terms: {},
68+
},
69+
ru: {
70+
name: 'Russian',
71+
guide: `
72+
- For technical terms that should not be fully translated, use the format: "Русский перевод (English term)"
73+
Example: "Рендеринг на стороне сервера (SSR)" instead of just "SSR" or just "Рендеринг на стороне сервера"
74+
- Maintain consistent translations for common terms across the entire document
75+
- Use proper Russian cases for technical terms where appropriate
76+
`,
77+
terms: {},
78+
},
79+
ar: {
80+
name: 'Arabic',
81+
guide: `
82+
- For technical terms that should not be fully translated, use the format: "الترجمة العربية (English term)"
83+
Example: "العرض من جانب الخادم (SSR)" instead of just "SSR" or just "العرض من جانب الخادم"
84+
- Maintain consistent translations for common terms across the entire document
85+
- Arabic text should flow right-to-left, but keep code examples and technical terms left-to-right
86+
`,
87+
terms: {},
88+
},
89+
},
90+
docsRoot: 'content/en/',
91+
outputRoot: 'content/',
92+
docsContext: `Next.js is a React framework for building full-stack web applications. You use React Components to build user interfaces, and Next.js for additional features and optimizations.
93+
It also automatically configures lower-level tools like bundlers and compilers. You can instead focus on building your product and shipping quickly.
94+
Whether you're an individual developer or part of a larger team, Next.js can help you build interactive, dynamic, and fast React applications.`,
95+
docsPath: ['**/*.mdx'],
96+
pattern: [],
97+
};

packages/translate/package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "@next-i18n/translate",
3+
"version": "1.0.0",
4+
"description": "Translate nextjs documentation",
5+
"type": "module",
6+
"main": "dist/index.js",
7+
"module": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"bin": {
10+
"translate-docs": "dist/index.js"
11+
},
12+
"files": ["dist"],
13+
"scripts": {
14+
"build": "tsup",
15+
"dev": "tsup --watch",
16+
"start": "node dist/index.js",
17+
"typecheck": "tsc --noEmit",
18+
"test": "vitest run",
19+
"format": "biome format --write .",
20+
"lint": "biome lint .",
21+
"lint:fix": "biome lint --apply .",
22+
"test:watch": "vitest",
23+
"test:coverage": "vitest run --coverage"
24+
},
25+
"keywords": [],
26+
"license": "ISC",
27+
"devDependencies": {
28+
"@biomejs/biome": "1.5.3",
29+
"@semantic-release/git": "^10.0.1",
30+
"@types/commander": "^2.12.5",
31+
"@types/glob": "^8.1.0",
32+
"@types/micromatch": "^4.0.9",
33+
"@types/node": "^22.14.0",
34+
"@vitest/coverage-v8": "^3.1.3",
35+
"c8": "^10.1.3",
36+
"semantic-release": "^21.0.0",
37+
"tsup": "^8.4.0",
38+
"typescript": "^5.8.3",
39+
"vitest": "^3.1.3"
40+
},
41+
"dependencies": {
42+
"commander": "^12.0.0",
43+
"cosmiconfig": "^9.0.0",
44+
"glob": "^10.3.10",
45+
"gray-matter": "^4.0.3",
46+
"micromatch": "^4.0.8",
47+
"openai": "^4.92.1"
48+
},
49+
"publishConfig": {
50+
"access": "public"
51+
}
52+
}

packages/translate/src/batch.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { logger } from './logger';
2+
3+
// Batch processing execution function, controlling concurrency
4+
export async function executeInBatches<T, R>(
5+
items: T[],
6+
fn: (item: T) => Promise<R>,
7+
concurrency: number,
8+
): Promise<R[]> {
9+
// Copy array to avoid modifying original
10+
const queue = [...items];
11+
const inProgress = new Set<T>();
12+
const results: R[] = [];
13+
14+
logger.debug(
15+
`Starting batch processing of ${items.length} items with concurrency ${concurrency}`,
16+
);
17+
18+
// Process next item in queue
19+
async function processNext(): Promise<void> {
20+
if (queue.length === 0) return;
21+
22+
// biome-ignore lint/style/noNonNullAssertion: <explanation>
23+
const item = queue.shift()!;
24+
inProgress.add(item);
25+
26+
try {
27+
const result = await fn(item);
28+
results.push(result);
29+
} catch (error) {
30+
console.error(error);
31+
logger.error(
32+
`Error processing batch item: ${error instanceof Error ? error.message : String(error)}`,
33+
);
34+
} finally {
35+
inProgress.delete(item);
36+
37+
// Continue processing next item
38+
await processNext();
39+
}
40+
}
41+
42+
// Start initial batch
43+
const initialBatch = Math.min(concurrency, queue.length);
44+
const initialPromises: Promise<void>[] = [];
45+
46+
for (let i = 0; i < initialBatch; i++) {
47+
initialPromises.push(processNext());
48+
}
49+
50+
// Wait for all tasks to complete
51+
await Promise.all(initialPromises);
52+
53+
// Wait for all in-progress tasks to complete
54+
while (inProgress.size > 0) {
55+
await new Promise((resolve) => setTimeout(resolve, 100));
56+
}
57+
58+
logger.debug(`Batch processing completed, processed ${results.length} items`);
59+
return results;
60+
}

packages/translate/src/config.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { cosmiconfig } from 'cosmiconfig';
2+
import { logger } from './logger';
3+
import type { MainConfig } from './types';
4+
5+
interface Options {
6+
config?: string;
7+
targetLanguage?: string;
8+
}
9+
10+
export async function getConfig(
11+
options: Options,
12+
): Promise<MainConfig | MainConfig[]> {
13+
// Load config file if specified, otherwise search for one
14+
let config = {} as MainConfig;
15+
16+
try {
17+
const explorer = cosmiconfig('translation', {
18+
searchPlaces: [
19+
'package.json',
20+
'.translationrc',
21+
'.translationrc.json',
22+
'.translationrc.yaml',
23+
'.translationrc.yml',
24+
'.translationrc.js',
25+
'translation.config.js',
26+
'.translationrc.mjs',
27+
'translation.config.mjs',
28+
],
29+
});
30+
31+
// biome-ignore lint/suspicious/noImplicitAnyLet: <explanation>
32+
let result;
33+
34+
if (options.config) {
35+
// Load specific config file
36+
result = await explorer.load(options.config);
37+
if (result) {
38+
logger.debug(`Loaded configuration from ${options.config}`);
39+
}
40+
} else {
41+
// Search for config in standard locations
42+
result = await explorer.search();
43+
if (result) {
44+
logger.debug(`Loaded configuration from ${result.filepath}`);
45+
} else {
46+
logger.warn(
47+
'No configuration file found, using command line options only',
48+
);
49+
}
50+
}
51+
52+
if (result) {
53+
config = result.config;
54+
}
55+
return config;
56+
} catch (error) {
57+
logger.error(
58+
`Error loading config file: ${error instanceof Error ? error.message : String(error)}`,
59+
);
60+
if (options.config) {
61+
// Only exit if a specific config file was requested but couldn't be loaded
62+
process.exit(1);
63+
}
64+
return config;
65+
}
66+
}

packages/translate/src/index.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env node
2+
3+
import { Command } from 'commander';
4+
import { getConfig } from './config';
5+
import { logger } from './logger';
6+
import { main } from './main';
7+
import type { MainConfig } from './types';
8+
9+
export type Config = MainConfig | MainConfig[];
10+
11+
// This string will be replaced during build
12+
const version = '__VERSION__';
13+
14+
const program = new Command();
15+
16+
program
17+
.name('nextjs-translation')
18+
.description('Translate tanstack docs')
19+
.version(version, '-v, --version', 'Show version number')
20+
.option('-c, --config <path>', 'Path to configuration file')
21+
.option('--verbose', 'Enable verbose logging')
22+
.option(
23+
'-p, --pattern <pattern>',
24+
'File pattern to match for updating (e.g., "*.mdx" or "docs/**/*.mdx"). The .md extension is optional.',
25+
)
26+
27+
.option(
28+
'-d, --docs-path <pattern>',
29+
'File pattern for docs to translate, useful when not relying on docsConfig (e.g., "**/*.mdx"). The .md extension is optional.',
30+
)
31+
.option('-l, --list-only', 'Only list file status without updating docs')
32+
.option(
33+
'-t, --target-language <language>',
34+
'Specify the target language code for translation (e.g., "zh-CN", "fr", "es")',
35+
)
36+
.action(
37+
async (options: {
38+
config?: string;
39+
verbose?: boolean;
40+
pattern?: string;
41+
docsPath?: string;
42+
listOnly?: boolean;
43+
targetLanguage?: string;
44+
}) => {
45+
if (options.verbose) {
46+
logger.setVerbose(true);
47+
}
48+
49+
const config = await getConfig(options);
50+
const configs: MainConfig[] = Array.isArray(config) ? config : [config];
51+
52+
for (const config of configs) {
53+
await main({
54+
...config,
55+
...(options.pattern ? { pattern: options.pattern } : {}),
56+
...(options.docsPath ? { docsPath: options.docsPath } : {}),
57+
listOnly: options.listOnly,
58+
targetLanguage: options.targetLanguage,
59+
});
60+
}
61+
logger.success('Process completed successfully');
62+
},
63+
);
64+
65+
program.parse();

0 commit comments

Comments
 (0)