Skip to content

Commit f590aa4

Browse files
committed
🚧 一些i18n的准备工作
1 parent 9f45ee4 commit f590aa4

File tree

11 files changed

+538
-7
lines changed

11 files changed

+538
-7
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,5 @@ __pycache__
1616
package-lock.json
1717
pnpm-lock.yaml
1818
en.bak
19+
.env.local
20+
.env

docs/.vitepress/config.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default defineConfig({
4646
);
4747
},
4848
}),
49-
] as any,
49+
] ,
5050
},
5151
head: [["link", { rel: "icon", type: "image/svg+xml", href: "/logo.svg" }]],
5252
themeConfig: {

docs/.vitepress/i18n/en.json

Whitespace-only changes.

package.json

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,32 @@
44
"description": "dweb-browser开发者文档",
55
"type": "module",
66
"devDependencies": {
7+
"@gaubee/util": "^0.21.0",
78
"@plaoc/is-dweb": "^0.1.2",
89
"@plaoc/plugins": "^1.1.1",
910
"@plaoc/server": "^0.4.1",
10-
"@shikijs/twoslash": "^1.27.0",
11-
"@shikijs/vitepress-twoslash": "^1.26.2",
11+
"@shikijs/twoslash": "^2.1.0",
12+
"@shikijs/vitepress-twoslash": "^2.1.0",
13+
"@std/cli": "npm:@jsr/std__cli@^1.0.11",
14+
"@std/path": "npm:@jsr/std__path@^1.0.8",
1215
"@types/node": "^22.10.6",
1316
"autoprefixer": "^10.4.19",
17+
"dotenv": "^16.4.7",
18+
"front-matter": "^4.0.2",
1419
"gray-matter": "^4.0.3",
20+
"import-meta-ponyfill": "^3.2.1",
21+
"openai": "^4.80.1",
1522
"postcss": "^8.5.1",
1623
"sass": "^1.83.4",
17-
"shiki": "^1.27.0",
24+
"shiki": "^2.1.0",
1825
"tailwindcss": "^3.4.17",
1926
"unocss": "^65.4.0",
2027
"unplugin-vue-components": "^28.0.0",
21-
"vite": "^6.0.7",
22-
"vitepress": "^1.5.0",
28+
"vite": "^6.0.11",
29+
"vitepress": "^1.6.3",
2330
"vitepress-plugin-group-icons": "^1.3.4",
24-
"vue": "^3.4.27"
31+
"vue": "^3.4.27",
32+
"zod": "^3.24.1"
2533
},
2634
"optionalDependencies": {
2735
"@rollup/rollup-linux-x64-gnu": "4.30.1"

scripts/config.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { isGlob, globToRegExp } from "@std/path";
2+
export type I18nConfig = {
3+
ignore?: Array<(path: string) => boolean>;
4+
};
5+
6+
export const createI18nConfig = (options: {
7+
ignore?: string[];
8+
}): I18nConfig => {
9+
const ignore = options.ignore?.map((ignore) => {
10+
if (isGlob(ignore)) {
11+
const reg = globToRegExp(ignore);
12+
return (path: string) => reg.test(path);
13+
}
14+
return (path: string) => path.includes(path);
15+
});
16+
return { ignore };
17+
};

scripts/file-processor.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import fs from "fs";
2+
import path from "path";
3+
import frontmatter from "front-matter";
4+
import { I18nConfig } from "./config.ts";
5+
6+
export class FileProcessor {
7+
constructor(private config: I18nConfig) {}
8+
9+
processMarkdown(content: string) {
10+
const { attributes, body } = frontmatter(content);
11+
const { ignoreBlocks, i11nBlocks } = this.parseComments(body);
12+
13+
return {
14+
frontmatter: attributes,
15+
content: body,
16+
ignoreBlocks,
17+
i11nBlocks,
18+
};
19+
}
20+
21+
processJson(content: string) {
22+
const data = JSON.parse(content);
23+
const ignoredKeys = new Set<string>();
24+
const i11nKeys = new Map<string, string>();
25+
26+
for (const [key, value] of Object.entries(data)) {
27+
if (key.startsWith("//")) {
28+
if (Array.isArray(value) && value.includes("i18n-ignore")) {
29+
ignoredKeys.add(key.replace("//", ""));
30+
}
31+
if (Array.isArray(value) && value.some((v) => v.startsWith("i11n:"))) {
32+
const msg =
33+
value.find((v) => v.startsWith("i11n:"))?.split(":")[1] || "";
34+
i11nKeys.set(key.replace("//", ""), msg);
35+
}
36+
}
37+
}
38+
39+
return { data, ignoredKeys, i11nKeys };
40+
}
41+
42+
private parseComments(content: string) {
43+
const ignoreRegex =
44+
/<!-- i18n-ignore-start -->([\s\S]*?)<!-- i18n-ignore-end -->/g;
45+
const i11nRegex = /<!-- i11n-start: (.*?) -->([\s\S]*?)<!-- i11n-end -->/g;
46+
47+
return {
48+
ignoreBlocks: [...content.matchAll(ignoreRegex)],
49+
i11nBlocks: [...content.matchAll(i11nRegex)],
50+
};
51+
}
52+
53+
walkDir(dir: string) {
54+
const results: string[] = [];
55+
const list = fs.readdirSync(dir);
56+
57+
list.forEach((file) => {
58+
const fullPath = path.join(dir, file);
59+
const stat = fs.statSync(fullPath);
60+
61+
if (this.config.ignore?.some((isIgnore) => isIgnore(fullPath))) return;
62+
63+
if (stat?.isDirectory()) {
64+
results.push(...this.walkDir(fullPath));
65+
} else if (this.isTargetFile(fullPath)) {
66+
results.push(fullPath);
67+
}
68+
});
69+
70+
return results;
71+
}
72+
73+
private isTargetFile(filePath: string) {
74+
const ext = path.extname(filePath);
75+
return [".md", ".json"].includes(ext);
76+
}
77+
}

scripts/git-helper.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { execSync } from "child_process";
2+
3+
interface GitConfig {
4+
commitPrefix: string;
5+
targetBranches: string[];
6+
}
7+
8+
export class GitHelper {
9+
constructor(private config: GitConfig) {}
10+
11+
commit(message: string, files: string[] = ["."]) {
12+
const fullMessage = `${this.config.commitPrefix} ${message}`;
13+
execSync(`git add ${files.join(" ")}`);
14+
execSync(`git commit -m "${fullMessage}"`);
15+
}
16+
17+
getTranslationLogs() {
18+
const cmd = [
19+
"git log",
20+
`--grep='^${this.config.commitPrefix}' -E --invert-grep`,
21+
'--pretty=format:"%h|%ad|%s"',
22+
"--date=iso",
23+
"--name-status",
24+
...this.config.targetBranches.map((b) => `origin/${b}`),
25+
].join(" ");
26+
27+
const output = execSync(cmd).toString();
28+
return this.parseLogOutput(output);
29+
}
30+
31+
private parseLogOutput(output: string) {
32+
const commits: CommitChange[] = [];
33+
let currentCommit: CommitChange | null = null;
34+
35+
output.split("\n").forEach((line) => {
36+
if (line.startsWith('"')) {
37+
const [hash, date, ...msgParts] = line.replace(/"/g, "").split("|");
38+
currentCommit = {
39+
hash,
40+
date: new Date(date),
41+
message: msgParts.join("|"),
42+
changes: [],
43+
};
44+
commits.push(currentCommit);
45+
} else if (currentCommit && line.trim()) {
46+
const [type, path] = line.split("\t");
47+
currentCommit.changes?.push({
48+
type: this.normalizeChangeType(type),
49+
date: currentCommit.date,
50+
path: path.trim(),
51+
diff: this.getLineDiff(path),
52+
});
53+
}
54+
});
55+
56+
return commits.filter((c) => c.changes?.length);
57+
}
58+
59+
private getLineDiff(path: string) {
60+
try {
61+
return execSync(`git diff -U0 ${path}`).toString();
62+
} catch {
63+
return "";
64+
}
65+
}
66+
67+
private normalizeChangeType(type: string): ChangeType {
68+
const mapping: Record<string, ChangeType> = {
69+
A: "file-add",
70+
D: "file-delete",
71+
M: "file-modify",
72+
R: "file-rename",
73+
C: "file-copy",
74+
};
75+
return mapping[type[0]] || "unknown";
76+
}
77+
}
78+
79+
export type ChangeType =
80+
| "file-add"
81+
| "file-delete"
82+
| "file-modify"
83+
| "file-rename"
84+
| "file-copy"
85+
| "unknown";
86+
87+
export interface CommitChange {
88+
hash: string;
89+
date: Date;
90+
message: string;
91+
changes: FileChange[];
92+
}
93+
94+
export interface FileChange {
95+
type: ChangeType;
96+
date: Date;
97+
path: string;
98+
diff: string;
99+
}

scripts/i18n-core.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import path from "node:path";
2+
import fs from "node:fs";
3+
import { CommitChange } from "./git-helper";
4+
import { FileProcessor } from "./file-processor";
5+
6+
interface TranslationContext {
7+
sourceLang: string;
8+
targetLangs: string[];
9+
priorityOrder: string[];
10+
changes: CommitChange[];
11+
}
12+
13+
class I18nCore {
14+
async generateTranslationPrompt(
15+
filePath: string,
16+
context: TranslationContext
17+
) {
18+
const { ext } = path.parse(filePath);
19+
const processor = new FileProcessor(this.config);
20+
const content = fs.readFileSync(filePath, "utf-8");
21+
22+
let parsed: any;
23+
if (ext === ".md") {
24+
parsed = processor.processMarkdown(content);
25+
} else {
26+
parsed = processor.processJson(content);
27+
}
28+
29+
const historyPrompt = this.buildHistoryPrompt(filePath, context);
30+
const instructionPrompt = this.buildInstructionPrompt(parsed);
31+
32+
return `
33+
${this.config.systemPrompt}
34+
35+
## Translation Task
36+
${historyPrompt}
37+
38+
## Content Structure
39+
${JSON.stringify(parsed, null, 2)}
40+
41+
## Special Instructions
42+
${instructionPrompt}
43+
44+
Please output in ${this.config.outputFormat} format.
45+
`;
46+
}
47+
48+
private buildHistoryPrompt(filePath: string, context: TranslationContext) {
49+
const relatedChanges = context.changes
50+
.flatMap((c) => c.changes)
51+
.filter((c) => c.path === filePath);
52+
53+
if (!relatedChanges.length) return "No recent changes detected.";
54+
55+
return `
56+
Recent changes detected:
57+
${relatedChanges
58+
.map(
59+
(c) => `
60+
- ${c.type} at ${c.date.toISOString()}
61+
${c.diff.split("\n").slice(0, 5).join("\n")}
62+
`
63+
)
64+
.join("\n")}
65+
66+
Please pay special attention to these changes and ensure:
67+
1. Consistency across all language versions
68+
2. Preserve manual modifications marked with i11n
69+
3. Follow priority order: ${context.priorityOrder.join(" > ")}
70+
`;
71+
}
72+
}

scripts/i18n.ai.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
协助我做一个针对 vitepress 的的 AI 翻译。
2+
我的思路大概是这样的,首先我会用代码遍历我的 vitepress 项目中的 docs 文件夹,找出其中的 md 文件和 json 文件。这些文件将会交给 AI 翻译。(母语使用中文)
3+
给 AI 翻译的时候,有两种情况,一种是,这是一个新文件,因此只有中文,所以提供给 AI 的时候,AI 就直接翻译出英文等其它配置好的语言。
4+
还有一种情况,是这个文件之前已经翻译过了,因此我们需要通过 git 命令,来查阅最后一次执行翻译指令的 commit-hash,对比现在的 commit-hash,将这个过程中的 changelog 提取出来(当然这需要排除掉 AI 翻译本身的提交,只针对人类的提交,这就意味着 AI 翻译的提交需要在 commit-message 中添加一些特定的消息标识,这样就能跳过 AI 翻译内容带来的困扰,只针对人类的提交,因为人类通常只会提交某一种或者少数的几种语言翻译)。于是这样就能知道,最后一次修改的是中文还是英文还是其它语言,根据每行变更的最后一次的修改,来将这些修改同步翻译到其它语言中。
5+
6+
因此,在代码中,我需要解决这几个问题,首先,是关于 git 工具的封装。
7+
你需要协助我封装出一套 git 工具,如上文所述:
8+
9+
1. commit 函数:这个 commit 函数能根据配置,附加上特定的标识作为前缀,比如我会习惯使用 git-emoji 来做标识
10+
2. loglist 函数:这个 loglist 函数能根据特定的前缀标识,找到最后一次具有相同标识的 commit(就是最后一次执行 AI 翻译提交的 comit),然后这次 commit 之后的 commit 罗列出来(就是后续人类提交的 commit),然后整理出是变动信息。比如说“行变动”、“行新增”、“行删除”、“文件新增”、“文件重命名”、“文件删除”等情况。以“行新增”为例,需要描述是哪一个文件、使用什么语言、哪一行代码变动了,变动前是什么,变动后是什么;再比如以新增文件为例,需要描述哪一个文件,使用说明语言,文件内容是什么。注意,这里是整理多次提交,因此我们需要将多次提交合并成一次提交来看待(我想 git 应该有相关的命令可以直接做到这个效果)。
11+
12+
然后是翻译提示词的问题,通常情况下,我们需要尊重原文来进行翻译的。
13+
这里主要是关于多种语言同时输入的情况:
14+
因为人类是会对 AI 翻译过后的内容做出一些修改的,虽然 AI 可以对原文进行准确的翻译,不过一旦人类介入后,内容可能会发生一些改变,因此同时输入英文中文,AI 不能单纯地就把中文当作母本、把英文当作译本,而是要结合内容以及 loglist 来判断,到底有没有必要对内容内容进行修改。比如说我作为人类,在提交中文后,接着执行 AI 翻译代码,生成新的 commit 把英文的内容也提交了,然后人类对 AI 的英文再做出了一些修改再做了一次提交,此时再将双语内容提交给 AI,这时候 AI 其实就应该把英文和中文都当是人类审核过的内容,都应该当作原文来对待。此时 AI 的目标是减少不同语言之间的含义差距,同时也应该尊重人类的修改。
15+
另外就是,人类在执行 AI 翻译的时候,会告诉 AI 自己的熟悉的语言顺序,比如说:`['中文、繁体中文', '英文']`。那么就意味着,AI 在根据 loglist 进行多语言进行翻译的过程中,可以先翻译 人类已知的语言,然后等人类审核通过后,来为其它语言做翻译,这样可能会更加准确。(这也就意味着 AI 翻译可以分两种模式:一种是全量翻译模式(一次新处理所有需要进行翻译的语言),一种是增量翻译模式(先处理人类熟悉的语言,然后再根据审核修改过的结果,对其余内容做翻译)。
16+
17+
在有就是有些情况下,人类需要介入翻译,这里列举三种情况:
18+
19+
1. 有些翻译只出现在某一种语言中,因此需要用特定的标识来标记出来,从而让 AI 忽略它。在 markdown 中,可以用 html-comment 来做这个隐藏的注释(比如`<!-- i18n-ignore-start -->``<!-- i18n-ignore-end -->`),在 json 中,可以用特殊的字段来做配置(比如`"//key":["i18n-ignore"]`就意味着对`key`对应的 value 做`i18n-ignore`的处理)。
20+
1. 有些翻译是需要进行本土化翻译,因此也需要用特定的标识标记出来,让 AI 根据本地的情况来进行翻译。在 markdown 中,可以用 html-comment 来做这个隐藏的注释(比如`<!-- i11n-start: some comment message for i11n -->``<!-- i11n-end -->`),在 json 中,可以用特殊的字段来做配置(比如`"//key":["i11n: some comment message for i11n"]`就意味着对`key`对应的 value 做`i18n-ignore`的处理)。
21+
> 关于本土化的翻译,需要依赖 AI 的发散性思维来进行,因此译文和原文差别可能非常大。因此这里的`some comment message for i11n`目的就是为了引导 AI 的翻译方向。如果是使用中文,我会写:`"//key":["i11n: 这是一些内容说明"]`,那么翻译成英文后,这些内容说明同样也要翻译成英文。
22+
> 另外,在 AI 本土化翻译完成后,AI 需要额外补充本土化翻译的一些说明:`//key":["i11n: some comment message for i11n","i11n-reason: some reason"]`,这里的`some reason`确实就是写给 AI 自己看的,目的是未来 AI 再对这段进行翻译的时候,配合原文与之前的翻译思路,才能更加正确地对这段本土化翻译的内容做准确的修改。
23+
> 另外就需要依赖 loglist,来优化判断关于本土化的翻译到底要不要进行改动。
24+
25+
---
26+
27+
总结来说,这里的工作主要有四个部分:
28+
29+
1. git 工具的封装
30+
2. 文件读写的封装
31+
3. 提示词的编写
32+
4. cli 工具的开发
33+
34+
注意,我使用的语言是 typescript,运行时是 nodejs。

0 commit comments

Comments
 (0)