i18n-a11y 1.0.1 → 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.
@@ -1,25 +1,11 @@
1
1
  export default {
2
- "entryDirs": [
3
- "/src/views",
4
- "/src/pages",
5
- "/src/components"
6
- ],
7
- "extensions": [
8
- "vue",
9
- "tsx",
10
- "jsx"
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,3 +1,5 @@
1
+ import { loadEnv } from "./utils/index.js";
2
+ loadEnv();
1
3
  import OpenAI from "openai";
2
4
  const openai = new OpenAI({
3
5
  baseURL: process.env.DEEPSEEK_API_URL,
@@ -25,7 +27,7 @@ const rules = `
25
27
 
26
28
  输出:标题
27
29
  `;
28
- export default async function translate(key, target) {
30
+ const translate = async (key, target) => {
29
31
  const completion = await openai.chat.completions.create({
30
32
  model: "deepseek-chat",
31
33
  messages: [
@@ -42,4 +44,5 @@ export default async function translate(key, target) {
42
44
  });
43
45
  const content = completion.choices[0].message.content;
44
46
  return content?.trim() || "";
45
- }
47
+ };
48
+ export default translate;
package/dist/src/cli.js CHANGED
@@ -2,16 +2,12 @@
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, loadEnv } from "./utils/index.js";
6
- loadEnv();
5
+ import { logger, print, pickKeys } from "./utils/index.js";
7
6
  import { scanFiles } from "./scanFiles.js";
8
7
  import { extractKeys } from "./extractKeys.js";
9
8
  import translate from "./ai.js";
10
9
  const program = new Command();
11
- program
12
- .name("auto-i18n")
13
- .description("I18n a11y CLI")
14
- .version("1.0.0");
10
+ program.name("auto-i18n").description("I18n a11y CLI").version("1.0.0");
15
11
  const onAction = async () => {
16
12
  try {
17
13
  await print("AUTO I18N");
@@ -25,8 +21,10 @@ const onAction = async () => {
25
21
  await genI18nConfig();
26
22
  }
27
23
  const files = await scanFiles();
28
- if (!files.length)
24
+ if (!files.length) {
25
+ logger.warn("No files found, please check i18n.config.js entryDirs and extensions");
29
26
  return;
27
+ }
30
28
  const { default: config } = await import("../i18n.config.js");
31
29
  const translateResult = {};
32
30
  const keyFileMap = new Map();
@@ -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 function extractKeysFromCode(code, i18nFns) {
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 function extractKeysFromVueTemplate(template, i18nFns) {
34
+ };
35
+ export const extractKeysFromVueTemplate = (template, i18nFns) => {
36
36
  const keys = new Set();
37
37
  const ast = parseTemplate(template);
38
- function walk(node) {
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 function extractKeys(file) {
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
+ };
@@ -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
- export async function scanFiles() {
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 (err) {
9
- throw new Error("i18n.config.ts not exists or format error");
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.ts missing entryDirs/extensions config");
16
+ throw new Error("i18n.config.js missing entryDirs/extensions config");
14
17
  }
15
- const files = await fg(entryDirs.map((dir) => `${dir}/**/*.{${extensions.join(",")}}`));
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 'fs';
2
- import path from 'path';
3
- import inquirer from 'inquirer';
4
- import chalk from 'chalk';
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
- // step 1:scan entry directories
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: 'input',
10
- name: 'entryDirs',
11
- message: chalk.green('Step 1: Enter entry directories (comma separated)'),
12
- default: 'src/views,src/pages,src/components',
13
- filter: (input) => input.split(',').map(dir => dir.trim()),
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
- // step 2:scan file extensions
20
+ // Step 2: file extensions
17
21
  const { extensions } = await inquirer.prompt([
18
22
  {
19
- type: 'checkbox',
20
- name: 'extensions',
21
- message: chalk.green('Step 2: Select file extensions to scan'),
22
- choices: ['vue', 'tsx', 'jsx', 'ts', 'js'],
23
- default: ['vue', 'tsx', 'jsx'],
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
- // step 3output format
30
+ // Step 3: output format
27
31
  const { output } = await inquirer.prompt([
28
32
  {
29
- type: 'list',
30
- name: 'output',
31
- message: chalk.green('Step 3: Select output format'),
32
- choices: ['ts', 'js', 'json'],
33
- default: 'ts',
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
- // step 4output language directory
40
+ // Step 4: output directory
37
41
  const { outputDir } = await inquirer.prompt([
38
42
  {
39
- type: 'input',
40
- name: 'outputDir',
41
- message: chalk.green('Step 4: Enter output language directory'),
42
- default: 'src/i18n/locale',
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
- // step 5:include file comment (source info)?
49
+ // Step 5: file header comment
46
50
  const { withFileComment } = await inquirer.prompt([
47
51
  {
48
- type: 'confirm',
49
- name: 'withFileComment',
50
- message: chalk.green('Step 5: Include file comment (source info)?'),
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
- // step 6:i18n functions to match
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: 'checkbox',
58
- name: 'i18nFns',
59
- message: chalk.green('Step 6: Select i18n functions to match'),
60
- choices: ['t', '$t', 'i18n.t'],
61
- default: ['t', '$t', 'i18n.t'],
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
- // step 7:support chinese keys?
77
+ // Step 8: support Chinese key
65
78
  const { supportChineseKey } = await inquirer.prompt([
66
79
  {
67
- type: 'confirm',
68
- name: 'supportChineseKey',
69
- message: chalk.green('Step 7: Support Chinese keys?'),
80
+ type: "confirm",
81
+ name: "supportChineseKey",
82
+ message: chalk.green("Step 8: Support Chinese keys?"),
70
83
  default: true,
71
84
  },
72
85
  ]);
73
- // step 8:language packages to generate
86
+ // Step 9: languages
74
87
  const { languages } = await inquirer.prompt([
75
88
  {
76
- type: 'input',
77
- name: 'languages',
78
- message: chalk.green('Step 8: Enter languages to generate (comma separated)'),
79
- default: 'zh-CN,en-US',
80
- filter: (input) => input.split(',').map(lang => lang.trim()).filter(Boolean),
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
- // step 9:generate config object
84
- const configContent = `export default ${JSON.stringify({
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
- }, null, 2)};\n`;
94
- // step 9:write config file
95
- const filePath = path.resolve(process.cwd(), 'i18n.config.ts');
96
- fs.writeFileSync(filePath, configContent, 'utf-8');
97
- console.log(chalk.yellow.bold(`\n🎉i18n.config.ts created successfully at ${filePath}\n`));
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
  };
@@ -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,9 +25,6 @@ export const pickKeys = (keys) => {
22
25
  });
23
26
  return Array.from(result);
24
27
  };
25
- import fs from "fs";
26
- import path from "path";
27
- import dotenv from "dotenv";
28
28
  export function loadEnv() {
29
29
  const cwd = process.cwd();
30
30
  const candidates = [
@@ -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
- // 顶层路径注释(非 json)
52
- if (!isJSON && isTopLevel && keyFileMap) {
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) ? `'${key}'` : 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,54 +1,60 @@
1
- {
2
- "name": "i18n-a11y",
3
- "version": "1.0.1",
4
- "description": "CLI tool for generating i18n files",
5
- "main": "dist/index.js",
6
- "bin": {
7
- "auto-i18n": "dist/src/cli.js"
8
- },
9
- "type": "module",
10
- "scripts": {
11
- "build": "tsc",
12
- "i18n:build": "node dist/src/cli.js scan",
13
- "i18n:local": "node src/cli.js scan",
14
- "prepublishOnly": "npm run build"
15
- },
16
- "keywords": [
17
- "i18n",
18
- "cli",
19
- "typescript"
20
- ],
21
- "repository": {
22
- "type": "git",
23
- "url": "https://github.com/flylea/auto-i18n.git"
24
- },
25
- "author": "hi@flylea.com",
26
- "license": "MIT",
27
- "bugs": {
28
- "url": "https://github.com/flylea/auto-i18n/issues"
29
- },
30
- "homepage": "https://github.com/flylea/auto-i18n#readme",
31
- "devDependencies": {
32
- "@types/babel__traverse": "^7.28.0",
33
- "@types/node": "^24.10.2",
34
- "dotenv": "^17.2.3",
35
- "typescript": "^5.9.3"
36
- },
37
- "dependencies": {
38
- "@babel/parser": "^7.28.5",
39
- "@babel/traverse": "^7.28.5",
40
- "@babel/types": "^7.28.5",
41
- "@types/cli-progress": "^3.11.6",
42
- "@vue/compiler-dom": "^3.5.25",
43
- "@vue/compiler-sfc": "^3.3.4",
44
- "chalk": "^4.1.2",
45
- "cli-progress": "^3.12.0",
46
- "colors": "^1.4.0",
47
- "commander": "^14.0.2",
48
- "fast-glob": "^3.3.3",
49
- "figlet": "^1.9.4",
50
- "inquirer": "^13.1.0",
51
- "openai": "^4.26.0",
52
- "p-limit": "^5.0.0"
53
- }
54
- }
1
+ {
2
+ "name": "i18n-a11y",
3
+ "version": "1.0.2",
4
+ "description": "CLI tool for generating i18n files",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "auto-i18n": "dist/src/cli.js"
8
+ },
9
+ "type": "module",
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "i18n:build": "node dist/src/cli.js scan",
13
+ "i18n:local": "tsx src/cli.ts scan",
14
+ "prepublishOnly": "npm run build",
15
+ "patch": "pnpm version patch",
16
+ "publish": "npm publish --access public"
17
+ },
18
+ "keywords": [
19
+ "i18n",
20
+ "cli",
21
+ "typescript"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/flylea/auto-i18n.git"
26
+ },
27
+ "files": [
28
+ "dist"
29
+ ],
30
+ "author": "hi@flylea.com",
31
+ "license": "MIT",
32
+ "bugs": {
33
+ "url": "https://github.com/flylea/auto-i18n/issues"
34
+ },
35
+ "homepage": "https://github.com/flylea/auto-i18n#readme",
36
+ "devDependencies": {
37
+ "@types/babel__traverse": "^7.28.0",
38
+ "@types/node": "^24.10.2",
39
+ "dotenv": "^17.2.3",
40
+ "tsx": "^4.21.0",
41
+ "typescript": "^5.9.3"
42
+ },
43
+ "dependencies": {
44
+ "@babel/parser": "^7.28.5",
45
+ "@babel/traverse": "^7.28.5",
46
+ "@babel/types": "^7.28.5",
47
+ "@types/cli-progress": "^3.11.6",
48
+ "@vue/compiler-dom": "^3.5.25",
49
+ "@vue/compiler-sfc": "^3.3.4",
50
+ "chalk": "^4.1.2",
51
+ "cli-progress": "^3.12.0",
52
+ "colors": "^1.4.0",
53
+ "commander": "^14.0.2",
54
+ "fast-glob": "^3.3.3",
55
+ "figlet": "^1.9.4",
56
+ "inquirer": "^13.1.0",
57
+ "openai": "^4.26.0",
58
+ "p-limit": "^5.0.0"
59
+ }
60
+ }
File without changes
package/i18n.config.ts DELETED
@@ -1,25 +0,0 @@
1
- export default {
2
- "entryDirs": [
3
- "/src/views",
4
- "/src/pages",
5
- "/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,49 +0,0 @@
1
- import OpenAI from "openai";
2
-
3
- const openai = new OpenAI({
4
- baseURL: process.env.DEEPSEEK_API_URL,
5
- apiKey: process.env.DEEPSEEK_API_KEY,
6
- });
7
-
8
- const rules = `
9
- 你是一个国际化语言包翻译助手。
10
-
11
- 输入一个 key, 例如:
12
- - "email"
13
- - "title"
14
- - "name"
15
- - "user.avatar"
16
-
17
- 根据 key 翻译成目标语言,遵循以下
18
-
19
- 规则:
20
- 1. 输出必须是字符串。
21
- 2. 只返回 target 目标语言的内容,不要解释、不要日志、不要 markdown。
22
- 3. 如果 key 包含. 表示嵌套对象的路径,说明是一个页面的模块,只翻译最后一级。
23
- eg: "user.avatar" 只翻译 "avatar"
24
-
25
- 示例:
26
- 输入 translate ("title", "zh-CN")
27
-
28
- 输出:标题
29
- `;
30
-
31
- export default async function translate(key: string, target: string) {
32
- const completion = await openai.chat.completions.create({
33
- model: "deepseek-chat",
34
- messages: [
35
- {
36
- role: "system",
37
- content: rules,
38
- },
39
- {
40
- role: "user",
41
- content: `翻译 key: "${key}",目标语言:${target}`,
42
- },
43
- ],
44
- temperature: 0,
45
- });
46
-
47
- const content = completion.choices[0].message.content;
48
- return content?.trim() || "";
49
- }
package/src/cli.ts DELETED
@@ -1,83 +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
-
6
- import { logger, print, pickKeys,loadEnv } from "./utils/index.js";
7
- loadEnv();
8
-
9
- import { scanFiles } from "./scanFiles.js";
10
- import { extractKeys } from "./extractKeys.js";
11
- import translate from "./ai.js";
12
-
13
- const program = new Command();
14
-
15
- program
16
- .name("auto-i18n")
17
- .description("I18n a11y CLI")
18
- .version("1.0.0");
19
-
20
- const onAction = async () => {
21
- try {
22
- await print("AUTO I18N");
23
-
24
- const configPath = path.resolve(process.cwd(), "i18n.config.ts");
25
- const { genI18nConfig } = await import("./utils/genI18nConfig.js");
26
-
27
- const exists = await fs
28
- .access(configPath)
29
- .then(() => true)
30
- .catch(() => false);
31
-
32
- if (!exists) {
33
- await genI18nConfig();
34
- }
35
-
36
- const files = await scanFiles();
37
- if (!files.length) return;
38
-
39
- const { default: config } = await import("../i18n.config.js");
40
-
41
- const translateResult: Record<string, Record<string, string>> = {};
42
- const keyFileMap = new Map<string, Set<string>>();
43
-
44
- for (const file of files) {
45
- try {
46
- const keys = await extractKeys(file);
47
- const allKeys = pickKeys(keys);
48
-
49
- for (const key of allKeys) {
50
- if (!keyFileMap.has(key)) {
51
- keyFileMap.set(key, new Set());
52
- }
53
- keyFileMap.get(key)!.add(file);
54
-
55
- for (const lang of config.languages) {
56
- translateResult[key] ??= {};
57
- const result = await translate(key, lang);
58
- translateResult[key][lang] = result;
59
-
60
- logger.info(`Translated "${key}" to ${lang}: ${result}`);
61
- }
62
- }
63
- } catch (err) {
64
- logger.error(
65
- `Extract keys from ${file} failed: ${(err as Error).message}`
66
- );
67
- }
68
- }
69
-
70
- const { writeI18nFiles } = await import("./writeFile.js");
71
- await writeI18nFiles(translateResult, keyFileMap, config, logger);
72
- } catch (err) {
73
- logger.error(`Execute failed: ${(err as Error).message}`);
74
- process.exit(1);
75
- }
76
- };
77
-
78
- program
79
- .command("scan")
80
- .description("scan project and generate i18n files")
81
- .action(onAction);
82
-
83
- program.parse(process.argv);
@@ -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
- };
@@ -1,60 +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
-
36
- import fs from "fs";
37
- import path from "path";
38
- import dotenv from "dotenv";
39
-
40
- export function loadEnv() {
41
- const cwd = process.cwd();
42
-
43
- const candidates = [
44
- path.resolve(cwd, "env/.env.local"),
45
- path.resolve(cwd, "env/.env"),
46
- path.resolve(cwd, ".env.local"),
47
- path.resolve(cwd, ".env"),
48
- ];
49
-
50
- for (const filePath of candidates) {
51
- if (fs.existsSync(filePath)) {
52
- dotenv.config({ path: filePath });
53
- return filePath;
54
- }
55
- }
56
-
57
- return null;
58
- }
59
-
60
-
@@ -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
- }