i18n-a11y 1.0.0 → 1.0.2
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/dist/i18n.config.js +9 -23
- package/dist/src/ai.js +5 -5
- package/dist/src/cli.js +17 -8
- package/dist/src/extractKeys.js +8 -8
- package/dist/src/scanFiles.js +25 -6
- package/dist/src/utils/genI18nConfig.js +73 -54
- package/dist/src/utils/index.js +19 -0
- package/dist/src/writeFile.js +14 -7
- package/package.json +10 -4
- package/i18n.config.ts +0 -25
- package/src/ai.ts +0 -52
- package/src/cli.ts +0 -61
- package/src/extractKeys.ts +0 -110
- package/src/scanFiles.ts +0 -22
- package/src/utils/genI18nConfig.ts +0 -112
- package/src/utils/index.ts +0 -35
- package/src/utils/progress.ts +0 -37
- package/src/writeFile.ts +0 -183
- package/tsconfig.json +0 -14
package/dist/i18n.config.js
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
],
|
|
12
|
-
"output": "ts",
|
|
13
|
-
"outputDir": "src/i18n/locale",
|
|
14
|
-
"withFileComment": true,
|
|
15
|
-
"i18nFns": [
|
|
16
|
-
"t",
|
|
17
|
-
"$t",
|
|
18
|
-
"i18n.t"
|
|
19
|
-
],
|
|
20
|
-
"supportChineseKey": true,
|
|
21
|
-
"languages": [
|
|
22
|
-
"zh-CN",
|
|
23
|
-
"en-US"
|
|
24
|
-
]
|
|
2
|
+
entryDirs: ["src/views", "src/components"],
|
|
3
|
+
extensions: ["vue"],
|
|
4
|
+
output: "ts",
|
|
5
|
+
outputDir: "src/i18n/locale",
|
|
6
|
+
withFileComment: false,
|
|
7
|
+
withKeyFileComment: false,
|
|
8
|
+
i18nFns: ["t", "$t"],
|
|
9
|
+
supportChineseKey: true,
|
|
10
|
+
languages: ["zh-CN", "en-US"],
|
|
25
11
|
};
|
package/dist/src/ai.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
import { loadEnv } from "./utils/index.js";
|
|
2
|
+
loadEnv();
|
|
1
3
|
import OpenAI from "openai";
|
|
2
|
-
import dotenv from "dotenv";
|
|
3
|
-
import path from "path";
|
|
4
|
-
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
|
|
5
4
|
const openai = new OpenAI({
|
|
6
5
|
baseURL: process.env.DEEPSEEK_API_URL,
|
|
7
6
|
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
@@ -28,7 +27,7 @@ const rules = `
|
|
|
28
27
|
|
|
29
28
|
输出:标题
|
|
30
29
|
`;
|
|
31
|
-
|
|
30
|
+
const translate = async (key, target) => {
|
|
32
31
|
const completion = await openai.chat.completions.create({
|
|
33
32
|
model: "deepseek-chat",
|
|
34
33
|
messages: [
|
|
@@ -45,4 +44,5 @@ export default async function translate(key, target) {
|
|
|
45
44
|
});
|
|
46
45
|
const content = completion.choices[0].message.content;
|
|
47
46
|
return content?.trim() || "";
|
|
48
|
-
}
|
|
47
|
+
};
|
|
48
|
+
export default translate;
|
package/dist/src/cli.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
|
+
import { logger, print, pickKeys } from "./utils/index.js";
|
|
5
6
|
import { scanFiles } from "./scanFiles.js";
|
|
6
7
|
import { extractKeys } from "./extractKeys.js";
|
|
7
8
|
import translate from "./ai.js";
|
|
8
|
-
import { logger, print, pickKeys } from "./utils/index.js";
|
|
9
9
|
const program = new Command();
|
|
10
10
|
program.name("auto-i18n").description("I18n a11y CLI").version("1.0.0");
|
|
11
11
|
const onAction = async () => {
|
|
@@ -13,12 +13,18 @@ const onAction = async () => {
|
|
|
13
13
|
await print("AUTO I18N");
|
|
14
14
|
const configPath = path.resolve(process.cwd(), "i18n.config.ts");
|
|
15
15
|
const { genI18nConfig } = await import("./utils/genI18nConfig.js");
|
|
16
|
-
|
|
16
|
+
const exists = await fs
|
|
17
|
+
.access(configPath)
|
|
18
|
+
.then(() => true)
|
|
19
|
+
.catch(() => false);
|
|
20
|
+
if (!exists) {
|
|
17
21
|
await genI18nConfig();
|
|
18
22
|
}
|
|
19
23
|
const files = await scanFiles();
|
|
20
|
-
if (!files.length)
|
|
24
|
+
if (!files.length) {
|
|
25
|
+
logger.warn("No files found, please check i18n.config.js entryDirs and extensions");
|
|
21
26
|
return;
|
|
27
|
+
}
|
|
22
28
|
const { default: config } = await import("../i18n.config.js");
|
|
23
29
|
const translateResult = {};
|
|
24
30
|
const keyFileMap = new Map();
|
|
@@ -27,12 +33,12 @@ const onAction = async () => {
|
|
|
27
33
|
const keys = await extractKeys(file);
|
|
28
34
|
const allKeys = pickKeys(keys);
|
|
29
35
|
for (const key of allKeys) {
|
|
30
|
-
if (!keyFileMap.has(key))
|
|
36
|
+
if (!keyFileMap.has(key)) {
|
|
31
37
|
keyFileMap.set(key, new Set());
|
|
32
|
-
|
|
38
|
+
}
|
|
39
|
+
keyFileMap.get(key).add(file);
|
|
33
40
|
for (const lang of config.languages) {
|
|
34
|
-
|
|
35
|
-
translateResult[key] = {};
|
|
41
|
+
translateResult[key] ?? (translateResult[key] = {});
|
|
36
42
|
const result = await translate(key, lang);
|
|
37
43
|
translateResult[key][lang] = result;
|
|
38
44
|
logger.info(`Translated "${key}" to ${lang}: ${result}`);
|
|
@@ -51,5 +57,8 @@ const onAction = async () => {
|
|
|
51
57
|
process.exit(1);
|
|
52
58
|
}
|
|
53
59
|
};
|
|
54
|
-
program
|
|
60
|
+
program
|
|
61
|
+
.command("scan")
|
|
62
|
+
.description("scan project and generate i18n files")
|
|
63
|
+
.action(onAction);
|
|
55
64
|
program.parse(process.argv);
|
package/dist/src/extractKeys.js
CHANGED
|
@@ -4,7 +4,7 @@ import traverseModule from "@babel/traverse";
|
|
|
4
4
|
import { parse as parseVue } from "@vue/compiler-sfc";
|
|
5
5
|
import { baseParse as parseTemplate } from "@vue/compiler-dom";
|
|
6
6
|
const traverse = traverseModule.default;
|
|
7
|
-
export
|
|
7
|
+
export const extractKeysFromCode = (code, i18nFns) => {
|
|
8
8
|
const keys = new Set();
|
|
9
9
|
let ast;
|
|
10
10
|
try {
|
|
@@ -31,11 +31,11 @@ export function extractKeysFromCode(code, i18nFns) {
|
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
return Array.from(keys);
|
|
34
|
-
}
|
|
35
|
-
export
|
|
34
|
+
};
|
|
35
|
+
export const extractKeysFromVueTemplate = (template, i18nFns) => {
|
|
36
36
|
const keys = new Set();
|
|
37
37
|
const ast = parseTemplate(template);
|
|
38
|
-
|
|
38
|
+
const walk = (node) => {
|
|
39
39
|
if (!node || typeof node !== "object")
|
|
40
40
|
return;
|
|
41
41
|
if (node.type === 5 && node.content?.content) {
|
|
@@ -66,11 +66,11 @@ export function extractKeysFromVueTemplate(template, i18nFns) {
|
|
|
66
66
|
if (child && typeof child === "object")
|
|
67
67
|
walk(child);
|
|
68
68
|
});
|
|
69
|
-
}
|
|
69
|
+
};
|
|
70
70
|
walk(ast);
|
|
71
71
|
return Array.from(keys);
|
|
72
|
-
}
|
|
73
|
-
export async
|
|
72
|
+
};
|
|
73
|
+
export const extractKeys = async (file) => {
|
|
74
74
|
let config;
|
|
75
75
|
try {
|
|
76
76
|
const imported = await import("../i18n.config.js");
|
|
@@ -95,4 +95,4 @@ export async function extractKeys(file) {
|
|
|
95
95
|
extractKeysFromVueTemplate(template, i18nFns).forEach((k) => keys.add(k));
|
|
96
96
|
}
|
|
97
97
|
return Array.from(keys);
|
|
98
|
-
}
|
|
98
|
+
};
|
package/dist/src/scanFiles.js
CHANGED
|
@@ -1,17 +1,36 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
1
3
|
import fg from "fast-glob";
|
|
2
|
-
|
|
4
|
+
import { logger } from "./utils/index.js";
|
|
5
|
+
export const scanFiles = async () => {
|
|
3
6
|
let config;
|
|
4
7
|
try {
|
|
5
8
|
const imported = await import("../i18n.config.js");
|
|
6
9
|
config = imported.default;
|
|
7
10
|
}
|
|
8
|
-
catch
|
|
9
|
-
throw new Error("i18n.config.
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("i18n.config.js not exists or format error");
|
|
10
13
|
}
|
|
11
14
|
const { entryDirs, extensions } = config;
|
|
12
15
|
if (!entryDirs || !extensions) {
|
|
13
|
-
throw new Error("i18n.config.
|
|
16
|
+
throw new Error("i18n.config.js missing entryDirs/extensions config");
|
|
14
17
|
}
|
|
15
|
-
const
|
|
18
|
+
const existingDirs = [];
|
|
19
|
+
for (const dir of entryDirs) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(dir, constants.F_OK);
|
|
22
|
+
existingDirs.push(dir);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
logger.warn(`Directory does not exist: ${dir}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (existingDirs.length === 0) {
|
|
29
|
+
logger.error("None of the entryDirs exist. Please check your i18n.config.js");
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
const patterns = existingDirs.map(dir => `${dir.replace(/\\/g, "/")}/**/*.${extensions.join(",")}`);
|
|
33
|
+
logger.info(`Scanning directories: ${existingDirs.join(", ")}`);
|
|
34
|
+
const files = await fg(patterns, { onlyFiles: true, absolute: true });
|
|
16
35
|
return files;
|
|
17
|
-
}
|
|
36
|
+
};
|
|
@@ -1,98 +1,117 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import path from
|
|
3
|
-
import inquirer from
|
|
4
|
-
import chalk from
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import inquirer from "inquirer";
|
|
4
|
+
import chalk from "chalk";
|
|
5
5
|
export const genI18nConfig = async () => {
|
|
6
|
-
|
|
6
|
+
console.log(chalk.cyan.bold("\n🚀 Auto I18n Config Generator\n"));
|
|
7
|
+
// Step 1: entry directories
|
|
7
8
|
const { entryDirs } = await inquirer.prompt([
|
|
8
9
|
{
|
|
9
|
-
type:
|
|
10
|
-
name:
|
|
11
|
-
message: chalk.green(
|
|
12
|
-
default:
|
|
13
|
-
filter: (input) => input
|
|
10
|
+
type: "input",
|
|
11
|
+
name: "entryDirs",
|
|
12
|
+
message: chalk.green("Step 1: Enter entry directories (comma separated)"),
|
|
13
|
+
default: "src/views,src/pages,src/components",
|
|
14
|
+
filter: (input) => input
|
|
15
|
+
.split(",")
|
|
16
|
+
.map((dir) => dir.trim())
|
|
17
|
+
.filter(Boolean),
|
|
14
18
|
},
|
|
15
19
|
]);
|
|
16
|
-
//
|
|
20
|
+
// Step 2: file extensions
|
|
17
21
|
const { extensions } = await inquirer.prompt([
|
|
18
22
|
{
|
|
19
|
-
type:
|
|
20
|
-
name:
|
|
21
|
-
message: chalk.green(
|
|
22
|
-
choices: [
|
|
23
|
-
default: [
|
|
23
|
+
type: "checkbox",
|
|
24
|
+
name: "extensions",
|
|
25
|
+
message: chalk.green("Step 2: Select file extensions to scan"),
|
|
26
|
+
choices: ["vue", "ts", "js"],
|
|
27
|
+
default: ["vue"],
|
|
24
28
|
},
|
|
25
29
|
]);
|
|
26
|
-
//
|
|
30
|
+
// Step 3: output format
|
|
27
31
|
const { output } = await inquirer.prompt([
|
|
28
32
|
{
|
|
29
|
-
type:
|
|
30
|
-
name:
|
|
31
|
-
message: chalk.green(
|
|
32
|
-
choices: [
|
|
33
|
-
default:
|
|
33
|
+
type: "list",
|
|
34
|
+
name: "output",
|
|
35
|
+
message: chalk.green("Step 3: Select output format"),
|
|
36
|
+
choices: ["ts", "js", "json"],
|
|
37
|
+
default: "ts",
|
|
34
38
|
},
|
|
35
39
|
]);
|
|
36
|
-
//
|
|
40
|
+
// Step 4: output directory
|
|
37
41
|
const { outputDir } = await inquirer.prompt([
|
|
38
42
|
{
|
|
39
|
-
type:
|
|
40
|
-
name:
|
|
41
|
-
message: chalk.green(
|
|
42
|
-
default:
|
|
43
|
+
type: "input",
|
|
44
|
+
name: "outputDir",
|
|
45
|
+
message: chalk.green("Step 4: Enter output language directory"),
|
|
46
|
+
default: "src/i18n/locale",
|
|
43
47
|
},
|
|
44
48
|
]);
|
|
45
|
-
//
|
|
49
|
+
// Step 5: file header comment
|
|
46
50
|
const { withFileComment } = await inquirer.prompt([
|
|
47
51
|
{
|
|
48
|
-
type:
|
|
49
|
-
name:
|
|
50
|
-
message: chalk.green(
|
|
52
|
+
type: "confirm",
|
|
53
|
+
name: "withFileComment",
|
|
54
|
+
message: chalk.green("Step 5: Include file header comment?"),
|
|
51
55
|
default: true,
|
|
52
56
|
},
|
|
53
57
|
]);
|
|
54
|
-
//
|
|
58
|
+
// Step 6: key → file path comment
|
|
59
|
+
const { withKeyFileComment } = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: "confirm",
|
|
62
|
+
name: "withKeyFileComment",
|
|
63
|
+
message: chalk.green("Step 6: Include key-to-source-file comments?"),
|
|
64
|
+
default: true,
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
// Step 7: i18n functions
|
|
55
68
|
const { i18nFns } = await inquirer.prompt([
|
|
56
69
|
{
|
|
57
|
-
type:
|
|
58
|
-
name:
|
|
59
|
-
message: chalk.green(
|
|
60
|
-
choices: [
|
|
61
|
-
default: [
|
|
70
|
+
type: "checkbox",
|
|
71
|
+
name: "i18nFns",
|
|
72
|
+
message: chalk.green("Step 7: Select i18n functions to match"),
|
|
73
|
+
choices: ["t", "$t", "i18n.t"],
|
|
74
|
+
default: ["t", "$t", "i18n.t"],
|
|
62
75
|
},
|
|
63
76
|
]);
|
|
64
|
-
//
|
|
77
|
+
// Step 8: support Chinese key
|
|
65
78
|
const { supportChineseKey } = await inquirer.prompt([
|
|
66
79
|
{
|
|
67
|
-
type:
|
|
68
|
-
name:
|
|
69
|
-
message: chalk.green(
|
|
80
|
+
type: "confirm",
|
|
81
|
+
name: "supportChineseKey",
|
|
82
|
+
message: chalk.green("Step 8: Support Chinese keys?"),
|
|
70
83
|
default: true,
|
|
71
84
|
},
|
|
72
85
|
]);
|
|
73
|
-
//
|
|
86
|
+
// Step 9: languages
|
|
74
87
|
const { languages } = await inquirer.prompt([
|
|
75
88
|
{
|
|
76
|
-
type:
|
|
77
|
-
name:
|
|
78
|
-
message: chalk.green(
|
|
79
|
-
default:
|
|
80
|
-
filter: (input) => input
|
|
89
|
+
type: "input",
|
|
90
|
+
name: "languages",
|
|
91
|
+
message: chalk.green("Step 9: Enter languages to generate (comma separated)"),
|
|
92
|
+
default: "zh-CN,en-US",
|
|
93
|
+
filter: (input) => input
|
|
94
|
+
.split(",")
|
|
95
|
+
.map((lang) => lang.trim())
|
|
96
|
+
.filter(Boolean),
|
|
81
97
|
},
|
|
82
98
|
]);
|
|
83
|
-
//
|
|
84
|
-
const
|
|
99
|
+
// Step 10: generate config content
|
|
100
|
+
const configObject = {
|
|
85
101
|
entryDirs,
|
|
86
102
|
extensions,
|
|
87
103
|
output,
|
|
88
104
|
outputDir,
|
|
89
105
|
withFileComment,
|
|
106
|
+
withKeyFileComment,
|
|
90
107
|
i18nFns,
|
|
91
108
|
supportChineseKey,
|
|
92
109
|
languages,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
};
|
|
111
|
+
const configContent = `export default ${JSON.stringify(configObject, null, 2)};\n`;
|
|
112
|
+
// Step 11: write config file
|
|
113
|
+
const filePath = path.resolve(process.cwd(), "i18n.config.ts");
|
|
114
|
+
fs.writeFileSync(filePath, configContent, "utf-8");
|
|
115
|
+
console.log(chalk.yellow.bold(`\n✅ i18n.config.ts created successfully`));
|
|
116
|
+
console.log(chalk.gray(`📄 Path: ${filePath}\n`));
|
|
98
117
|
};
|
package/dist/src/utils/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import figlet from "figlet";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import dotenv from "dotenv";
|
|
3
6
|
const getTimestamp = () => new Date().toLocaleString();
|
|
4
7
|
export const logger = {
|
|
5
8
|
info: (...args) => console.log(chalk.blue(`[INFO] [${getTimestamp()}]`), ...args),
|
|
@@ -22,3 +25,19 @@ export const pickKeys = (keys) => {
|
|
|
22
25
|
});
|
|
23
26
|
return Array.from(result);
|
|
24
27
|
};
|
|
28
|
+
export function loadEnv() {
|
|
29
|
+
const cwd = process.cwd();
|
|
30
|
+
const candidates = [
|
|
31
|
+
path.resolve(cwd, "env/.env.local"),
|
|
32
|
+
path.resolve(cwd, "env/.env"),
|
|
33
|
+
path.resolve(cwd, ".env.local"),
|
|
34
|
+
path.resolve(cwd, ".env"),
|
|
35
|
+
];
|
|
36
|
+
for (const filePath of candidates) {
|
|
37
|
+
if (fs.existsSync(filePath)) {
|
|
38
|
+
dotenv.config({ path: filePath });
|
|
39
|
+
return filePath;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
package/dist/src/writeFile.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
/**
|
|
5
5
|
* 扁平 key => 嵌套对象
|
|
6
|
+
* user.avatar => { user: { avatar: ... } }
|
|
6
7
|
*/
|
|
7
8
|
function flatToNested(flat) {
|
|
8
9
|
const nested = {};
|
|
@@ -38,18 +39,21 @@ function buildTopLevelFileMap(keyFileMap) {
|
|
|
38
39
|
}
|
|
39
40
|
/**
|
|
40
41
|
* 递归生成对象字符串
|
|
41
|
-
*
|
|
42
|
+
* - 只在顶层输出 key → 文件路径注释
|
|
43
|
+
* - 是否输出完全由配置控制
|
|
42
44
|
*/
|
|
43
45
|
function stringifyObject(obj, options) {
|
|
44
|
-
const { indent, level, isJSON, keyFileMap, supportChineseKey } = options;
|
|
45
|
-
const pad = " ".repeat(indent * level);
|
|
46
|
+
const { indent, level, isJSON, keyFileMap, supportChineseKey, withKeyFileComment, } = options;
|
|
46
47
|
const nextPad = " ".repeat(indent * (level + 1));
|
|
47
48
|
const lines = [];
|
|
48
49
|
const entries = Object.entries(obj);
|
|
49
50
|
entries.forEach(([key, value], index) => {
|
|
50
51
|
const isTopLevel = level === 0;
|
|
51
|
-
//
|
|
52
|
-
if (!isJSON &&
|
|
52
|
+
// 顶层 key → 来源文件注释
|
|
53
|
+
if (!isJSON &&
|
|
54
|
+
isTopLevel &&
|
|
55
|
+
withKeyFileComment &&
|
|
56
|
+
keyFileMap) {
|
|
53
57
|
const files = keyFileMap.get(key);
|
|
54
58
|
if (files && files.size > 0) {
|
|
55
59
|
lines.push("");
|
|
@@ -58,7 +62,9 @@ function stringifyObject(obj, options) {
|
|
|
58
62
|
});
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
|
-
const keyStr = supportChineseKey && /[\u4e00-\u9fa5]/.test(key)
|
|
65
|
+
const keyStr = supportChineseKey && /[\u4e00-\u9fa5]/.test(key)
|
|
66
|
+
? `'${key}'`
|
|
67
|
+
: key;
|
|
62
68
|
if (typeof value === "object" && value !== null) {
|
|
63
69
|
lines.push(`${nextPad}${keyStr}: {`);
|
|
64
70
|
lines.push(stringifyObject(value, {
|
|
@@ -92,7 +98,7 @@ export async function writeI18nFiles(translateResult, keyFileMap, config, logger
|
|
|
92
98
|
});
|
|
93
99
|
const nested = flatToNested(flat);
|
|
94
100
|
const lines = [];
|
|
95
|
-
//
|
|
101
|
+
// 文件头注释(严格受控)
|
|
96
102
|
if (config.withFileComment && !isJSON) {
|
|
97
103
|
lines.push("/**", ` * I18n locale file: ${lang}`, ` * Auto-generated by auto-i18n`, ` * Generated at: ${new Date().toLocaleString()}`, " */");
|
|
98
104
|
}
|
|
@@ -107,6 +113,7 @@ export async function writeI18nFiles(translateResult, keyFileMap, config, logger
|
|
|
107
113
|
isJSON,
|
|
108
114
|
keyFileMap: topLevelFileMap,
|
|
109
115
|
supportChineseKey: config.supportChineseKey,
|
|
116
|
+
withKeyFileComment: config.withKeyFileComment,
|
|
110
117
|
}));
|
|
111
118
|
lines.push("};");
|
|
112
119
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18n-a11y",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CLI tool for generating i18n files",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,18 +10,23 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"build": "tsc",
|
|
12
12
|
"i18n:build": "node dist/src/cli.js scan",
|
|
13
|
-
"i18n:local": "
|
|
14
|
-
"prepublishOnly": "npm run build"
|
|
13
|
+
"i18n:local": "tsx src/cli.ts scan",
|
|
14
|
+
"prepublishOnly": "npm run build",
|
|
15
|
+
"patch": "pnpm version patch",
|
|
16
|
+
"publish": "npm publish --access public"
|
|
15
17
|
},
|
|
16
18
|
"keywords": [
|
|
17
19
|
"i18n",
|
|
18
|
-
"cli",
|
|
20
|
+
"cli",
|
|
19
21
|
"typescript"
|
|
20
22
|
],
|
|
21
23
|
"repository": {
|
|
22
24
|
"type": "git",
|
|
23
25
|
"url": "https://github.com/flylea/auto-i18n.git"
|
|
24
26
|
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
25
30
|
"author": "hi@flylea.com",
|
|
26
31
|
"license": "MIT",
|
|
27
32
|
"bugs": {
|
|
@@ -32,6 +37,7 @@
|
|
|
32
37
|
"@types/babel__traverse": "^7.28.0",
|
|
33
38
|
"@types/node": "^24.10.2",
|
|
34
39
|
"dotenv": "^17.2.3",
|
|
40
|
+
"tsx": "^4.21.0",
|
|
35
41
|
"typescript": "^5.9.3"
|
|
36
42
|
},
|
|
37
43
|
"dependencies": {
|
package/i18n.config.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
"entryDirs": [
|
|
3
|
-
"playground/src/views",
|
|
4
|
-
"playground/src/pages",
|
|
5
|
-
"playground/src/components"
|
|
6
|
-
],
|
|
7
|
-
"extensions": [
|
|
8
|
-
"vue",
|
|
9
|
-
"tsx",
|
|
10
|
-
"jsx"
|
|
11
|
-
],
|
|
12
|
-
"output": "ts" as "ts" | "js" | "json",
|
|
13
|
-
"outputDir": "src/i18n/locale",
|
|
14
|
-
"withFileComment": true,
|
|
15
|
-
"i18nFns": [
|
|
16
|
-
"t",
|
|
17
|
-
"$t",
|
|
18
|
-
"i18n.t"
|
|
19
|
-
],
|
|
20
|
-
"supportChineseKey": true,
|
|
21
|
-
"languages": [
|
|
22
|
-
"zh-CN",
|
|
23
|
-
"en-US"
|
|
24
|
-
]
|
|
25
|
-
};
|
package/src/ai.ts
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import OpenAI from "openai";
|
|
2
|
-
import dotenv from "dotenv";
|
|
3
|
-
import path from "path";
|
|
4
|
-
dotenv.config({ path: path.resolve(process.cwd(), ".env.local") });
|
|
5
|
-
|
|
6
|
-
const openai = new OpenAI({
|
|
7
|
-
baseURL: process.env.DEEPSEEK_API_URL,
|
|
8
|
-
apiKey: process.env.DEEPSEEK_API_KEY,
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
const rules = `
|
|
12
|
-
你是一个国际化语言包翻译助手。
|
|
13
|
-
|
|
14
|
-
输入一个 key, 例如:
|
|
15
|
-
- "email"
|
|
16
|
-
- "title"
|
|
17
|
-
- "name"
|
|
18
|
-
- "user.avatar"
|
|
19
|
-
|
|
20
|
-
根据 key 翻译成目标语言,遵循以下
|
|
21
|
-
|
|
22
|
-
规则:
|
|
23
|
-
1. 输出必须是字符串。
|
|
24
|
-
2. 只返回 target 目标语言的内容,不要解释、不要日志、不要 markdown。
|
|
25
|
-
3. 如果 key 包含. 表示嵌套对象的路径,说明是一个页面的模块,只翻译最后一级。
|
|
26
|
-
eg: "user.avatar" 只翻译 "avatar"
|
|
27
|
-
|
|
28
|
-
示例:
|
|
29
|
-
输入 translate ("title", "zh-CN")
|
|
30
|
-
|
|
31
|
-
输出:标题
|
|
32
|
-
`;
|
|
33
|
-
|
|
34
|
-
export default async function translate(key: string, target: string) {
|
|
35
|
-
const completion = await openai.chat.completions.create({
|
|
36
|
-
model: "deepseek-chat",
|
|
37
|
-
messages: [
|
|
38
|
-
{
|
|
39
|
-
role: "system",
|
|
40
|
-
content: rules,
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
role: "user",
|
|
44
|
-
content: `翻译 key: "${key}",目标语言:${target}`,
|
|
45
|
-
},
|
|
46
|
-
],
|
|
47
|
-
temperature: 0,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const content = completion.choices[0].message.content;
|
|
51
|
-
return content?.trim() || "";
|
|
52
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import fs from "node:fs/promises";
|
|
5
|
-
import { scanFiles } from "./scanFiles.js";
|
|
6
|
-
import { extractKeys } from "./extractKeys.js";
|
|
7
|
-
import translate from "./ai.js";
|
|
8
|
-
import { logger, print, pickKeys } from "./utils/index.js";
|
|
9
|
-
|
|
10
|
-
const program = new Command();
|
|
11
|
-
program.name("auto-i18n").description("I18n a11y CLI").version("1.0.0");
|
|
12
|
-
|
|
13
|
-
const onAction = async () => {
|
|
14
|
-
try {
|
|
15
|
-
await print("AUTO I18N");
|
|
16
|
-
|
|
17
|
-
const configPath = path.resolve(process.cwd(), "i18n.config.ts");
|
|
18
|
-
const { genI18nConfig } = await import("./utils/genI18nConfig.js");
|
|
19
|
-
|
|
20
|
-
if (!(await fs.access(configPath).then(() => true).catch(() => false))) {
|
|
21
|
-
await genI18nConfig();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const files = await scanFiles();
|
|
25
|
-
if (!files.length) return;
|
|
26
|
-
|
|
27
|
-
const { default: config } = await import("../i18n.config.js");
|
|
28
|
-
const translateResult: Record<string, Record<string, string>> = {};
|
|
29
|
-
const keyFileMap = new Map<string, Set<string>>();
|
|
30
|
-
|
|
31
|
-
for (const file of files) {
|
|
32
|
-
try {
|
|
33
|
-
const keys = await extractKeys(file);
|
|
34
|
-
const allKeys = pickKeys(keys);
|
|
35
|
-
|
|
36
|
-
for (const key of allKeys) {
|
|
37
|
-
if (!keyFileMap.has(key)) keyFileMap.set(key, new Set());
|
|
38
|
-
keyFileMap.get(key)?.add(file);
|
|
39
|
-
|
|
40
|
-
for (const lang of config.languages) {
|
|
41
|
-
if (!translateResult[key]) translateResult[key] = {};
|
|
42
|
-
const result = await translate(key, lang);
|
|
43
|
-
translateResult[key][lang] = result;
|
|
44
|
-
logger.info(`Translated "${key}" to ${lang}: ${result}`);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
} catch (err) {
|
|
48
|
-
logger.error(`Extract keys from ${file} failed: ${(err as Error).message}`);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const { writeI18nFiles } = await import("./writeFile.js");
|
|
53
|
-
await writeI18nFiles(translateResult, keyFileMap, config, logger);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
logger.error(`Execute failed: ${(err as Error).message}`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
program.command("scan").description("scan project and print i18n keys").action(onAction);
|
|
61
|
-
program.parse(process.argv);
|
package/src/extractKeys.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
2
|
-
import { parse } from "@babel/parser";
|
|
3
|
-
import traverseModule from "@babel/traverse";
|
|
4
|
-
import { parse as parseVue } from "@vue/compiler-sfc";
|
|
5
|
-
import { baseParse as parseTemplate } from "@vue/compiler-dom";
|
|
6
|
-
import type { CallExpression } from "@babel/types";
|
|
7
|
-
import type { NodePath } from '@babel/traverse';
|
|
8
|
-
|
|
9
|
-
const traverse = traverseModule.default;
|
|
10
|
-
|
|
11
|
-
export function extractKeysFromCode(code: string, i18nFns: string[]) {
|
|
12
|
-
const keys = new Set<string>();
|
|
13
|
-
let ast;
|
|
14
|
-
try {
|
|
15
|
-
ast = parse(code, {
|
|
16
|
-
sourceType: "unambiguous",
|
|
17
|
-
plugins: ["typescript", "jsx"]
|
|
18
|
-
});
|
|
19
|
-
} catch {
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
traverse(ast, {
|
|
24
|
-
CallExpression(path: NodePath<CallExpression>) {
|
|
25
|
-
const callee = path.node.callee;
|
|
26
|
-
if (
|
|
27
|
-
(callee.type === "Identifier" && i18nFns.includes(callee.name)) ||
|
|
28
|
-
(callee.type === "MemberExpression" &&
|
|
29
|
-
callee.property &&
|
|
30
|
-
"name" in callee.property &&
|
|
31
|
-
i18nFns.includes(callee.property.name))
|
|
32
|
-
) {
|
|
33
|
-
const arg = path.node.arguments[0];
|
|
34
|
-
if (arg?.type === "StringLiteral") keys.add(arg.value);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
return Array.from(keys);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function extractKeysFromVueTemplate(template: string, i18nFns: string[]) {
|
|
43
|
-
const keys = new Set<string>();
|
|
44
|
-
const ast = parseTemplate(template);
|
|
45
|
-
|
|
46
|
-
function walk(node: any) {
|
|
47
|
-
if (!node || typeof node !== "object") return;
|
|
48
|
-
|
|
49
|
-
if (node.type === 5 && node.content?.content) {
|
|
50
|
-
const expr = node.content.content;
|
|
51
|
-
try {
|
|
52
|
-
const exprAst = parse(expr, {
|
|
53
|
-
sourceType: "unambiguous",
|
|
54
|
-
plugins: ["typescript"]
|
|
55
|
-
});
|
|
56
|
-
traverse(exprAst, {
|
|
57
|
-
CallExpression(path: NodePath<CallExpression>) {
|
|
58
|
-
const callee = path.node.callee;
|
|
59
|
-
if (
|
|
60
|
-
(callee.type === "Identifier" && i18nFns.includes(callee.name)) ||
|
|
61
|
-
(callee.type === "MemberExpression" &&
|
|
62
|
-
callee.property &&
|
|
63
|
-
"name" in callee.property &&
|
|
64
|
-
i18nFns.includes(callee.property.name))
|
|
65
|
-
) {
|
|
66
|
-
const arg = path.node.arguments[0];
|
|
67
|
-
if (arg?.type === "StringLiteral") keys.add(arg.value);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
} catch {}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
Object.values(node).forEach((child) => {
|
|
75
|
-
if (child && typeof child === "object") walk(child);
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
walk(ast);
|
|
80
|
-
return Array.from(keys);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function extractKeys(file: string) {
|
|
84
|
-
let config: any;
|
|
85
|
-
try {
|
|
86
|
-
const imported = await import("../i18n.config.js");
|
|
87
|
-
config = imported.default || imported;
|
|
88
|
-
} catch (err: any) {
|
|
89
|
-
throw new Error(`load i18n.config.ts failed: ${err.message}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const i18nFns: string[] = config.i18nFns || ["t", "$t"];
|
|
93
|
-
const content = await readFile(file, "utf-8");
|
|
94
|
-
const keys = new Set<string>();
|
|
95
|
-
|
|
96
|
-
if (/\.(js|ts|jsx|tsx)$/.test(file)) {
|
|
97
|
-
extractKeysFromCode(content, i18nFns).forEach((k) => keys.add(k));
|
|
98
|
-
} else if (/\.vue$/.test(file)) {
|
|
99
|
-
const sfc = parseVue(content);
|
|
100
|
-
const script = sfc.descriptor.script?.content || "";
|
|
101
|
-
const scriptSetup = sfc.descriptor.scriptSetup?.content || "";
|
|
102
|
-
const template = sfc.descriptor.template?.content || "";
|
|
103
|
-
|
|
104
|
-
extractKeysFromCode(script, i18nFns).forEach((k) => keys.add(k));
|
|
105
|
-
extractKeysFromCode(scriptSetup, i18nFns).forEach((k) => keys.add(k));
|
|
106
|
-
extractKeysFromVueTemplate(template, i18nFns).forEach((k) => keys.add(k));
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return Array.from(keys);
|
|
110
|
-
}
|
package/src/scanFiles.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import fg from "fast-glob";
|
|
2
|
-
|
|
3
|
-
export async function scanFiles() {
|
|
4
|
-
|
|
5
|
-
let config;
|
|
6
|
-
try {
|
|
7
|
-
const imported = await import("../i18n.config.js");
|
|
8
|
-
config = imported.default;
|
|
9
|
-
} catch (err) {
|
|
10
|
-
throw new Error("i18n.config.ts not exists or format error");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const { entryDirs, extensions } = config;
|
|
14
|
-
if (!entryDirs || !extensions) {
|
|
15
|
-
throw new Error("i18n.config.ts missing entryDirs/extensions config");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const files = await fg(
|
|
19
|
-
entryDirs.map((dir: string) => `${dir}/**/*.{${extensions.join(",")}}`)
|
|
20
|
-
);
|
|
21
|
-
return files;
|
|
22
|
-
}
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import inquirer from 'inquirer';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
|
|
6
|
-
export const genI18nConfig = async () => {
|
|
7
|
-
// step 1:scan entry directories
|
|
8
|
-
const { entryDirs } = await inquirer.prompt([
|
|
9
|
-
{
|
|
10
|
-
type: 'input',
|
|
11
|
-
name: 'entryDirs',
|
|
12
|
-
message: chalk.green('Step 1: Enter entry directories (comma separated)'),
|
|
13
|
-
default: 'src/views,src/pages,src/components',
|
|
14
|
-
filter: (input: string) => input.split(',').map(dir => dir.trim()),
|
|
15
|
-
},
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
// step 2:scan file extensions
|
|
19
|
-
const { extensions } = await inquirer.prompt([
|
|
20
|
-
{
|
|
21
|
-
type: 'checkbox',
|
|
22
|
-
name: 'extensions',
|
|
23
|
-
message: chalk.green('Step 2: Select file extensions to scan'),
|
|
24
|
-
choices: ['vue', 'tsx', 'jsx', 'ts', 'js'],
|
|
25
|
-
default: ['vue', 'tsx', 'jsx'],
|
|
26
|
-
},
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
// step 3:output format
|
|
30
|
-
const { output } = await inquirer.prompt([
|
|
31
|
-
{
|
|
32
|
-
type: 'list',
|
|
33
|
-
name: 'output',
|
|
34
|
-
message: chalk.green('Step 3: Select output format'),
|
|
35
|
-
choices: ['ts', 'js', 'json'],
|
|
36
|
-
default: 'ts',
|
|
37
|
-
},
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
// step 4:output language directory
|
|
41
|
-
const { outputDir } = await inquirer.prompt([
|
|
42
|
-
{
|
|
43
|
-
type: 'input',
|
|
44
|
-
name: 'outputDir',
|
|
45
|
-
message: chalk.green('Step 4: Enter output language directory'),
|
|
46
|
-
default: 'src/i18n/locale',
|
|
47
|
-
},
|
|
48
|
-
]);
|
|
49
|
-
|
|
50
|
-
// step 5:include file comment (source info)?
|
|
51
|
-
const { withFileComment } = await inquirer.prompt([
|
|
52
|
-
{
|
|
53
|
-
type: 'confirm',
|
|
54
|
-
name: 'withFileComment',
|
|
55
|
-
message: chalk.green('Step 5: Include file comment (source info)?'),
|
|
56
|
-
default: true,
|
|
57
|
-
},
|
|
58
|
-
]);
|
|
59
|
-
|
|
60
|
-
// step 6:i18n functions to match
|
|
61
|
-
const { i18nFns } = await inquirer.prompt([
|
|
62
|
-
{
|
|
63
|
-
type: 'checkbox',
|
|
64
|
-
name: 'i18nFns',
|
|
65
|
-
message: chalk.green('Step 6: Select i18n functions to match'),
|
|
66
|
-
choices: ['t', '$t', 'i18n.t'],
|
|
67
|
-
default: ['t', '$t', 'i18n.t'],
|
|
68
|
-
},
|
|
69
|
-
]);
|
|
70
|
-
|
|
71
|
-
// step 7:support chinese keys?
|
|
72
|
-
const { supportChineseKey } = await inquirer.prompt([
|
|
73
|
-
{
|
|
74
|
-
type: 'confirm',
|
|
75
|
-
name: 'supportChineseKey',
|
|
76
|
-
message: chalk.green('Step 7: Support Chinese keys?'),
|
|
77
|
-
default: true,
|
|
78
|
-
},
|
|
79
|
-
]);
|
|
80
|
-
// step 8:language packages to generate
|
|
81
|
-
const { languages } = await inquirer.prompt([
|
|
82
|
-
{
|
|
83
|
-
type: 'input',
|
|
84
|
-
name: 'languages',
|
|
85
|
-
message: chalk.green('Step 8: Enter languages to generate (comma separated)'),
|
|
86
|
-
default: 'zh-CN,en-US',
|
|
87
|
-
filter: (input: string) => input.split(',').map(lang => lang.trim()).filter(Boolean),
|
|
88
|
-
},
|
|
89
|
-
]);
|
|
90
|
-
|
|
91
|
-
// step 9:generate config object
|
|
92
|
-
const configContent = `export default ${JSON.stringify(
|
|
93
|
-
{
|
|
94
|
-
entryDirs,
|
|
95
|
-
extensions,
|
|
96
|
-
output,
|
|
97
|
-
outputDir,
|
|
98
|
-
withFileComment,
|
|
99
|
-
i18nFns,
|
|
100
|
-
supportChineseKey,
|
|
101
|
-
languages,
|
|
102
|
-
},
|
|
103
|
-
null,
|
|
104
|
-
2
|
|
105
|
-
)};\n`;
|
|
106
|
-
|
|
107
|
-
// step 9:write config file
|
|
108
|
-
const filePath = path.resolve(process.cwd(), 'i18n.config.ts');
|
|
109
|
-
fs.writeFileSync(filePath, configContent, 'utf-8');
|
|
110
|
-
|
|
111
|
-
console.log(chalk.yellow.bold(`\n🎉i18n.config.ts created successfully at ${filePath}\n`));
|
|
112
|
-
};
|
package/src/utils/index.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import figlet from "figlet";
|
|
3
|
-
|
|
4
|
-
const getTimestamp = () => new Date().toLocaleString();
|
|
5
|
-
|
|
6
|
-
export const logger = {
|
|
7
|
-
info: (...args: unknown[]) =>
|
|
8
|
-
console.log(chalk.blue(`[INFO] [${getTimestamp()}]`), ...args),
|
|
9
|
-
success: (...args: unknown[]) =>
|
|
10
|
-
console.log(chalk.green(`[SUCCESS] [${getTimestamp()}]`), ...args),
|
|
11
|
-
warn: (...args: unknown[]) =>
|
|
12
|
-
console.log(chalk.yellow(`[WARN] [${getTimestamp()}]`), ...args),
|
|
13
|
-
error: (...args: unknown[]) =>
|
|
14
|
-
console.error(chalk.red(`[ERROR] [${getTimestamp()}]`), ...args),
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const print = async (message:string) => {
|
|
18
|
-
const text = figlet.textSync(message);
|
|
19
|
-
return await console.log(text)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export const pickKeys = (keys: string[] | string) => {
|
|
23
|
-
const keyList = typeof keys === "string" ? [keys] : keys || [];
|
|
24
|
-
const result = new Set<string>();
|
|
25
|
-
|
|
26
|
-
keyList.forEach((rawKey) => {
|
|
27
|
-
const cleanKey = (rawKey || "").trim();
|
|
28
|
-
if (!cleanKey) return;
|
|
29
|
-
|
|
30
|
-
result.add(cleanKey);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
return Array.from(result);
|
|
34
|
-
};
|
|
35
|
-
|
package/src/utils/progress.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import cliProgress from "cli-progress";
|
|
2
|
-
import colors from "colors";
|
|
3
|
-
|
|
4
|
-
let bar: cliProgress.SingleBar | null = null;
|
|
5
|
-
|
|
6
|
-
export function progressStart(total: number) {
|
|
7
|
-
if (bar) return;
|
|
8
|
-
|
|
9
|
-
bar = new cliProgress.SingleBar(
|
|
10
|
-
{
|
|
11
|
-
format:
|
|
12
|
-
colors.cyan("Progress") +
|
|
13
|
-
" |" +
|
|
14
|
-
colors.green("{bar}") +
|
|
15
|
-
"| " +
|
|
16
|
-
colors.yellow("{percentage}%"),
|
|
17
|
-
barCompleteChar: "█",
|
|
18
|
-
barIncompleteChar: "░",
|
|
19
|
-
hideCursor: true,
|
|
20
|
-
barsize: 30,
|
|
21
|
-
},
|
|
22
|
-
cliProgress.Presets.shades_classic
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
bar.start(total, 0);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function progressInc(step = 1) {
|
|
29
|
-
if (!bar) return;
|
|
30
|
-
bar.increment(step);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function progressStop() {
|
|
34
|
-
if (!bar) return;
|
|
35
|
-
bar.stop();
|
|
36
|
-
bar = null;
|
|
37
|
-
}
|
package/src/writeFile.ts
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
|
|
5
|
-
export interface I18nConfig {
|
|
6
|
-
output: "ts" | "js" | "json";
|
|
7
|
-
outputDir: string;
|
|
8
|
-
withFileComment: boolean;
|
|
9
|
-
supportChineseKey: boolean;
|
|
10
|
-
languages: string[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* 扁平 key => 嵌套对象
|
|
15
|
-
*/
|
|
16
|
-
function flatToNested(flat: Record<string, string>) {
|
|
17
|
-
const nested: Record<string, any> = {};
|
|
18
|
-
|
|
19
|
-
for (const [fullKey, value] of Object.entries(flat)) {
|
|
20
|
-
const segments = fullKey.split(".");
|
|
21
|
-
let cur = nested;
|
|
22
|
-
|
|
23
|
-
segments.forEach((seg, idx) => {
|
|
24
|
-
if (idx === segments.length - 1) {
|
|
25
|
-
cur[seg] = value;
|
|
26
|
-
} else {
|
|
27
|
-
cur[seg] ||= {};
|
|
28
|
-
cur = cur[seg];
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return nested;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 构建「顶层 key -> 文件路径集合」映射
|
|
38
|
-
* user.avatar -> user
|
|
39
|
-
*/
|
|
40
|
-
function buildTopLevelFileMap(
|
|
41
|
-
keyFileMap: Map<string, Set<string>>
|
|
42
|
-
): Map<string, Set<string>> {
|
|
43
|
-
const result = new Map<string, Set<string>>();
|
|
44
|
-
|
|
45
|
-
for (const [fullKey, files] of keyFileMap.entries()) {
|
|
46
|
-
const topKey = fullKey.split(".")[0];
|
|
47
|
-
|
|
48
|
-
if (!result.has(topKey)) {
|
|
49
|
-
result.set(topKey, new Set());
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
files.forEach((f) => result.get(topKey)!.add(f));
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 递归生成对象字符串
|
|
60
|
-
* 注释只出现在顶层
|
|
61
|
-
*/
|
|
62
|
-
function stringifyObject(
|
|
63
|
-
obj: Record<string, any>,
|
|
64
|
-
options: {
|
|
65
|
-
indent: number;
|
|
66
|
-
level: number;
|
|
67
|
-
isJSON: boolean;
|
|
68
|
-
keyFileMap?: Map<string, Set<string>>;
|
|
69
|
-
supportChineseKey: boolean;
|
|
70
|
-
}
|
|
71
|
-
): string {
|
|
72
|
-
const { indent, level, isJSON, keyFileMap, supportChineseKey } = options;
|
|
73
|
-
const pad = " ".repeat(indent * level);
|
|
74
|
-
const nextPad = " ".repeat(indent * (level + 1));
|
|
75
|
-
|
|
76
|
-
const lines: string[] = [];
|
|
77
|
-
const entries = Object.entries(obj);
|
|
78
|
-
|
|
79
|
-
entries.forEach(([key, value], index) => {
|
|
80
|
-
const isTopLevel = level === 0;
|
|
81
|
-
|
|
82
|
-
// 顶层路径注释(非 json)
|
|
83
|
-
if (!isJSON && isTopLevel && keyFileMap) {
|
|
84
|
-
const files = keyFileMap.get(key);
|
|
85
|
-
if (files && files.size > 0) {
|
|
86
|
-
lines.push("");
|
|
87
|
-
files.forEach((file) => {
|
|
88
|
-
lines.push(`${nextPad}// ${file}`);
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const keyStr =
|
|
94
|
-
supportChineseKey && /[\u4e00-\u9fa5]/.test(key) ? `'${key}'` : key;
|
|
95
|
-
|
|
96
|
-
if (typeof value === "object" && value !== null) {
|
|
97
|
-
lines.push(`${nextPad}${keyStr}: {`);
|
|
98
|
-
lines.push(
|
|
99
|
-
stringifyObject(value, {
|
|
100
|
-
...options,
|
|
101
|
-
level: level + 1,
|
|
102
|
-
})
|
|
103
|
-
);
|
|
104
|
-
lines.push(
|
|
105
|
-
`${nextPad}}${index < entries.length - 1 ? "," : ""}`
|
|
106
|
-
);
|
|
107
|
-
} else {
|
|
108
|
-
const val =
|
|
109
|
-
typeof value === "string"
|
|
110
|
-
? `'${value.replace(/'/g, "\\'")}'`
|
|
111
|
-
: JSON.stringify(value);
|
|
112
|
-
|
|
113
|
-
lines.push(
|
|
114
|
-
`${nextPad}${keyStr}: ${val}${
|
|
115
|
-
index < entries.length - 1 ? "," : ""
|
|
116
|
-
}`
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
return lines.join("\n");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 写入 i18n 文件(最终入口)
|
|
126
|
-
*/
|
|
127
|
-
export async function writeI18nFiles(
|
|
128
|
-
translateResult: Record<string, Record<string, string>>,
|
|
129
|
-
keyFileMap: Map<string, Set<string>>,
|
|
130
|
-
config: I18nConfig,
|
|
131
|
-
logger: any
|
|
132
|
-
) {
|
|
133
|
-
const outputDir = path.resolve(process.cwd(), config.outputDir);
|
|
134
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
135
|
-
|
|
136
|
-
const isJSON = config.output === "json";
|
|
137
|
-
const topLevelFileMap = buildTopLevelFileMap(keyFileMap);
|
|
138
|
-
|
|
139
|
-
for (const lang of config.languages) {
|
|
140
|
-
const flat: Record<string, string> = {};
|
|
141
|
-
|
|
142
|
-
Object.entries(translateResult).forEach(([key, map]) => {
|
|
143
|
-
if (map[lang]) flat[key] = map[lang];
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
const nested = flatToNested(flat);
|
|
147
|
-
const lines: string[] = [];
|
|
148
|
-
|
|
149
|
-
// 文件头注释
|
|
150
|
-
if (config.withFileComment && !isJSON) {
|
|
151
|
-
lines.push(
|
|
152
|
-
"/**",
|
|
153
|
-
` * I18n locale file: ${lang}`,
|
|
154
|
-
` * Auto-generated by auto-i18n`,
|
|
155
|
-
` * Generated at: ${new Date().toLocaleString()}`,
|
|
156
|
-
" */"
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (isJSON) {
|
|
161
|
-
lines.push(JSON.stringify(nested, null, 2));
|
|
162
|
-
} else {
|
|
163
|
-
lines.push("export default {");
|
|
164
|
-
lines.push(
|
|
165
|
-
stringifyObject(nested, {
|
|
166
|
-
indent: 2,
|
|
167
|
-
level: 0,
|
|
168
|
-
isJSON,
|
|
169
|
-
keyFileMap: topLevelFileMap,
|
|
170
|
-
supportChineseKey: config.supportChineseKey,
|
|
171
|
-
})
|
|
172
|
-
);
|
|
173
|
-
lines.push("};");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const filePath = path.join(outputDir, `${lang}.${config.output}`);
|
|
177
|
-
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
|
|
178
|
-
|
|
179
|
-
logger.success(
|
|
180
|
-
`Generated ${chalk.yellow(`${lang}.${config.output}`)}`
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "nodenext",
|
|
5
|
-
"moduleResolution": "nodenext",
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"rootDir": ".",
|
|
8
|
-
"strict": true,
|
|
9
|
-
"esModuleInterop": true,
|
|
10
|
-
"allowSyntheticDefaultImports": true,
|
|
11
|
-
"forceConsistentCasingInFileNames": true
|
|
12
|
-
},
|
|
13
|
-
"include": ["src/**/*"]
|
|
14
|
-
}
|