dsh-cait 1.0.3 → 1.0.4
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/CHANGELOG.md +367 -352
- package/LICENSE +201 -201
- package/README.ja.md +4 -3
- package/README.md +424 -423
- package/README.zh-CN.md +391 -390
- package/client/components.js +2377 -2377
- package/client/helpers.js +103 -103
- package/client/locales.js +730 -730
- package/client/rpc.js +93 -93
- package/client/styles.js +258 -258
- package/client.js +3556 -3556
- package/icon.svg +8 -8
- package/index.js +45 -45
- package/lib/api.js +223 -223
- package/lib/docx.js +389 -389
- package/lib/errors.js +8 -8
- package/lib/formats.js +917 -917
- package/lib/glossary.js +178 -178
- package/lib/import-export.js +605 -605
- package/lib/libraries.js +183 -183
- package/lib/markdown.js +28 -28
- package/lib/mt.js +152 -152
- package/lib/persistence.js +321 -321
- package/lib/pptx.js +89 -89
- package/lib/segmenter.js +84 -84
- package/lib/similarity.js +113 -113
- package/lib/store-utils.js +339 -339
- package/lib/store.js +734 -734
- package/lib/tm.js +160 -160
- package/lib/tmx.js +85 -85
- package/lib/tools.js +472 -472
- package/lib/xlsx.js +218 -218
- package/lib/xml.js +31 -31
- package/locale/en.json +11 -11
- package/locale/zh.json +11 -11
- package/package.json +83 -83
- package/scripts/build-client.mjs +13 -13
package/lib/xlsx.js
CHANGED
|
@@ -1,218 +1,218 @@
|
|
|
1
|
-
import { readZipEntries, writeZip } from './docx.js'
|
|
2
|
-
import { CatError } from './errors.js'
|
|
3
|
-
import { decodeXml, escapeXml } from './xml.js'
|
|
4
|
-
|
|
5
|
-
export const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
6
|
-
|
|
7
|
-
const SOURCE_HEADERS = new Set(['source', 'src', 'original', '原文', '源文', '源文本'])
|
|
8
|
-
const TARGET_HEADERS = new Set(['target', 'tgt', 'translation', '译文', '目标', '目标文本'])
|
|
9
|
-
|
|
10
|
-
function fail(code, message) {
|
|
11
|
-
return new CatError(code, message)
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function zipText(entries, name) {
|
|
15
|
-
const entry = entries.find((item) => item.name === name)
|
|
16
|
-
return entry ? entry.data.toString('utf8') : ''
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function attributes(raw) {
|
|
20
|
-
const attrs = {}
|
|
21
|
-
for (const match of String(raw).matchAll(/([\w:.-]+)\s*=\s*"([^"]*)"/g)) {
|
|
22
|
-
attrs[match[1]] = decodeXml(match[2])
|
|
23
|
-
}
|
|
24
|
-
return attrs
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function sharedStringTable(xml) {
|
|
28
|
-
if (!xml) return []
|
|
29
|
-
return [...xml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/gi)].map((match) => (
|
|
30
|
-
[...match[1].matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
|
|
31
|
-
.map((text) => decodeXml(text[1]))
|
|
32
|
-
.join('')
|
|
33
|
-
))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function firstSheetPath(entries) {
|
|
37
|
-
const workbook = zipText(entries, 'xl/workbook.xml')
|
|
38
|
-
const rels = zipText(entries, 'xl/_rels/workbook.xml.rels')
|
|
39
|
-
if (workbook && rels) {
|
|
40
|
-
const sheet = workbook.match(/<sheet\b([^>]*)\/?>/i)
|
|
41
|
-
const id = sheet ? attributes(sheet[1])['r:id'] : ''
|
|
42
|
-
if (id) {
|
|
43
|
-
const relation = [...rels.matchAll(/<Relationship\b([^>]*)\/?>/gi)]
|
|
44
|
-
.map((match) => attributes(match[1]))
|
|
45
|
-
.find((item) => item.Id === id)
|
|
46
|
-
if (relation?.Target) {
|
|
47
|
-
const target = relation.Target.replace(/^\/+/, '')
|
|
48
|
-
return target.startsWith('xl/') ? target : `xl/${target}`
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const fallback = entries.find((item) => /^xl\/worksheets\/sheet\d+\.xml$/i.test(item.name))
|
|
53
|
-
return fallback?.name ?? 'xl/worksheets/sheet1.xml'
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function columnIndex(reference) {
|
|
57
|
-
const letters = String(reference).match(/^[A-Z]+/i)?.[0]?.toUpperCase() ?? ''
|
|
58
|
-
let value = 0
|
|
59
|
-
for (const letter of letters) value = value * 26 + (letter.charCodeAt(0) - 64)
|
|
60
|
-
return value - 1
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function cellValue(attrs, body, shared) {
|
|
64
|
-
const type = attrs.t || ''
|
|
65
|
-
const inline = body.match(/<is\b[^>]*>([\s\S]*?)<\/is>/i)
|
|
66
|
-
if (type === 'inlineStr' || inline) {
|
|
67
|
-
const source = inline ? inline[1] : body
|
|
68
|
-
return [...source.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
|
|
69
|
-
.map((match) => decodeXml(match[1]))
|
|
70
|
-
.join('')
|
|
71
|
-
}
|
|
72
|
-
const value = body.match(/<v\b[^>]*>([\s\S]*?)<\/v>/i)
|
|
73
|
-
if (!value) return ''
|
|
74
|
-
if (type === 's') return shared[Number(value[1])] ?? ''
|
|
75
|
-
if (type === 'b') return value[1] === '1' ? 'TRUE' : 'FALSE'
|
|
76
|
-
return decodeXml(value[1])
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function parseRows(xml, shared) {
|
|
80
|
-
const rows = new Map()
|
|
81
|
-
for (const rowMatch of String(xml).matchAll(/<row\b([^>]*)>([\s\S]*?)<\/row>/gi)) {
|
|
82
|
-
const rowAttrs = attributes(rowMatch[1])
|
|
83
|
-
const rowNumber = Number(rowAttrs.r) || rows.size + 1
|
|
84
|
-
const cells = new Map()
|
|
85
|
-
for (const cellMatch of rowMatch[2].matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/gi)) {
|
|
86
|
-
const attrs = attributes(cellMatch[1])
|
|
87
|
-
if (!attrs.r) continue
|
|
88
|
-
cells.set(columnIndex(attrs.r), cellValue(attrs, cellMatch[2] ?? '', shared))
|
|
89
|
-
}
|
|
90
|
-
if (cells.size) rows.set(rowNumber, cells)
|
|
91
|
-
}
|
|
92
|
-
return [...rows.entries()].sort((left, right) => left[0] - right[0])
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export function parseXlsx(buffer) {
|
|
96
|
-
let entries
|
|
97
|
-
try {
|
|
98
|
-
entries = readZipEntries(buffer)
|
|
99
|
-
} catch (error) {
|
|
100
|
-
throw fail('XLSX_PARSE', `XLSX 文件不是有效的 ZIP/Excel 包: ${error.message}`)
|
|
101
|
-
}
|
|
102
|
-
const shared = sharedStringTable(zipText(entries, 'xl/sharedStrings.xml'))
|
|
103
|
-
const sheetPath = firstSheetPath(entries)
|
|
104
|
-
const sheetXml = zipText(entries, sheetPath)
|
|
105
|
-
if (!sheetXml) throw fail('XLSX_PARSE', 'XLSX 中找不到工作表。')
|
|
106
|
-
const rows = parseRows(sheetXml, shared)
|
|
107
|
-
if (!rows.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的行。')
|
|
108
|
-
|
|
109
|
-
const maxColumns = rows.reduce((max, [, cells]) => Math.max(max, ...cells.keys(), -1), -1) + 1
|
|
110
|
-
const first = rows[0][1]
|
|
111
|
-
const header = (index) => String(first.get(index) ?? '').trim().toLowerCase()
|
|
112
|
-
let sourceIndex = -1
|
|
113
|
-
let targetIndex = -1
|
|
114
|
-
for (let index = 0; index < maxColumns; index += 1) {
|
|
115
|
-
if (SOURCE_HEADERS.has(header(index))) sourceIndex = index
|
|
116
|
-
if (TARGET_HEADERS.has(header(index))) targetIndex = index
|
|
117
|
-
}
|
|
118
|
-
const hasHeader = sourceIndex >= 0 && targetIndex >= 0
|
|
119
|
-
if (!hasHeader) {
|
|
120
|
-
sourceIndex = 0
|
|
121
|
-
targetIndex = maxColumns > 1 ? 1 : -1
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const segments = []
|
|
125
|
-
for (const [rowNumber, cells] of rows) {
|
|
126
|
-
if (hasHeader && rowNumber === rows[0][0]) continue
|
|
127
|
-
const source = String(cells.get(sourceIndex) ?? '').trim()
|
|
128
|
-
if (!source) continue
|
|
129
|
-
const target = targetIndex >= 0 ? String(cells.get(targetIndex) ?? '').trim() : ''
|
|
130
|
-
segments.push({
|
|
131
|
-
source,
|
|
132
|
-
target,
|
|
133
|
-
paragraph: segments.length,
|
|
134
|
-
meta: { format: 'xlsx', xlsxRow: rowNumber },
|
|
135
|
-
})
|
|
136
|
-
}
|
|
137
|
-
if (!segments.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的源文行。')
|
|
138
|
-
return { format: 'xlsx', segments, warnings: [] }
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function inlineCell(reference, value) {
|
|
142
|
-
return `<c r="${reference}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function columnName(index) {
|
|
146
|
-
let value = index + 1
|
|
147
|
-
let name = ''
|
|
148
|
-
while (value > 0) {
|
|
149
|
-
const remainder = (value - 1) % 26
|
|
150
|
-
name = String.fromCharCode(65 + remainder) + name
|
|
151
|
-
value = Math.floor((value - 1) / 26)
|
|
152
|
-
}
|
|
153
|
-
return name
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function buildXlsx(doc) {
|
|
157
|
-
const rows = [[
|
|
158
|
-
inlineCell('A1', 'Source'),
|
|
159
|
-
inlineCell('B1', 'Target'),
|
|
160
|
-
]]
|
|
161
|
-
doc.segments.forEach((segment, index) => {
|
|
162
|
-
const row = index + 2
|
|
163
|
-
rows.push([
|
|
164
|
-
inlineCell(`A${row}`, segment.source || ''),
|
|
165
|
-
inlineCell(`B${row}`, segment.target || ''),
|
|
166
|
-
])
|
|
167
|
-
})
|
|
168
|
-
const sheetData = rows
|
|
169
|
-
.map((cells, index) => ` <row r="${index + 1}">${cells.join('')}</row>`)
|
|
170
|
-
.join('\n')
|
|
171
|
-
const lastRow = rows.length + 1
|
|
172
|
-
const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
173
|
-
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
174
|
-
<dimension ref="A1:B${lastRow}"/>
|
|
175
|
-
<sheetViews><sheetView workbookViewId="0"/></sheetViews>
|
|
176
|
-
<sheetFormatPr defaultRowHeight="15"/>
|
|
177
|
-
<sheetData>
|
|
178
|
-
${sheetData}
|
|
179
|
-
</sheetData>
|
|
180
|
-
</worksheet>`
|
|
181
|
-
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
182
|
-
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
183
|
-
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
184
|
-
<Default Extension="xml" ContentType="application/xml"/>
|
|
185
|
-
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
|
186
|
-
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
|
187
|
-
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
|
188
|
-
</Types>`
|
|
189
|
-
const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
190
|
-
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
191
|
-
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
|
192
|
-
</Relationships>`
|
|
193
|
-
const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
194
|
-
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
195
|
-
<sheets><sheet name="CAIT" sheetId="1" r:id="rId1"/></sheets>
|
|
196
|
-
</workbook>`
|
|
197
|
-
const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
198
|
-
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
199
|
-
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
|
200
|
-
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
|
201
|
-
</Relationships>`
|
|
202
|
-
const styles = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
203
|
-
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
204
|
-
<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
|
|
205
|
-
<fills count="1"><fill><patternFill patternType="none"/></fill></fills>
|
|
206
|
-
<borders count="1"><border/></borders>
|
|
207
|
-
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
|
|
208
|
-
<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
|
|
209
|
-
</styleSheet>`
|
|
210
|
-
return writeZip([
|
|
211
|
-
{ name: '[Content_Types].xml', data: Buffer.from(contentTypes, 'utf8') },
|
|
212
|
-
{ name: '_rels/.rels', data: Buffer.from(rootRels, 'utf8') },
|
|
213
|
-
{ name: 'xl/workbook.xml', data: Buffer.from(workbook, 'utf8') },
|
|
214
|
-
{ name: 'xl/_rels/workbook.xml.rels', data: Buffer.from(workbookRels, 'utf8') },
|
|
215
|
-
{ name: 'xl/worksheets/sheet1.xml', data: Buffer.from(sheet, 'utf8') },
|
|
216
|
-
{ name: 'xl/styles.xml', data: Buffer.from(styles, 'utf8') },
|
|
217
|
-
])
|
|
218
|
-
}
|
|
1
|
+
import { readZipEntries, writeZip } from './docx.js'
|
|
2
|
+
import { CatError } from './errors.js'
|
|
3
|
+
import { decodeXml, escapeXml } from './xml.js'
|
|
4
|
+
|
|
5
|
+
export const XLSX_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
6
|
+
|
|
7
|
+
const SOURCE_HEADERS = new Set(['source', 'src', 'original', '原文', '源文', '源文本'])
|
|
8
|
+
const TARGET_HEADERS = new Set(['target', 'tgt', 'translation', '译文', '目标', '目标文本'])
|
|
9
|
+
|
|
10
|
+
function fail(code, message) {
|
|
11
|
+
return new CatError(code, message)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function zipText(entries, name) {
|
|
15
|
+
const entry = entries.find((item) => item.name === name)
|
|
16
|
+
return entry ? entry.data.toString('utf8') : ''
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function attributes(raw) {
|
|
20
|
+
const attrs = {}
|
|
21
|
+
for (const match of String(raw).matchAll(/([\w:.-]+)\s*=\s*"([^"]*)"/g)) {
|
|
22
|
+
attrs[match[1]] = decodeXml(match[2])
|
|
23
|
+
}
|
|
24
|
+
return attrs
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sharedStringTable(xml) {
|
|
28
|
+
if (!xml) return []
|
|
29
|
+
return [...xml.matchAll(/<si\b[^>]*>([\s\S]*?)<\/si>/gi)].map((match) => (
|
|
30
|
+
[...match[1].matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
|
|
31
|
+
.map((text) => decodeXml(text[1]))
|
|
32
|
+
.join('')
|
|
33
|
+
))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function firstSheetPath(entries) {
|
|
37
|
+
const workbook = zipText(entries, 'xl/workbook.xml')
|
|
38
|
+
const rels = zipText(entries, 'xl/_rels/workbook.xml.rels')
|
|
39
|
+
if (workbook && rels) {
|
|
40
|
+
const sheet = workbook.match(/<sheet\b([^>]*)\/?>/i)
|
|
41
|
+
const id = sheet ? attributes(sheet[1])['r:id'] : ''
|
|
42
|
+
if (id) {
|
|
43
|
+
const relation = [...rels.matchAll(/<Relationship\b([^>]*)\/?>/gi)]
|
|
44
|
+
.map((match) => attributes(match[1]))
|
|
45
|
+
.find((item) => item.Id === id)
|
|
46
|
+
if (relation?.Target) {
|
|
47
|
+
const target = relation.Target.replace(/^\/+/, '')
|
|
48
|
+
return target.startsWith('xl/') ? target : `xl/${target}`
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const fallback = entries.find((item) => /^xl\/worksheets\/sheet\d+\.xml$/i.test(item.name))
|
|
53
|
+
return fallback?.name ?? 'xl/worksheets/sheet1.xml'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function columnIndex(reference) {
|
|
57
|
+
const letters = String(reference).match(/^[A-Z]+/i)?.[0]?.toUpperCase() ?? ''
|
|
58
|
+
let value = 0
|
|
59
|
+
for (const letter of letters) value = value * 26 + (letter.charCodeAt(0) - 64)
|
|
60
|
+
return value - 1
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function cellValue(attrs, body, shared) {
|
|
64
|
+
const type = attrs.t || ''
|
|
65
|
+
const inline = body.match(/<is\b[^>]*>([\s\S]*?)<\/is>/i)
|
|
66
|
+
if (type === 'inlineStr' || inline) {
|
|
67
|
+
const source = inline ? inline[1] : body
|
|
68
|
+
return [...source.matchAll(/<t\b[^>]*>([\s\S]*?)<\/t>/gi)]
|
|
69
|
+
.map((match) => decodeXml(match[1]))
|
|
70
|
+
.join('')
|
|
71
|
+
}
|
|
72
|
+
const value = body.match(/<v\b[^>]*>([\s\S]*?)<\/v>/i)
|
|
73
|
+
if (!value) return ''
|
|
74
|
+
if (type === 's') return shared[Number(value[1])] ?? ''
|
|
75
|
+
if (type === 'b') return value[1] === '1' ? 'TRUE' : 'FALSE'
|
|
76
|
+
return decodeXml(value[1])
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseRows(xml, shared) {
|
|
80
|
+
const rows = new Map()
|
|
81
|
+
for (const rowMatch of String(xml).matchAll(/<row\b([^>]*)>([\s\S]*?)<\/row>/gi)) {
|
|
82
|
+
const rowAttrs = attributes(rowMatch[1])
|
|
83
|
+
const rowNumber = Number(rowAttrs.r) || rows.size + 1
|
|
84
|
+
const cells = new Map()
|
|
85
|
+
for (const cellMatch of rowMatch[2].matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/gi)) {
|
|
86
|
+
const attrs = attributes(cellMatch[1])
|
|
87
|
+
if (!attrs.r) continue
|
|
88
|
+
cells.set(columnIndex(attrs.r), cellValue(attrs, cellMatch[2] ?? '', shared))
|
|
89
|
+
}
|
|
90
|
+
if (cells.size) rows.set(rowNumber, cells)
|
|
91
|
+
}
|
|
92
|
+
return [...rows.entries()].sort((left, right) => left[0] - right[0])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function parseXlsx(buffer) {
|
|
96
|
+
let entries
|
|
97
|
+
try {
|
|
98
|
+
entries = readZipEntries(buffer)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
throw fail('XLSX_PARSE', `XLSX 文件不是有效的 ZIP/Excel 包: ${error.message}`)
|
|
101
|
+
}
|
|
102
|
+
const shared = sharedStringTable(zipText(entries, 'xl/sharedStrings.xml'))
|
|
103
|
+
const sheetPath = firstSheetPath(entries)
|
|
104
|
+
const sheetXml = zipText(entries, sheetPath)
|
|
105
|
+
if (!sheetXml) throw fail('XLSX_PARSE', 'XLSX 中找不到工作表。')
|
|
106
|
+
const rows = parseRows(sheetXml, shared)
|
|
107
|
+
if (!rows.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的行。')
|
|
108
|
+
|
|
109
|
+
const maxColumns = rows.reduce((max, [, cells]) => Math.max(max, ...cells.keys(), -1), -1) + 1
|
|
110
|
+
const first = rows[0][1]
|
|
111
|
+
const header = (index) => String(first.get(index) ?? '').trim().toLowerCase()
|
|
112
|
+
let sourceIndex = -1
|
|
113
|
+
let targetIndex = -1
|
|
114
|
+
for (let index = 0; index < maxColumns; index += 1) {
|
|
115
|
+
if (SOURCE_HEADERS.has(header(index))) sourceIndex = index
|
|
116
|
+
if (TARGET_HEADERS.has(header(index))) targetIndex = index
|
|
117
|
+
}
|
|
118
|
+
const hasHeader = sourceIndex >= 0 && targetIndex >= 0
|
|
119
|
+
if (!hasHeader) {
|
|
120
|
+
sourceIndex = 0
|
|
121
|
+
targetIndex = maxColumns > 1 ? 1 : -1
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const segments = []
|
|
125
|
+
for (const [rowNumber, cells] of rows) {
|
|
126
|
+
if (hasHeader && rowNumber === rows[0][0]) continue
|
|
127
|
+
const source = String(cells.get(sourceIndex) ?? '').trim()
|
|
128
|
+
if (!source) continue
|
|
129
|
+
const target = targetIndex >= 0 ? String(cells.get(targetIndex) ?? '').trim() : ''
|
|
130
|
+
segments.push({
|
|
131
|
+
source,
|
|
132
|
+
target,
|
|
133
|
+
paragraph: segments.length,
|
|
134
|
+
meta: { format: 'xlsx', xlsxRow: rowNumber },
|
|
135
|
+
})
|
|
136
|
+
}
|
|
137
|
+
if (!segments.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的源文行。')
|
|
138
|
+
return { format: 'xlsx', segments, warnings: [] }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function inlineCell(reference, value) {
|
|
142
|
+
return `<c r="${reference}" t="inlineStr"><is><t xml:space="preserve">${escapeXml(value)}</t></is></c>`
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function columnName(index) {
|
|
146
|
+
let value = index + 1
|
|
147
|
+
let name = ''
|
|
148
|
+
while (value > 0) {
|
|
149
|
+
const remainder = (value - 1) % 26
|
|
150
|
+
name = String.fromCharCode(65 + remainder) + name
|
|
151
|
+
value = Math.floor((value - 1) / 26)
|
|
152
|
+
}
|
|
153
|
+
return name
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function buildXlsx(doc) {
|
|
157
|
+
const rows = [[
|
|
158
|
+
inlineCell('A1', 'Source'),
|
|
159
|
+
inlineCell('B1', 'Target'),
|
|
160
|
+
]]
|
|
161
|
+
doc.segments.forEach((segment, index) => {
|
|
162
|
+
const row = index + 2
|
|
163
|
+
rows.push([
|
|
164
|
+
inlineCell(`A${row}`, segment.source || ''),
|
|
165
|
+
inlineCell(`B${row}`, segment.target || ''),
|
|
166
|
+
])
|
|
167
|
+
})
|
|
168
|
+
const sheetData = rows
|
|
169
|
+
.map((cells, index) => ` <row r="${index + 1}">${cells.join('')}</row>`)
|
|
170
|
+
.join('\n')
|
|
171
|
+
const lastRow = rows.length + 1
|
|
172
|
+
const sheet = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
173
|
+
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
174
|
+
<dimension ref="A1:B${lastRow}"/>
|
|
175
|
+
<sheetViews><sheetView workbookViewId="0"/></sheetViews>
|
|
176
|
+
<sheetFormatPr defaultRowHeight="15"/>
|
|
177
|
+
<sheetData>
|
|
178
|
+
${sheetData}
|
|
179
|
+
</sheetData>
|
|
180
|
+
</worksheet>`
|
|
181
|
+
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
182
|
+
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
183
|
+
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
184
|
+
<Default Extension="xml" ContentType="application/xml"/>
|
|
185
|
+
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
|
186
|
+
<Override PartName="/xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>
|
|
187
|
+
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
|
188
|
+
</Types>`
|
|
189
|
+
const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
190
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
191
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
|
|
192
|
+
</Relationships>`
|
|
193
|
+
const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
194
|
+
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
195
|
+
<sheets><sheet name="CAIT" sheetId="1" r:id="rId1"/></sheets>
|
|
196
|
+
</workbook>`
|
|
197
|
+
const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
198
|
+
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
199
|
+
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
|
200
|
+
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
|
201
|
+
</Relationships>`
|
|
202
|
+
const styles = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
203
|
+
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
204
|
+
<fonts count="1"><font><sz val="11"/><name val="Calibri"/></font></fonts>
|
|
205
|
+
<fills count="1"><fill><patternFill patternType="none"/></fill></fills>
|
|
206
|
+
<borders count="1"><border/></borders>
|
|
207
|
+
<cellStyleXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0"/></cellStyleXfs>
|
|
208
|
+
<cellXfs count="1"><xf numFmtId="0" fontId="0" fillId="0" borderId="0" xfId="0"/></cellXfs>
|
|
209
|
+
</styleSheet>`
|
|
210
|
+
return writeZip([
|
|
211
|
+
{ name: '[Content_Types].xml', data: Buffer.from(contentTypes, 'utf8') },
|
|
212
|
+
{ name: '_rels/.rels', data: Buffer.from(rootRels, 'utf8') },
|
|
213
|
+
{ name: 'xl/workbook.xml', data: Buffer.from(workbook, 'utf8') },
|
|
214
|
+
{ name: 'xl/_rels/workbook.xml.rels', data: Buffer.from(workbookRels, 'utf8') },
|
|
215
|
+
{ name: 'xl/worksheets/sheet1.xml', data: Buffer.from(sheet, 'utf8') },
|
|
216
|
+
{ name: 'xl/styles.xml', data: Buffer.from(styles, 'utf8') },
|
|
217
|
+
])
|
|
218
|
+
}
|
package/lib/xml.js
CHANGED
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
/** Minimal, dependency-free XML helpers for TMX and DOCX. */
|
|
2
|
-
|
|
3
|
-
const NAMED = {
|
|
4
|
-
amp: '&',
|
|
5
|
-
lt: '<',
|
|
6
|
-
gt: '>',
|
|
7
|
-
quot: '"',
|
|
8
|
-
apos: "'",
|
|
9
|
-
nbsp: '\u00a0',
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function decodeXml(value) {
|
|
13
|
-
return String(value ?? '').replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (all, body) => {
|
|
14
|
-
if (body[0] === '#') {
|
|
15
|
-
const code = body[1].toLowerCase() === 'x'
|
|
16
|
-
? Number.parseInt(body.slice(2), 16)
|
|
17
|
-
: Number.parseInt(body.slice(1), 10)
|
|
18
|
-
return Number.isFinite(code) ? String.fromCodePoint(code) : all
|
|
19
|
-
}
|
|
20
|
-
return NAMED[body.toLowerCase()] ?? all
|
|
21
|
-
})
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function escapeXml(value) {
|
|
25
|
-
return String(value ?? '')
|
|
26
|
-
.replace(/&/g, '&')
|
|
27
|
-
.replace(/</g, '<')
|
|
28
|
-
.replace(/>/g, '>')
|
|
29
|
-
.replace(/"/g, '"')
|
|
30
|
-
.replace(/'/g, ''')
|
|
31
|
-
}
|
|
1
|
+
/** Minimal, dependency-free XML helpers for TMX and DOCX. */
|
|
2
|
+
|
|
3
|
+
const NAMED = {
|
|
4
|
+
amp: '&',
|
|
5
|
+
lt: '<',
|
|
6
|
+
gt: '>',
|
|
7
|
+
quot: '"',
|
|
8
|
+
apos: "'",
|
|
9
|
+
nbsp: '\u00a0',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function decodeXml(value) {
|
|
13
|
+
return String(value ?? '').replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (all, body) => {
|
|
14
|
+
if (body[0] === '#') {
|
|
15
|
+
const code = body[1].toLowerCase() === 'x'
|
|
16
|
+
? Number.parseInt(body.slice(2), 16)
|
|
17
|
+
: Number.parseInt(body.slice(1), 10)
|
|
18
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : all
|
|
19
|
+
}
|
|
20
|
+
return NAMED[body.toLowerCase()] ?? all
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function escapeXml(value) {
|
|
25
|
+
return String(value ?? '')
|
|
26
|
+
.replace(/&/g, '&')
|
|
27
|
+
.replace(/</g, '<')
|
|
28
|
+
.replace(/>/g, '>')
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, ''')
|
|
31
|
+
}
|
package/locale/en.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
{
|
|
2
|
-
"meta": {
|
|
3
|
-
"title": "CAIT (dsh-cait)",
|
|
4
|
-
"description": "A CAT (computer-aided translation) workbench inside DSH: translation memory, glossary, segment editing, AI translation suggestions, and TXT/Markdown/DOCX/TMX import and export."
|
|
5
|
-
},
|
|
6
|
-
"match.exact": "Exact",
|
|
7
|
-
"match.near_exact": "Near exact",
|
|
8
|
-
"match.high_fuzzy": "High fuzzy",
|
|
9
|
-
"match.medium_fuzzy": "Medium fuzzy",
|
|
10
|
-
"match.low_fuzzy": "Low fuzzy"
|
|
11
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "CAIT (dsh-cait)",
|
|
4
|
+
"description": "A CAT (computer-aided translation) workbench inside DSH: translation memory, glossary, segment editing, AI translation suggestions, and TXT/Markdown/DOCX/TMX import and export."
|
|
5
|
+
},
|
|
6
|
+
"match.exact": "Exact",
|
|
7
|
+
"match.near_exact": "Near exact",
|
|
8
|
+
"match.high_fuzzy": "High fuzzy",
|
|
9
|
+
"match.medium_fuzzy": "Medium fuzzy",
|
|
10
|
+
"match.low_fuzzy": "Low fuzzy"
|
|
11
|
+
}
|
package/locale/zh.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
{
|
|
2
|
-
"meta": {
|
|
3
|
-
"title": "CAIT 翻译工作台 (dsh-cait)",
|
|
4
|
-
"description": "DSH 内置的 CAT(计算机辅助翻译)工作台:翻译记忆、术语库、句段编辑、AI 翻译建议,以及 TXT/Markdown/DOCX/TMX 导入导出。"
|
|
5
|
-
},
|
|
6
|
-
"match.exact": "精确",
|
|
7
|
-
"match.near_exact": "近似精确",
|
|
8
|
-
"match.high_fuzzy": "高模糊",
|
|
9
|
-
"match.medium_fuzzy": "中模糊",
|
|
10
|
-
"match.low_fuzzy": "低模糊"
|
|
11
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "CAIT 翻译工作台 (dsh-cait)",
|
|
4
|
+
"description": "DSH 内置的 CAT(计算机辅助翻译)工作台:翻译记忆、术语库、句段编辑、AI 翻译建议,以及 TXT/Markdown/DOCX/TMX 导入导出。"
|
|
5
|
+
},
|
|
6
|
+
"match.exact": "精确",
|
|
7
|
+
"match.near_exact": "近似精确",
|
|
8
|
+
"match.high_fuzzy": "高模糊",
|
|
9
|
+
"match.medium_fuzzy": "中模糊",
|
|
10
|
+
"match.low_fuzzy": "低模糊"
|
|
11
|
+
}
|