@ansstory/hias 1.0.4 → 1.0.6
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.
Potentially problematic release.
This version of @ansstory/hias might be problematic. Click here for more details.
- package/.gitattributes +1 -1
- package/.vscode/settings.json +8 -0
- package/LICENSE +22 -22
- package/README.md +533 -11
- package/README.zh-CN.md +531 -0
- package/lib/config.js +22 -18
- package/lib/core/action.js +95 -68
- package/lib/core/close-port.js +173 -0
- package/lib/core/commander.js +106 -40
- package/lib/core/download.js +32 -19
- package/lib/core/help.js +17 -6
- package/lib/core/lang.js +50 -0
- package/lib/core/translate.js +1049 -0
- package/lib/extractors/index.js +694 -0
- package/lib/i18n/index.js +77 -0
- package/lib/i18n/resources/en.json +91 -0
- package/lib/i18n/resources/zh-CN.json +91 -0
- package/lib/i18n/store.js +85 -0
- package/lib/index.js +14 -6
- package/lib/template/component.jsx.ejs +11 -11
- package/lib/template/component.tsx.ejs +12 -12
- package/lib/template/component.vue.ejs +13 -13
- package/lib/template/reduxStore.jsx.ejs +16 -16
- package/lib/template/reduxTsStore.tsx.ejs +22 -22
- package/lib/utils/baidu-translate.js +78 -0
- package/lib/utils/compile-ejs.js +28 -21
- package/lib/utils/tencent-translate.js +187 -0
- package/lib/utils/translation-cache.js +73 -0
- package/lib/utils/write-file.js +16 -10
- package/package.json +11 -3
- package/test/close-port.test.js +88 -0
- package/test/translate.test.js +545 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
const test = require('node:test')
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
shortLang,
|
|
9
|
+
sanitizeKey,
|
|
10
|
+
generateKeysFromTranslations,
|
|
11
|
+
generateFallbackKey,
|
|
12
|
+
splitTemplateLiteralSegments,
|
|
13
|
+
splitByByteLength,
|
|
14
|
+
formatI18nCall,
|
|
15
|
+
extractChineseWithPositions,
|
|
16
|
+
extractChineseFromVueTemplateWithPositions,
|
|
17
|
+
stripExistingI18nCalls,
|
|
18
|
+
normalizeExistingCalls,
|
|
19
|
+
applyReplacements,
|
|
20
|
+
applyJsonTranslation,
|
|
21
|
+
mergeAndWriteLocaleFiles,
|
|
22
|
+
collectReferencedKeys,
|
|
23
|
+
saveBackupSync,
|
|
24
|
+
resolveSetting,
|
|
25
|
+
} = require('../lib/core/translate')
|
|
26
|
+
|
|
27
|
+
// ─── Utility functions ───────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
test('shortLang maps zh-CN to zh', () => {
|
|
30
|
+
assert.equal(shortLang('zh-CN'), 'zh')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('shortLang maps ja-JP to jp (baidu mapping)', () => {
|
|
34
|
+
assert.equal(shortLang('ja-JP'), 'jp')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('shortLang maps kor to ko (baidu mapping)', () => {
|
|
38
|
+
assert.equal(shortLang('ko-KR'), 'ko')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('shortLang maps fra to fr (baidu mapping)', () => {
|
|
42
|
+
assert.equal(shortLang('fr-FR'), 'fr')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('shortLang maps en-US to en', () => {
|
|
46
|
+
assert.equal(shortLang('en-US'), 'en')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('shortLang returns zh for falsy input', () => {
|
|
50
|
+
assert.equal(shortLang(null), 'zh')
|
|
51
|
+
assert.equal(shortLang(undefined), 'zh')
|
|
52
|
+
assert.equal(shortLang(''), 'zh')
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('sanitizeKey replaces non-alphanumeric chars with underscore', () => {
|
|
56
|
+
assert.equal(sanitizeKey('Hello World!'), 'hello_world')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('sanitizeKey truncates to maxLen', () => {
|
|
60
|
+
const long = 'a'.repeat(50)
|
|
61
|
+
assert.equal(sanitizeKey(long, 40).length, 40)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('sanitizeKey lowercases the result', () => {
|
|
65
|
+
assert.equal(sanitizeKey('HelloWorld'), 'helloworld')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('generateKeysFromTranslations returns unique keys for each item', () => {
|
|
69
|
+
const keys = generateKeysFromTranslations(['Hello', 'World', 'Foo Bar'])
|
|
70
|
+
assert.equal(keys.length, 3)
|
|
71
|
+
assert.equal(keys[0], 'hello')
|
|
72
|
+
assert.equal(keys[2], 'foo_bar')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('generateKeysFromTranslations deduplicates keys', () => {
|
|
76
|
+
const keys = generateKeysFromTranslations(['Hello', 'hello'])
|
|
77
|
+
assert.equal(keys[0], 'hello')
|
|
78
|
+
assert.ok(keys[1].startsWith('hello'))
|
|
79
|
+
assert.notEqual(keys[0], keys[1])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('generateFallbackKey returns non-empty string', () => {
|
|
83
|
+
const key = generateFallbackKey('你好世界')
|
|
84
|
+
assert.ok(typeof key === 'string')
|
|
85
|
+
assert.ok(key.length > 0)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('splitTemplateLiteralSegments extracts static parts', () => {
|
|
89
|
+
const segs = splitTemplateLiteralSegments('你好${name}世界')
|
|
90
|
+
assert.deepEqual(segs, ['你好', '世界'])
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('splitTemplateLiteralSegments handles no interpolation', () => {
|
|
94
|
+
const segs = splitTemplateLiteralSegments('你好世界')
|
|
95
|
+
assert.deepEqual(segs, ['你好世界'])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('splitTemplateLiteralSegments handles consecutive interpolation', () => {
|
|
99
|
+
const segs = splitTemplateLiteralSegments('${a}${b}中间${c}')
|
|
100
|
+
assert.deepEqual(segs, ['中间'])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('splitTemplateLiteralSegments handles nested braces', () => {
|
|
104
|
+
const segs = splitTemplateLiteralSegments('前缀${obj.arr.map(x => x.name)}后缀')
|
|
105
|
+
assert.deepEqual(segs, ['前缀', '后缀'])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('splitByByteLength splits array into chunks within byte limit', () => {
|
|
109
|
+
const items = ['a'.repeat(100), 'b'.repeat(100), 'c'.repeat(100)]
|
|
110
|
+
const chunks = splitByByteLength(items, 150)
|
|
111
|
+
assert.ok(chunks.length >= 2)
|
|
112
|
+
for (const chunk of chunks) {
|
|
113
|
+
const bytes = Buffer.byteLength(chunk.join(''))
|
|
114
|
+
assert.ok(bytes <= 150, `chunk ${bytes} > 150`)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('splitByByteLength preserves all items across chunks', () => {
|
|
119
|
+
const items = ['中文', 'hello', '测试']
|
|
120
|
+
const chunks = splitByByteLength(items, 100)
|
|
121
|
+
const flattened = chunks.flat()
|
|
122
|
+
assert.deepEqual(flattened, items)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('formatI18nCall uses default $t template', () => {
|
|
126
|
+
const result = formatI18nCall('$t', 'ns.key')
|
|
127
|
+
assert.equal(result, "$t('ns.key')")
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('formatI18nCall uses custom template', () => {
|
|
131
|
+
const result = formatI18nCall('{i18n}', 'ns.key')
|
|
132
|
+
assert.equal(result, "{i18n}('ns.key')")
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// ─── Vue extraction ──────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
test('extractChineseWithPositions Vue template text nodes', () => {
|
|
138
|
+
const content = '<template><div>你好世界</div></template>'
|
|
139
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
140
|
+
assert.equal(result.length, 1)
|
|
141
|
+
assert.ok(result.some(r => r.text === '你好世界'))
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('extractChineseWithPositions Vue template text node with nested children', () => {
|
|
145
|
+
const content = '<template><div>你好<span>子</span>世界</div></template>'
|
|
146
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
147
|
+
assert.ok(result.some(r => r.text === '你好'))
|
|
148
|
+
assert.ok(result.some(r => r.text === '子'))
|
|
149
|
+
assert.ok(result.some(r => r.text === '世界'))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test('extractChineseWithPositions Vue: v-bind pure string attr', () => {
|
|
153
|
+
const content = '<template><div :title="\'中文标题\'"></div></template>'
|
|
154
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
155
|
+
assert.ok(result.some(r => r.text === '中文标题'))
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('extractChineseWithPositions Vue: v-bind expression with Chinese string', () => {
|
|
159
|
+
const content = '<template><div :class="\'active\' + status + \'中文类名\'"></div></template>'
|
|
160
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
161
|
+
assert.ok(result.some(r => r.text === '中文类名'), `should extract '中文类名': ${JSON.stringify(result)}`)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
test('extractChineseWithPositions Vue: v-bind object/array syntax', () => {
|
|
165
|
+
const content = '<template><div :style="{ color: \'红色\' }"></div></template>'
|
|
166
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
167
|
+
assert.ok(result.some(r => r.text === '红色'))
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('extractChineseWithPositions Vue: v-if expression with Chinese', () => {
|
|
171
|
+
const content = '<template><div v-if="type === \'成功\'"></div></template>'
|
|
172
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
173
|
+
assert.ok(result.some(r => r.text === '成功'))
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
test('extractChineseWithPositions Vue: v-if backtick template literal', () => {
|
|
177
|
+
const content = '<template><div v-if="`状态${type}成功`"></div></template>'
|
|
178
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
179
|
+
assert.ok(result.some(r => r.text === '状态'), `should extract '状态' from backtick: ${JSON.stringify(result)}`)
|
|
180
|
+
assert.ok(result.some(r => r.text === '成功'), `should extract '成功' from backtick: ${JSON.stringify(result)}`)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('extractChineseWithPositions Vue: plain attr', () => {
|
|
184
|
+
const content = '<template><div title="中文标题"></div></template>'
|
|
185
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
186
|
+
assert.ok(result.some(r => r.text === '中文标题'))
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test('extractChineseWithPositions Vue: script section with Chinese strings', () => {
|
|
190
|
+
const content = '<template><div></div></template><script>const msg = "脚本中文"</script>'
|
|
191
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
192
|
+
assert.ok(result.some(r => r.text === '脚本中文'))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
test('extractChineseWithPositions Vue: script backtick template literal with interpolation', () => {
|
|
196
|
+
const content = '<template><div></div></template><script>const msg = `状态${type}消息`</script>'
|
|
197
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
198
|
+
assert.ok(result.some(r => r.text === '状态'), `should extract '状态': ${JSON.stringify(result)}`)
|
|
199
|
+
assert.ok(result.some(r => r.text === '消息'), `should extract '消息': ${JSON.stringify(result)}`)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('extractChineseWithPositions Vue: template literal without interpolation', () => {
|
|
203
|
+
const content = '<template><div></div></template><script>const msg = `纯模板字面量中文`</script>'
|
|
204
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
205
|
+
assert.ok(result.some(r => r.text === '纯模板字面量中文'))
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
test('extractChineseWithPositions Vue: do not extract existing i18n calls', () => {
|
|
209
|
+
const content = '<template><div>{{ $t(\'ns.key\') }}</div></template><script>const a = t(\'ns.other\')</script>'
|
|
210
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
211
|
+
assert.equal(result.length, 0, `should not extract existing i18n calls: ${JSON.stringify(result)}`)
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('extractChineseWithPositions Vue: do not extract {{}} expressions', () => {
|
|
215
|
+
const content = '<template><div>{{ count + 1 }}</div></template>'
|
|
216
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
217
|
+
assert.equal(result.length, 0)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
test('extractChineseWithPositions Vue: {{ }} with Chinese string inside', () => {
|
|
221
|
+
const content = '<template><div>{{ "中文" }}</div></template>'
|
|
222
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
223
|
+
assert.equal(result.length, 1, `should extract exactly 1 Chinese text from {{}}: ${JSON.stringify(result)}`)
|
|
224
|
+
assert.ok(result.some(r => r.text === '中文'))
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
test('extractChineseWithPositions Vue: skips template literals with no Chinese', () => {
|
|
228
|
+
const content = '<template><div>{{ `hello ${name}` }}</div></template>'
|
|
229
|
+
const result = extractChineseWithPositions(content, 'vue')
|
|
230
|
+
assert.equal(result.length, 0)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// ─── JS/TS extraction ────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
test('extractChineseWithPositions JS: regular string', () => {
|
|
236
|
+
const content = 'const msg = "你好世界"'
|
|
237
|
+
const result = extractChineseWithPositions(content, 'js')
|
|
238
|
+
assert.ok(result.some(r => r.text === '你好世界'))
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test('extractChineseWithPositions JS: single-quote string', () => {
|
|
242
|
+
const content = "const msg = '你好世界'"
|
|
243
|
+
const result = extractChineseWithPositions(content, 'js')
|
|
244
|
+
assert.ok(result.some(r => r.text === '你好世界'))
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
test('extractChineseWithPositions JS: backtick template literal', () => {
|
|
248
|
+
const content = 'const msg = `你好${name}世界`'
|
|
249
|
+
const result = extractChineseWithPositions(content, 'js')
|
|
250
|
+
assert.ok(result.some(r => r.text === '你好'), `should extract '你好': ${JSON.stringify(result)}`)
|
|
251
|
+
assert.ok(result.some(r => r.text === '世界'), `should extract '世界': ${JSON.stringify(result)}`)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
test('extractChineseWithPositions JS: skips comments', () => {
|
|
255
|
+
const content = '// 中文注释\nconst msg = "actual"'
|
|
256
|
+
const result = extractChineseWithPositions(content, 'js')
|
|
257
|
+
assert.ok(!result.some(r => r.text === '中文注释'))
|
|
258
|
+
assert.ok(!result.some(r => r.text === 'actual'))
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
test('extractChineseWithPositions JS: skips i18n call arguments', () => {
|
|
262
|
+
const content = 't("ns.key")'
|
|
263
|
+
const result = extractChineseWithPositions(content, 'js')
|
|
264
|
+
assert.equal(result.length, 0)
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('extractChineseWithPositions JS: console.log arguments are extracted', () => {
|
|
268
|
+
const content = 'console.log("测试")'
|
|
269
|
+
const result = extractChineseWithPositions(content, 'js')
|
|
270
|
+
assert.equal(result.length, 1, `should extract exactly 1 console.log arg: ${JSON.stringify(result)}`)
|
|
271
|
+
assert.ok(result.some(r => r.text === '测试'))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('extractChineseWithPositions JS: skips debugger statements', () => {
|
|
275
|
+
const content = 'debugger'
|
|
276
|
+
const result = extractChineseWithPositions(content, 'js')
|
|
277
|
+
assert.deepEqual(result, [])
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// ─── JSX extraction ──────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
test('extractChineseWithPositions JSX: text node', () => {
|
|
283
|
+
const content = 'const el = <div>你好世界</div>'
|
|
284
|
+
const result = extractChineseWithPositions(content, 'jsx')
|
|
285
|
+
assert.ok(result.some(r => r.text === '你好世界'))
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('extractChineseWithPositions JSX: text node with nested children', () => {
|
|
289
|
+
const content = 'const el = <div>你好<span>子</span>世界</div>'
|
|
290
|
+
const result = extractChineseWithPositions(content, 'jsx')
|
|
291
|
+
assert.ok(result.some(r => r.text === '你好'))
|
|
292
|
+
assert.ok(result.some(r => r.text === '子'))
|
|
293
|
+
assert.ok(result.some(r => r.text === '世界'))
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
test('extractChineseWithPositions JSX: attribute', () => {
|
|
297
|
+
const content = 'const el = <div title="中文标题"></div>'
|
|
298
|
+
const result = extractChineseWithPositions(content, 'jsx')
|
|
299
|
+
assert.ok(result.some(r => r.text === '中文标题'))
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('extractChineseWithPositions JSX: string expression', () => {
|
|
303
|
+
const content = "const el = <div title={'中文标题'}></div>"
|
|
304
|
+
const result = extractChineseWithPositions(content, 'jsx')
|
|
305
|
+
assert.ok(result.some(r => r.text === '中文标题'))
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// ─── TS/TSX extraction ───────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
test('extractChineseWithPositions TS: regular string', () => {
|
|
311
|
+
const content = 'const msg: string = "你好世界"'
|
|
312
|
+
const result = extractChineseWithPositions(content, 'ts')
|
|
313
|
+
assert.ok(result.some(r => r.text === '你好世界'))
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
test('extractChineseWithPositions TSX: JSX text node', () => {
|
|
317
|
+
const content = 'const el: JSX.Element = <div>你好世界</div>'
|
|
318
|
+
const result = extractChineseWithPositions(content, 'tsx')
|
|
319
|
+
assert.ok(result.some(r => r.text === '你好世界'))
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
// ─── JSON extraction ─────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
test('extractChineseWithPositions JSON: string values', () => {
|
|
325
|
+
const content = JSON.stringify({ title: '中文标题', desc: '中文描述' })
|
|
326
|
+
const result = extractChineseWithPositions(content, 'json')
|
|
327
|
+
assert.ok(result.some(r => r.text === '中文标题'))
|
|
328
|
+
assert.ok(result.some(r => r.text === '中文描述'))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// ─── stripExistingI18nCalls ──────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
test('stripExistingI18nCalls removes $t() calls entirely', () => {
|
|
334
|
+
const input = '{{ $t("ns.key") }} 普通中文'
|
|
335
|
+
const output = stripExistingI18nCalls(input, '$t')
|
|
336
|
+
assert.equal(output.includes('$t'), false)
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('stripExistingI18nCalls removes t() calls entirely', () => {
|
|
340
|
+
const input = '<div>{t("ns.key")}</div> 普通中文'
|
|
341
|
+
const output = stripExistingI18nCalls(input, 't')
|
|
342
|
+
assert.equal(output.includes('t('), false)
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
test('stripExistingI18nCalls keeps non-i18n text', () => {
|
|
346
|
+
const input = '普通中文 text'
|
|
347
|
+
const output = stripExistingI18nCalls(input, '$t')
|
|
348
|
+
assert.ok(output.includes('普通中文'))
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
// ─── normalizeExistingCalls ──────────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
test('normalizeExistingCalls updates namespace in existing i18n calls', () => {
|
|
354
|
+
const input = "$t('old.hello')"
|
|
355
|
+
const output = normalizeExistingCalls(input, 'new', '$t')
|
|
356
|
+
assert.equal(output, "$t('new.hello')")
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('normalizeExistingCalls does not change text without i18n calls', () => {
|
|
360
|
+
const input = 'const a = 1'
|
|
361
|
+
const output = normalizeExistingCalls(input, 'ns', '$t')
|
|
362
|
+
assert.equal(output, input)
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// ─── applyReplacements ───────────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
test('applyReplacements replaces Chinese with i18n call in HTML', () => {
|
|
368
|
+
const content = '<div>你好世界</div>'
|
|
369
|
+
const extractions = [{ text: '你好世界', fullMatch: '你好世界', start: 5, end: 9, isAttrExprString: false }]
|
|
370
|
+
const result = applyReplacements(content, [{ ...extractions[0], key: 'ns.hello' }], 'ns', 'vue', '$t')
|
|
371
|
+
assert.ok(result.includes("$t('ns.hello')"))
|
|
372
|
+
assert.ok(!result.includes('你好世界'))
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test('applyReplacements replaces multiple Chinese texts', () => {
|
|
376
|
+
const content = '<div>你好</div><span>世界</span>'
|
|
377
|
+
const extractions = [
|
|
378
|
+
{ text: '你好', fullMatch: '你好', start: 5, end: 7, key: 'ns.hello' },
|
|
379
|
+
{ text: '世界', fullMatch: '世界', start: 20, end: 22, key: 'ns.world' },
|
|
380
|
+
]
|
|
381
|
+
const result = applyReplacements(content, extractions, 'ns', 'vue', '$t')
|
|
382
|
+
assert.ok(result.includes("$t('ns.hello')"))
|
|
383
|
+
assert.ok(result.includes("$t('ns.world')"))
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test('applyReplacements handles v-bind attr string replacement', () => {
|
|
387
|
+
const content = '<div :title="\'中文标题\'"></div>'
|
|
388
|
+
const extractions = [
|
|
389
|
+
{ text: '中文标题', fullMatch: "'中文标题'", start: 16, end: 22, isAttrExprString: true, attrName: ':title' },
|
|
390
|
+
]
|
|
391
|
+
const result = applyReplacements(content, [{ ...extractions[0], key: 'ns.title' }], 'ns', 'vue', '$t')
|
|
392
|
+
assert.ok(result.includes("$t('ns.title')"))
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test('applyReplacements handles v-bind expression string replacement', () => {
|
|
396
|
+
const content = '<div :class="\'active\' + \'中文\'"></div>'
|
|
397
|
+
const extractions = [
|
|
398
|
+
{ text: '中文', fullMatch: "'中文'", start: 27, end: 32, isAttrExprString: true, attrName: ':class' },
|
|
399
|
+
]
|
|
400
|
+
const result = applyReplacements(content, [{ ...extractions[0], key: 'ns.zh' }], 'ns', 'vue', '$t')
|
|
401
|
+
assert.ok(result.includes("$t('ns.zh')"))
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
// ─── applyJsonTranslation ────────────────────────────────────────────────────
|
|
405
|
+
|
|
406
|
+
test('applyJsonTranslation replaces Chinese values with translated text', () => {
|
|
407
|
+
const content = '{"title":"中文标题"}'
|
|
408
|
+
// 匹配 "中文标题" (包含 JSON 双引号), start=9, end=15
|
|
409
|
+
const extractions = [{ text: '中文标题', start: 9, end: 15, key: 'ns.title' }]
|
|
410
|
+
const enUSMap = { ns: { title: 'Chinese Title' } }
|
|
411
|
+
const result = applyJsonTranslation(content, extractions, enUSMap, 'ns')
|
|
412
|
+
const expected = '{"title":"Chinese Title"}'
|
|
413
|
+
assert.equal(result, expected)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
// ─── collectReferencedKeys ───────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
test('collectReferencedKeys finds $t calls in content', () => {
|
|
419
|
+
const content = "const a = $t('ns.hello')"
|
|
420
|
+
const keys = collectReferencedKeys(content, 'ns', '$t')
|
|
421
|
+
assert.deepEqual(keys, new Set(['hello']))
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
test('collectReferencedKeys finds multiple keys', () => {
|
|
425
|
+
const content = "$t('ns.hello') + $t('ns.world')"
|
|
426
|
+
const keys = collectReferencedKeys(content, 'ns', '$t')
|
|
427
|
+
assert.deepEqual(keys, new Set(['hello', 'world']))
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('collectReferencedKeys returns empty for different namespace', () => {
|
|
431
|
+
const content = "$t('other.hello')"
|
|
432
|
+
const keys = collectReferencedKeys(content, 'ns', '$t')
|
|
433
|
+
assert.deepEqual(keys, new Set())
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
// ─── mergeAndWriteLocaleFiles ────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
test('mergeAndWriteLocaleFiles creates locale files with correct structure', async () => {
|
|
439
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hias-test-'))
|
|
440
|
+
try {
|
|
441
|
+
await mergeAndWriteLocaleFiles(
|
|
442
|
+
tmpDir,
|
|
443
|
+
'zh-CN', ['en-US'], 'testns',
|
|
444
|
+
{
|
|
445
|
+
'zh-CN': { testns: { hello: '你好', world: '世界' } },
|
|
446
|
+
'en-US': { testns: { hello: 'Hello', world: 'World' } },
|
|
447
|
+
},
|
|
448
|
+
[],
|
|
449
|
+
'$t',
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
const zhPath = path.join(tmpDir, 'zh-CN.json')
|
|
453
|
+
const enPath = path.join(tmpDir, 'en-US.json')
|
|
454
|
+
assert.ok(fs.existsSync(zhPath))
|
|
455
|
+
assert.ok(fs.existsSync(enPath))
|
|
456
|
+
|
|
457
|
+
const zh = JSON.parse(fs.readFileSync(zhPath, 'utf-8'))
|
|
458
|
+
assert.equal(zh.testns.hello, '你好')
|
|
459
|
+
assert.equal(zh.testns.world, '世界')
|
|
460
|
+
|
|
461
|
+
const en = JSON.parse(fs.readFileSync(enPath, 'utf-8'))
|
|
462
|
+
assert.equal(en.testns.hello, 'Hello')
|
|
463
|
+
assert.equal(en.testns.world, 'World')
|
|
464
|
+
} finally {
|
|
465
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
test('mergeAndWriteLocaleFiles merges with existing locale file', async () => {
|
|
470
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hias-test-'))
|
|
471
|
+
try {
|
|
472
|
+
// Create existing locale file with old key
|
|
473
|
+
fs.mkdirSync(tmpDir, { recursive: true })
|
|
474
|
+
fs.writeFileSync(path.join(tmpDir, 'zh-CN.json'), JSON.stringify({ testns: { old: '旧值' } }), 'utf-8')
|
|
475
|
+
fs.writeFileSync(path.join(tmpDir, 'en-US.json'), JSON.stringify({ testns: { old: 'Old' } }), 'utf-8')
|
|
476
|
+
|
|
477
|
+
// Provide content referencing the old key so it's not removed as stale
|
|
478
|
+
const fileContents = ["$t('testns.old')"]
|
|
479
|
+
|
|
480
|
+
await mergeAndWriteLocaleFiles(
|
|
481
|
+
tmpDir,
|
|
482
|
+
'zh-CN', ['en-US'], 'testns',
|
|
483
|
+
{
|
|
484
|
+
'zh-CN': { testns: { hello: '你好', old: '旧值' } },
|
|
485
|
+
'en-US': { testns: { hello: 'Hello', old: 'Old' } },
|
|
486
|
+
},
|
|
487
|
+
fileContents,
|
|
488
|
+
'$t',
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
const zh = JSON.parse(fs.readFileSync(path.join(tmpDir, 'zh-CN.json'), 'utf-8'))
|
|
492
|
+
assert.equal(zh.testns.old, '旧值')
|
|
493
|
+
assert.equal(zh.testns.hello, '你好')
|
|
494
|
+
} finally {
|
|
495
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
496
|
+
}
|
|
497
|
+
})
|
|
498
|
+
|
|
499
|
+
// ─── saveBackupSync ──────────────────────────────────────────────────────────
|
|
500
|
+
|
|
501
|
+
test('saveBackupSync creates backup directory with manifest', async () => {
|
|
502
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hias-test-'))
|
|
503
|
+
const cwd = path.join(tmpDir, 'project')
|
|
504
|
+
fs.mkdirSync(cwd, { recursive: true })
|
|
505
|
+
|
|
506
|
+
const testFile = path.join(cwd, 'test.txt')
|
|
507
|
+
fs.writeFileSync(testFile, 'content', 'utf-8')
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
await saveBackupSync('testns', [testFile], cwd, true)
|
|
511
|
+
|
|
512
|
+
const backupRoot = path.join(cwd, '.hias', '.langbackup')
|
|
513
|
+
assert.ok(fs.existsSync(backupRoot), `backup root should exist at ${backupRoot}`)
|
|
514
|
+
|
|
515
|
+
const entries = fs.readdirSync(backupRoot)
|
|
516
|
+
assert.ok(entries.length > 0)
|
|
517
|
+
|
|
518
|
+
const latest = entries[entries.length - 1]
|
|
519
|
+
const manifestPath = path.join(backupRoot, latest, 'manifest.json')
|
|
520
|
+
assert.ok(fs.existsSync(manifestPath), `manifest should exist at ${manifestPath}`)
|
|
521
|
+
|
|
522
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
523
|
+
assert.equal(manifest.name, 'testns')
|
|
524
|
+
assert.equal(manifest.replaceOriginalFile, true)
|
|
525
|
+
assert.equal(typeof manifest.files, 'object')
|
|
526
|
+
assert.notEqual(manifest.files, null)
|
|
527
|
+
} finally {
|
|
528
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
529
|
+
}
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
// ─── resolveSetting ──────────────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
test('resolveSetting returns defaults when no config files exist', () => {
|
|
535
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hias-test-'))
|
|
536
|
+
try {
|
|
537
|
+
const result = resolveSetting(tmpDir)
|
|
538
|
+
assert.equal(result.provider, 'tencent')
|
|
539
|
+
assert.deepEqual(result.locales, ['zh-CN', 'en-US'])
|
|
540
|
+
assert.equal(result.outDir, '.hias/lang')
|
|
541
|
+
assert.equal(result.replaceOriginalFile, false)
|
|
542
|
+
} finally {
|
|
543
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
544
|
+
}
|
|
545
|
+
})
|