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.
- package/CHANGELOG.md +43 -0
- package/README.ja.md +20 -10
- package/README.md +91 -72
- package/README.zh-CN.md +20 -10
- package/client/components.js +126 -13
- package/client/locales.js +74 -2
- package/client/styles.js +6 -2
- package/client.js +206 -17
- package/lib/aligner.js +6 -6
- package/lib/api.js +6 -0
- package/lib/docx.js +124 -26
- package/lib/import-export.js +115 -10
- package/lib/localization.js +1098 -0
- package/lib/persistence.js +14 -0
- package/lib/pptx.js +33 -1
- package/lib/qa.js +18 -0
- package/lib/store-utils.js +8 -3
- package/lib/store.js +14 -2
- package/lib/subtitles.js +213 -0
- package/lib/xlsx.js +176 -66
- package/package.json +1 -1
package/lib/xlsx.js
CHANGED
|
@@ -33,24 +33,33 @@ function sharedStringTable(xml) {
|
|
|
33
33
|
))
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
function
|
|
36
|
+
function workbookSheets(entries) {
|
|
37
37
|
const workbook = zipText(entries, 'xl/workbook.xml')
|
|
38
38
|
const rels = zipText(entries, 'xl/_rels/workbook.xml.rels')
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
39
|
+
const relationMap = new Map(
|
|
40
|
+
[...rels.matchAll(/<Relationship\b([^>]*)\/?>/gi)]
|
|
41
|
+
.map((match) => attributes(match[1]))
|
|
42
|
+
.filter((item) => item.Id && item.Target)
|
|
43
|
+
.map((item) => [item.Id, item.Target]),
|
|
44
|
+
)
|
|
45
|
+
const sheets = []
|
|
46
|
+
for (const match of workbook.matchAll(/<sheet\b([^>]*)\/?>/gi)) {
|
|
47
|
+
const attrs = attributes(match[1])
|
|
48
|
+
const id = attrs['r:id']
|
|
49
|
+
const target = relationMap.get(id)
|
|
50
|
+
if (!target) continue
|
|
51
|
+
const path = target.replace(/^\/+/, '')
|
|
52
|
+
sheets.push({
|
|
53
|
+
name: attrs.name || `Sheet${sheets.length + 1}`,
|
|
54
|
+
path: path.startsWith('xl/') ? path : `xl/${path}`,
|
|
55
|
+
})
|
|
51
56
|
}
|
|
52
|
-
|
|
53
|
-
return
|
|
57
|
+
if (sheets.length) return sheets
|
|
58
|
+
return entries
|
|
59
|
+
.map((entry) => entry.name)
|
|
60
|
+
.filter((name) => /^xl\/worksheets\/sheet\d+\.xml$/i.test(name))
|
|
61
|
+
.sort((left, right) => Number(left.match(/(\d+)\.xml$/)[1]) - Number(right.match(/(\d+)\.xml$/)[1]))
|
|
62
|
+
.map((path, index) => ({ name: `Sheet${index + 1}`, path }))
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
function columnIndex(reference) {
|
|
@@ -85,7 +94,12 @@ function parseRows(xml, shared) {
|
|
|
85
94
|
for (const cellMatch of rowMatch[2].matchAll(/<c\b([^>]*?)(?:\/>|>([\s\S]*?)<\/c>)/gi)) {
|
|
86
95
|
const attrs = attributes(cellMatch[1])
|
|
87
96
|
if (!attrs.r) continue
|
|
88
|
-
|
|
97
|
+
const body = cellMatch[2] ?? ''
|
|
98
|
+
cells.set(columnIndex(attrs.r), {
|
|
99
|
+
cell: attrs.r,
|
|
100
|
+
value: cellValue(attrs, body, shared),
|
|
101
|
+
formula: /<f\b/i.test(body),
|
|
102
|
+
})
|
|
89
103
|
}
|
|
90
104
|
if (cells.size) rows.set(rowNumber, cells)
|
|
91
105
|
}
|
|
@@ -100,39 +114,53 @@ export function parseXlsx(buffer) {
|
|
|
100
114
|
throw fail('XLSX_PARSE', `XLSX 文件不是有效的 ZIP/Excel 包: ${error.message}`)
|
|
101
115
|
}
|
|
102
116
|
const shared = sharedStringTable(zipText(entries, 'xl/sharedStrings.xml'))
|
|
103
|
-
const
|
|
104
|
-
|
|
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
|
-
}
|
|
117
|
+
const sheets = workbookSheets(entries)
|
|
118
|
+
if (!sheets.length) throw fail('XLSX_PARSE', 'XLSX 中找不到工作表。')
|
|
123
119
|
|
|
124
120
|
const segments = []
|
|
125
|
-
for (const [
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
121
|
+
for (const [sheetIndex, sheet] of sheets.entries()) {
|
|
122
|
+
const sheetXml = zipText(entries, sheet.path)
|
|
123
|
+
if (!sheetXml) continue
|
|
124
|
+
const rows = parseRows(sheetXml, shared)
|
|
125
|
+
if (!rows.length) continue
|
|
126
|
+
|
|
127
|
+
const maxColumns = rows.reduce((max, [, cells]) => Math.max(max, ...cells.keys(), -1), -1) + 1
|
|
128
|
+
const first = rows[0][1]
|
|
129
|
+
const header = (index) => String(first.get(index)?.value ?? '').trim().toLowerCase()
|
|
130
|
+
let sourceIndex = -1
|
|
131
|
+
let targetIndex = -1
|
|
132
|
+
for (let index = 0; index < maxColumns; index += 1) {
|
|
133
|
+
if (SOURCE_HEADERS.has(header(index))) sourceIndex = index
|
|
134
|
+
if (TARGET_HEADERS.has(header(index))) targetIndex = index
|
|
135
|
+
}
|
|
136
|
+
const hasHeader = sourceIndex >= 0 && targetIndex >= 0
|
|
137
|
+
if (!hasHeader) {
|
|
138
|
+
sourceIndex = 0
|
|
139
|
+
targetIndex = maxColumns > 1 ? 1 : -1
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
for (const [rowNumber, cells] of rows) {
|
|
143
|
+
if (hasHeader && rowNumber === rows[0][0]) continue
|
|
144
|
+
const sourceCell = cells.get(sourceIndex)
|
|
145
|
+
if (!sourceCell || sourceCell.formula) continue
|
|
146
|
+
const source = String(sourceCell.value ?? '').trim()
|
|
147
|
+
if (!source) continue
|
|
148
|
+
const targetCell = targetIndex >= 0 ? cells.get(targetIndex) : null
|
|
149
|
+
const target = targetCell ? String(targetCell.value ?? '').trim() : ''
|
|
150
|
+
segments.push({
|
|
151
|
+
source,
|
|
152
|
+
target,
|
|
153
|
+
paragraph: segments.length,
|
|
154
|
+
meta: {
|
|
155
|
+
format: 'xlsx',
|
|
156
|
+
xlsxRow: rowNumber,
|
|
157
|
+
sheet: sheetIndex + 1,
|
|
158
|
+
sheetName: sheet.name,
|
|
159
|
+
cell: sourceCell.cell,
|
|
160
|
+
...(targetCell?.cell ? { targetCell: targetCell.cell } : {}),
|
|
161
|
+
},
|
|
162
|
+
})
|
|
163
|
+
}
|
|
136
164
|
}
|
|
137
165
|
if (!segments.length) throw fail('XLSX_NO_UNITS', 'XLSX 中没有可用的源文行。')
|
|
138
166
|
return { format: 'xlsx', segments, warnings: [] }
|
|
@@ -153,23 +181,12 @@ function columnName(index) {
|
|
|
153
181
|
return name
|
|
154
182
|
}
|
|
155
183
|
|
|
156
|
-
|
|
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
|
-
})
|
|
184
|
+
function sheetXml(rows) {
|
|
168
185
|
const sheetData = rows
|
|
169
186
|
.map((cells, index) => ` <row r="${index + 1}">${cells.join('')}</row>`)
|
|
170
187
|
.join('\n')
|
|
171
|
-
const lastRow = rows.length
|
|
172
|
-
|
|
188
|
+
const lastRow = Math.max(rows.length, 1)
|
|
189
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
173
190
|
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
174
191
|
<dimension ref="A1:B${lastRow}"/>
|
|
175
192
|
<sheetViews><sheetView workbookViewId="0"/></sheetViews>
|
|
@@ -178,12 +195,105 @@ export function buildXlsx(doc) {
|
|
|
178
195
|
${sheetData}
|
|
179
196
|
</sheetData>
|
|
180
197
|
</worksheet>`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function safeSheetName(name, used) {
|
|
201
|
+
let base = String(name ?? '').replace(/[\[\]:*?\/\\]/g, ' ').trim().slice(0, 31) || 'Sheet'
|
|
202
|
+
let candidate = base
|
|
203
|
+
let index = 2
|
|
204
|
+
while (used.has(candidate.toLowerCase())) {
|
|
205
|
+
const suffix = ` ${index}`
|
|
206
|
+
candidate = `${base.slice(0, 31 - suffix.length)}${suffix}`
|
|
207
|
+
index += 1
|
|
208
|
+
}
|
|
209
|
+
used.add(candidate.toLowerCase())
|
|
210
|
+
return candidate
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function replaceCellText(xml, reference, value) {
|
|
214
|
+
const escaped = escapeXml(value)
|
|
215
|
+
const pattern = new RegExp(
|
|
216
|
+
`<c\\b([^>]*\\br="${reference}"[^>]*?)(?:/>|>([\\s\\S]*?)<\\/c>)`,
|
|
217
|
+
'i',
|
|
218
|
+
)
|
|
219
|
+
let found = false
|
|
220
|
+
const output = String(xml).replace(pattern, (all, rawAttrs, body = '') => {
|
|
221
|
+
if (/<f\b/i.test(body)) return all
|
|
222
|
+
found = true
|
|
223
|
+
const attrs = rawAttrs.replace(/\s+t="[^"]*"/gi, '').replace(/\s*\/\s*$/g, '')
|
|
224
|
+
return `<c${attrs} t="inlineStr"><is><t xml:space="preserve">${escaped}</t></is></c>`
|
|
225
|
+
})
|
|
226
|
+
return { xml: output, found }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Write target text back into the original workbook. Rebuilding a workbook
|
|
231
|
+
* drops styles, column widths and merged ranges; patching the source zip keeps
|
|
232
|
+
* everything except the translated cell content intact.
|
|
233
|
+
*/
|
|
234
|
+
export function patchXlsx(doc, template) {
|
|
235
|
+
let entries
|
|
236
|
+
try {
|
|
237
|
+
entries = readZipEntries(template)
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw fail('XLSX_SOURCE_INVALID', `XLSX 源文件不是有效的 ZIP/Excel 包: ${error.message}`)
|
|
240
|
+
}
|
|
241
|
+
const sheetPaths = new Map(workbookSheets(entries).map((sheet) => [sheet.name, sheet.path]))
|
|
242
|
+
const bySheet = new Map()
|
|
243
|
+
for (const segment of doc.segments) {
|
|
244
|
+
const sheetName = segment.meta?.sheetName
|
|
245
|
+
const cell = segment.meta?.targetCell
|
|
246
|
+
if (!sheetName || !cell) continue
|
|
247
|
+
if (!bySheet.has(sheetName)) bySheet.set(sheetName, new Map())
|
|
248
|
+
bySheet.get(sheetName).set(cell, String(segment.target || segment.source || ''))
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
let patched = 0
|
|
252
|
+
let missing = 0
|
|
253
|
+
const next = entries.map((entry) => {
|
|
254
|
+
const sheetName = [...sheetPaths.entries()].find(([, path]) => path === entry.name)?.[0]
|
|
255
|
+
const replacements = sheetName ? bySheet.get(sheetName) : null
|
|
256
|
+
if (!replacements?.size) return entry
|
|
257
|
+
let xml = entry.data.toString('utf8')
|
|
258
|
+
for (const [cell, text] of replacements) {
|
|
259
|
+
const result = replaceCellText(xml, cell, text)
|
|
260
|
+
xml = result.xml
|
|
261
|
+
if (result.found) patched += 1
|
|
262
|
+
else missing += 1
|
|
263
|
+
}
|
|
264
|
+
return { ...entry, data: Buffer.from(xml, 'utf8') }
|
|
265
|
+
})
|
|
266
|
+
return { buffer: writeZip(next), patched, missing }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function buildXlsx(doc) {
|
|
270
|
+
const groups = new Map()
|
|
271
|
+
for (const segment of doc.segments) {
|
|
272
|
+
const key = segment.meta?.sheetName || `Sheet ${Number(segment.meta?.sheet) || 1}`
|
|
273
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
274
|
+
groups.get(key).push(segment)
|
|
275
|
+
}
|
|
276
|
+
if (!groups.size) groups.set('Sheet 1', [])
|
|
277
|
+
|
|
278
|
+
const used = new Set()
|
|
279
|
+
const sheets = [...groups.entries()].map(([name, segments], index) => ({
|
|
280
|
+
name: safeSheetName(name, used),
|
|
281
|
+
path: `xl/worksheets/sheet${index + 1}.xml`,
|
|
282
|
+
rows: [
|
|
283
|
+
[inlineCell('A1', 'Source'), inlineCell('B1', 'Target')],
|
|
284
|
+
...segments.map((segment, rowIndex) => [
|
|
285
|
+
inlineCell(`A${rowIndex + 2}`, segment.source || ''),
|
|
286
|
+
inlineCell(`B${rowIndex + 2}`, segment.target || ''),
|
|
287
|
+
]),
|
|
288
|
+
],
|
|
289
|
+
}))
|
|
290
|
+
|
|
181
291
|
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
182
292
|
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
|
183
293
|
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
|
184
294
|
<Default Extension="xml" ContentType="application/xml"/>
|
|
185
295
|
<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>
|
|
186
|
-
<Override PartName="
|
|
296
|
+
${sheets.map((sheet) => ` <Override PartName="/${sheet.path}" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`).join('\n')}
|
|
187
297
|
<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>
|
|
188
298
|
</Types>`
|
|
189
299
|
const rootRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
@@ -192,12 +302,12 @@ ${sheetData}
|
|
|
192
302
|
</Relationships>`
|
|
193
303
|
const workbook = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
194
304
|
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
|
195
|
-
<sheets
|
|
305
|
+
<sheets>${sheets.map((sheet, index) => `<sheet name="${escapeXml(sheet.name)}" sheetId="${index + 1}" r:id="rId${index + 1}"/>`).join('')}</sheets>
|
|
196
306
|
</workbook>`
|
|
197
307
|
const workbookRels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
198
308
|
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
|
199
|
-
<Relationship Id="
|
|
200
|
-
<Relationship Id="
|
|
309
|
+
${sheets.map((sheet, index) => ` <Relationship Id="rId${index + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet${index + 1}.xml"/>`).join('\n')}
|
|
310
|
+
<Relationship Id="rId${sheets.length + 1}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
|
201
311
|
</Relationships>`
|
|
202
312
|
const styles = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
203
313
|
<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
|
|
@@ -212,7 +322,7 @@ ${sheetData}
|
|
|
212
322
|
{ name: '_rels/.rels', data: Buffer.from(rootRels, 'utf8') },
|
|
213
323
|
{ name: 'xl/workbook.xml', data: Buffer.from(workbook, 'utf8') },
|
|
214
324
|
{ name: 'xl/_rels/workbook.xml.rels', data: Buffer.from(workbookRels, 'utf8') },
|
|
215
|
-
{ name:
|
|
325
|
+
...sheets.map((sheet) => ({ name: sheet.path, data: Buffer.from(sheetXml(sheet.rows), 'utf8') })),
|
|
216
326
|
{ name: 'xl/styles.xml', data: Buffer.from(styles, 'utf8') },
|
|
217
327
|
])
|
|
218
328
|
}
|