td-octopus 0.0.17 → 0.0.20
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/README.md +2 -1
- package/package.json +1 -1
- package/src/back/index.js +149 -142
- package/src/extract/findChineseText.js +17 -9
- package/src/extract/replace.js +33 -1
- package/src/utils/syncLang.js +2 -3
package/README.md
CHANGED
|
@@ -49,7 +49,8 @@ copyright 2022 同盾
|
|
|
49
49
|
"importI18N": "import I18N from 'src/utils/I18N';",
|
|
50
50
|
"include": [], // 需要翻译的目录&也可以命令行输入参数
|
|
51
51
|
"exclude": [], // 过滤目录&文件
|
|
52
|
-
"reservedKey": ["template", "case"] // js关键词以及I18N内置方法不能作为目录名,会统一添加td作为前缀
|
|
52
|
+
"reservedKey": ["template", "case"], // js关键词以及I18N内置方法不能作为目录名,会统一添加td作为前缀
|
|
53
|
+
"tabSize": number // 如果I18N.template内部有多个变量时,用于空格锁进的个数,默认为4
|
|
53
54
|
}
|
|
54
55
|
```
|
|
55
56
|

|
package/package.json
CHANGED
package/src/back/index.js
CHANGED
|
@@ -3,191 +3,198 @@
|
|
|
3
3
|
* @Author: 郑泳健
|
|
4
4
|
* @Date: 2022-06-01 13:56:18
|
|
5
5
|
* @LastEditors: 郑泳健
|
|
6
|
-
* @LastEditTime: 2023-01-
|
|
6
|
+
* @LastEditTime: 2023-01-18 15:25:47
|
|
7
7
|
*/
|
|
8
8
|
const path = require('path')
|
|
9
9
|
const fs = require('fs')
|
|
10
10
|
const ts = require('typescript');
|
|
11
|
+
const shell = require('shelljs');
|
|
11
12
|
const { getSpecifiedFiles, isFile } = require('../utils/file')
|
|
12
13
|
const syncLang = require('../utils/syncLang')
|
|
13
14
|
const { flatObject } = require('../utils/translate');
|
|
14
15
|
const { failInfo, highlightText } = require('../utils/colors');
|
|
15
16
|
const { getProjectConfig } = require('../utils/index')
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
// 匹配I18N.
|
|
19
|
+
const I18N_REGEX = /I18N(\.[a-zA-Z0-9_]+)+/g
|
|
19
20
|
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// 匹配I18N.template()里面的内容
|
|
22
|
+
const TEMPLATE_INNER_REGEX = /I18N\.template\(([\s\S]*?})\)/g
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
// 匹配{}之间的内容
|
|
25
|
+
const JSON_KEY_REGEX = /{([\s\S]*?)}/g
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// 匹配{}之间的value
|
|
28
|
+
const JSON_VALYR_REGEX = /(?<=: )[^,}]+/g
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
/**
|
|
31
|
+
* 获取需要back的所有文件目录
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
function getFilePaths() {
|
|
34
35
|
const CONFIG = getProjectConfig()
|
|
35
36
|
const dirArr = CONFIG.include && CONFIG.include.length > 0 ? CONFIG.include : ['./'];
|
|
36
37
|
let filePaths = []
|
|
37
38
|
|
|
38
39
|
dirArr.forEach(i => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const dirPath = path.resolve(process.cwd(), i);
|
|
41
|
+
const files = getSpecifiedFiles(dirPath)
|
|
42
|
+
filePaths = filePaths.concat(files)
|
|
42
43
|
})
|
|
43
44
|
|
|
44
45
|
return filePaths.filter(file => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
let flag = false;
|
|
47
|
+
for (let index = 0; index < CONFIG.fileSuffix.length; index++) {
|
|
48
|
+
const element = CONFIG.fileSuffix[index];
|
|
49
|
+
flag = file.endsWith(element);
|
|
50
|
+
if (flag) {
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
51
53
|
}
|
|
52
|
-
|
|
53
|
-
return (isFile(file) && flag);
|
|
54
|
+
return (isFile(file) && flag);
|
|
54
55
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 获取I18N.template里面{}每个value对应的value
|
|
60
|
+
* @param {*} str {val1: h, val2: m, val3: s}
|
|
61
|
+
* @returns [h, m, s]
|
|
62
|
+
*/
|
|
63
|
+
function getTemplateValue(str) {
|
|
63
64
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const values = str.match(JSON_VALYR_REGEX)
|
|
66
|
+
return values;
|
|
66
67
|
} catch (e) {
|
|
67
|
-
|
|
68
|
+
throw e;
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 如果文件中存在I18N.template,先转换一下
|
|
74
|
+
* @param {*} code
|
|
75
|
+
* @param {*} flatObj
|
|
76
|
+
* @returns
|
|
77
|
+
*/
|
|
78
|
+
function transformTemplate(filePath, code, flatObj) {
|
|
78
79
|
try {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
const templates = code.match(TEMPLATE_INNER_REGEX) || [];
|
|
81
|
+
let sum = Array.isArray(templates) ? templates.length : 0;
|
|
82
|
+
if (Array.isArray(templates) && templates.length) {
|
|
83
|
+
templates.forEach(i => {
|
|
84
|
+
const [jsonStr] = i ? i.match(JSON_KEY_REGEX) : []
|
|
85
|
+
const keys = getTemplateValue(jsonStr)
|
|
86
|
+
let matchList = getValueByI18N(filePath, i, flatObj) || [];
|
|
87
|
+
matchText = matchList[0] && matchList[0]['value']
|
|
88
|
+
if (matchText) {
|
|
89
|
+
matchText = matchText.replace(/{(val\d+)}/g, (match, group, index) => {
|
|
90
|
+
if (!keys[0]) {
|
|
91
|
+
throw new Error(`${filePath}文件中的 ${highlightText(i)} 转换有问题,请手动检查`)
|
|
92
|
+
}
|
|
93
|
+
const res = '${' + keys[0].trim() + '}'
|
|
94
|
+
keys.splice(0, 1)
|
|
95
|
+
return res
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
code = code.replace(i, '`' + matchText + '`')
|
|
99
|
+
}
|
|
95
100
|
})
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
})
|
|
100
|
-
}
|
|
101
|
-
return { code, sum }
|
|
101
|
+
}
|
|
102
|
+
return { code, sum }
|
|
102
103
|
} catch (e) {
|
|
103
|
-
|
|
104
|
+
throw e;
|
|
104
105
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 获取I18N.xx.xx所对应的中文
|
|
110
|
+
* @param {*} filePath 文件路径
|
|
111
|
+
* @param {*} str
|
|
112
|
+
* @param {*} flatObj
|
|
113
|
+
* @returns { key: I18N.xx, value: '测试' }
|
|
114
|
+
*/
|
|
115
|
+
function getValueByI18N(filePath, str, flatObj) {
|
|
115
116
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const matchList = matchs.reduce((total, item) => {
|
|
122
|
-
if (!item.includes('I18N.template') && !item.includes('I18N.setLang')) {
|
|
123
|
-
const text = flatObj[item.replace('I18N.', '')]
|
|
124
|
-
if (text) {
|
|
125
|
-
total.push({ key: item, value: text })
|
|
126
|
-
} else {
|
|
127
|
-
throw new Error(`${filePath}文件中的 ${highlightText(item)} 未发现有对应的中文文案,请检查`);
|
|
128
|
-
}
|
|
117
|
+
const matchs = str.match(I18N_REGEX);
|
|
118
|
+
if (!Array.isArray(matchs) || !matchs.length) {
|
|
119
|
+
return [];
|
|
129
120
|
}
|
|
130
|
-
return total
|
|
131
|
-
}, [])
|
|
132
121
|
|
|
133
|
-
|
|
122
|
+
const matchList = matchs.reduce((total, item) => {
|
|
123
|
+
if (!['I18N.template', 'I18N.setLang'].includes(item)) {
|
|
124
|
+
const text = flatObj[item.replace('I18N.', '')]
|
|
125
|
+
if (text) {
|
|
126
|
+
total.push({ key: item, value: text })
|
|
127
|
+
} else {
|
|
128
|
+
throw new Error(`${filePath}文件中的 ${highlightText(item)} 未发现有对应的中文文案,请检查`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return total
|
|
132
|
+
}, [])
|
|
133
|
+
|
|
134
|
+
return matchList
|
|
134
135
|
} catch (e) {
|
|
135
|
-
|
|
136
|
+
throw e;
|
|
136
137
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 获取需要被重写的文件
|
|
142
|
+
* @param {*} filePath 文件路径
|
|
143
|
+
* @param {*} flatObj 所有key对应的中文
|
|
144
|
+
* @returns
|
|
145
|
+
*/
|
|
146
|
+
function getNeedRewriteFiles(filePath, flatObj) {
|
|
146
147
|
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
const str = fs.readFileSync(filePath, 'utf-8');
|
|
149
|
+
let { code, sum } = transformTemplate(filePath, str, flatObj)
|
|
150
|
+
|
|
151
|
+
let matchList = getValueByI18N(filePath, code, flatObj) || [];
|
|
152
|
+
if (Array.isArray(matchList) && matchList.length) {
|
|
153
|
+
sum += matchList.length;
|
|
154
|
+
matchList.forEach(({ key, value }) => {
|
|
155
|
+
// 兼容翻译的文案中有\n \r \t 这种情况
|
|
156
|
+
value = value.replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
157
|
+
const replaceVal = value && value.includes("'") ? '"' + value + '"' : "'" + value + "'"
|
|
158
|
+
code = code.replace(key, replaceVal)
|
|
159
|
+
})
|
|
160
|
+
}
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
return {
|
|
163
|
+
filePath,
|
|
164
|
+
code,
|
|
165
|
+
sum
|
|
166
|
+
}
|
|
164
167
|
} catch (e) {
|
|
165
|
-
|
|
168
|
+
throw e;
|
|
166
169
|
}
|
|
167
|
-
|
|
170
|
+
}
|
|
168
171
|
|
|
169
|
-
|
|
170
|
-
|
|
172
|
+
// 同步不同的语言包
|
|
173
|
+
function back() {
|
|
171
174
|
try {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
175
|
+
const filePaths = getFilePaths()
|
|
176
|
+
const zhCN = syncLang('zh-CN');
|
|
177
|
+
const zhCNFlat = flatObject(zhCN);
|
|
178
|
+
/** 获取需要被重写的文件 */
|
|
179
|
+
const needRewriteFiles = filePaths.map(i => getNeedRewriteFiles(i, zhCNFlat))
|
|
180
|
+
|
|
181
|
+
needRewriteFiles.forEach(({ filePath, code, sum }) => {
|
|
182
|
+
if (sum > 0) {
|
|
183
|
+
console.log(`正在对 ${highlightText(filePath)} 文件进行回滚操作, 共计${highlightText(sum)}处`);
|
|
184
|
+
|
|
185
|
+
fs.writeFileSync(filePath, code)
|
|
186
|
+
console.log(`${highlightText(filePath)} 文件回滚完毕`);
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
console.log('正在删除zh-CN文件夹下的文件');
|
|
191
|
+
shell.rm('-rf', path.resolve(process.cwd(), './.octopus/zh-CN/*'));
|
|
192
|
+
console.log('已成功删除zh-CN文件夹下的文件');
|
|
186
193
|
} catch (e) {
|
|
187
|
-
|
|
194
|
+
failInfo(e.message)
|
|
188
195
|
}
|
|
189
|
-
|
|
196
|
+
}
|
|
190
197
|
|
|
191
|
-
|
|
198
|
+
module.exports = {
|
|
192
199
|
back
|
|
193
|
-
|
|
200
|
+
}
|
|
@@ -83,10 +83,12 @@ function visit(node) {
|
|
|
83
83
|
const start = node.getStart();
|
|
84
84
|
const end = node.getEnd();
|
|
85
85
|
const range = { start, end };
|
|
86
|
+
const { line } = ast.getLineAndCharacterOfPosition(start)
|
|
86
87
|
matches.push({
|
|
87
88
|
range,
|
|
88
89
|
text,
|
|
89
|
-
isString: true
|
|
90
|
+
isString: true,
|
|
91
|
+
line
|
|
90
92
|
});
|
|
91
93
|
}
|
|
92
94
|
break;
|
|
@@ -104,11 +106,12 @@ function visit(node) {
|
|
|
104
106
|
const start = child.getStart();
|
|
105
107
|
const end = child.getEnd();
|
|
106
108
|
const range = { start, end };
|
|
107
|
-
|
|
109
|
+
const { line } = ast.getLineAndCharacterOfPosition(start)
|
|
108
110
|
matches.push({
|
|
109
111
|
range,
|
|
110
112
|
text: text.trim(),
|
|
111
|
-
isString: false
|
|
113
|
+
isString: false,
|
|
114
|
+
line
|
|
112
115
|
});
|
|
113
116
|
}
|
|
114
117
|
}
|
|
@@ -123,10 +126,12 @@ function visit(node) {
|
|
|
123
126
|
const start = node.getStart();
|
|
124
127
|
const end = node.getEnd();
|
|
125
128
|
const range = { start, end };
|
|
129
|
+
const { line } = ast.getLineAndCharacterOfPosition(start)
|
|
126
130
|
matches.push({
|
|
127
131
|
range,
|
|
128
132
|
text: code.slice(start + 1, end - 1),
|
|
129
|
-
isString: true
|
|
133
|
+
isString: true,
|
|
134
|
+
line
|
|
130
135
|
});
|
|
131
136
|
}
|
|
132
137
|
break;
|
|
@@ -139,10 +144,12 @@ function visit(node) {
|
|
|
139
144
|
const start = node.getStart();
|
|
140
145
|
const end = node.getEnd();
|
|
141
146
|
const range = { start, end };
|
|
147
|
+
const { line } = ast.getLineAndCharacterOfPosition(start)
|
|
142
148
|
matches.push({
|
|
143
149
|
range,
|
|
144
150
|
text: code.slice(start + 1, end - 1),
|
|
145
|
-
isString: true
|
|
151
|
+
isString: true,
|
|
152
|
+
line
|
|
146
153
|
});
|
|
147
154
|
}
|
|
148
155
|
break;
|
|
@@ -157,19 +164,20 @@ function visit(node) {
|
|
|
157
164
|
const start = node.getStart();
|
|
158
165
|
const end = node.getEnd();
|
|
159
166
|
const range = { start, end };
|
|
160
|
-
|
|
167
|
+
const { line } = ast.getLineAndCharacterOfPosition(start)
|
|
161
168
|
matches.push({
|
|
162
169
|
range,
|
|
163
170
|
text: text.trim(),
|
|
164
|
-
isString: false
|
|
171
|
+
isString: false,
|
|
172
|
+
line
|
|
165
173
|
});
|
|
166
174
|
}
|
|
167
175
|
}
|
|
168
176
|
}
|
|
169
177
|
|
|
170
|
-
ts.forEachChild(node, visit);
|
|
178
|
+
ts.forEachChild(node, (node) => visit(node, ast));
|
|
171
179
|
}
|
|
172
|
-
ts.forEachChild(ast, visit);
|
|
180
|
+
ts.forEachChild(ast, (node) => visit(node, ast));
|
|
173
181
|
// 过滤相同的选项
|
|
174
182
|
return matches.reduce((total, item) => {
|
|
175
183
|
const bool = total.some(i => _.isEqual(i, item));
|
package/src/extract/replace.js
CHANGED
|
@@ -156,6 +156,29 @@ function createImportI18N(filePath) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* 判断I18N.template所在行前面有多少个空格/tab,以及类型是tab还是空格
|
|
161
|
+
* @param {*} code
|
|
162
|
+
* @param {*} text
|
|
163
|
+
* @returns { tabSize: number, isSpace: bool } || null
|
|
164
|
+
*/
|
|
165
|
+
function getLineTableSize(code, line){
|
|
166
|
+
try{
|
|
167
|
+
const list = code ? code.split('\n') : []
|
|
168
|
+
const str = list[line];
|
|
169
|
+
// 用于判断是空格还是space
|
|
170
|
+
const match = str.match(/^[ \t]+/);
|
|
171
|
+
// 判断空格/tab的个数
|
|
172
|
+
let spaces = str.match(/^\s+/);
|
|
173
|
+
return {
|
|
174
|
+
tabSize: spaces[0].length,
|
|
175
|
+
isSpace: match[0].startsWith(" ")
|
|
176
|
+
}
|
|
177
|
+
}catch(e) {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
159
182
|
/**
|
|
160
183
|
* 更新文件
|
|
161
184
|
* @param filePath 当前文件路径
|
|
@@ -193,7 +216,16 @@ function replaceAndUpdate(filePath, arg, val, validateDuplicate, needWrite = tru
|
|
|
193
216
|
const kvPair = varInStr.map((str, index) => {
|
|
194
217
|
return `val${index + 1}: ${str.replace(/^\${([^\}]+)\}$/, '$1')}`;
|
|
195
218
|
});
|
|
196
|
-
|
|
219
|
+
|
|
220
|
+
// 多个变量情况下格式化
|
|
221
|
+
const { tabSize, isSpace } = getLineTableSize(code, arg.line) || {}
|
|
222
|
+
if(kvPair.length > 1 && !isNaN(tabSize)) {
|
|
223
|
+
const space = '\n' + (isSpace ? " " : "\t").repeat(tabSize + (isSpace ? (CONFIG.tabSize || 4) : 1))
|
|
224
|
+
const startSpace = '\n' + (isSpace ? " " : "\t").repeat(tabSize)
|
|
225
|
+
finalReplaceVal = `I18N.template(${val}, {${space}${kvPair.join(`,${space}`)}${startSpace}})`;
|
|
226
|
+
}else{
|
|
227
|
+
finalReplaceVal = `I18N.template(${val}, { ${kvPair.join(',\n')} })`;
|
|
228
|
+
}
|
|
197
229
|
|
|
198
230
|
varInStr.forEach((str, index) => {
|
|
199
231
|
finalReplaceText = finalReplaceText.replace(str, `{val${index + 1}}`);
|
package/src/utils/syncLang.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const { otpPath } = require('./translate');
|
|
3
|
+
const { getLangData } = require('../extract/getLangData')
|
|
3
4
|
|
|
4
5
|
module.exports = (lang) => {
|
|
5
6
|
try {
|
|
@@ -8,9 +9,7 @@ module.exports = (lang) => {
|
|
|
8
9
|
list.forEach((i) => {
|
|
9
10
|
const suffixCheck = ['.js', '.ts', '.jsx', 'tsx'].some(it => i.endsWith(it));
|
|
10
11
|
if (suffixCheck && !['index.js', 'index.jsx', 'index.ts', 'index.tsx'].includes(i)) {
|
|
11
|
-
const
|
|
12
|
-
const replaceStr = str.replace(/export default|\;/g, '');
|
|
13
|
-
const json = eval("(" + replaceStr + ")");
|
|
12
|
+
const json = getLangData(`${otpPath}/${lang}/${i}`)
|
|
14
13
|
const key = i.split('.')[0];
|
|
15
14
|
|
|
16
15
|
langMap[key] = json;
|