vue-auto-i18n-zlp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
File without changes
package/bin/index.js ADDED
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require("commander");
4
+ const inquirer = require("inquirer");
5
+ const chalk = require("chalk");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ // 引入所有模块
10
+ const scan = require("../lib/scan");
11
+ const extract = require("../lib/extract");
12
+ const translate = require("../lib/translate");
13
+ const write = require("../lib/write");
14
+ const buildMap = require("../lib/buildMap");
15
+ const refactor = require("../lib/refactor");
16
+ const fixScript = require("../lib/fixScript");
17
+
18
+ // 读取 package.json 获取版本号
19
+ const pkg = require("../package.json");
20
+
21
+ program
22
+ .version(pkg.version)
23
+ .description("Nuxt 3 自动化国际化工具")
24
+ .option("-k, --key <type>", "DeepSeek API Key");
25
+
26
+ program.parse(process.argv);
27
+
28
+ const options = program.opts();
29
+
30
+ async function run() {
31
+ console.log(chalk.blue.bold("\n🚀 启动 Auto I18n CLI \n"));
32
+
33
+ // 1. 获取 Key
34
+ let apiKey = options.key || process.env.DEEPSEEK_API_KEY;
35
+
36
+ if (!apiKey) {
37
+ // 尝试从项目根目录的 .env 读取 (兼容性)
38
+ if (fs.existsSync(path.join(process.cwd(), ".env"))) {
39
+ require("dotenv").config({ path: path.join(process.cwd(), ".env") });
40
+ apiKey = process.env.DEEPSEEK_API_KEY;
41
+ }
42
+ }
43
+
44
+ if (!apiKey) {
45
+ const answers = await inquirer.prompt([
46
+ {
47
+ type: "password",
48
+ name: "key",
49
+ message: "请输入 DeepSeek API Key:",
50
+ mask: "*",
51
+ },
52
+ ]);
53
+ apiKey = answers.key;
54
+ }
55
+
56
+ const userRoot = process.cwd();
57
+
58
+ try {
59
+ // Step 1: 扫描
60
+ await scan(userRoot);
61
+
62
+ // Step 2: 提取
63
+ await extract(userRoot);
64
+
65
+ // Step 3: 翻译
66
+ await translate(userRoot, apiKey);
67
+
68
+ // Step 4: 写入
69
+ await write(userRoot);
70
+
71
+ // Step 5: 确认重构
72
+ console.log("\n----------------------------------------");
73
+ const { confirmRefactor } = await inquirer.prompt([
74
+ {
75
+ type: "confirm",
76
+ name: "confirmRefactor",
77
+ message: chalk.yellow(
78
+ "⚠️ 即将修改项目源码进行替换,建议先 Git Commit。是否继续?",
79
+ ),
80
+ default: true,
81
+ },
82
+ ]);
83
+
84
+ if (confirmRefactor) {
85
+ await buildMap(userRoot);
86
+ await refactor(userRoot);
87
+ await fixScript(userRoot);
88
+ console.log(
89
+ chalk.green.bold("\n✨✨ 全部流程执行完毕!国际化迁移成功! ✨✨"),
90
+ );
91
+ console.log(chalk.gray("请手动检查 git diff 确认修改无误。"));
92
+ } else {
93
+ console.log(chalk.yellow("已取消源码修改。仅生成了语言包文件。"));
94
+ }
95
+ } catch (e) {
96
+ console.error(chalk.red("\n❌ 程序执行出错:"));
97
+ console.error(e);
98
+ process.exit(1);
99
+ }
100
+ }
101
+
102
+ run();
@@ -0,0 +1,58 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * 构建反向映射表 (zh -> key)
6
+ * @param {string} userRoot
7
+ */
8
+ module.exports = async function buildMap(userRoot) {
9
+ console.log("🚀 [Step 5-1] 生成反向映射表...");
10
+
11
+ // 源文件在 locales 目录 (上一步生成的)
12
+ const INPUT_FILE = path.join(userRoot, "locales", "zh.json");
13
+ // 输出到 cache 目录
14
+ const OUTPUT_FILE = path.join(userRoot, ".i18n_cache", "zh_to_key_map.json");
15
+
16
+ function flattenObject(obj, prefix = "") {
17
+ return Object.keys(obj).reduce((acc, k) => {
18
+ const pre = prefix.length ? prefix + "." : "";
19
+ if (typeof obj[k] === "object" && obj[k] !== null) {
20
+ Object.assign(acc, flattenObject(obj[k], pre + k));
21
+ } else {
22
+ acc[pre + k] = obj[k];
23
+ }
24
+ return acc;
25
+ }, {});
26
+ }
27
+
28
+ if (!fs.existsSync(INPUT_FILE)) {
29
+ console.error(`❌ 找不到源文件: ${INPUT_FILE}`);
30
+ return;
31
+ }
32
+
33
+ try {
34
+ const rawData = fs.readFileSync(INPUT_FILE, "utf-8");
35
+ const zhData = JSON.parse(rawData);
36
+
37
+ const flatData = flattenObject(zhData);
38
+
39
+ const entries = Object.entries(flatData).map(([key, zhText]) => {
40
+ return { key, zh: String(zhText) };
41
+ });
42
+
43
+ // 按长度降序,优先匹配长词
44
+ entries.sort((a, b) => b.zh.length - a.zh.length);
45
+
46
+ const reverseMap = {};
47
+ entries.forEach((item) => {
48
+ reverseMap[item.zh] = item.key;
49
+ });
50
+
51
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify(reverseMap, null, 2), "utf-8");
52
+
53
+ console.log(`✅ 映射表生成成功 (包含 ${entries.length} 条)`);
54
+ console.log(`💾 缓存至: .i18n_cache/zh_to_key_map.json`);
55
+ } catch (error) {
56
+ console.error("❌ 处理失败:", error);
57
+ }
58
+ };
package/lib/extract.js ADDED
@@ -0,0 +1,78 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * 提取纯中文清单
6
+ * @param {string} userRoot 用户执行命令的根目录
7
+ */
8
+ module.exports = async function extract(userRoot) {
9
+ // 1. 确定路径
10
+ const cacheDir = path.join(userRoot, ".i18n_cache");
11
+ const INPUT_FILE = path.join(cacheDir, "scan_result.json");
12
+ const OUTPUT_FILE = path.join(cacheDir, "todo_list.json");
13
+
14
+ // 正则表达式集合
15
+ const REGEX_QUOTES = /(['"`])([^'"`\n\r]*[\u4e00-\u9fa5]+[^'"`\n\r]*)\1/g;
16
+ const REGEX_TAG_CONTENT =
17
+ />([^<]*[\u4e00-\u9fa5]+[^<]*)<|^\s*([\u4e00-\u9fa5]+)\s*$/gm;
18
+ const REGEX_COMMENT_BLOCK = /\/\*[\s\S]*?\*\//;
19
+ const REGEX_COMMENT_HTML = /<!--[\s\S]*?-->/;
20
+
21
+ console.log("🔍 [Step 2] 正在提取中文清单...");
22
+
23
+ try {
24
+ if (!fs.existsSync(INPUT_FILE)) {
25
+ console.error(`❌ 未找到扫描结果,请先执行扫描步骤。路径: ${INPUT_FILE}`);
26
+ return;
27
+ }
28
+
29
+ const rawData = fs.readFileSync(INPUT_FILE, "utf-8");
30
+ const fileList = JSON.parse(rawData);
31
+ let allChinese = new Set();
32
+
33
+ fileList.forEach((file) => {
34
+ let content = file.content;
35
+ if (!content) return;
36
+
37
+ // 移除块注释和HTML注释
38
+ content = content
39
+ .replace(REGEX_COMMENT_BLOCK, "")
40
+ .replace(REGEX_COMMENT_HTML, "");
41
+
42
+ // 移除行内注释 //
43
+ const lines = content.split("\n");
44
+ const cleanLines = lines.map((line) => {
45
+ if (line.trim().startsWith("//")) return "";
46
+ return line;
47
+ });
48
+ content = cleanLines.join("\n");
49
+
50
+ // 提取策略 1: 引号包裹
51
+ let match;
52
+ while ((match = REGEX_QUOTES.exec(content)) !== null) {
53
+ const text = match[2].trim();
54
+ if (text) allChinese.add(text);
55
+ }
56
+
57
+ // 提取策略 2: 标签内容
58
+ REGEX_TAG_CONTENT.lastIndex = 0;
59
+ const tagMatches = content.matchAll(REGEX_TAG_CONTENT);
60
+ for (const m of tagMatches) {
61
+ const text = (m[1] || m[2] || "").trim();
62
+ if (text && !text.includes("{{") && !text.includes("}}")) {
63
+ allChinese.add(text);
64
+ }
65
+ }
66
+ });
67
+
68
+ // 排序并保存
69
+ const sortedList = Array.from(allChinese).sort();
70
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify(sortedList, null, 2), "utf-8");
71
+
72
+ console.log(`✅ 提取完成!共 ${sortedList.length} 条唯一中文词条。`);
73
+ console.log(`💾 提取结果缓存至: .i18n_cache/todo_list.json`);
74
+ } catch (error) {
75
+ console.error("❌ 提取出错:", error);
76
+ throw error; // 抛出错误让主程序捕获
77
+ }
78
+ };
@@ -0,0 +1,78 @@
1
+ /*
2
+ * @Author: zhouliping
3
+ * @Date: 2026-01-20 17:36:23
4
+ * @LastEditors:
5
+ * @LastEditTime: 2026-01-20 17:49:13
6
+ * @Description:
7
+ * @FilePath: \学习\auto-i18n-cli\lib\fixScript.js
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+
12
+ /**
13
+ * 修复 Nuxt 3 Script Setup 引入
14
+ * @param {string} userRoot
15
+ */
16
+ module.exports = async function fixScript(userRoot) {
17
+ console.log("🚀 [Step 5-3] 修复 <script setup> 引入...");
18
+
19
+ const cacheDir = path.join(userRoot, ".i18n_cache");
20
+ const FILE_LIST_JSON = path.join(cacheDir, "scan_result.json"); // 使用 scan_result 以保持一致
21
+
22
+ const REGEX_SCRIPT_SETUP = /<script\s+setup[^>]*>([\s\S]*?)<\/script>/;
23
+ const REGEX_HAS_USE_I18N = /const\s+\{\s*[^}]*\bt\b[^}]*\}\s*=\s*useI18n\(\)/;
24
+
25
+ let files = [];
26
+ if (fs.existsSync(FILE_LIST_JSON)) {
27
+ const list = JSON.parse(fs.readFileSync(FILE_LIST_JSON, "utf-8"));
28
+ files = list.map((item) => item.path);
29
+ } else {
30
+ console.error("❌ 找不到 scan_result.json");
31
+ return;
32
+ }
33
+
34
+ let modifiedCount = 0;
35
+
36
+ files.forEach((filePath) => {
37
+ if (!fs.existsSync(filePath)) return;
38
+
39
+ let content = fs.readFileSync(filePath, "utf-8");
40
+ const match = content.match(REGEX_SCRIPT_SETUP);
41
+
42
+ if (match) {
43
+ const scriptContent = match[1];
44
+ // 检查是否已经引入
45
+ const hasImport = REGEX_HAS_USE_I18N.test(scriptContent);
46
+
47
+ if (!hasImport) {
48
+ const lines = scriptContent.split("\n");
49
+ let insertIndex = 0;
50
+
51
+ // 智能定位插入点:跳过 import 语句
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const line = lines[i].trim();
54
+ if (line.startsWith("import ")) {
55
+ insertIndex = i + 1;
56
+ } else if (line === "" && insertIndex === i) {
57
+ insertIndex = i + 1;
58
+ } else if (line !== "" && !line.startsWith("//")) {
59
+ break;
60
+ }
61
+ }
62
+
63
+ // 插入 useI18n
64
+ lines.splice(insertIndex, 0, `const { t } = useI18n()`);
65
+ const newScriptContent = lines.join("\n");
66
+
67
+ // 只替换 script 部分
68
+ const newFileContent = content.replace(scriptContent, newScriptContent);
69
+
70
+ fs.writeFileSync(filePath, newFileContent, "utf-8");
71
+ // console.log(`🔧 修复引用: ${path.basename(filePath)}`);
72
+ modifiedCount++;
73
+ }
74
+ }
75
+ });
76
+
77
+ console.log(`🎉 修复完成!补充了 ${modifiedCount} 个文件的 useI18n 引用。`);
78
+ };
@@ -0,0 +1,135 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * 核心替换逻辑
6
+ * @param {string} userRoot
7
+ */
8
+ module.exports = async function refactor(userRoot) {
9
+ console.log('🚀 [Step 5-2] 执行代码重构 (替换中文为 Key)...');
10
+
11
+ const cacheDir = path.join(userRoot, '.i18n_cache');
12
+ const MAP_FILE = path.join(cacheDir, 'zh_to_key_map.json');
13
+ const FILE_LIST_JSON = path.join(cacheDir, 'scan_result.json');
14
+
15
+ // 正则表达式
16
+ const REGEX_ATTR = /([@:a-zA-Z0-9-]+)\s*=\s*(['"])([^'"]*?[\u4e00-\u9fa5]+[^'"]*?)\2/g;
17
+ const REGEX_TEXT = />([^<]*?[\u4e00-\u9fa5]+[^<]*?)</g;
18
+ const REGEX_STRING = /(['"`])([^'"`\n]*?[\u4e00-\u9fa5]+[^'"`\n]*?)\1/g;
19
+ const REGEX_SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/g;
20
+ const REGEX_STYLE_BLOCK = /<style[^>]*>([\s\S]*?)<\/style>/g;
21
+ const REGEX_DEFINE_PROPS = /defineProps\s*\(\s*\{[\s\S]*?\}\s*\)/g;
22
+ const REGEX_WITH_DEFAULTS = /withDefaults\s*\([\s\S]*?\}\s*\)/g;
23
+
24
+ if (!fs.existsSync(MAP_FILE)) {
25
+ console.error('❌ 找不到映射表 zh_to_key_map.json');
26
+ return;
27
+ }
28
+ const map = JSON.parse(fs.readFileSync(MAP_FILE, 'utf-8'));
29
+
30
+ let files = [];
31
+ if (fs.existsSync(FILE_LIST_JSON)) {
32
+ const list = JSON.parse(fs.readFileSync(FILE_LIST_JSON, 'utf-8'));
33
+ files = list.map(item => item.path);
34
+ } else {
35
+ console.error('❌ 找不到文件列表 scan_result.json');
36
+ return;
37
+ }
38
+
39
+ let globalReplaceCount = 0;
40
+
41
+ files.forEach(filePath => {
42
+ if (!fs.existsSync(filePath)) return;
43
+
44
+ let content = fs.readFileSync(filePath, 'utf-8');
45
+ const originalContent = content;
46
+
47
+ // 保护 <style>
48
+ const stylePlaceholders = [];
49
+ content = content.replace(REGEX_STYLE_BLOCK, (match) => {
50
+ const pid = `__STYLE_PLACEHOLDER_${stylePlaceholders.length}__`;
51
+ stylePlaceholders.push(match);
52
+ return pid;
53
+ });
54
+
55
+ // 保护 <script>
56
+ const scriptPlaceholders = [];
57
+ content = content.replace(REGEX_SCRIPT_BLOCK, (match) => {
58
+ const pid = `__SCRIPT_PLACEHOLDER_${scriptPlaceholders.length}__`;
59
+ scriptPlaceholders.push(match);
60
+ return pid;
61
+ });
62
+
63
+ // --- Template 替换 ---
64
+ content = content.replace(REGEX_ATTR, (match, attr, quote, value) => {
65
+ const key = map[value.trim()];
66
+ if (key) {
67
+ if (attr.startsWith(':') || attr.startsWith('v-bind')) return match;
68
+ return `:${attr}="$t('${key}')"`;
69
+ }
70
+ return match;
71
+ });
72
+
73
+ content = content.replace(REGEX_TEXT, (match, value) => {
74
+ const trimmed = value.trim();
75
+ const key = map[trimmed];
76
+ if (key) return match.replace(trimmed, `{{ $t('${key}') }}`);
77
+ return match;
78
+ });
79
+
80
+ content = content.replace(REGEX_STRING, (match, quote, value) => {
81
+ if (value.includes('__')) return match;
82
+ const key = map[value.trim()];
83
+ if (key) return `$t('${key}')`;
84
+ return match;
85
+ });
86
+
87
+ // --- Script 替换 ---
88
+ scriptPlaceholders.forEach((scriptContent, index) => {
89
+ const propsPlaceholders = []; // 这里用一个数组存所有的 props 占位符
90
+
91
+ // 保护 withDefaults
92
+ let safeScript = scriptContent.replace(REGEX_WITH_DEFAULTS, (match) => {
93
+ const pid = `__PROPS_PLACEHOLDER_${propsPlaceholders.length}__`;
94
+ propsPlaceholders.push(match);
95
+ return pid;
96
+ });
97
+
98
+ // 保护 defineProps
99
+ safeScript = safeScript.replace(REGEX_DEFINE_PROPS, (match) => {
100
+ const pid = `__PROPS_PLACEHOLDER_${propsPlaceholders.length}__`;
101
+ propsPlaceholders.push(match);
102
+ return pid;
103
+ });
104
+
105
+ // 执行 Script 内的替换 (t)
106
+ safeScript = safeScript.replace(REGEX_STRING, (match, quote, value) => {
107
+ if (value.includes('__PROPS_PLACEHOLDER')) return match;
108
+ const key = map[value.trim()];
109
+ if (key) return `t('${key}')`;
110
+ return match;
111
+ });
112
+
113
+ // 还原 Props (按索引还原)
114
+ safeScript = safeScript.replace(/__PROPS_PLACEHOLDER_(\d+)__/g, (match, idx) => {
115
+ return propsPlaceholders[parseInt(idx)];
116
+ });
117
+
118
+ // 还原到 content
119
+ content = content.replace(`__SCRIPT_PLACEHOLDER_${index}__`, () => safeScript);
120
+ });
121
+
122
+ // 还原 <style>
123
+ stylePlaceholders.forEach((styleContent, index) => {
124
+ content = content.replace(`__STYLE_PLACEHOLDER_${index}__`, () => styleContent);
125
+ });
126
+
127
+ if (content !== originalContent) {
128
+ fs.writeFileSync(filePath, content, 'utf-8');
129
+ console.log(`✅ 修改: ${path.basename(filePath)}`);
130
+ globalReplaceCount++;
131
+ }
132
+ });
133
+
134
+ console.log(`🎉 重构完成,修改了 ${globalReplaceCount} 个文件。`);
135
+ };
package/lib/scan.js ADDED
@@ -0,0 +1,104 @@
1
+ /*
2
+ * @Author: zhouliping
3
+ * @Date: 2026-01-20 17:35:24
4
+ * @LastEditors:
5
+ * @LastEditTime: 2026-01-20 17:44:33
6
+ * @Description:
7
+ * @FilePath: \学习\auto-i18n-cli\lib\scan.js
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+
12
+ /**
13
+ * 扫描 Vue 文件中的中文
14
+ * @param {string} userRoot 用户执行命令的根目录 (process.cwd())
15
+ */
16
+ module.exports = async function scan(userRoot) {
17
+ // 1. 确定缓存目录 (存放中间 json 文件)
18
+ const cacheDir = path.join(userRoot, ".i18n_cache");
19
+ if (!fs.existsSync(cacheDir)) {
20
+ fs.mkdirSync(cacheDir, { recursive: true });
21
+ }
22
+
23
+ // 配置项
24
+ const CONFIG = {
25
+ rootDir: userRoot, // 使用传入的用户根目录
26
+ outputFile: path.join(cacheDir, "scan_result.json"), // 输出到缓存目录
27
+ extensions: [".vue"],
28
+ ignoreDirs: [
29
+ "node_modules",
30
+ ".git",
31
+ ".nuxt",
32
+ ".output",
33
+ ".history",
34
+ "dist",
35
+ "public",
36
+ "assets",
37
+ "locales",
38
+ ".i18n_cache", // 忽略我们自己创建的缓存目录
39
+ "scripts",
40
+ ],
41
+ chineseRegex: /[\u4e00-\u9fa5]/,
42
+ };
43
+
44
+ /**
45
+ * 递归遍历目录获取文件列表
46
+ */
47
+ function getAllFiles(dirPath, arrayOfFiles) {
48
+ const files = fs.readdirSync(dirPath);
49
+ arrayOfFiles = arrayOfFiles || [];
50
+
51
+ files.forEach(function (file) {
52
+ const fullPath = path.join(dirPath, file);
53
+
54
+ // 如果是文件夹
55
+ try {
56
+ const stat = fs.statSync(fullPath);
57
+ if (stat.isDirectory()) {
58
+ // 检查是否忽略
59
+ if (!CONFIG.ignoreDirs.includes(file)) {
60
+ arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
61
+ }
62
+ } else {
63
+ // 如果是文件
64
+ if (CONFIG.extensions.includes(path.extname(file))) {
65
+ arrayOfFiles.push(fullPath);
66
+ }
67
+ }
68
+ } catch (err) {
69
+ // skip permission errors etc.
70
+ }
71
+ });
72
+
73
+ return arrayOfFiles;
74
+ }
75
+
76
+ console.log("🔍 [Step 1] 开始扫描项目中的 Vue 文件...");
77
+
78
+ const allVueFiles = getAllFiles(CONFIG.rootDir, []);
79
+ const result = [];
80
+ let chineseFileCount = 0;
81
+
82
+ // console.log(`📂 找到 ${allVueFiles.length} 个 Vue 文件,正在检查中文...`);
83
+
84
+ allVueFiles.forEach((filePath) => {
85
+ try {
86
+ const content = fs.readFileSync(filePath, "utf-8");
87
+ if (CONFIG.chineseRegex.test(content)) {
88
+ result.push({
89
+ path: filePath,
90
+ content: content,
91
+ });
92
+ chineseFileCount++;
93
+ }
94
+ } catch (err) {
95
+ console.error(`❌ 读取文件失败: ${filePath}`, err);
96
+ }
97
+ });
98
+
99
+ // 写入结果
100
+ fs.writeFileSync(CONFIG.outputFile, JSON.stringify(result, null, 2), "utf-8");
101
+
102
+ console.log(`✅ 扫描完成!共发现 ${chineseFileCount} 个包含中文的文件。`);
103
+ console.log(`💾 扫描结果缓存至: .i18n_cache/scan_result.json`);
104
+ };
@@ -0,0 +1,140 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ // 注意:移除了 dotenv,因为 apiKey 由外部传入
4
+ const fetch = require("node-fetch"); // 如果 Node 版本较低可能需要安装这个,Node 18+ 自带 fetch
5
+
6
+ /**
7
+ * AI 翻译核心逻辑
8
+ * @param {string} userRoot 用户根目录
9
+ * @param {string} apiKey DeepSeek API Key
10
+ */
11
+ module.exports = async function translate(userRoot, apiKey) {
12
+ // 路径配置
13
+ const cacheDir = path.join(userRoot, ".i18n_cache");
14
+ const INPUT_FILE = path.join(cacheDir, "todo_list.json");
15
+ const OUTPUT_FILE = path.join(cacheDir, "translation_map.json");
16
+
17
+ // API 配置
18
+ const BASE_URL = "https://api.deepseek.com";
19
+ const MODEL_NAME = "deepseek-chat";
20
+ const CHUNK_SIZE = 30;
21
+
22
+ console.log("🔍 [Step 3] AI 正在进行翻译 (DeepSeek)...");
23
+
24
+ if (!apiKey) {
25
+ throw new Error("❌ 未提供 API Key,无法进行翻译。");
26
+ }
27
+
28
+ if (!fs.existsSync(INPUT_FILE)) {
29
+ console.error("⚠️ 未找到 todo_list.json,跳过翻译步骤。");
30
+ return;
31
+ }
32
+
33
+ const allItems = JSON.parse(fs.readFileSync(INPUT_FILE, "utf-8"));
34
+ const total = allItems.length;
35
+
36
+ if (total === 0) {
37
+ console.log("ℹ️ 待翻译列表为空,跳过。");
38
+ return;
39
+ }
40
+
41
+ // 读取已有的翻译结果(如果翻译了一半断了,可以接着跑)
42
+ let finalMap = {};
43
+ if (fs.existsSync(OUTPUT_FILE)) {
44
+ try {
45
+ finalMap = JSON.parse(fs.readFileSync(OUTPUT_FILE, "utf-8"));
46
+ } catch (e) {}
47
+ }
48
+
49
+ // 过滤掉已经翻译过的(可选优化,这里暂时简单全量处理,或者根据你的逻辑调整)
50
+ // 简单起见,这里还是按原文逻辑跑
51
+
52
+ let successCount = 0;
53
+ console.log(`🚀 开始翻译,共 ${total} 条数据...`);
54
+
55
+ // 定义 AI 调用函数
56
+ async function callAI(list) {
57
+ const prompt = `
58
+ 你是一个 i18n 专家。请将以下中文数组转换为 i18n 配置对象。
59
+ 输入数据:${JSON.stringify(list)}
60
+ 要求:
61
+ 1. 为每个中文生成语义化 snake_case Key。
62
+ 2. 提供 en 和 de 的翻译。
63
+ 3. 只输出标准的 JSON 格式,不要 Markdown 标记。
64
+ 4. 格式示例:
65
+ {
66
+ "中文原文": { "key": "hello_world", "zh": "中文原文", "en": "Hello", "de": "Hallo" }
67
+ }
68
+ `;
69
+
70
+ try {
71
+ const response = await fetch(`${BASE_URL}/chat/completions`, {
72
+ method: "POST",
73
+ headers: {
74
+ "Content-Type": "application/json",
75
+ Authorization: `Bearer ${apiKey}`,
76
+ },
77
+ body: JSON.stringify({
78
+ model: MODEL_NAME,
79
+ messages: [
80
+ {
81
+ role: "system",
82
+ content: "You are a helpful assistant that outputs JSON only.",
83
+ },
84
+ { role: "user", content: prompt },
85
+ ],
86
+ response_format: { type: "json_object" },
87
+ }),
88
+ });
89
+
90
+ const data = await response.json();
91
+ if (data.error) throw new Error(data.error.message);
92
+
93
+ const content = data.choices[0].message.content;
94
+ return JSON.parse(content);
95
+ } catch (error) {
96
+ console.error("❌ API 调用失败:", error.message);
97
+ return null;
98
+ }
99
+ }
100
+
101
+ // 分批处理
102
+ for (let i = 0; i < total; i += CHUNK_SIZE) {
103
+ const chunk = allItems.slice(i, i + CHUNK_SIZE);
104
+
105
+ // 简单检查:如果这一批的中文都已经存在于 finalMap 中,跳过
106
+ const allExist = chunk.every((text) => finalMap[text]);
107
+ if (allExist) {
108
+ console.log(
109
+ `⏭️ 跳过第 ${i + 1} - ${Math.min(i + CHUNK_SIZE, total)} 条 (已存在)`,
110
+ );
111
+ successCount += chunk.length;
112
+ continue;
113
+ }
114
+
115
+ console.log(
116
+ `🔄 正在处理第 ${i + 1} - ${Math.min(i + CHUNK_SIZE, total)} 条...`,
117
+ );
118
+
119
+ let result = null;
120
+ let retries = 3;
121
+ while (retries > 0 && !result) {
122
+ result = await callAI(chunk);
123
+ if (!result) {
124
+ console.log(`⚠️ 请求失败,正在重试 (${retries}次剩余)...`);
125
+ retries--;
126
+ }
127
+ }
128
+
129
+ if (result) {
130
+ Object.assign(finalMap, result);
131
+ successCount += chunk.length;
132
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify(finalMap, null, 2), "utf-8");
133
+ } else {
134
+ console.error(`❌ 这一批数据处理失败。`);
135
+ }
136
+ }
137
+
138
+ console.log(`✅ 翻译完成: ${successCount} / ${total}`);
139
+ console.log(`💾 结果已保存至: .i18n_cache/translation_map.json`);
140
+ };
package/lib/write.js ADDED
@@ -0,0 +1,100 @@
1
+ /*
2
+ * @Author: zhouliping
3
+ * @Date: 2026-01-20 17:35:51
4
+ * @LastEditors:
5
+ * @LastEditTime: 2026-01-20 17:46:35
6
+ * @Description:
7
+ * @FilePath: \学习\auto-i18n-cli\lib\write.js
8
+ */
9
+ const fs = require("fs");
10
+ const path = require("path");
11
+
12
+ /**
13
+ * 将翻译结果写入 locales 文件夹
14
+ * @param {string} userRoot 用户根目录
15
+ */
16
+ module.exports = async function write(userRoot) {
17
+ console.log("🔍 [Step 4] 正在生成语言包文件...");
18
+
19
+ const cacheDir = path.join(userRoot, ".i18n_cache");
20
+ const INPUT_MAP = path.join(cacheDir, "translation_map.json");
21
+ // 关键修改:生成到用户项目根目录下的 locales
22
+ const LOCALES_DIR = path.join(userRoot, "locales");
23
+
24
+ const FILES = {
25
+ zh: "zh.json",
26
+ en: "en.json",
27
+ de: "de.json",
28
+ };
29
+
30
+ if (!fs.existsSync(INPUT_MAP)) {
31
+ console.error("❌ 错误:找不到翻译结果 translation_map.json");
32
+ return;
33
+ }
34
+
35
+ const fileContent = fs.readFileSync(INPUT_MAP, "utf-8");
36
+ if (!fileContent.trim()) return;
37
+
38
+ let translationMap;
39
+ try {
40
+ translationMap = JSON.parse(fileContent);
41
+ } catch (e) {
42
+ console.error("❌ JSON 解析失败:", e.message);
43
+ return;
44
+ }
45
+
46
+ const isArray = Array.isArray(translationMap);
47
+ const items = isArray ? translationMap : Object.values(translationMap);
48
+
49
+ if (items.length === 0) {
50
+ console.warn("⚠️ 数据为空,跳过写入。");
51
+ return;
52
+ }
53
+
54
+ const maps = { zh: {}, en: {}, de: {} };
55
+ let validCount = 0;
56
+
57
+ items.forEach((item) => {
58
+ if (item && item.key) {
59
+ maps.zh[item.key] = item.zh;
60
+ maps.en[item.key] = item.en;
61
+ maps.de[item.key] = item.de;
62
+ validCount++;
63
+ }
64
+ });
65
+
66
+ if (!fs.existsSync(LOCALES_DIR)) {
67
+ fs.mkdirSync(LOCALES_DIR, { recursive: true });
68
+ }
69
+
70
+ Object.keys(FILES).forEach((lang) => {
71
+ const filePath = path.join(LOCALES_DIR, FILES[lang]);
72
+ const newMap = maps[lang];
73
+ let existingMap = {};
74
+
75
+ // 读取旧文件合并(保留旧翻译)
76
+ if (fs.existsSync(filePath)) {
77
+ try {
78
+ existingMap = JSON.parse(fs.readFileSync(filePath, "utf-8"));
79
+ } catch (e) {}
80
+ }
81
+
82
+ // 注意:这里采取“旧文件优先”还是“新翻译优先”取决于需求
83
+ // 通常这里是 merge,如果 key 相同,是用 AI 的新翻译覆盖,还是保留旧人工翻译?
84
+ // 这里使用:新生成的 key 会覆盖旧的,或者追加新的
85
+ const finalMap = { ...existingMap, ...newMap };
86
+
87
+ // 按 key 排序让文件好看点
88
+ const sortedMap = {};
89
+ Object.keys(finalMap)
90
+ .sort()
91
+ .forEach((key) => (sortedMap[key] = finalMap[key]));
92
+
93
+ fs.writeFileSync(filePath, JSON.stringify(sortedMap, null, 2), "utf-8");
94
+ console.log(
95
+ `💾 已写入: locales/${FILES[lang]} (Keys: ${Object.keys(sortedMap).length})`,
96
+ );
97
+ });
98
+
99
+ console.log("✅ 语言包生成完毕!");
100
+ };
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "vue-auto-i18n-zlp",
3
+ "version": "1.0.0",
4
+ "description": "Vue 自动化国际化工具",
5
+ "main": "bin/index.js",
6
+ "bin": {
7
+ "auto-i18n": "./bin/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "nuxt",
14
+ "i18n",
15
+ "vue",
16
+ "automation"
17
+ ],
18
+ "author": "zhouliping",
19
+ "license": "ISC",
20
+ "dependencies": {
21
+ "chalk": "^4.1.2",
22
+ "commander": "^9.5.0",
23
+ "dotenv": "^16.4.5",
24
+ "inquirer": "^8.2.6",
25
+ "node-fetch": "^2.7.0"
26
+ }
27
+ }