dsh-cait 1.1.6 → 1.1.7

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.
@@ -6,14 +6,16 @@ import { stripMarkdown } from './markdown.js'
6
6
  import { DOCX_MIME, buildBilingualDocx, buildDocx, buildDocxFromText, parseDocxDetailed } from './docx.js'
7
7
  import { EPUB_MIME, buildEpub, buildEpubFromText, parseEpub } from './epub.js'
8
8
  import { PPTX_MIME, buildPptx, parsePptxDetailed } from './pptx.js'
9
- import { XLSX_MIME, buildXlsx, parseXlsx } from './xlsx.js'
9
+ import { XLSX_MIME, buildXlsx, parseXlsx, patchXlsx } from './xlsx.js'
10
10
  import { buildTmx, parseTmx } from './tmx.js'
11
11
  import { buildHtml, buildJson, buildPo, buildRtf, buildSrt, buildVtt, buildXliff, buildXliff2, buildXliffReview, parseCsv, parseHtml, parseJson, parsePo, parseRtf, parseSrt, parseVtt, parseXliff } from './formats.js'
12
+ import { LOCALIZATION_EXPORT, LOCALIZATION_EXTENSIONS, buildLocalization, parseLocalization } from './localization.js'
12
13
  import { decodeXml } from './xml.js'
13
14
 
14
15
  const BINARY_SOURCES = {
15
16
  docx: { label: 'DOCX', fallback: 'source.docx' },
16
17
  pptx: { label: 'PPTX', fallback: 'source.pptx' },
18
+ xlsx: { label: 'XLSX', fallback: 'source.xlsx' },
17
19
  epub: { label: 'EPUB', fallback: 'source.epub' },
18
20
  }
19
21
 
@@ -143,6 +145,18 @@ export const importExportMethods = {
143
145
  return readSource(this, doc, 'pptx')
144
146
  },
145
147
 
148
+ attachXlsxSource(doc, buffer, name = 'source.xlsx') {
149
+ attachSource(this, doc, 'xlsx', buffer, name)
150
+ },
151
+
152
+ removeXlsxSource(doc) {
153
+ removeSource(this, doc, 'xlsx')
154
+ },
155
+
156
+ readXlsxSource(doc) {
157
+ return readSource(this, doc, 'xlsx')
158
+ },
159
+
146
160
  attachEpubSource(doc, buffer, name = 'source.epub') {
147
161
  attachSource(this, doc, 'epub', buffer, name)
148
162
  },
@@ -333,11 +347,13 @@ export const importExportMethods = {
333
347
  name: fileName,
334
348
  text: content.text,
335
349
  paragraphMap: content.paragraphs.map((paragraph) => paragraph.index),
350
+ paragraphMeta: Object.fromEntries(content.paragraphs.map((paragraph) => [paragraph.index, paragraph.meta])),
336
351
  docxSource: buffer,
337
352
  docxName: fileName,
338
353
  sourceLang,
339
354
  targetLang,
340
355
  format: 'docx',
356
+ formatMeta: content.formatMeta,
341
357
  })
342
358
  }
343
359
 
@@ -391,6 +407,7 @@ export const importExportMethods = {
391
407
  targetLang,
392
408
  format: extension,
393
409
  ...(extension === 'pptx' ? { pptxSource: buffer, pptxName: fileName } : {}),
410
+ ...(extension === 'xlsx' ? { xlsxSource: buffer, xlsxName: fileName } : {}),
394
411
  })
395
412
  return {
396
413
  ...created,
@@ -411,6 +428,49 @@ export const importExportMethods = {
411
428
  })
412
429
  }
413
430
 
431
+ let localizationKind = LOCALIZATION_EXTENSIONS[extension]
432
+ if (!localizationKind && extension === 'xml' && /<resources\b/i.test(String(text ?? ''))) {
433
+ localizationKind = 'android'
434
+ }
435
+ if (localizationKind) {
436
+ const sourceText = String(text ?? '')
437
+ if (!sourceText.trim()) throw new CatError('EMPTY_FILE', '文件内容为空。')
438
+ if (sourceText.length > MAX_IMPORT_CHARS) throw new CatError('FILE_TOO_LARGE', '文件过大。')
439
+ let parsed
440
+ try {
441
+ parsed = parseLocalization(sourceText, localizationKind)
442
+ } catch (error) {
443
+ if (error instanceof CatError) throw error
444
+ throw new CatError(
445
+ `${localizationKind.toUpperCase()}_PARSE`,
446
+ `${localizationKind} 解析失败: ${error?.message ?? error}`,
447
+ )
448
+ }
449
+ const src = cleanLang(
450
+ sourceLang || parsed.sourceLang || this.settings.sourceLang,
451
+ this.settings.sourceLang,
452
+ )
453
+ const tgt = cleanLang(
454
+ targetLang || parsed.targetLang || this.settings.targetLang,
455
+ this.settings.targetLang,
456
+ )
457
+ const created = this.createDocument({
458
+ name: fileName,
459
+ segments: parsed.segments,
460
+ sourceLang: src,
461
+ targetLang: tgt,
462
+ format: parsed.format || localizationKind,
463
+ formatMeta: parsed.formatMeta,
464
+ })
465
+ return {
466
+ ...created,
467
+ sourceLang: src,
468
+ targetLang: tgt,
469
+ units: parsed.segments.length,
470
+ warnings: parsed.warnings ?? [],
471
+ }
472
+ }
473
+
414
474
  const structured = {
415
475
  csv: { parser: parseCsv, format: 'csv' },
416
476
  srt: { parser: parseSrt, format: 'srt' },
@@ -507,13 +567,26 @@ export const importExportMethods = {
507
567
 
508
568
  if (format === 'docx') {
509
569
  const grouped = new Map()
510
- for (const segment of doc.segments) {
511
- const key = Number.isInteger(segment.paragraph) ? segment.paragraph : 0
570
+ const byPart = new Map()
571
+ let hasPartMeta = false
572
+ for (const [index, segment] of doc.segments.entries()) {
573
+ const meta = segment.meta && typeof segment.meta === 'object' ? segment.meta : null
574
+ const part = meta?.docxPart || 'word/document.xml'
575
+ const ordinal = Number.isInteger(meta?.docxParagraph)
576
+ ? meta.docxParagraph
577
+ : Number.isInteger(segment.paragraph)
578
+ ? segment.paragraph
579
+ : index
580
+ if (meta?.docxPart && Number.isInteger(meta?.docxParagraph)) hasPartMeta = true
581
+ const key = `${part}\u0000${ordinal}`
512
582
  if (!grouped.has(key)) grouped.set(key, [])
513
583
  grouped.get(key).push(segment)
584
+ if (!byPart.has(part)) byPart.set(part, new Map())
585
+ const partMap = byPart.get(part)
586
+ if (!partMap.has(ordinal)) partMap.set(ordinal, [])
587
+ partMap.get(ordinal).push(segment)
514
588
  }
515
- const replacements = new Map()
516
- for (const [key, segments] of grouped) {
589
+ const combine = (segments) => {
517
590
  const translated = segments
518
591
  .map((segment) => segment.target)
519
592
  .filter(Boolean)
@@ -524,22 +597,42 @@ export const importExportMethods = {
524
597
  .filter(Boolean)
525
598
  .join(' ')
526
599
  .trim()
527
- if (translated || fallback) replacements.set(key, translated || fallback)
600
+ return translated || fallback
601
+ }
602
+ const replacements = new Map()
603
+ for (const [key, segments] of grouped) {
604
+ const ordinal = Number(key.split('\u0000')[1])
605
+ const text = combine(segments)
606
+ if (text) replacements.set(ordinal, text)
607
+ }
608
+ for (const [part, partMap] of byPart) {
609
+ for (const [ordinal, segments] of partMap) {
610
+ const text = combine(segments)
611
+ if (text) partMap.set(ordinal, text)
612
+ else partMap.delete(ordinal)
613
+ }
528
614
  }
529
615
 
530
616
  let content = null
531
617
  const template = this.readDocxSource(doc)
532
618
  if (template) {
533
619
  try {
534
- content = buildDocx(template, replacements)
620
+ content = hasPartMeta
621
+ ? buildDocx(template, null, byPart)
622
+ : buildDocx(template, replacements)
535
623
  } catch (error) {
536
624
  this.logger?.warn?.(`CAIT: DOCX 模板套用失败,改用纯文本 DOCX: ${error.message}`)
537
625
  }
538
626
  }
539
627
  if (!content) {
540
628
  const paragraphs = [...grouped.keys()]
541
- .sort((left, right) => left - right)
542
- .map((key) => replacements.get(key))
629
+ .sort((left, right) => {
630
+ const [leftPart, leftOrdinal] = left.split('\u0000')
631
+ const [rightPart, rightOrdinal] = right.split('\u0000')
632
+ if (leftPart !== rightPart) return leftPart.localeCompare(rightPart)
633
+ return Number(leftOrdinal) - Number(rightOrdinal)
634
+ })
635
+ .map((key) => replacements.get(Number(key.split('\u0000')[1])))
543
636
  .filter(Boolean)
544
637
  content = buildDocxFromText(paragraphs)
545
638
  }
@@ -583,6 +676,16 @@ export const importExportMethods = {
583
676
  }
584
677
  }
585
678
 
679
+ const localizationExport = LOCALIZATION_EXPORT[format]
680
+ if (localizationExport) {
681
+ return {
682
+ filename: `${base}.${doc.targetLang}.${localizationExport.extension}`,
683
+ mime: localizationExport.mime,
684
+ content: buildLocalization(doc, format),
685
+ count: doc.segments.length,
686
+ }
687
+ }
688
+
586
689
  if (format === 'srt') {
587
690
  return {
588
691
  filename: `${base}.${doc.targetLang}.srt`,
@@ -668,10 +771,12 @@ export const importExportMethods = {
668
771
  }
669
772
 
670
773
  if (format === 'xlsx') {
774
+ const source = this.readXlsxSource(doc)
775
+ const patched = source ? patchXlsx(doc, source) : null
671
776
  return {
672
777
  filename: `${base}.${doc.targetLang}.xlsx`,
673
778
  mime: XLSX_MIME,
674
- content: buildXlsx(doc),
779
+ content: patched ? patched.buffer : buildXlsx(doc),
675
780
  count: doc.segments.length,
676
781
  }
677
782
  }