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,111 @@
1
+ // src/eslint/react/i18n_easy_plugin.ts
2
+ import { batchFixAll } from "./report_and_fix.js";
3
+ import { createI18nIgnoreChecker, canCollectOperateNodes } from "./i18n_utils.js";
4
+ var ReactPlugin = {
5
+ rules: {
6
+ "i18n-transform": {
7
+ meta: {
8
+ type: "problem",
9
+ messages: { hasChinese: "当前文件存在中文字符‘{{ char }}’,可进行国际化替换" },
10
+ fixable: "code"
11
+ },
12
+ create(context) {
13
+ const customIgnore = context.settings.customIgnore;
14
+ let isIgnored;
15
+ const state = {
16
+ inComponent: [],
17
+ // 组件内中文
18
+ outComponent: []
19
+ // 组件外中文
20
+ };
21
+ let inComp = false;
22
+ let compNode = null;
23
+ const enterComp = () => inComp = true;
24
+ const exitComp = () => inComp = false;
25
+ const collectNodes = (textNode, programNode) => {
26
+ if (customIgnore == null ? void 0 : customIgnore(textNode, context)) return;
27
+ if (!canCollectOperateNodes(textNode, isIgnored)) return;
28
+ const payload = { textNode, componentNode: inComp ? compNode : null, programNode: inComp ? null : programNode, inComp };
29
+ if (inComp) {
30
+ state.inComponent.push(payload);
31
+ return;
32
+ }
33
+ state.outComponent.push(payload);
34
+ };
35
+ const collectScriptOperateNodesVisitor = {
36
+ // 字面量节点
37
+ Literal(node) {
38
+ collectNodes(node, context.sourceCode.ast);
39
+ },
40
+ // 模版字符串节点
41
+ TemplateLiteral(node) {
42
+ collectNodes(node, context.sourceCode.ast);
43
+ },
44
+ // JSX文本节点
45
+ JSXText(node) {
46
+ collectNodes(node, context.sourceCode.ast);
47
+ }
48
+ };
49
+ const injectConfigCodeVisitor = {
50
+ /**
51
+ * 进入时 记录@i18n-ignore注释所在行号
52
+ */
53
+ Program: function() {
54
+ isIgnored = createI18nIgnoreChecker(context);
55
+ },
56
+ /**
57
+ * 退出时
58
+ * 1、替换中文为国际化写法
59
+ * 2、注入配置代码
60
+ */
61
+ "Program:exit": function() {
62
+ batchFixAll(context, state);
63
+ },
64
+ // 组件 function Component(){}
65
+ "FunctionDeclaration[id.name=/^[A-Z]/]": function(node) {
66
+ compNode = node;
67
+ enterComp();
68
+ },
69
+ "FunctionDeclaration[id.name=/^[A-Z]/]:exit": exitComp,
70
+ // 组件 const Component = () => {}
71
+ "VariableDeclarator[id.name=/^[A-Z]/] > ArrowFunctionExpression": function(node) {
72
+ compNode = node.parent;
73
+ enterComp();
74
+ },
75
+ "VariableDeclarator[id.name=/^[A-Z]/] > ArrowFunctionExpression:exit": exitComp,
76
+ // 组件 const Component = function(){}
77
+ "VariableDeclarator[id.name=/^[A-Z]/] > FunctionExpression": function(node) {
78
+ compNode = node.parent;
79
+ enterComp();
80
+ },
81
+ "VariableDeclarator[id.name=/^[A-Z]/] > FunctionExpression:exit": exitComp,
82
+ // 自定义hooks(同理)
83
+ "FunctionDeclaration[id.name=/^use[A-Z]/]": function(node) {
84
+ compNode = node;
85
+ enterComp();
86
+ },
87
+ "FunctionDeclaration[id.name=/^use[A-Z]/]:exit": exitComp,
88
+ "VariableDeclarator[id.name=/^use[A-Z]/] > ArrowFunctionExpression": function(node) {
89
+ compNode = node.parent;
90
+ enterComp();
91
+ },
92
+ "VariableDeclarator[id.name=/^use[A-Z]/] > ArrowFunctionExpression:exit": exitComp,
93
+ "VariableDeclarator[id.name=/^use[A-Z]/] > FunctionExpression": function(node) {
94
+ compNode = node.parent;
95
+ enterComp();
96
+ },
97
+ "VariableDeclarator[id.name=/^use[A-Z]/] > FunctionExpression:exit": exitComp
98
+ };
99
+ const replaceOperateNodesAndInjectVisitor = {
100
+ ...collectScriptOperateNodesVisitor,
101
+ ...injectConfigCodeVisitor
102
+ };
103
+ return replaceOperateNodesAndInjectVisitor;
104
+ }
105
+ }
106
+ }
107
+ };
108
+ var i18n_easy_plugin_default = ReactPlugin;
109
+ export {
110
+ i18n_easy_plugin_default as default
111
+ };
@@ -0,0 +1,61 @@
1
+ import type { Rule } from 'eslint';
2
+ import * as ESTree from 'estree';
3
+ import { ReactCodeType, ReactCollectItemType, ReactInjectCodeType } from '../../type.js';
4
+ /**
5
+ * @description 创建忽略注释检查器
6
+ * @param context - 规则上下文
7
+ * @returns 忽略注释检查函数
8
+ */
9
+ export declare function createI18nIgnoreChecker(context: Rule.RuleContext): (node: ReactCollectItemType['textNode']) => boolean;
10
+ /**
11
+ * @description 判断能否注入
12
+ * @param context - 规则上下文
13
+ * @param node - 节点 (Program、组件、hooks节点)
14
+ * @param codeType - 代码类型
15
+ * @returns 是否能注入
16
+ */
17
+ export declare function canInject(context: Rule.RuleContext, node: ReactInjectCodeType, codeType: ReactCodeType): boolean;
18
+ /**
19
+ * @description 获取最后一个 import 或第一个非 import 节点
20
+ ** 1、不存在节点返回null
21
+ ** 2、存在import节点,返回最后一个import节点
22
+ ** 3、不存在import节点,返回第一个非import节点
23
+ * @param context - 规则上下文
24
+ * @returns 节点信息
25
+ */
26
+ export declare function getLastImportOrFirstNotImportNode(context: Rule.RuleContext): {
27
+ node: ESTree.Node;
28
+ type: string;
29
+ } | null;
30
+ /**
31
+ * @description 获取注入节点
32
+ * @param context - 规则上下文
33
+ * @param node - BlockStatement节点
34
+ * @returns 第一个子节点(包含注释节点)
35
+ */
36
+ export declare function getInjectNode(context: Rule.RuleContext, node: ESTree.BlockStatement): ESTree.Statement | ESTree.Comment;
37
+ /**
38
+ * @description 判断能否收集操作节点
39
+ ** 排除:
40
+ ** 1、显示使用@i18n-ignore注释的节点
41
+ ** 2、未使用中文的节点
42
+ * @param node - 节点 (TemplateLiteral、Literal、JSXText节点)
43
+ * @param isIgnored - 判断是否被忽略的函数
44
+ * @returns 是否能收集操作节点
45
+ */
46
+ export declare function canCollectOperateNodes(node: ReactCollectItemType['textNode'], isIgnored: (n: ReactCollectItemType['textNode']) => boolean): boolean;
47
+ /**
48
+ * @description 获取节点前的空格
49
+ * @param context - 规则上下文
50
+ * @param node - 节点
51
+ * @returns 节点前的空格
52
+ */
53
+ export declare function getLeadingSpaces(context: Rule.RuleContext, node: ESTree.Node | ESTree.Comment): string;
54
+ /**
55
+ * @description 获取国际化调用方法
56
+ * @param callMethodName - 国际化调用方法
57
+ * @param key - 国际化键值
58
+ * @param value - 国际化参数
59
+ * @returns 国际化调用方法
60
+ */
61
+ export declare function getI18nCallMethod(callMethodName: string, key: string, value?: string): string;
@@ -0,0 +1,115 @@
1
+ // src/eslint/react/i18n_utils.ts
2
+ import { hasChineseChar } from "../../utils.js";
3
+ function createI18nIgnoreChecker(context) {
4
+ const ignoreLines = [];
5
+ const jsComments = context.sourceCode.getAllComments();
6
+ for (const comment of jsComments) {
7
+ if (["HTMLComment", "Line", "Block"].includes(comment.type) && comment.value.trim().includes("@i18n-ignore")) {
8
+ ignoreLines.push([comment.loc.start.line, comment.loc.end.line]);
9
+ }
10
+ }
11
+ return function isIgnored(node) {
12
+ const nodeStartLine = node.loc.start.line;
13
+ const nodeEndLine = node.loc.end.line;
14
+ if (nodeStartLine !== nodeEndLine) return false;
15
+ return ignoreLines.some(([_start, end]) => end === nodeEndLine - 1);
16
+ };
17
+ }
18
+ function getScope(context, node) {
19
+ if (node.type === "Program") {
20
+ const programScope = context.sourceCode.getScope(node);
21
+ return programScope.type == "global" ? programScope.childScopes[0] : programScope;
22
+ }
23
+ if (["FunctionDeclaration", "VariableDeclarator"].includes(node.type)) {
24
+ return context.sourceCode.getScope(node);
25
+ }
26
+ return null;
27
+ }
28
+ function canInject(context, node, codeType) {
29
+ if (!node) return false;
30
+ const { callMethodName } = context.settings.i18nInfo[codeType] || {};
31
+ if (!callMethodName) return false;
32
+ const scope = getScope(context, node);
33
+ if (!scope) return false;
34
+ return scope.variables.some((ref) => ref.name === callMethodName) === false;
35
+ }
36
+ function getLastImportOrFirstNotImportNode(context) {
37
+ const src = context.sourceCode;
38
+ const body = src.ast.body;
39
+ let lastImport = body.filter((n) => n.type === "ImportDeclaration").pop();
40
+ if (lastImport) {
41
+ return {
42
+ node: lastImport,
43
+ type: "lastImport"
44
+ };
45
+ }
46
+ let firstNonImport = null;
47
+ const allNodes = [...body];
48
+ const commentsAsNodes = src.getAllComments();
49
+ const sorted = [...allNodes, ...commentsAsNodes].sort((a, b) => {
50
+ if (a.range && b.range) {
51
+ return a.range[0] - b.range[0];
52
+ }
53
+ return 0;
54
+ });
55
+ firstNonImport = sorted.find((n) => n.type !== "ImportDeclaration");
56
+ if (firstNonImport) {
57
+ return {
58
+ node: firstNonImport,
59
+ type: "firstNonImport"
60
+ };
61
+ }
62
+ return null;
63
+ }
64
+ function getInjectNode(context, node) {
65
+ const commentNodes = context.sourceCode.getAllComments();
66
+ const comments = commentNodes.filter((item) => {
67
+ if (item.range && node.range) {
68
+ return item.range[0] >= node.range[0] && item.range[1] <= node.range[1];
69
+ }
70
+ return false;
71
+ });
72
+ const sorted = [...comments, ...node.body].sort((a, b) => {
73
+ if (a.range && b.range) {
74
+ return a.range[0] - b.range[0];
75
+ }
76
+ return 0;
77
+ });
78
+ return sorted[0];
79
+ }
80
+ function canCollectOperateNodes(node, isIgnored) {
81
+ if (isIgnored(node)) return false;
82
+ switch (node.type) {
83
+ case "TemplateLiteral":
84
+ if (!node.quasis.some((q) => hasChineseChar(q.value.cooked))) return false;
85
+ return true;
86
+ case "Literal":
87
+ if (!hasChineseChar(node.value)) return false;
88
+ return true;
89
+ case "JSXText":
90
+ if (!hasChineseChar(node.value)) return false;
91
+ return true;
92
+ default:
93
+ return false;
94
+ }
95
+ }
96
+ function getLeadingSpaces(context, node) {
97
+ if (!node.loc) return "";
98
+ const line = context.sourceCode.lines[node.loc.start.line - 1];
99
+ return line.slice(0, node.loc.start.column);
100
+ }
101
+ function getI18nCallMethod(callMethodName, key, value) {
102
+ if (value) {
103
+ return `${callMethodName}('${key}', { ${value} })`;
104
+ }
105
+ return `${callMethodName}('${key}')`;
106
+ }
107
+ export {
108
+ canCollectOperateNodes,
109
+ canInject,
110
+ createI18nIgnoreChecker,
111
+ getI18nCallMethod,
112
+ getInjectNode,
113
+ getLastImportOrFirstNotImportNode,
114
+ getLeadingSpaces
115
+ };
@@ -0,0 +1,67 @@
1
+ import type { Rule } from 'eslint';
2
+ import * as ESTree from 'estree';
3
+ import { ReactCollectItemType, ReactCollectListType, ReactCodeType } from '../../type.js';
4
+ declare type FixFunType = ((fixer: Rule.RuleFixer) => Rule.Fix | Iterable<Rule.Fix> | null) | null;
5
+ /**
6
+ * @description 生成替换中文为国际化写法修复函数
7
+ * @param context - 规则上下文
8
+ * @param node - 节点
9
+ * @param codeType - 代码类型
10
+ * @returns 修复函数
11
+ * @example 原始代码:<span>张三</span> 修复后代码:<span>{ formatMessage({ id: 'zhang_san'}) }</span>
12
+ */
13
+ export declare function generateReplaceKeyFixFun(context: Rule.RuleContext, node: ReactCollectItemType['textNode'], codeType: ReactCodeType): FixFunType;
14
+ /**
15
+ * @description 生成注入import语句的修复函数
16
+ * @param context - 规则上下文
17
+ * @param codeType - 代码类型
18
+ * @returns 修复函数
19
+ * @example
20
+ * 原始代码:
21
+ * const a = 1
22
+ * 修复后代码:
23
+ * import i18n from '@/i18n/i18n.js' // 注入导入语句
24
+ * const a = 1
25
+ */
26
+ export declare function generateInjectImportFixFun(context: Rule.RuleContext, codeType: ReactCodeType): FixFunType;
27
+ /**
28
+ * @description 生成注入hooks调用语句的修复函数
29
+ * @param context - 规则上下文
30
+ * @param node - 节点
31
+ * @returns 修复函数
32
+ * @example
33
+ * 原始代码:
34
+ * function Button() {
35
+ * return <span>按钮</span>
36
+ * }
37
+ * 修复后代码:
38
+ * function Button() {
39
+ * const { formatMessage } = useI18n() // 注入hooks调用语句
40
+ * return <span>按钮</span>
41
+ * }
42
+ */
43
+ export declare function generateInjectHooksCallFixFun(context: Rule.RuleContext, node: ESTree.FunctionDeclaration | ESTree.VariableDeclarator): FixFunType;
44
+ /**
45
+ * @description 生成注入代码到顶部的修复函数
46
+ * @param context - 规则上下文
47
+ * @param codeType - 代码类型
48
+ * @returns 修复函数
49
+ * @example
50
+ * 原始代码:
51
+ * function Button() {
52
+ * return <span>{formatMessage('button')}</span>
53
+ * }
54
+ * 修复后代码:
55
+ * const { formatMessage } = getIntl(); // 注入代码到顶部
56
+ * function Button() {
57
+ * return <span>{formatMessage('button')}</span>
58
+ * }
59
+ */
60
+ export declare function generateInjectCodeToTopLevelFixFun(context: Rule.RuleContext, codeType: ReactCodeType): FixFunType;
61
+ /**
62
+ * @description 批量执行修复函数 根据收集到节点生成修复函数并执行
63
+ * @param context - 规则上下文
64
+ * @param state - 收集到的节点列表
65
+ */
66
+ export declare function batchFixAll(context: Rule.RuleContext, state: ReactCollectListType): void;
67
+ export {};
@@ -0,0 +1,118 @@
1
+ // src/eslint/react/report_and_fix.ts
2
+ import { canInject, 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) return null;
8
+ switch (node.type) {
9
+ case "TemplateLiteral":
10
+ return (fixer) => {
11
+ const str = node.quasis.map((q, index) => q.tail ? q.value.cooked : `${q.value.cooked}{arg${index + 1}}`).join("");
12
+ const key = keyMapper.generateKey(str);
13
+ const expressionsArr = node.expressions;
14
+ if (expressionsArr.length) {
15
+ const paramsStr = expressionsArr.map((i, index) => `arg${index + 1}: ${context.sourceCode.getText(i)}`).join(", ");
16
+ const value = context.sourceCode.getText(node);
17
+ const afterReplaceKey = value.replace(value.trim(), key);
18
+ const replaceStr2 = `${getI18nCallMethod(callMethodName, afterReplaceKey, paramsStr)}`;
19
+ return fixer.replaceTextRange(node.range, replaceStr2);
20
+ }
21
+ const replaceStr = `${getI18nCallMethod(callMethodName, key)}`;
22
+ return fixer.replaceTextRange(node.range, replaceStr);
23
+ };
24
+ case "Literal":
25
+ return (fixer) => {
26
+ const value = node.value;
27
+ const key = keyMapper.generateKey(value);
28
+ const str = node.parent.type === "JSXAttribute" ? `{ ${getI18nCallMethod(callMethodName, key)} }` : `${getI18nCallMethod(callMethodName, key)}`;
29
+ const replaceStr = value.replace(value.trim(), str);
30
+ return fixer.replaceTextRange(node.range, replaceStr);
31
+ };
32
+ case "JSXText":
33
+ return (fixer) => {
34
+ const value = node.value;
35
+ const key = keyMapper.generateKey(value);
36
+ return fixer.replaceTextRange(node.range, `{ ${getI18nCallMethod(callMethodName, key)} }`);
37
+ };
38
+ default:
39
+ return () => null;
40
+ }
41
+ }
42
+ function generateInjectImportFixFun(context, codeType) {
43
+ const { injectImport } = context.settings.i18nInfo[codeType] || {};
44
+ if (!injectImport) return null;
45
+ return (fixer) => {
46
+ const res = getLastImportOrFirstNotImportNode(context);
47
+ if (!res) return null;
48
+ const { node: injectNode, type } = res;
49
+ const indent = getLeadingSpaces(context, injectNode);
50
+ if (type === "firstNonImport") {
51
+ return fixer.insertTextBeforeRange(injectNode.range, `${injectImport}
52
+ ${indent}`);
53
+ }
54
+ return fixer.insertTextAfterRange(injectNode.range, `
55
+ ${indent}${injectImport}`);
56
+ };
57
+ }
58
+ function generateInjectHooksCallFixFun(context, node) {
59
+ const { injectHooksCall } = context.settings.i18nInfo["jsx"] || {};
60
+ if (!injectHooksCall) return null;
61
+ return function(fixer) {
62
+ const blockStatementNode = node.type === "FunctionDeclaration" ? node.body : node.init.body;
63
+ const injectNode = getInjectNode(context, blockStatementNode);
64
+ const indent = getLeadingSpaces(context, injectNode);
65
+ return fixer.insertTextBeforeRange(injectNode.range, `${injectHooksCall}
66
+ ${indent}`);
67
+ };
68
+ }
69
+ function generateInjectCodeToTopLevelFixFun(context, codeType) {
70
+ const { injectCodeToTopLevel } = context.settings.i18nInfo[codeType] || {};
71
+ if (!injectCodeToTopLevel) return null;
72
+ return (fixer) => {
73
+ const res = getLastImportOrFirstNotImportNode(context);
74
+ if (!res) return null;
75
+ const { node: injectNode, type } = res;
76
+ const indent = getLeadingSpaces(context, injectNode);
77
+ if (type === "firstNonImport") {
78
+ return fixer.insertTextBeforeRange(injectNode.range, `${injectCodeToTopLevel}
79
+ ${indent}`);
80
+ }
81
+ return fixer.insertTextAfterRange(injectNode.range, `
82
+ ${indent}${injectCodeToTopLevel}`);
83
+ };
84
+ }
85
+ function batchFixAll(context, state) {
86
+ if (!state.inComponent.length && !state.outComponent.length) return;
87
+ const injectHasInjectCompOrHooksMap = /* @__PURE__ */ new Map();
88
+ const fixArr = [...state.inComponent, ...state.outComponent].map((item) => generateReplaceKeyFixFun(context, item.textNode, item.inComp ? "jsx" : "js"));
89
+ if (state.outComponent.length && canInject(context, context.sourceCode.ast, "js")) {
90
+ fixArr.unshift(generateInjectImportFixFun(context, "js"));
91
+ fixArr.push(generateInjectCodeToTopLevelFixFun(context, "js"));
92
+ }
93
+ for (const { componentNode } of state.inComponent) {
94
+ const name = componentNode.id.name;
95
+ if (!injectHasInjectCompOrHooksMap.has(name) && canInject(context, componentNode, "jsx")) {
96
+ fixArr.push(generateInjectHooksCallFixFun(context, componentNode));
97
+ injectHasInjectCompOrHooksMap.set(name, componentNode);
98
+ }
99
+ }
100
+ if (injectHasInjectCompOrHooksMap.size) {
101
+ fixArr.unshift(generateInjectImportFixFun(context, "jsx"));
102
+ fixArr.push(generateInjectCodeToTopLevelFixFun(context, "jsx"));
103
+ }
104
+ const fixes = fixArr.filter((f) => f !== null);
105
+ if (!fixes.length) return;
106
+ context.report({
107
+ loc: context.sourceCode.ast.loc,
108
+ message: "存在中文字符,可以进行国际化替换",
109
+ fix: (fixer) => fixes.map((f) => f(fixer)).filter(Boolean)
110
+ });
111
+ }
112
+ export {
113
+ batchFixAll,
114
+ generateInjectCodeToTopLevelFixFun,
115
+ generateInjectHooksCallFixFun,
116
+ generateInjectImportFixFun,
117
+ generateReplaceKeyFixFun
118
+ };
@@ -0,0 +1,5 @@
1
+ import type { ESLint } from 'eslint';
2
+ import { BaseCodeType, TextNode } from '../../type.js';
3
+ export declare type CollectNodes = (textNode: TextNode, codeType: Extract<BaseCodeType, 'js' | 'jsx' | 'script' | 'template'>) => void;
4
+ declare const VuePlugin: ESLint.Plugin;
5
+ export default VuePlugin;
@@ -0,0 +1,141 @@
1
+ // src/eslint/vue/i18n_easy_plugin.ts
2
+ import { batchFixAll } from "./report_and_fix.js";
3
+ import { createI18nIgnoreChecker, canCollectOperateNodes, isScriptSetup } from "./i18n_utils.js";
4
+ var DiFF_FILE_JS_CODE_MAP = {
5
+ vue: "script",
6
+ js: "js",
7
+ jsx: "jsx",
8
+ ts: "js",
9
+ tsx: "jsx"
10
+ };
11
+ var VuePlugin = {
12
+ rules: {
13
+ "i18n-transform": {
14
+ meta: {
15
+ type: "problem",
16
+ messages: { hasChinese: "当前文件存在中文字符,可进行国际化替换" },
17
+ fixable: "code"
18
+ },
19
+ create(context) {
20
+ const fileType = context.settings.fileType;
21
+ const customIgnore = context.settings.customIgnore;
22
+ const state = {
23
+ templateTextNodeList: [],
24
+ scriptTextNodeList: [],
25
+ jsTextNodeList: [],
26
+ jsxTextNodeList: []
27
+ };
28
+ let inSetup = false;
29
+ let setupNode = null;
30
+ let isIgnored;
31
+ const enterSetup = (node) => {
32
+ setupNode = node;
33
+ inSetup = true;
34
+ };
35
+ const leaveSetup = () => {
36
+ setupNode = null;
37
+ inSetup = false;
38
+ };
39
+ const collectNodes = (textNode, codeType) => {
40
+ if (customIgnore == null ? void 0 : customIgnore(textNode, context)) {
41
+ return;
42
+ }
43
+ if (!canCollectOperateNodes(textNode, isIgnored)) return;
44
+ switch (codeType) {
45
+ case "template":
46
+ state.templateTextNodeList.push({ textNode, setUpNode: null, inSetup: false, codeType });
47
+ break;
48
+ case "script":
49
+ state.scriptTextNodeList.push({ textNode, setUpNode: inSetup ? setupNode : null, inSetup, codeType });
50
+ break;
51
+ case "js":
52
+ state.jsTextNodeList.push({ textNode, setUpNode: null, inSetup: false, codeType });
53
+ break;
54
+ case "jsx":
55
+ state.jsxTextNodeList.push({ textNode, setUpNode: inSetup ? setupNode : null, inSetup, codeType });
56
+ break;
57
+ }
58
+ };
59
+ const collectTemplateChineseTextNodeVisitor = {
60
+ // 文本节点
61
+ VText(node) {
62
+ collectNodes(node, "template");
63
+ },
64
+ // 属性节点
65
+ VAttribute(node) {
66
+ collectNodes(node, "template");
67
+ },
68
+ // 模板字符串节点
69
+ TemplateLiteral(node) {
70
+ collectNodes(node, "template");
71
+ },
72
+ // 字面量节点
73
+ Literal(node) {
74
+ collectNodes(node, "template");
75
+ }
76
+ };
77
+ const collectScriptChineseTextNodeVisitor = {
78
+ // 字面量节点
79
+ Literal(node) {
80
+ collectNodes(node, DiFF_FILE_JS_CODE_MAP[fileType]);
81
+ },
82
+ // 模版字符串节点
83
+ TemplateLiteral(node) {
84
+ collectNodes(node, DiFF_FILE_JS_CODE_MAP[fileType]);
85
+ },
86
+ // JSX文本节点
87
+ JSXText(node) {
88
+ collectNodes(node, DiFF_FILE_JS_CODE_MAP[fileType]);
89
+ }
90
+ };
91
+ const replaceNodeAndInjectCodeVisitor = {
92
+ /**
93
+ * 进入时
94
+ * 1、记录@i18n-ignore注释所在行号
95
+ * 2、如果是vue文件,判断是否是否存在setup函数(如果存在,记录setup函数节点)
96
+ */
97
+ Program: function(node) {
98
+ isIgnored = createI18nIgnoreChecker(context, node.templateBody);
99
+ if (fileType === "vue" && isScriptSetup(context)) {
100
+ enterSetup(node);
101
+ }
102
+ },
103
+ // 选择器匹配setup函数节点
104
+ 'ExportDefaultDeclaration > ObjectExpression > Property[key.name="setup"] > :function': function(node) {
105
+ if (fileType === "vue" || fileType === "jsx") {
106
+ enterSetup(node);
107
+ }
108
+ },
109
+ 'ExportDefaultDeclaration > ObjectExpression > Property[key.name="setup"] > :function:exit': leaveSetup,
110
+ 'ExportDefaultDeclaration > CallExpression[callee.name="defineComponent"] > ObjectExpression > Property[key.name="setup"] > :function': function(node) {
111
+ if (fileType === "vue" || fileType === "jsx") {
112
+ enterSetup(node);
113
+ }
114
+ },
115
+ 'ExportDefaultDeclaration > CallExpression[callee.name="defineComponent"] > ObjectExpression > Property[key.name="setup"] > :function:exit': leaveSetup,
116
+ /**
117
+ * 退出时,批量进行修复
118
+ */
119
+ "Program:exit": async function() {
120
+ batchFixAll(context, state);
121
+ }
122
+ };
123
+ return context.sourceCode.parserServices.defineTemplateBodyVisitor(
124
+ collectTemplateChineseTextNodeVisitor,
125
+ {
126
+ ...collectScriptChineseTextNodeVisitor,
127
+ ...replaceNodeAndInjectCodeVisitor
128
+ },
129
+ {
130
+ templateBodyTriggerSelector: "Program"
131
+ // 执行顺序 Program -> templateBodyVisitor -> scriptBodyVisitor -> Program:exit
132
+ }
133
+ );
134
+ }
135
+ }
136
+ }
137
+ };
138
+ var i18n_easy_plugin_default = VuePlugin;
139
+ export {
140
+ i18n_easy_plugin_default as default
141
+ };
@@ -0,0 +1,65 @@
1
+ import type { Rule } from 'eslint';
2
+ import type { AST } from 'vue-eslint-parser';
3
+ import * as ESTree from 'estree';
4
+ import { TextNode, SetUpNode, CodeType } from '../../type.js';
5
+ /**
6
+ * @description 创建i18n-ignore注释检查器
7
+ * @param context - ESLint规则上下文
8
+ * @param templateBody - Vue模板体节点
9
+ * @returns 检查节点是否被忽略的函数
10
+ */
11
+ export declare function createI18nIgnoreChecker(context: Rule.RuleContext, templateBody: AST.VElement & AST.HasConcreteInfo): (node: any) => boolean;
12
+ /**
13
+ * @description 判断能否注入
14
+ * @param context - ESLint规则上下文
15
+ * @param node - Program节点、FunctionExpression节点、ArrowFunctionExpression节点
16
+ * @param codeType - 代码类型
17
+ * @returns 是否能注入
18
+ */
19
+ export declare function callNameNotExist(context: Rule.RuleContext, node: SetUpNode, codeType: Exclude<CodeType, 'template'>): boolean;
20
+ /**
21
+ * 1、不存在节点返回null
22
+ * 2、存在import节点,返回最后一个import节点
23
+ * 3、不存在import节点,返回第一个非import节点
24
+ * @description 获取最后一个 import 或第一个非 import 节点
25
+ * @param context - ESLint规则上下文
26
+ * @returns 最后一个 import 节点或第一个非 import 节点或 null
27
+ */
28
+ export declare function getLastImportOrFirstNotImportNode(context: Rule.RuleContext): {
29
+ node: ESTree.Node;
30
+ type: string;
31
+ } | null;
32
+ /**
33
+ * @description 获取hooks注入节点
34
+ * @param context - ESLint规则上下文
35
+ * @param node - BlockStatement节点或Program节点
36
+ * @returns 注入节点或null
37
+ */
38
+ export declare function getInjectNode(context: Rule.RuleContext, node: ESTree.BlockStatement | ESTree.Program): ESTree.ModuleDeclaration | ESTree.Statement | ESTree.Comment;
39
+ /**
40
+ * 判断能否收集操作节点
41
+ * 排除:
42
+ * 1、使用@i18n-ignore注释的节点
43
+ * 2、未使用中文的节点
44
+ * @description 判断能否收集操作节点
45
+ * @param node - TextNode节点
46
+ * @param isIgnored - 判断节点是否被忽略的函数
47
+ * @returns 是否能收集操作节点
48
+ */
49
+ export declare function canCollectOperateNodes(node: TextNode, isIgnored: (n: TextNode) => boolean): boolean;
50
+ /**
51
+ * @description 获取节点前的空格
52
+ * @param context - ESLint规则上下文
53
+ * @param node - 节点
54
+ * @returns 节点前的空格
55
+ */
56
+ export declare function getLeadingSpaces(context: Rule.RuleContext, node: ESTree.Node | ESTree.Comment): string;
57
+ /**
58
+ * @description 获取国际化调用方法
59
+ * @param callMethodName - 国际化调用方法
60
+ * @param key - 国际化键值
61
+ * @param value - 国际化参数
62
+ * @returns 国际化调用方法
63
+ */
64
+ export declare function getI18nCallMethod(callMethodName: string, key: string, value?: string): string;
65
+ export declare function isScriptSetup(context: Rule.RuleContext): boolean;