i18n-easy 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.
Files changed (70) hide show
  1. package/README.md +527 -0
  2. package/esm/assets/const.js +7 -0
  3. package/esm/ast_transformer.d.ts +19 -0
  4. package/esm/ast_transformer.js +54 -0
  5. package/esm/eslint/react/i18n_easy_plugin.d.ts +3 -0
  6. package/esm/eslint/react/i18n_easy_plugin.js +111 -0
  7. package/esm/eslint/react/i18n_utils.d.ts +61 -0
  8. package/esm/eslint/react/i18n_utils.js +115 -0
  9. package/esm/eslint/react/report_and_fix.d.ts +67 -0
  10. package/esm/eslint/react/report_and_fix.js +118 -0
  11. package/esm/eslint/vue/i18n_easy_plugin.d.ts +5 -0
  12. package/esm/eslint/vue/i18n_easy_plugin.js +141 -0
  13. package/esm/eslint/vue/i18n_utils.d.ts +65 -0
  14. package/esm/eslint/vue/i18n_utils.js +133 -0
  15. package/esm/eslint/vue/report_and_fix.d.ts +8 -0
  16. package/esm/eslint/vue/report_and_fix.js +169 -0
  17. package/esm/file_processor_utils.d.ts +14 -0
  18. package/esm/file_processor_utils.js +68 -0
  19. package/esm/i18n_easy.d.ts +77 -0
  20. package/esm/i18n_easy.js +171 -0
  21. package/esm/index.d.ts +4 -0
  22. package/esm/index.js +9 -0
  23. package/esm/key_map.d.ts +14 -0
  24. package/esm/key_map.js +51 -0
  25. package/esm/patch.d.ts +5 -0
  26. package/esm/progressBus.d.ts +7 -0
  27. package/esm/progressBus.js +25 -0
  28. package/esm/translate.d.ts +34 -0
  29. package/esm/translate.js +107 -0
  30. package/esm/type.d.ts +94 -0
  31. package/esm/type.js +0 -0
  32. package/esm/utils.d.ts +54 -0
  33. package/esm/utils.js +102 -0
  34. package/lib/assets/const.js +32 -0
  35. package/lib/ast_transformer.js +88 -0
  36. package/lib/cli.d.ts +2 -0
  37. package/lib/cli.js +5 -0
  38. package/lib/eslint/react/i18n_easy_plugin.js +131 -0
  39. package/lib/eslint/react/i18n_utils.js +145 -0
  40. package/lib/eslint/react/report_and_fix.js +156 -0
  41. package/lib/eslint/vue/i18n_easy_plugin.js +161 -0
  42. package/lib/eslint/vue/i18n_utils.js +164 -0
  43. package/lib/eslint/vue/report_and_fix.js +203 -0
  44. package/lib/file_processor_utils.js +109 -0
  45. package/lib/i18n_easy.js +205 -0
  46. package/lib/index.js +35 -0
  47. package/lib/key_map.js +71 -0
  48. package/lib/package.json +3 -0
  49. package/lib/patch.d.ts +5 -0
  50. package/lib/progressBus.js +50 -0
  51. package/lib/translate.js +127 -0
  52. package/lib/type.js +17 -0
  53. package/lib/utils.js +142 -0
  54. package/package.json +68 -0
  55. package/types/assets/const.d.ts +2 -0
  56. package/types/ast_transformer.d.ts +19 -0
  57. package/types/eslint/react/i18n_easy_plugin.d.ts +3 -0
  58. package/types/eslint/react/i18n_utils.d.ts +61 -0
  59. package/types/eslint/react/report_and_fix.d.ts +67 -0
  60. package/types/eslint/vue/i18n_easy_plugin.d.ts +5 -0
  61. package/types/eslint/vue/i18n_utils.d.ts +65 -0
  62. package/types/eslint/vue/report_and_fix.d.ts +8 -0
  63. package/types/file_processor_utils.d.ts +14 -0
  64. package/types/i18n_easy.d.ts +77 -0
  65. package/types/index.d.ts +4 -0
  66. package/types/key_map.d.ts +14 -0
  67. package/types/progressBus.d.ts +6 -0
  68. package/types/translate.d.ts +34 -0
  69. package/types/type.d.ts +94 -0
  70. package/types/utils.d.ts +54 -0
@@ -0,0 +1,133 @@
1
+ // src/eslint/vue/i18n_utils.ts
2
+ import { hasChineseChar } from "../../utils.js";
3
+ function createI18nIgnoreChecker(context, templateBody) {
4
+ const ignoreLines = [];
5
+ const jsComments = context.sourceCode.getAllComments();
6
+ const templateComments = templateBody ? templateBody.comments : [];
7
+ for (const comment of [...jsComments, ...templateComments]) {
8
+ if (["HTMLComment", "Line", "Block"].includes(comment.type) && comment.value.trim().includes("@i18n-ignore")) {
9
+ ignoreLines.push([comment.loc.start.line, comment.loc.end.line]);
10
+ }
11
+ }
12
+ return function isIgnored(node) {
13
+ const nodeStartLine = node.loc.start.line;
14
+ const nodeEndLine = node.loc.end.line;
15
+ if (nodeStartLine !== nodeEndLine) return false;
16
+ return ignoreLines.some(([_start, end]) => end === nodeEndLine - 1);
17
+ };
18
+ }
19
+ function getScope(context, node) {
20
+ if (node.type === "Program") {
21
+ const programScope = context.sourceCode.getScope(node);
22
+ return programScope.type == "global" ? programScope.childScopes[0] : programScope;
23
+ }
24
+ if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
25
+ return context.sourceCode.getScope(node);
26
+ }
27
+ return null;
28
+ }
29
+ function callNameNotExist(context, node, codeType) {
30
+ if (!node) return false;
31
+ const { callMethodName } = context.settings.i18nInfo[codeType] || {};
32
+ if (!callMethodName) return false;
33
+ const scope = getScope(context, node);
34
+ if (!scope) return false;
35
+ return scope.variables.some((ref) => ref.name === callMethodName) === false;
36
+ }
37
+ function getLastImportOrFirstNotImportNode(context) {
38
+ const src = context.sourceCode;
39
+ const body = src.ast.body;
40
+ let lastImport = body.filter((n) => n.type === "ImportDeclaration").pop();
41
+ if (lastImport) {
42
+ return {
43
+ node: lastImport,
44
+ type: "lastImport"
45
+ };
46
+ }
47
+ let firstNonImport = null;
48
+ const allNodes = [...body];
49
+ const commentsAsNodes = src.getAllComments();
50
+ const sorted = [...allNodes, ...commentsAsNodes].sort((a, b) => {
51
+ if (a.range && b.range) {
52
+ return a.range[0] - b.range[0];
53
+ }
54
+ return 0;
55
+ });
56
+ firstNonImport = sorted.find((n) => n.type !== "ImportDeclaration");
57
+ if (firstNonImport) {
58
+ return {
59
+ node: firstNonImport,
60
+ type: "firstNonImport"
61
+ };
62
+ }
63
+ return null;
64
+ }
65
+ function getInjectNode(context, node) {
66
+ const commentNodes = context.sourceCode.getAllComments();
67
+ const comments = commentNodes.filter((item) => {
68
+ if (item.range && node.range) {
69
+ return item.range[0] >= node.range[0] && item.range[1] <= node.range[1];
70
+ }
71
+ return false;
72
+ });
73
+ const sorted = [...comments, ...node.body].sort((a, b) => {
74
+ if (a.range && b.range) {
75
+ return a.range[0] - b.range[0];
76
+ }
77
+ return 0;
78
+ });
79
+ return sorted[0];
80
+ }
81
+ function canCollectOperateNodes(node, isIgnored) {
82
+ if (isIgnored(node)) return false;
83
+ switch (node.type) {
84
+ case "VText":
85
+ if (!hasChineseChar(node.value)) return false;
86
+ break;
87
+ case "VAttribute":
88
+ if (!node.value) return false;
89
+ if (node.value.type !== "VLiteral") return false;
90
+ if (!hasChineseChar(node.value.value)) return false;
91
+ break;
92
+ case "TemplateLiteral":
93
+ if (!node.quasis.some((q) => hasChineseChar(q.value.cooked))) return false;
94
+ break;
95
+ case "Literal":
96
+ if (!hasChineseChar(node.value)) return false;
97
+ break;
98
+ case "JSXText":
99
+ if (!hasChineseChar(node.value)) return false;
100
+ break;
101
+ }
102
+ return true;
103
+ }
104
+ function getLeadingSpaces(context, node) {
105
+ if (!node.loc) return "";
106
+ const line = context.sourceCode.lines[node.loc.start.line - 1];
107
+ return line.slice(0, node.loc.start.column);
108
+ }
109
+ function getI18nCallMethod(callMethodName, key, value) {
110
+ if (value) {
111
+ return `${callMethodName}('${key}', { ${value} })`;
112
+ }
113
+ return `${callMethodName}('${key}')`;
114
+ }
115
+ function isScriptSetup(context) {
116
+ var _a, _b;
117
+ const df = (_b = (_a = context.sourceCode.parserServices).getDocumentFragment) == null ? void 0 : _b.call(_a);
118
+ if (!df) {
119
+ return false;
120
+ }
121
+ const scripts = df.children.filter((e) => e.type === "VElement" && e.name === "script");
122
+ return scripts.some((node) => node.startTag.attributes.some((i) => i.key.name = "setup"));
123
+ }
124
+ export {
125
+ callNameNotExist,
126
+ canCollectOperateNodes,
127
+ createI18nIgnoreChecker,
128
+ getI18nCallMethod,
129
+ getInjectNode,
130
+ getLastImportOrFirstNotImportNode,
131
+ getLeadingSpaces,
132
+ isScriptSetup
133
+ };
@@ -0,0 +1,8 @@
1
+ import type { Rule } from 'eslint';
2
+ import { CollectListType } from '../../type.js';
3
+ /**
4
+ * @description 批量执行修复函数 根据收集到节点生成修复函数并执行
5
+ * @param context - 规则上下文
6
+ * @param state - 收集到的节点信息
7
+ */
8
+ export declare function batchFixAll(context: Rule.RuleContext, state: CollectListType): void;
@@ -0,0 +1,169 @@
1
+ // src/eslint/vue/report_and_fix.ts
2
+ import { callNameNotExist, getI18nCallMethod, getInjectNode, getLastImportOrFirstNotImportNode, getLeadingSpaces } from "./i18n_utils.js";
3
+ import KeyMap from "../../key_map.js";
4
+ var keyMapper = KeyMap.getInstance();
5
+ function generateReplaceKeyFixFun(context, node, codeType) {
6
+ const { callMethodName } = context.settings.i18nInfo[codeType] || {};
7
+ if (!callMethodName) {
8
+ return null;
9
+ }
10
+ switch (node.type) {
11
+ case "VText":
12
+ return (fixer) => {
13
+ const value = node.value;
14
+ const key = keyMapper.generateKey(value);
15
+ const replaceStr = value.replace(value.trim(), `{{ ${getI18nCallMethod(callMethodName, key)} }}`);
16
+ return fixer.replaceTextRange(node.range, replaceStr);
17
+ };
18
+ case "VAttribute":
19
+ return (fixer) => {
20
+ const value = node.value.value;
21
+ const key = keyMapper.generateKey(value);
22
+ const afterReplaceKey = value.replace(value.trim(), key);
23
+ const replaceStr = `:${node.key.name}="${getI18nCallMethod(callMethodName, afterReplaceKey)}"`;
24
+ return fixer.replaceTextRange(node.range, replaceStr);
25
+ };
26
+ case "TemplateLiteral":
27
+ return (fixer) => {
28
+ const str = node.quasis.map((q, index) => q.tail ? q.value.cooked : `${q.value.cooked}{arg${index + 1}}`).join("");
29
+ const key = keyMapper.generateKey(str);
30
+ const expressionsArr = node.expressions;
31
+ if (expressionsArr.length) {
32
+ const paramsStr = expressionsArr.map((i, index) => `arg${index + 1}: ${context.sourceCode.getText(i)}`).join(", ");
33
+ const value = context.sourceCode.getText(node);
34
+ const afterReplaceKey = value.replace(value.trim(), key);
35
+ const replaceStr2 = `${getI18nCallMethod(callMethodName, afterReplaceKey, paramsStr)}`;
36
+ return fixer.replaceTextRange(node.range, replaceStr2);
37
+ }
38
+ const replaceStr = `${getI18nCallMethod(callMethodName, key)}`;
39
+ return fixer.replaceTextRange(node.range, replaceStr);
40
+ };
41
+ case "Literal":
42
+ return (fixer) => {
43
+ const value = node.value;
44
+ const key = keyMapper.generateKey(value);
45
+ const str = node.parent.type === "JSXAttribute" ? `{ ${getI18nCallMethod(callMethodName, key)} }` : `${getI18nCallMethod(callMethodName, key)}`;
46
+ const replaceStr = value.replace(value.trim(), str);
47
+ return fixer.replaceTextRange(node.range, replaceStr);
48
+ };
49
+ case "JSXText":
50
+ return (fixer) => {
51
+ const value = node.value;
52
+ const key = keyMapper.generateKey(value);
53
+ const replaceStr = value.replace(value.trim(), `{ ${getI18nCallMethod(callMethodName, key)} }`);
54
+ return fixer.replaceTextRange(node.range, replaceStr);
55
+ };
56
+ default:
57
+ return null;
58
+ }
59
+ }
60
+ function generateInjectImportFixFun(context, codeType) {
61
+ const { injectImport } = context.settings.i18nInfo[codeType] || {};
62
+ if (!injectImport) return null;
63
+ return (fixer) => {
64
+ const res = getLastImportOrFirstNotImportNode(context);
65
+ if (!res) return null;
66
+ const { node: injectNode, type } = res;
67
+ const indent = getLeadingSpaces(context, injectNode);
68
+ if (type === "firstNonImport") {
69
+ return fixer.insertTextBeforeRange(injectNode.range, `${injectImport}
70
+ ${indent}`);
71
+ }
72
+ return fixer.insertTextAfterRange(injectNode.range, `
73
+ ${indent}${injectImport}`);
74
+ };
75
+ }
76
+ function generateInjectCodeToTopLevelFixFun(context, codeType) {
77
+ const { injectCodeToTopLevel } = context.settings.i18nInfo[codeType] || {};
78
+ if (!injectCodeToTopLevel) return null;
79
+ return (fixer) => {
80
+ const res = getLastImportOrFirstNotImportNode(context);
81
+ if (!res) return null;
82
+ const { node: injectNode, type } = res;
83
+ const indent = getLeadingSpaces(context, injectNode);
84
+ if (type === "firstNonImport") {
85
+ return fixer.insertTextBeforeRange(injectNode.range, `${injectCodeToTopLevel}
86
+ ${indent}`);
87
+ }
88
+ return fixer.insertTextAfterRange(injectNode.range, `
89
+ ${indent}${injectCodeToTopLevel}`);
90
+ };
91
+ }
92
+ function generateInjectHooksCallFixFun(context, node, codeType) {
93
+ const { injectHooksCall } = context.settings.i18nInfo[codeType] || {};
94
+ if (!injectHooksCall) return null;
95
+ return (fixer) => {
96
+ const blockStatementNode = (node == null ? void 0 : node.type) === "Program" ? node : node == null ? void 0 : node.body;
97
+ const injectNode = getInjectNode(context, blockStatementNode);
98
+ const indent = getLeadingSpaces(context, injectNode);
99
+ return fixer.insertTextBeforeRange(injectNode.range, `${injectHooksCall}
100
+ ${indent}`);
101
+ };
102
+ }
103
+ function batchFixAll(context, state) {
104
+ const allNodeList = [...state.templateTextNodeList, ...state.scriptTextNodeList, ...state.jsTextNodeList, ...state.jsxTextNodeList];
105
+ if (!allNodeList.length) return;
106
+ const textNodeFixes = [];
107
+ const injectImportFixes = [];
108
+ const injectCodeToTopLevelFixes = [];
109
+ const injectHooksCallFixes = [];
110
+ const injectFlag = /* @__PURE__ */ new Map();
111
+ for (const item of allNodeList) {
112
+ textNodeFixes.push(generateReplaceKeyFixFun(context, item.textNode, item.inSetup ? "scriptSetup" : item.codeType));
113
+ switch (item.codeType) {
114
+ case "script":
115
+ if (item.inSetup) {
116
+ if (callNameNotExist(context, item.setUpNode, "scriptSetup") && !injectFlag.has("scriptSetup")) {
117
+ injectFlag.set("scriptSetup", true);
118
+ injectHooksCallFixes.push(generateInjectHooksCallFixFun(context, item.setUpNode, "scriptSetup"));
119
+ injectImportFixes.push(generateInjectImportFixFun(context, "scriptSetup"));
120
+ injectCodeToTopLevelFixes.push(generateInjectCodeToTopLevelFixFun(context, "scriptSetup"));
121
+ }
122
+ break;
123
+ }
124
+ if (callNameNotExist(context, context.sourceCode.ast, "script") && !injectFlag.has("script")) {
125
+ injectFlag.set("script", true);
126
+ injectImportFixes.push(generateInjectImportFixFun(context, "script"));
127
+ injectCodeToTopLevelFixes.push(generateInjectCodeToTopLevelFixFun(context, "script"));
128
+ }
129
+ break;
130
+ case "jsx":
131
+ if (item.inSetup) {
132
+ if (callNameNotExist(context, item.setUpNode, "scriptSetup") && !injectFlag.has("scriptSetup")) {
133
+ injectFlag.set("scriptSetup", true);
134
+ injectHooksCallFixes.push(generateInjectHooksCallFixFun(context, item.setUpNode, "scriptSetup"));
135
+ injectImportFixes.push(generateInjectImportFixFun(context, "scriptSetup"));
136
+ injectCodeToTopLevelFixes.push(generateInjectCodeToTopLevelFixFun(context, "scriptSetup"));
137
+ }
138
+ break;
139
+ }
140
+ if (callNameNotExist(context, context.sourceCode.ast, "jsx") && !injectFlag.has("jsx")) {
141
+ injectFlag.set("jsx", true);
142
+ injectImportFixes.push(generateInjectImportFixFun(context, "jsx"));
143
+ injectCodeToTopLevelFixes.push(generateInjectCodeToTopLevelFixFun(context, "jsx"));
144
+ }
145
+ break;
146
+ case "js":
147
+ if (callNameNotExist(context, context.sourceCode.ast, "js") && !injectFlag.has("js")) {
148
+ injectFlag.set("js", true);
149
+ injectImportFixes.push(generateInjectImportFixFun(context, "js"));
150
+ injectCodeToTopLevelFixes.push(generateInjectCodeToTopLevelFixFun(context, "js"));
151
+ }
152
+ break;
153
+ default:
154
+ break;
155
+ }
156
+ }
157
+ const fixes = [...textNodeFixes, ...injectImportFixes, ...injectCodeToTopLevelFixes, ...injectHooksCallFixes].filter((item) => item !== null);
158
+ if (!fixes.length) return;
159
+ context.report({
160
+ loc: context.sourceCode.ast.loc,
161
+ message: "存在中文字符,可以进行国际化替换",
162
+ fix: (fixer) => {
163
+ return fixes.map((f) => f(fixer)).filter(Boolean);
164
+ }
165
+ });
166
+ }
167
+ export {
168
+ batchFixAll
169
+ };
@@ -0,0 +1,14 @@
1
+ import { BaseCodeType } from './type.js';
2
+ declare type LangFileType = Extract<BaseCodeType, 'json' | 'js' | 'ts'>;
3
+ export declare function getModifiedFiles(): Promise<string[]>;
4
+ export declare function readFile(filePath: string): Promise<string>;
5
+ export declare function getFileType(filePath: string): string;
6
+ export declare function writeFile(filePath: string, content: string): Promise<void>;
7
+ export declare function fileExists(filePath: string): Promise<boolean>;
8
+ export declare function writeI18nFile(filePath: string, content: Record<string, string>): Promise<void>;
9
+ export declare function getFileContent(filePath: string, fileType: LangFileType): Promise<{
10
+ exportPrefix: string;
11
+ code: any;
12
+ }>;
13
+ export declare function isDescendant(file: string, dir: string): boolean;
14
+ export {};
@@ -0,0 +1,68 @@
1
+ // src/file_processor_utils.ts
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import { checkReg } from "./assets/const.js";
5
+ import JSON5 from "json5";
6
+ import { simpleGit } from "simple-git";
7
+ async function getModifiedFiles() {
8
+ const raw = await simpleGit().diff(["--cached", "--name-only", "--diff-filter=AM"]);
9
+ const files = raw.split("\n").filter(Boolean);
10
+ return files.map((item) => path.resolve(item));
11
+ }
12
+ async function readFile(filePath) {
13
+ return await fs.readFile(filePath, "utf8");
14
+ }
15
+ function getFileType(filePath) {
16
+ return path.extname(filePath).slice(1);
17
+ }
18
+ async function writeFile(filePath, content) {
19
+ await fs.writeFile(path.resolve(filePath), content, "utf8");
20
+ }
21
+ async function fileExists(filePath) {
22
+ return Promise.resolve(fs.stat(path.resolve(filePath)).then(() => true).catch(() => false));
23
+ }
24
+ async function writeI18nFile(filePath, content) {
25
+ const fileType = getFileType(filePath);
26
+ if (!["js", "ts", "json"].includes(fileType)) {
27
+ throw new Error(`国际化语言文件${filePath}}仅支持 JS、TS、JSON格式`);
28
+ }
29
+ try {
30
+ const { exportPrefix, code } = await getFileContent(filePath, fileType);
31
+ const allContent = Object.assign(code, content);
32
+ await writeFile(filePath, `${exportPrefix} ${JSON.stringify(allContent, null, 2)}`);
33
+ } catch (error) {
34
+ throw new Error(`国际化语言文件${filePath}写入内容失败,${error}`);
35
+ }
36
+ }
37
+ async function getFileContent(filePath, fileType) {
38
+ const fullPath = path.resolve(filePath);
39
+ const isExist = await fileExists(filePath);
40
+ const exportPrefix = fileType === "json" ? "" : "export default";
41
+ if (!isExist) {
42
+ return { exportPrefix, code: {} };
43
+ }
44
+ const code = await readFile(fullPath);
45
+ const result = code.match(checkReg);
46
+ if (!result) {
47
+ return { exportPrefix, code: {} };
48
+ }
49
+ if (fileType === "json") {
50
+ return { exportPrefix: "", code: JSON.parse(code) };
51
+ }
52
+ return { exportPrefix: result[1], code: JSON5.parse(result[2]) };
53
+ }
54
+ function isDescendant(file, dir) {
55
+ const f = path.resolve(file).toLowerCase();
56
+ const d = path.resolve(dir).toLowerCase();
57
+ return f !== d && f.startsWith(d + "/");
58
+ }
59
+ export {
60
+ fileExists,
61
+ getFileContent,
62
+ getFileType,
63
+ getModifiedFiles,
64
+ isDescendant,
65
+ readFile,
66
+ writeFile,
67
+ writeI18nFile
68
+ };
@@ -0,0 +1,77 @@
1
+ import { Framework, I18nInfoType, KeyType, ReactCollectItemType, ReactI18nInfoType, TextNode } from './type.js';
2
+ import type { Rule } from 'eslint';
3
+ interface FileToI18nConfig<T extends Framework> {
4
+ type: T;
5
+ keyType?: KeyType;
6
+ i18nInfo: T extends 'vue' ? I18nInfoType : ReactI18nInfoType;
7
+ customIgnore?: (node: T extends 'vue' ? TextNode : ReactCollectItemType['textNode'], context: Rule.RuleContext) => boolean;
8
+ }
9
+ interface FileToI18nAndTranslateConfig<T extends Framework> extends FileToI18nConfig<T> {
10
+ lang: string[];
11
+ apiKey: string;
12
+ }
13
+ export declare class I18nEasy {
14
+ /**
15
+ * vue框架中将文件内容转换成i18n写法并翻译
16
+ * @param filePath 文件绝对路径
17
+ * @param options 配置项
18
+ */
19
+ private fileToI18nVue;
20
+ /**
21
+ * react框架中将文件内容转换成i18n写法并翻译
22
+ * @param filePath 文件绝对路径
23
+ * @param options 配置项
24
+ */
25
+ private fileToI18nReact;
26
+ /**
27
+ * 将文件内容转换成i18n写法
28
+ * @param filePath 文件绝对路径
29
+ * @param options 配置项
30
+ */
31
+ fileToI18n<T extends Framework>(filePath: string, options: FileToI18nConfig<T>): Promise<{
32
+ newFileContent: string;
33
+ fixed: boolean;
34
+ filePath: string;
35
+ oldFileContent: string;
36
+ keyValueMap: [string, string][];
37
+ }>;
38
+ /**
39
+ * 将文件内容转换成i18n写法并翻译
40
+ * @param filePath 文件绝对路径
41
+ * @param options 配置项
42
+ */
43
+ fileToI18nAndTranslate<T extends Framework>(filePath: string, options: FileToI18nAndTranslateConfig<T>): Promise<{
44
+ translateResult: Record<string, Record<string, string>>;
45
+ newFileContent: string;
46
+ fixed: boolean;
47
+ filePath: string;
48
+ oldFileContent: string;
49
+ keyValueMap: [string, string][];
50
+ }>;
51
+ /**
52
+ * 将文件内容转换成i18n写法并翻译,支持批量
53
+ * @param filePath 文件绝对路径
54
+ * @param options 配置项
55
+ */
56
+ fileToI18nAndTranslateBatch<T extends Framework>(filePaths: string[], options: FileToI18nAndTranslateConfig<T>): Promise<{
57
+ transformResult: {
58
+ newFileContent: string;
59
+ fixed: boolean;
60
+ filePath: string;
61
+ oldFileContent: string;
62
+ keyValueMap: [string, string][];
63
+ }[];
64
+ translateResult: Record<string, Record<string, string>>;
65
+ }>;
66
+ /**
67
+ * @description 获取需要处理的文件信息
68
+ * @returns 文件信息
69
+ */
70
+ private getPendingFiles;
71
+ /**
72
+ * @description 对git暂存区的文件进行i18n转换和翻译 并将翻译结果写入语言文件
73
+ * @returns {Promise<void>}
74
+ */
75
+ transformModifiedFiles(): Promise<void>;
76
+ }
77
+ export {};
@@ -0,0 +1,171 @@
1
+ // src/i18n_easy.ts
2
+ import { configFileNames } from "./assets/const.js";
3
+ import KeyMap from "./key_map.js";
4
+ import { AstTransformer } from "./ast_transformer.js";
5
+ import path from "node:path";
6
+ import { isSupportFileType, isVueScriptSrcImport, searchFileContent } from "./utils.js";
7
+ import { getFileType, getModifiedFiles, isDescendant, readFile, writeFile, writeI18nFile } from "./file_processor_utils.js";
8
+ import { log } from "./progressBus.js";
9
+ import Translate from "./translate.js";
10
+ var I18nEasy = class {
11
+ /**
12
+ * vue框架中将文件内容转换成i18n写法并翻译
13
+ * @param filePath 文件绝对路径
14
+ * @param options 配置项
15
+ */
16
+ async fileToI18nVue(filePath, options) {
17
+ const { keyType = "pinyin", i18nInfo, customIgnore } = options;
18
+ const fileType = getFileType(filePath);
19
+ if (!isSupportFileType(fileType, "vue")) {
20
+ throw new Error(`${filePath}出错了,不支持的文件类型:${fileType}`);
21
+ }
22
+ const fileContent = await readFile(filePath);
23
+ const isVueScriptSrc = await isVueScriptSrcImport(filePath, fileType);
24
+ const astTransformer = new AstTransformer();
25
+ const keyMapper = KeyMap.getInstance();
26
+ keyMapper.setKeyType(keyType);
27
+ keyMapper.clearCharMap();
28
+ const fixedResult = astTransformer.convertFileContent({
29
+ fileContent,
30
+ fileName: path.basename(filePath),
31
+ fileType: isVueScriptSrc ? "vue" : fileType,
32
+ type: "vue",
33
+ i18nInfo,
34
+ customIgnore
35
+ });
36
+ return {
37
+ filePath,
38
+ oldFileContent: fileContent,
39
+ keyValueMap: Array.from(keyMapper.charList),
40
+ ...fixedResult
41
+ };
42
+ }
43
+ /**
44
+ * react框架中将文件内容转换成i18n写法并翻译
45
+ * @param filePath 文件绝对路径
46
+ * @param options 配置项
47
+ */
48
+ async fileToI18nReact(filePath, options) {
49
+ const { keyType = "pinyin", i18nInfo } = options;
50
+ const fileType = getFileType(filePath);
51
+ if (!isSupportFileType(fileType, "react")) {
52
+ throw new Error(`${filePath}出错了,不支持的文件类型:${fileType}`);
53
+ }
54
+ const fileContent = await readFile(filePath);
55
+ const astTransformer = new AstTransformer();
56
+ const keyMapper = KeyMap.getInstance();
57
+ keyMapper.setKeyType(keyType);
58
+ keyMapper.clearCharMap();
59
+ const fixedResult = astTransformer.convertFileContent({
60
+ fileContent,
61
+ fileName: path.basename(filePath),
62
+ fileType,
63
+ type: "react",
64
+ i18nInfo
65
+ });
66
+ return {
67
+ filePath,
68
+ oldFileContent: fileContent,
69
+ keyValueMap: Array.from(keyMapper.charList),
70
+ ...fixedResult
71
+ };
72
+ }
73
+ /**
74
+ * 将文件内容转换成i18n写法
75
+ * @param filePath 文件绝对路径
76
+ * @param options 配置项
77
+ */
78
+ async fileToI18n(filePath, options) {
79
+ const { type } = options;
80
+ if (type === "vue") {
81
+ return this.fileToI18nVue(filePath, options);
82
+ } else if (type === "react") {
83
+ return this.fileToI18nReact(filePath, options);
84
+ } else {
85
+ throw new Error(`不支持的框架类型:${type}`);
86
+ }
87
+ }
88
+ /**
89
+ * 将文件内容转换成i18n写法并翻译
90
+ * @param filePath 文件绝对路径
91
+ * @param options 配置项
92
+ */
93
+ async fileToI18nAndTranslate(filePath, options) {
94
+ const { type, keyType = "pinyin", i18nInfo, lang, apiKey } = options;
95
+ const result = await this.fileToI18n(filePath, { type, keyType, i18nInfo });
96
+ const translator = Translate.getInstance();
97
+ translator.setApiKey(apiKey);
98
+ const arr = result.fixed ? result.keyValueMap : [];
99
+ const translateResult = await translator.translate(arr, lang);
100
+ return {
101
+ ...result,
102
+ translateResult
103
+ };
104
+ }
105
+ /**
106
+ * 将文件内容转换成i18n写法并翻译,支持批量
107
+ * @param filePath 文件绝对路径
108
+ * @param options 配置项
109
+ */
110
+ async fileToI18nAndTranslateBatch(filePaths, options) {
111
+ const { type, keyType = "pinyin", i18nInfo, lang, apiKey, customIgnore } = options;
112
+ const result = await Promise.all(filePaths.map((filePath) => this.fileToI18n(filePath, { type, keyType, i18nInfo, customIgnore })));
113
+ const translator = Translate.getInstance();
114
+ translator.setApiKey(apiKey);
115
+ const arr = result.filter((item) => item.fixed).flatMap((i) => i.keyValueMap);
116
+ const translateResult = await translator.translate(arr, lang);
117
+ return {
118
+ transformResult: result,
119
+ translateResult
120
+ };
121
+ }
122
+ /**
123
+ * @description 获取需要处理的文件信息
124
+ * @returns 文件信息
125
+ */
126
+ async getPendingFiles() {
127
+ const filePaths = await getModifiedFiles();
128
+ if (filePaths.length === 0) {
129
+ log(`暂存区暂无需要处理的文件`, "info");
130
+ return;
131
+ }
132
+ const configContext = await searchFileContent(path.resolve(), configFileNames);
133
+ if (!configContext) {
134
+ log(`未在目录${path.resolve()}下找到配置文件`, "info");
135
+ return;
136
+ }
137
+ const { apiKey, projects } = configContext;
138
+ const result = projects.map((item) => {
139
+ const projectPath = path.resolve(item.projectPath);
140
+ const files = filePaths.filter((filePath) => isDescendant(filePath, projectPath) && isSupportFileType(getFileType(filePath), item.type));
141
+ return { ...item, files, projectPath, apiKey };
142
+ });
143
+ return result;
144
+ }
145
+ /**
146
+ * @description 对git暂存区的文件进行i18n转换和翻译 并将翻译结果写入语言文件
147
+ * @returns {Promise<void>}
148
+ */
149
+ async transformModifiedFiles() {
150
+ log(`提示:对于vue框架,仅支持vue、js、jsx、ts、tsx文件,对于react框架;仅支持js、jsx、ts、tsx文件,其他文件类型将被忽略...
151
+ `, "info");
152
+ const pendingFiles = await this.getPendingFiles();
153
+ if (!pendingFiles) {
154
+ return;
155
+ }
156
+ for (const item of pendingFiles) {
157
+ const { projectPath, files, apiKey, i18nInfo, lang, type, keyType, customIgnore } = item;
158
+ log(`开始处理目录${projectPath}下文件...
159
+ `, "info");
160
+ const { translateResult, transformResult } = await this.fileToI18nAndTranslateBatch(files, { i18nInfo, apiKey, lang: lang.map((i) => i.name), type, keyType, customIgnore });
161
+ const fixed = transformResult.filter((item2) => item2.fixed);
162
+ const notFixed = transformResult.filter((item2) => !item2.fixed);
163
+ await Promise.all(fixed.map(({ filePath, newFileContent }) => writeFile(filePath, newFileContent)));
164
+ await Promise.all(lang.map(({ name, i18nFilePath }) => translateResult[name] ? writeI18nFile(i18nFilePath, translateResult[name]) : null).filter(Boolean));
165
+ log(`目录${projectPath}下支持的文件已处理完成;共${files.length}个文件,已处理${fixed.length}个文件,未处理${notFixed.length}个文件...`, "success");
166
+ }
167
+ }
168
+ };
169
+ export {
170
+ I18nEasy
171
+ };
package/esm/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export { I18nEasy } from './i18n_easy.js';
2
+ export { progressBus } from './progressBus.js';
3
+ export { defineConfig } from './utils.js';
4
+ export type { LogStatus } from './type.js';
package/esm/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // src/index.ts
2
+ import { I18nEasy } from "./i18n_easy.js";
3
+ import { progressBus } from "./progressBus.js";
4
+ import { defineConfig } from "./utils.js";
5
+ export {
6
+ I18nEasy,
7
+ defineConfig,
8
+ progressBus
9
+ };
@@ -0,0 +1,14 @@
1
+ import { KeyType } from './type.js';
2
+ export default class KeyMap {
3
+ private charMap;
4
+ private keyType;
5
+ private static instance;
6
+ private constructor();
7
+ static getInstance(): KeyMap;
8
+ get charList(): Map<string, string>;
9
+ generateKey(str: string): string;
10
+ clearCharMap(): void;
11
+ setCharMap(key: string, value: string): void;
12
+ isEmpty(): boolean;
13
+ setKeyType(type: KeyType): void;
14
+ }