@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.
@@ -0,0 +1,694 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const writeFile = require('../utils/write-file')
4
+
5
+ // 支持翻译的文件扩展名列表
6
+ const SUPPORTED_EXTENSIONS = ['.vue', '.js', '.ts', '.jsx', '.tsx', '.json']
7
+
8
+ /**
9
+ * 获取实际支持的扩展名列表:优先使用配置中的 extensions,否则使用默认值
10
+ */
11
+ function getSupportedExtensions(settings) {
12
+ return settings.extensions && settings.extensions.length > 0 ? settings.extensions.map(e => e.startsWith('.') ? e : '.' + e) : SUPPORTED_EXTENSIONS
13
+ }
14
+
15
+ /**
16
+ * 清理 JS/TS/Vue 脚本中的注释,用等长空格替换以保持原始位置
17
+ * 支持: // 单行注释 和 \/* 多行注释 *\/
18
+ */
19
+ function stripJsComments(content) {
20
+ let result = content
21
+
22
+ // 移除多行注释 /* ... */
23
+ result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
24
+ return match.replace(/[^\n\r]/g, ' ')
25
+ })
26
+
27
+ // 移除单行注释 // ...
28
+ result = result.replace(/\/\/.*/g, (match) => {
29
+ return match.replace(/[^\n\r]/g, ' ')
30
+ })
31
+
32
+ return result
33
+ }
34
+
35
+ /**
36
+ * 清理 Vue 模板中的 HTML 注释
37
+ */
38
+ function stripHtmlComments(content) {
39
+ return content.replace(/<!--[\s\S]*?-->/g, (match) => {
40
+ return match.replace(/[^\n\r]/g, ' ')
41
+ })
42
+ }
43
+
44
+ /**
45
+ * 从 JS/TS/Vue 脚本中提取带位置信息的中文字符串
46
+ * 返回: [{ text, fullMatch, start, end, quote }]
47
+ */
48
+ function extractChineseFromJsWithPositions(content) {
49
+ const cleaned = stripJsComments(content)
50
+ const result = []
51
+ const seen = new Set()
52
+ let i = 0
53
+ const len = cleaned.length
54
+
55
+ while (i < len) {
56
+ const ch = cleaned[i]
57
+
58
+ // 处理字符串: '', "", ``
59
+ if (ch === '"' || ch === "'" || ch === '`') {
60
+ const quote = ch
61
+ let str = ''
62
+ let j = i + 1
63
+ let closed = false
64
+
65
+ while (j < len) {
66
+ if (cleaned[j] === '\\') {
67
+ str += cleaned[j] + (cleaned[j + 1] || '')
68
+ j += 2
69
+ continue
70
+ }
71
+ if (cleaned[j] === quote) {
72
+ closed = true
73
+ break
74
+ }
75
+ // 处理模板字面量中的 ${}
76
+ if (quote === '`' && cleaned[j] === '$' && cleaned[j + 1] === '{') {
77
+ let depth = 1
78
+ j += 2
79
+ while (j < len && depth > 0) {
80
+ if (cleaned[j] === '{') depth++
81
+ if (cleaned[j] === '}') depth--
82
+ j++
83
+ }
84
+ continue
85
+ }
86
+ str += cleaned[j]
87
+ j++
88
+ }
89
+
90
+ if (closed) {
91
+ if (quote === '`' && str !== content.substring(i + 1, j)) {
92
+ // 模板字面量包含 ${expr}:提取各静态段中带中文的部分
93
+ const raw = content.substring(i + 1, j)
94
+ const parts = []
95
+ let p = 0
96
+ while (p < raw.length) {
97
+ if (raw[p] === '$' && raw[p + 1] === '{') {
98
+ let depth = 1
99
+ let k = p + 2
100
+ while (k < raw.length && depth > 0) {
101
+ if (raw[k] === '{') depth++
102
+ if (raw[k] === '}') depth--
103
+ k++
104
+ }
105
+ p = k
106
+ } else {
107
+ const start = p
108
+ while (p < raw.length && !(raw[p] === '$' && raw[p + 1] === '{')) p++
109
+ const staticPart = raw.substring(start, p).trim()
110
+ if (staticPart && /[\u4e00-\u9fff]/.test(staticPart) && !seen.has(staticPart)) {
111
+ seen.add(staticPart)
112
+ result.push({
113
+ text: staticPart,
114
+ fullMatch: raw.substring(start, p),
115
+ start: i + 1 + start,
116
+ end: i + 1 + p,
117
+ quote,
118
+ })
119
+ }
120
+ }
121
+ }
122
+ } else {
123
+ const fullMatch = content.substring(i, j + 1)
124
+ const trimmed = str.trim()
125
+ if (trimmed && /[\u4e00-\u9fff\u3400-\u4dbf\uf900-\ufaff]/.test(trimmed)) {
126
+ result.push({
127
+ text: trimmed,
128
+ fullMatch,
129
+ start: i,
130
+ end: j + 1,
131
+ quote,
132
+ })
133
+ }
134
+ }
135
+ i = j + 1
136
+ } else {
137
+ i = j
138
+ }
139
+ continue
140
+ }
141
+
142
+ i++
143
+ }
144
+
145
+ return result
146
+ }
147
+
148
+ /**
149
+ * 从 JSX/TSX 中额外提取标签之间的中文文本节点
150
+ * 例如: <div>兴趣</div> 中的 "兴趣"
151
+ */
152
+ function extractChineseFromJsxTextNodes(content) {
153
+ const result = []
154
+ const seen = new Set()
155
+
156
+ const cleaned = stripHtmlComments(content)
157
+
158
+ // 查找 JSX 标签之间的文本节点(不依赖标签配对,支持嵌套结构)
159
+ // 例如 <div>中文<span>子</span>更多中文</div>
160
+ const textBetweenTags = />([^<]*[\u4e00-\u9fff][^<]*)</g
161
+ let match
162
+ while ((match = textBetweenTags.exec(cleaned)) !== null) {
163
+ const textContent = match[1]
164
+ if (!textContent) continue
165
+ // 在文本内容中查找不含 { } 的中文片段
166
+ const chineseRunRe = /([\u4e00-\u9fff]+(?:\s*[\u4e00-\u9fff]+)*)/g
167
+ let cm
168
+ while ((cm = chineseRunRe.exec(textContent)) !== null) {
169
+ const txt = cm[1].trim()
170
+ if (txt && !seen.has(txt)) {
171
+ seen.add(txt)
172
+ result.push({
173
+ text: txt,
174
+ fullMatch: txt,
175
+ start: match.index + 1 + cm.index,
176
+ end: match.index + 1 + cm.index + cm[1].length,
177
+ isTextNode: true,
178
+ })
179
+ }
180
+ }
181
+ }
182
+
183
+ // 查找 JSX 属性中的中文字符串: attr="中文" 或 attr={'中文'}
184
+ const jsxAttrRegex = /\s+(?!data-|aria-)([\w-]+)\s*=\s*(?:"([^"]*[\u4e00-\u9fff][^"]*)"|\{['"]([^'"]*[\u4e00-\u9fff][^'"]*)['"]\})/g
185
+ while ((match = jsxAttrRegex.exec(cleaned)) !== null) {
186
+ const str = (match[2] || match[3]).trim()
187
+ if (str && !seen.has(str)) {
188
+ seen.add(str)
189
+ result.push({
190
+ text: str,
191
+ fullMatch: match[0],
192
+ start: match.index,
193
+ end: match.index + match[0].length,
194
+ isAttribute: true,
195
+ attrName: match[1],
196
+ })
197
+ }
198
+ }
199
+
200
+ return result
201
+ }
202
+
203
+ /**
204
+ * 完整的 JS/TS/JSX/TSX 提取,包括字符串和 JSX 文本节点
205
+ */
206
+ function extractChineseFromJsFamilyWithPositions(content, fileType) {
207
+ const jsResults = extractChineseFromJsWithPositions(content)
208
+ const allResults = [...jsResults]
209
+
210
+ if (fileType === 'jsx' || fileType === 'tsx') {
211
+ const jsxResults = extractChineseFromJsxTextNodes(content)
212
+ // 去重:避免与 script 提取重复
213
+ const seenTexts = new Set(jsResults.map((r) => r.text))
214
+ for (const r of jsxResults) {
215
+ if (!seenTexts.has(r.text)) {
216
+ allResults.push(r)
217
+ }
218
+ }
219
+ }
220
+
221
+ return allResults
222
+ }
223
+
224
+ /**
225
+ * 从 Vue 模板中提取带位置信息的中文文本和插值表达式
226
+ */
227
+ function extractChineseFromVueTemplateWithPositions(templateContent) {
228
+ const result = []
229
+ const seen = new Set()
230
+
231
+ // 清理 HTML 注释
232
+ const cleaned = stripHtmlComments(templateContent)
233
+
234
+ // 查找 {{ '中文' }} 或 {{ "中文" }} 或 {{ `中文` }} 形式的插值
235
+ const interpRegex = /\{\{\s*((['"`])([\s\S]*?)\2)\s*\}\}/g
236
+ let match
237
+ while ((match = interpRegex.exec(cleaned)) !== null) {
238
+ const str = match[3].trim()
239
+ if (str && /[\u4e00-\u9fff]/.test(str) && !seen.has(str)) {
240
+ seen.add(str)
241
+ result.push({
242
+ text: str,
243
+ fullMatch: match[0],
244
+ start: match.index,
245
+ end: match.index + match[0].length,
246
+ isInterpolation: true,
247
+ })
248
+ }
249
+ }
250
+
251
+ // 查找 {{ }} 内含表达式的复杂插值,例如 {{ isHidden || '中文' }}
252
+ const complexInterpRegex = /\{\{([\s\S]*?)\}\}/g
253
+ while ((match = complexInterpRegex.exec(cleaned)) !== null) {
254
+ const expr = match[1].trim()
255
+ // 跳过纯引号字符串(如 {{ '中文' }}),已在 interpRegex 处理
256
+ if (/^(['"`])[\s\S]*\1$/.test(expr)) continue
257
+ const exprStart = match.index + match[0].indexOf(match[1])
258
+ // 在表达式内查找引号包裹的中文字符串
259
+ const exprStrRe = /(['"`])([^'"`]*[\u4e00-\u9fff][^'"`]*?)\1/g
260
+ let es
261
+ while ((es = exprStrRe.exec(match[1])) !== null) {
262
+ const txt = es[2].trim()
263
+ if (txt) {
264
+ result.push({
265
+ text: txt,
266
+ fullMatch: es[0],
267
+ start: exprStart + es.index,
268
+ end: exprStart + es.index + es[0].length,
269
+ isExprString: true,
270
+ })
271
+ }
272
+ }
273
+ }
274
+
275
+ // 查找标签之间的纯文本节点(不依赖标签配对,支持嵌套结构):
276
+ // <div>中文</div> 或 <div>中文<span>嵌套</span>更多中文</div>
277
+ const textBetweenTags = />([^<]*[\u4e00-\u9fff][^<]*)</g
278
+ while ((match = textBetweenTags.exec(cleaned)) !== null) {
279
+ const textContent = match[1].trim()
280
+ if (!textContent || /\{\{/.test(textContent)) continue
281
+ result.push({
282
+ text: textContent,
283
+ fullMatch: textContent,
284
+ start: match.index + 1,
285
+ end: match.index + 1 + match[1].length,
286
+ isTextNode: true,
287
+ })
288
+ }
289
+
290
+ // 查找 v-bind 属性中的中文字符串: :attr="'中文'" → 替换整个属性为 :attr="$t('key')"
291
+ const vbindAttrRegex = /\s+(:(?!data-|aria-)[\w-]+)\s*=\s*"(')([^']*[\u4e00-\u9fff][^']*)\2"/g
292
+ while ((match = vbindAttrRegex.exec(cleaned)) !== null) {
293
+ const str = match[3].trim()
294
+ if (str) {
295
+ if (!seen.has(str)) seen.add(str)
296
+ result.push({
297
+ text: str,
298
+ fullMatch: match[0],
299
+ start: match.index,
300
+ end: match.index + match[0].length,
301
+ isAttribute: true,
302
+ attrName: match[1],
303
+ })
304
+ }
305
+ }
306
+
307
+ // 查找普通 HTML 属性中的中文字符串(排除 v- 指令): attr="中文" → 替换为 :attr="$t('key')"
308
+ const plainAttrRegex = /\s+(?!v-|data-|aria-)([\w-]+)\s*=\s*"(?:[^"]*[\u4e00-\u9fff][^"]*)"/g
309
+ while ((match = plainAttrRegex.exec(cleaned)) !== null) {
310
+ const full = match[0]
311
+ const inner = full.match(/"([^"]*)"/)
312
+ if (!inner) continue
313
+ const str = inner[1].trim()
314
+ if (str) {
315
+ if (!seen.has(str)) seen.add(str)
316
+ result.push({
317
+ text: str,
318
+ fullMatch: full,
319
+ start: match.index,
320
+ end: match.index + full.length,
321
+ isAttribute: true,
322
+ attrName: match[1],
323
+ })
324
+ }
325
+ }
326
+
327
+ // 查找 v-bind 表达式中的内联字符串: :attr="expr + '中文' + expr"
328
+ // 以及 v- 指令表达式中的内联字符串: v-if="expr + '中文' + expr"
329
+ const vbindExprRegex = /\s+(:\w[\w-]*)\s*=\s*"(?:[^"]*[\u4e00-\u9fff][^"]*)"/g
330
+ extractStringsFromVueAttr(cleaned, vbindExprRegex, seen, result)
331
+
332
+ const vDirectiveExprRegex = /\s+(v-[\w-]+)\s*=\s*"(?:[^"]*[\u4e00-\u9fff][^"]*)"/g
333
+ extractStringsFromVueAttr(cleaned, vDirectiveExprRegex, seen, result)
334
+
335
+ return result
336
+ }
337
+
338
+ /**
339
+ * 从 Vue 标签属性表达式(v-bind 或 v- 指令)中提取所有带中文的字符串
340
+ * 支持: :attr="expr + '中文' + expr" / v-if="`中文${var}文`" 等
341
+ */
342
+ function extractStringsFromVueAttr(cleaned, attrRegex, seen, result) {
343
+ let match
344
+ while ((match = attrRegex.exec(cleaned)) !== null) {
345
+ const attrName = match[1]
346
+ const eqPos = match[0].indexOf('=')
347
+ const valStart = match[0].indexOf('"', eqPos) + 1
348
+ const valEnd = match[0].lastIndexOf('"')
349
+ const exprValue = match[0].substring(valStart, valEnd)
350
+ // 排除纯字符串属性(已被 vbindAttrRegex 处理)
351
+ if (/^'[^']*'$/.test(exprValue)) continue
352
+ const strRe = /(['"`])([^'"`]*[\u4e00-\u9fff][^'"`]*?)\1/g
353
+ let strMatch
354
+ while ((strMatch = strRe.exec(exprValue)) !== null) {
355
+ const quote = strMatch[1]
356
+ const rawContent = strMatch[2]
357
+ const strAbsStart = match.index + valStart + strMatch.index
358
+ const strAbsEnd = match.index + valStart + strMatch.index + strMatch[0].length
359
+ if (quote === '`' && rawContent.includes('${')) {
360
+ const segs = splitTemplateLiteralSegments(rawContent)
361
+ for (const seg of segs) {
362
+ if (seg && /[\u4e00-\u9fff]/.test(seg) && !seen.has(seg)) {
363
+ seen.add(seg)
364
+ result.push({
365
+ text: seg,
366
+ fullMatch: seg,
367
+ start: strAbsStart + rawContent.indexOf(seg),
368
+ end: strAbsStart + rawContent.indexOf(seg) + seg.length,
369
+ isAttrExprString: true,
370
+ attrName: attrName,
371
+ })
372
+ }
373
+ }
374
+ } else {
375
+ const str = rawContent.trim()
376
+ if (str) {
377
+ if (!seen.has(str)) seen.add(str)
378
+ result.push({
379
+ text: str,
380
+ fullMatch: strMatch[0],
381
+ start: strAbsStart,
382
+ end: strAbsEnd,
383
+ isAttrExprString: true,
384
+ attrName: attrName,
385
+ })
386
+ }
387
+ }
388
+ }
389
+ }
390
+ }
391
+
392
+ /**
393
+ * 将模板字面量拆分为静态文本段(不含 ${} 插值的部分)
394
+ */
395
+ function splitTemplateLiteralSegments(raw) {
396
+ const segs = []
397
+ let p = 0
398
+ while (p < raw.length) {
399
+ if (raw[p] === '$' && raw[p + 1] === '{') {
400
+ let depth = 1
401
+ let k = p + 2
402
+ while (k < raw.length && depth > 0) {
403
+ if (raw[k] === '{') depth++
404
+ if (raw[k] === '}') depth--
405
+ k++
406
+ }
407
+ p = k
408
+ } else {
409
+ const start = p
410
+ while (p < raw.length && !(raw[p] === '$' && raw[p + 1] === '{')) p++
411
+ const seg = raw.substring(start, p).trim()
412
+ if (seg) segs.push(seg)
413
+ }
414
+ }
415
+ return segs
416
+ }
417
+
418
+ /**
419
+ * 从 Vue 文件中提取所有中文文本,包括 script 和 template 部分
420
+ */
421
+ function extractChineseFromVueWithPositions(content) {
422
+ const allResults = []
423
+
424
+ // 提取 template 部分
425
+ const templateMatch = content.match(/<template[\s\S]*?>([\s\S]*?)<\/template>/i)
426
+ if (templateMatch) {
427
+ const templateContent = templateMatch[1]
428
+ const templateStart = templateMatch.index + templateMatch[0].indexOf(templateContent)
429
+ const templateResults = extractChineseFromVueTemplateWithPositions(templateContent)
430
+ templateResults.forEach((r) => {
431
+ allResults.push({
432
+ ...r,
433
+ start: templateStart + r.start,
434
+ end: templateStart + r.end,
435
+ context: 'template',
436
+ })
437
+ })
438
+ }
439
+
440
+ // 提取 script 部分
441
+ const scriptMatch = content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/i)
442
+ if (scriptMatch) {
443
+ const scriptContent = scriptMatch[1]
444
+ const scriptStart = scriptMatch.index + scriptMatch[0].indexOf(scriptContent)
445
+ const scriptResults = extractChineseFromJsWithPositions(scriptContent)
446
+ scriptResults.forEach((r) => {
447
+ allResults.push({
448
+ ...r,
449
+ start: scriptStart + r.start,
450
+ end: scriptStart + r.end,
451
+ context: 'script',
452
+ })
453
+ })
454
+ }
455
+
456
+ return allResults
457
+ }
458
+
459
+ /**
460
+ * 从 JSON 文件中提取中文文本
461
+ */
462
+ function extractChineseFromJsonWithPositions(content) {
463
+ const result = []
464
+ const seen = new Set()
465
+
466
+ // 查找 JSON 字符串值中的中文: "key": "中文值"
467
+ const jsonStrRegex = /"((?:[^"\\]|\\.)*)"/g
468
+ let match
469
+ while ((match = jsonStrRegex.exec(content)) !== null) {
470
+ const str = match[1].trim()
471
+ if (str && /[\u4e00-\u9fff]/.test(str) && !seen.has(str)) {
472
+ // 判断是否为 key 还是 value(向前查找最近的非空白 :)
473
+ const before = content.substring(Math.max(0, match.index - 100), match.index)
474
+ const isValue = /:\s*$/.test(before)
475
+ if (isValue) {
476
+ seen.add(str)
477
+ result.push({
478
+ text: str,
479
+ fullMatch: match[0],
480
+ start: match.index,
481
+ end: match.index + match[0].length,
482
+ isJsonValue: true,
483
+ })
484
+ }
485
+ }
486
+ }
487
+
488
+ return result
489
+ }
490
+
491
+ /**
492
+ * 从源码中收集所有 $t('namespace.xxx') 调用中的 key
493
+ */
494
+ function collectReferencedKeys(content, name, i18nTemplate) {
495
+ const fnName = getI18nFnName(i18nTemplate)
496
+ const escapedFn = fnName.replace(/[.*+?^${}()|[\]\\$]/g, '\\$&')
497
+ const refRegex = new RegExp(escapedFn + '\\s*\\(\\s*[\'"](?:' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')\\.([^\'"\\s)]+)[\'"]\\s*\\)', 'g')
498
+ const keys = new Set()
499
+ let m
500
+ while ((m = refRegex.exec(content)) !== null) {
501
+ keys.add(m[1])
502
+ }
503
+ return keys
504
+ }
505
+
506
+ /**
507
+ * 合并并写入语言文件:读取已有文件,合并新 key,移除不再引用的 key
508
+ */
509
+ async function mergeAndWriteLocaleFiles(outDir, fromLang, toLangs, name, localeMaps, allContents, i18nTemplate) {
510
+ // 收集源码中所有被引用的 key
511
+ const sourceMap = localeMaps[fromLang] || localeMaps[Object.keys(localeMaps)[0]]
512
+ const referencedKeys = new Set(Object.keys((sourceMap && sourceMap[name]) || {}))
513
+ for (const content of allContents) {
514
+ for (const key of collectReferencedKeys(content, name, i18nTemplate)) {
515
+ referencedKeys.add(key)
516
+ }
517
+ }
518
+
519
+ const allLocales = [fromLang, ...toLangs]
520
+ for (const locale of allLocales) {
521
+ const filePath = path.join(outDir, `${locale}.json`)
522
+ let merged = {}
523
+
524
+ // 读取已有语言文件
525
+ if (fs.existsSync(filePath)) {
526
+ try {
527
+ const existing = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
528
+ if (existing[name]) {
529
+ // 仅保留仍被引用的 key
530
+ for (const [k, v] of Object.entries(existing[name])) {
531
+ if (referencedKeys.has(k)) {
532
+ merged[k] = v
533
+ }
534
+ }
535
+ }
536
+ } catch {
537
+ // 忽略解析错误,从头创建
538
+ }
539
+ }
540
+
541
+ // 合并当前操作产生的新 key(覆盖旧值)
542
+ const newMap = localeMaps[locale]
543
+ if (newMap && newMap[name]) {
544
+ for (const [k, v] of Object.entries(newMap[name])) {
545
+ merged[k] = v
546
+ }
547
+ }
548
+
549
+ const mergedMap = { [name]: merged }
550
+ await writeFile(filePath, JSON.stringify(mergedMap, null, 2))
551
+ }
552
+ }
553
+ function getI18nFnName(template) {
554
+ if (!template) return '$t'
555
+ const idx = template.indexOf('(')
556
+ return idx === -1 ? template.trim() : template.substring(0, idx).trim()
557
+ }
558
+
559
+ function stripExistingI18nCalls(content, i18nTemplate) {
560
+ const fnName = getI18nFnName(i18nTemplate)
561
+ const escapedFn = fnName.replace(/[.*+?^${}()|[\]\\$]/g, '\\$&')
562
+ const stripRegex = new RegExp(escapedFn + '\\s*\\(\\s*[\'"][^\'"]*?[\'"]\\s*\\)', 'g')
563
+ return content.replace(stripRegex, (match) => ' '.repeat(match.length))
564
+ }
565
+
566
+ /**
567
+ * 统一的中文提取入口,根据文件类型调用对应的提取方法
568
+ */
569
+ function extractChineseWithPositions(content, fileType) {
570
+ const ext = fileType.toLowerCase().replace(/^\./, '')
571
+
572
+ switch (ext) {
573
+ case 'vue':
574
+ return extractChineseFromVueWithPositions(content)
575
+ case 'json':
576
+ return extractChineseFromJsonWithPositions(content)
577
+ case 'js':
578
+ case 'ts':
579
+ case 'jsx':
580
+ case 'tsx':
581
+ return extractChineseFromJsFamilyWithPositions(content, ext)
582
+ default:
583
+ return extractChineseFromJsFamilyWithPositions(content, ext)
584
+ }
585
+ }
586
+
587
+ /**
588
+ * 将文件中已有的国际化调用 `$t('oldNamespace.xxx')` 的命名空间更新为当前 name
589
+ * 例如 `$t('com.test')` 在 name=demo 时变为 `$t('demo.test')`
590
+ */
591
+ function normalizeExistingCalls(content, name, i18nTemplate) {
592
+ const fnName = getI18nFnName(i18nTemplate)
593
+ const escapedFn = fnName.replace(/[.*+?^${}()|[\]\\$]/g, '\\$&')
594
+ const normRegex = new RegExp('(' + escapedFn + ')\\s*\\(\\s*[\'\"]([^\\.]+)\\.(.+?)[\'\"]\\s*\\)', 'g')
595
+ return content.replace(normRegex, (match, fn, oldNs, subKey) => {
596
+ if (oldNs !== name) {
597
+ return `${fn}('${name}.${subKey}')`
598
+ }
599
+ return match
600
+ })
601
+ }
602
+
603
+ /**
604
+ * 根据模板生成国际化调用字符串,{{key}} 会被替换为 "命名空间.key"
605
+ */
606
+ function formatI18nCall(template, key, defaultFnName) {
607
+ if (template && template.includes('{{key}}')) {
608
+ return template.replace(/\{\{key\}\}/g, key)
609
+ }
610
+ const fnName = defaultFnName || getI18nFnName(template)
611
+ return fnName + "('" + key + "')"
612
+ }
613
+
614
+ /**
615
+ * 对原始内容执行替换操作,从末尾向前替换以避免位置偏移
616
+ */
617
+ function applyReplacements(content, extractions, name, fileType, i18nTemplate) {
618
+ let result = content
619
+
620
+ const sorted = [...extractions].sort((a, b) => b.start - a.start)
621
+
622
+ for (const item of sorted) {
623
+ const key = item.key
624
+ const call = i18nTemplate ? formatI18nCall(i18nTemplate, key) : `$t('${key}')`
625
+ let replacement
626
+
627
+ if (fileType === 'vue' && item.context === 'template') {
628
+ if (item.isAttrExprString) {
629
+ replacement = call
630
+ } else if (item.isExprString) {
631
+ replacement = call
632
+ } else if (item.isAttribute) {
633
+ const prefix = item.attrName.startsWith(':') ? '' : ':'
634
+ replacement = ` ${prefix}${item.attrName}="${call}"`
635
+ } else {
636
+ replacement = `{{ ${call} }}`
637
+ }
638
+ } else if (fileType === 'json') {
639
+ continue
640
+ } else if ((fileType === 'jsx' || fileType === 'tsx') && item.isTextNode) {
641
+ replacement = `{${call}}`
642
+ } else if (item.isAttribute) {
643
+ replacement = ` ${item.attrName}={${call}}`
644
+ } else {
645
+ replacement = call
646
+ }
647
+
648
+ result = result.substring(0, item.start) + replacement + result.substring(item.end)
649
+ }
650
+
651
+ return result
652
+ }
653
+
654
+ /**
655
+ * 对 JSON 文件进行特殊处理:直接将中文值替换为英文翻译
656
+ */
657
+ function applyJsonTranslation(content, extractions, enUSMap, name) {
658
+ let result = content
659
+
660
+ const sorted = [...extractions].sort((a, b) => b.start - a.start)
661
+
662
+ for (const item of sorted) {
663
+ const key = item.key
664
+ const translated = enUSMap[name] ? enUSMap[name][key.split('.').pop()] || item.text : item.text
665
+ const replacement = `"${translated}"`
666
+ result = result.substring(0, item.start) + replacement + result.substring(item.end)
667
+ }
668
+
669
+ return result
670
+ }
671
+
672
+ module.exports = {
673
+ SUPPORTED_EXTENSIONS,
674
+ getSupportedExtensions,
675
+ stripJsComments,
676
+ stripHtmlComments,
677
+ extractChineseFromJsWithPositions,
678
+ extractChineseFromJsxTextNodes,
679
+ extractChineseFromJsFamilyWithPositions,
680
+ extractChineseFromVueTemplateWithPositions,
681
+ extractStringsFromVueAttr,
682
+ splitTemplateLiteralSegments,
683
+ extractChineseFromVueWithPositions,
684
+ extractChineseFromJsonWithPositions,
685
+ collectReferencedKeys,
686
+ mergeAndWriteLocaleFiles,
687
+ getI18nFnName,
688
+ stripExistingI18nCalls,
689
+ extractChineseWithPositions,
690
+ normalizeExistingCalls,
691
+ formatI18nCall,
692
+ applyReplacements,
693
+ applyJsonTranslation,
694
+ }