td-octopus 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/transLang/single.js +133 -66
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "td-octopus",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "I18N tool",
|
|
5
5
|
"author": "Anthony Li",
|
|
6
6
|
"bin": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"colors": "^1.4.0",
|
|
18
18
|
"eslint": "^8.15.0",
|
|
19
19
|
"eslint-plugin-prettier": "^4.2.1",
|
|
20
|
+
"eslint-plugin-td-rules-plugin": "^1.0.1",
|
|
20
21
|
"eslint-plugin-unused-imports": "^4.3.0",
|
|
21
22
|
"glob": "^8.0.3",
|
|
22
23
|
"google-translate": "^3.0.0",
|
package/src/transLang/single.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
const fs = require('fs');
|
|
3
2
|
const path = require('path');
|
|
4
3
|
const parser = require('@babel/parser');
|
|
@@ -9,36 +8,96 @@ const { ESLint } = require('eslint');
|
|
|
9
8
|
const prettier = require('eslint-plugin-prettier');
|
|
10
9
|
const unusedImports = require('eslint-plugin-unused-imports');
|
|
11
10
|
|
|
12
|
-
// =====
|
|
13
|
-
|
|
11
|
+
// ===== 工具函数:判断 key 是否匹配指定名称 =====
|
|
12
|
+
function isKeyNamed(node, name) {
|
|
13
|
+
if (t.isIdentifier(node)) {
|
|
14
|
+
return node.name === name;
|
|
15
|
+
}
|
|
14
16
|
if (t.isStringLiteral(node)) {
|
|
15
|
-
return node.value;
|
|
17
|
+
return node.value === name;
|
|
16
18
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractCnKeyDirect(node) {
|
|
23
|
+
if (t.isStringLiteral(node)) {
|
|
24
|
+
return node.value;
|
|
19
25
|
}
|
|
26
|
+
|
|
20
27
|
if (t.isMemberExpression(node)) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
28
|
+
let parts = [];
|
|
29
|
+
let current = node;
|
|
30
|
+
|
|
31
|
+
while (t.isMemberExpression(current)) {
|
|
32
|
+
if (current.computed) return null;
|
|
33
|
+
const prop = current.property;
|
|
34
|
+
if (!t.isIdentifier(prop)) return null;
|
|
35
|
+
parts.unshift(prop.name);
|
|
36
|
+
current = current.object;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (t.isIdentifier(current) && current.name === 'I18N') {
|
|
40
|
+
parts.unshift('I18N');
|
|
41
|
+
} else {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (parts.length === 4) {
|
|
46
|
+
return parts.join('.');
|
|
25
47
|
}
|
|
26
|
-
return `${objectStr}.${propertyStr}`;
|
|
27
48
|
}
|
|
28
|
-
|
|
49
|
+
|
|
50
|
+
return null;
|
|
29
51
|
}
|
|
30
52
|
|
|
31
|
-
|
|
53
|
+
// ===== 提取 cn 的 key(仅支持:字符串字面量 或 I18N.a.b.c 四层)=====
|
|
54
|
+
function extractCnKey(node, scope) {
|
|
55
|
+
// 情况1: 字符串字面量
|
|
56
|
+
if (t.isStringLiteral(node)) {
|
|
57
|
+
return node.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 情况2: Identifier → 查找其定义
|
|
32
61
|
if (t.isIdentifier(node)) {
|
|
33
|
-
|
|
62
|
+
const binding = scope.getBinding(node.name);
|
|
63
|
+
|
|
64
|
+
if (binding && binding.path.isVariableDeclarator()) {
|
|
65
|
+
const init = binding.path.node.init;
|
|
66
|
+
|
|
67
|
+
if (init) {
|
|
68
|
+
// 递归解析初始化表达式(不再传 scope,避免无限递归)
|
|
69
|
+
return extractCnKeyDirect(init);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
34
73
|
}
|
|
74
|
+
|
|
75
|
+
// 情况3: 检查是否为 I18N.a.b.c(必须是四层 MemberExpression,非 computed)
|
|
76
|
+
return extractCnKeyDirect(node);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function extractEnValue(node, scope) {
|
|
80
|
+
// 直接是字符串字面量
|
|
35
81
|
if (t.isStringLiteral(node)) {
|
|
36
|
-
return node.value
|
|
82
|
+
return node.value;
|
|
37
83
|
}
|
|
38
|
-
|
|
84
|
+
|
|
85
|
+
// 是变量(Identifier),尝试回溯
|
|
86
|
+
if (t.isIdentifier(node)) {
|
|
87
|
+
const binding = scope.getBinding(node.name);
|
|
88
|
+
if (binding && binding.path.isVariableDeclarator()) {
|
|
89
|
+
const init = binding.path.node.init;
|
|
90
|
+
if (init && t.isStringLiteral(init)) {
|
|
91
|
+
return init.value;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 其他情况(表达式、模板、多变量等)不支持
|
|
97
|
+
return null;
|
|
39
98
|
}
|
|
40
99
|
|
|
41
|
-
// =====
|
|
100
|
+
// ===== 单文件转换函数 =====
|
|
42
101
|
const transformToChinese = (code, globalList) => {
|
|
43
102
|
let localList = {}; // 每个文件独立的 list
|
|
44
103
|
|
|
@@ -49,57 +108,64 @@ const transformToChinese = (code, globalList) => {
|
|
|
49
108
|
|
|
50
109
|
const langVariableNames = new Set();
|
|
51
110
|
|
|
52
|
-
//
|
|
111
|
+
// 第一阶段:收集 getLang / getLanguage 的变量名
|
|
53
112
|
traverse(ast, {
|
|
54
113
|
VariableDeclarator(path) {
|
|
55
114
|
const { id, init } = path.node;
|
|
56
|
-
if (
|
|
115
|
+
if (
|
|
116
|
+
t.isIdentifier(id) &&
|
|
117
|
+
t.isCallExpression(init) &&
|
|
118
|
+
t.isIdentifier(init.callee) &&
|
|
119
|
+
['getLanguage', 'getLang'].includes(init.callee.name)
|
|
120
|
+
) {
|
|
57
121
|
langVariableNames.add(id.name);
|
|
58
122
|
}
|
|
59
123
|
}
|
|
60
124
|
});
|
|
61
125
|
|
|
62
|
-
//
|
|
126
|
+
// 第二阶段:处理对象和成员表达式
|
|
63
127
|
traverse(ast, {
|
|
64
128
|
ObjectExpression(path) {
|
|
65
129
|
const props = path.node.properties;
|
|
66
130
|
if (props.length === 0) return;
|
|
67
131
|
|
|
68
132
|
const cnProp = props.find(
|
|
69
|
-
(prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key,
|
|
70
|
-
|
|
133
|
+
(prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'cn') && !prop.method
|
|
71
134
|
);
|
|
72
|
-
|
|
73
135
|
const enProp = props.find(
|
|
74
|
-
(prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'en') && !prop.method
|
|
136
|
+
(prop) => t.isObjectProperty(prop) && !prop.computed && isKeyNamed(prop.key, 'en') && !prop.method
|
|
75
137
|
);
|
|
76
|
-
|
|
138
|
+
|
|
139
|
+
// 仅当 en 是字符串字面量,且 cn 符合提取规则时,才记录
|
|
77
140
|
if (cnProp && enProp) {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
141
|
+
const enStr = extractEnValue(enProp.value, path.scope);
|
|
142
|
+
const cnKey = extractCnKey(cnProp.value, path.scope);
|
|
143
|
+
|
|
144
|
+
if (cnKey !== null && enStr !== null) {
|
|
145
|
+
localList[cnKey] = enStr;
|
|
146
|
+
}
|
|
84
147
|
}
|
|
85
148
|
},
|
|
86
149
|
|
|
87
150
|
MemberExpression(path) {
|
|
88
151
|
const node = path.node;
|
|
89
|
-
if (
|
|
90
|
-
path.replaceWith(node.object);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
152
|
+
if (!node.computed) return;
|
|
93
153
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
154
|
+
const isLangAccess =
|
|
155
|
+
(t.isIdentifier(node.property) && langVariableNames.has(node.property.name)) ||
|
|
156
|
+
(t.isCallExpression(node.property) &&
|
|
157
|
+
t.isIdentifier(node.property.callee) &&
|
|
158
|
+
['getLang', 'getLanguage'].includes(node.property.callee.name));
|
|
159
|
+
|
|
160
|
+
if (isLangAccess) {
|
|
161
|
+
// 替换为 obj['cn']
|
|
162
|
+
path.replaceWith(
|
|
163
|
+
t.memberExpression(node.object, t.stringLiteral('cn'), true)
|
|
164
|
+
);
|
|
100
165
|
}
|
|
101
166
|
},
|
|
102
167
|
|
|
168
|
+
// 清理无用的 lang 变量声明
|
|
103
169
|
VariableDeclarator(path) {
|
|
104
170
|
const { id } = path.node;
|
|
105
171
|
if (t.isIdentifier(id) && langVariableNames.has(id.name)) {
|
|
@@ -112,16 +178,26 @@ const transformToChinese = (code, globalList) => {
|
|
|
112
178
|
}
|
|
113
179
|
}
|
|
114
180
|
}
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
Identifier(path) {
|
|
184
|
+
if (langVariableNames.has(path.node.name)) {
|
|
185
|
+
const parent = path.parentPath;
|
|
186
|
+
if (parent.isExpressionStatement()) {
|
|
187
|
+
parent.remove();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
115
190
|
}
|
|
116
191
|
});
|
|
117
192
|
|
|
118
|
-
// 第三阶段:清理 getLanguage 导入
|
|
193
|
+
// 第三阶段:清理 getLang / getLanguage 导入
|
|
119
194
|
traverse(ast, {
|
|
120
195
|
ImportDeclaration(path) {
|
|
121
196
|
const specifiers = path.node.specifiers;
|
|
122
197
|
const newSpecifiers = specifiers.filter((spec) => {
|
|
123
198
|
if (t.isImportSpecifier(spec)) {
|
|
124
|
-
return !t.isIdentifier(spec.imported, { name: 'getLanguage' })
|
|
199
|
+
return !t.isIdentifier(spec.imported, { name: 'getLanguage' }) &&
|
|
200
|
+
!t.isIdentifier(spec.imported, { name: 'getLang' });
|
|
125
201
|
}
|
|
126
202
|
return true;
|
|
127
203
|
});
|
|
@@ -140,19 +216,16 @@ const transformToChinese = (code, globalList) => {
|
|
|
140
216
|
comments: false
|
|
141
217
|
}).code;
|
|
142
218
|
|
|
143
|
-
//
|
|
144
|
-
|
|
219
|
+
// 合并到全局翻译表
|
|
145
220
|
Object.assign(globalList, localList);
|
|
146
221
|
return transformedCode;
|
|
147
|
-
}
|
|
222
|
+
};
|
|
148
223
|
|
|
149
|
-
// ===== ESLint
|
|
224
|
+
// ===== ESLint 实例(用于格式化和清理未使用导入)=====
|
|
150
225
|
const eslint = new ESLint({
|
|
151
226
|
fix: true,
|
|
152
227
|
cwd: process.cwd(),
|
|
153
|
-
useEslintrc: false,
|
|
154
228
|
plugins: {
|
|
155
|
-
prettier,
|
|
156
229
|
'unused-imports': unusedImports
|
|
157
230
|
},
|
|
158
231
|
baseConfig: {
|
|
@@ -164,15 +237,8 @@ const eslint = new ESLint({
|
|
|
164
237
|
}
|
|
165
238
|
},
|
|
166
239
|
overrideConfig: {
|
|
167
|
-
|
|
168
|
-
extends: ['eslint:recommended'],
|
|
169
|
-
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
|
|
240
|
+
plugins: ["td-rules-plugin"],
|
|
170
241
|
rules: {
|
|
171
|
-
'no-unused-vars': ['error', { vars: 'all', args: 'none', caughtErrors: 'none', ignoreRestSiblings: true }],
|
|
172
|
-
semi: ['error', 'always'],
|
|
173
|
-
indent: ['error', 4, { SwitchCase: 1 }],
|
|
174
|
-
quotes: ['error', 'single'],
|
|
175
|
-
'comma-dangle': ['error', 'never'],
|
|
176
242
|
'prettier/prettier': [
|
|
177
243
|
1,
|
|
178
244
|
{
|
|
@@ -182,14 +248,14 @@ const eslint = new ESLint({
|
|
|
182
248
|
singleQuote: true,
|
|
183
249
|
tabWidth: 4,
|
|
184
250
|
trailingComma: 'none',
|
|
185
|
-
jsxBracketSameLine: true
|
|
251
|
+
jsxBracketSameLine: true,
|
|
186
252
|
}
|
|
187
253
|
]
|
|
188
254
|
}
|
|
189
255
|
}
|
|
190
256
|
});
|
|
191
257
|
|
|
192
|
-
// ===== 递归读取所有 .js
|
|
258
|
+
// ===== 递归读取所有 .js 文件(排除 index.js)=====
|
|
193
259
|
const getAllJsFiles = (dir) => {
|
|
194
260
|
let results = [];
|
|
195
261
|
const files = fs.readdirSync(dir);
|
|
@@ -198,14 +264,13 @@ const getAllJsFiles = (dir) => {
|
|
|
198
264
|
const fullPath = path.join(dir, file);
|
|
199
265
|
const stat = fs.statSync(fullPath);
|
|
200
266
|
if (stat.isDirectory()) {
|
|
201
|
-
results = results.concat(getAllJsFiles(fullPath));
|
|
202
|
-
} else if (file.endsWith('.js') &&
|
|
267
|
+
results = results.concat(getAllJsFiles(fullPath));
|
|
268
|
+
} else if (file.endsWith('.js') && file !== 'index.js') {
|
|
203
269
|
results.push(fullPath);
|
|
204
270
|
}
|
|
205
271
|
}
|
|
206
|
-
|
|
207
272
|
return results;
|
|
208
|
-
}
|
|
273
|
+
};
|
|
209
274
|
|
|
210
275
|
// ===== 处理单个文件 =====
|
|
211
276
|
const processFile = async (filePath, globalList) => {
|
|
@@ -216,9 +281,11 @@ const processFile = async (filePath, globalList) => {
|
|
|
216
281
|
const finalCode = results[0].output || transformedCode;
|
|
217
282
|
|
|
218
283
|
fs.writeFileSync(filePath, finalCode, 'utf-8');
|
|
219
|
-
}
|
|
284
|
+
};
|
|
220
285
|
|
|
286
|
+
// ===== 导出接口 =====
|
|
221
287
|
module.exports = {
|
|
222
288
|
processFile,
|
|
223
|
-
getAllJsFiles
|
|
224
|
-
|
|
289
|
+
getAllJsFiles,
|
|
290
|
+
transformToChinese // 便于单元测试
|
|
291
|
+
};
|