dsh-cait 1.1.3 → 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 +186 -0
- package/README.ja.md +51 -14
- package/README.md +137 -30
- package/README.zh-CN.md +51 -14
- package/client/components.js +2033 -48
- package/client/entry.js +2 -0
- package/client/helpers.js +12 -0
- package/client/locales.js +658 -8
- package/client/styles.js +111 -4
- package/client.js +2814 -58
- package/lib/aligner.js +6 -6
- package/lib/analysis.js +88 -0
- package/lib/api.js +46 -0
- package/lib/brief.js +275 -0
- package/lib/docx.js +155 -26
- package/lib/formats.js +37 -0
- package/lib/glossary.js +20 -4
- package/lib/import-export.js +212 -13
- package/lib/libraries.js +156 -0
- package/lib/library-export.js +7 -1
- package/lib/localization.js +1098 -0
- package/lib/lock.js +30 -0
- package/lib/notes.js +144 -0
- package/lib/persistence.js +54 -7
- package/lib/pptx.js +33 -1
- package/lib/pre-translate.js +62 -45
- package/lib/propagate.js +3 -0
- package/lib/qa.js +94 -4
- package/lib/replace.js +243 -0
- package/lib/review.js +131 -0
- package/lib/segment-edit.js +199 -0
- package/lib/store-utils.js +33 -6
- package/lib/store.js +507 -47
- package/lib/subtitles.js +213 -0
- package/lib/term-extract.js +105 -3
- package/lib/terms.js +4 -2
- package/lib/tm-maintenance.js +157 -0
- package/lib/tm.js +4 -1
- package/lib/tools.js +294 -0
- package/lib/xlsx.js +176 -66
- package/package.json +1 -1
package/lib/docx.js
CHANGED
|
@@ -239,6 +239,88 @@ function paragraphText(paragraph) {
|
|
|
239
239
|
return out.replace(/\n{3,}/g, '\n\n').trim()
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
function contentPartRank(name) {
|
|
243
|
+
if (name === 'word/document.xml') return 0
|
|
244
|
+
if (/^word\/header\d*\.xml$/i.test(name)) return 1
|
|
245
|
+
if (/^word\/footer\d*\.xml$/i.test(name)) return 2
|
|
246
|
+
if (name === 'word/footnotes.xml') return 3
|
|
247
|
+
if (name === 'word/endnotes.xml') return 4
|
|
248
|
+
return 9
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function contentPartNames(entries) {
|
|
252
|
+
return entries
|
|
253
|
+
.map((entry) => entry.name)
|
|
254
|
+
.filter((name) => contentPartRank(name) < 9)
|
|
255
|
+
.sort((left, right) => {
|
|
256
|
+
const rank = contentPartRank(left) - contentPartRank(right)
|
|
257
|
+
return rank || left.localeCompare(right)
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Collect every paragraph in one document part, including table cells, in
|
|
263
|
+
* document order. `docxParagraph` is the ordinal within that part, which is
|
|
264
|
+
* what the exporter patches; table/row/cell are informative metadata.
|
|
265
|
+
*/
|
|
266
|
+
function collectPartParagraphs(xml, partName) {
|
|
267
|
+
const pattern = /<w:tbl\b[^>]*>|<\/w:tbl>|<w:tr\b[^>]*>|<\/w:tr>|<w:tc\b[^>]*>|<\/w:tc>|<w:p\b[^>]*>[\s\S]*?<\/w:p>/gi
|
|
268
|
+
const paragraphs = []
|
|
269
|
+
let tableDepth = 0
|
|
270
|
+
let tableIndex = -1
|
|
271
|
+
let tableCount = 0
|
|
272
|
+
let rowIndex = -1
|
|
273
|
+
let cellIndex = -1
|
|
274
|
+
let ordinal = 0
|
|
275
|
+
let match
|
|
276
|
+
|
|
277
|
+
while ((match = pattern.exec(xml)) !== null) {
|
|
278
|
+
const tag = match[0]
|
|
279
|
+
if (/^<w:tbl\b/i.test(tag)) {
|
|
280
|
+
if (tableDepth === 0) {
|
|
281
|
+
tableIndex = tableCount
|
|
282
|
+
tableCount += 1
|
|
283
|
+
rowIndex = -1
|
|
284
|
+
cellIndex = -1
|
|
285
|
+
}
|
|
286
|
+
tableDepth += 1
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
if (/^<\/w:tbl>/i.test(tag)) {
|
|
290
|
+
tableDepth = Math.max(0, tableDepth - 1)
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
if (/^<w:tr\b/i.test(tag)) {
|
|
294
|
+
if (tableDepth > 0) {
|
|
295
|
+
rowIndex += 1
|
|
296
|
+
cellIndex = -1
|
|
297
|
+
}
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
if (/^<\/w:tr>/i.test(tag)) continue
|
|
301
|
+
if (/^<w:tc\b/i.test(tag)) {
|
|
302
|
+
if (tableDepth > 0) cellIndex += 1
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
if (/^<\/w:tc>/i.test(tag)) continue
|
|
306
|
+
|
|
307
|
+
const inner = tag.replace(/^<w:p\b[^>]*>/i, '').replace(/<\/w:p>\s*$/i, '')
|
|
308
|
+
const text = paragraphText(inner)
|
|
309
|
+
if (text) {
|
|
310
|
+
const meta = { docxPart: partName, docxParagraph: ordinal }
|
|
311
|
+
if (tableDepth > 0) {
|
|
312
|
+
meta.table = tableIndex
|
|
313
|
+
meta.row = Math.max(0, rowIndex)
|
|
314
|
+
meta.cell = Math.max(0, cellIndex)
|
|
315
|
+
}
|
|
316
|
+
paragraphs.push({ text, meta })
|
|
317
|
+
}
|
|
318
|
+
ordinal += 1
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return paragraphs
|
|
322
|
+
}
|
|
323
|
+
|
|
242
324
|
/**
|
|
243
325
|
* Like parseDocx, but also returns the zero-based body paragraph ordinal for
|
|
244
326
|
* every non-empty paragraph. The ordinal counts all `<w:p>` elements outside
|
|
@@ -248,33 +330,35 @@ export function parseDocxDetailed(buffer) {
|
|
|
248
330
|
if (!Buffer.isBuffer(buffer) || buffer.length < 4) throw new Error('DOCX 内容为空')
|
|
249
331
|
if (buffer[0] !== 0x50 || buffer[1] !== 0x4b) throw new Error('DOCX 文件头不是 ZIP')
|
|
250
332
|
|
|
251
|
-
const
|
|
252
|
-
|
|
333
|
+
const entries = readZipEntries(buffer)
|
|
334
|
+
const documentEntry = entries.find((entry) => entry.name === 'word/document.xml')
|
|
335
|
+
if (!documentEntry) throw new Error('DOCX 中缺少 word/document.xml')
|
|
253
336
|
|
|
254
|
-
const
|
|
255
|
-
const body = document.match(/<w:body\b[^>]*>([\s\S]*?)<\/w:body>/i)?.[1] ?? document
|
|
256
|
-
const content = withoutTables(body)
|
|
337
|
+
const parts = contentPartNames(entries)
|
|
257
338
|
const paragraphs = []
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
paragraphIndex += 1
|
|
339
|
+
for (const name of parts) {
|
|
340
|
+
const entry = entries.find((item) => item.name === name)
|
|
341
|
+
if (!entry) continue
|
|
342
|
+
const found = collectPartParagraphs(entry.data.toString('utf8'), name)
|
|
343
|
+
for (const paragraph of found) {
|
|
344
|
+
paragraphs.push({ ...paragraph, index: paragraphs.length })
|
|
345
|
+
}
|
|
266
346
|
}
|
|
267
347
|
|
|
268
348
|
if (!paragraphs.length) {
|
|
269
|
-
const fallback =
|
|
349
|
+
const fallback = documentEntry.data
|
|
350
|
+
.toString('utf8')
|
|
351
|
+
.replace(/<w:tab\b[^>]*\/>/g, '\t')
|
|
352
|
+
.replace(/<w:br\b[^>]*\/>/g, '\n')
|
|
270
353
|
const text = decodeXml(fallback.replace(/<[^>]*>/g, '')).replace(/\n{3,}/g, '\n\n').trim()
|
|
271
|
-
if (text) paragraphs.push({ text, index: 0 })
|
|
354
|
+
if (text) paragraphs.push({ text, index: 0, meta: { docxPart: 'word/document.xml', docxParagraph: 0 } })
|
|
272
355
|
}
|
|
273
356
|
|
|
274
357
|
if (!paragraphs.length) throw new Error('DOCX 中未找到可翻译的文本')
|
|
275
358
|
return {
|
|
276
359
|
text: paragraphs.map((paragraph) => paragraph.text).join('\n\n'),
|
|
277
360
|
paragraphs,
|
|
361
|
+
formatMeta: { docxParts: parts },
|
|
278
362
|
}
|
|
279
363
|
}
|
|
280
364
|
|
|
@@ -282,9 +366,9 @@ export function parseDocx(buffer) {
|
|
|
282
366
|
return parseDocxDetailed(buffer).text
|
|
283
367
|
}
|
|
284
368
|
|
|
285
|
-
/** Read
|
|
286
|
-
export function readDocxXml(buffer) {
|
|
287
|
-
const xml = readZipEntry(buffer,
|
|
369
|
+
/** Read one DOCX part as text; used by the export tests. */
|
|
370
|
+
export function readDocxXml(buffer, partName = 'word/document.xml') {
|
|
371
|
+
const xml = readZipEntry(buffer, partName)
|
|
288
372
|
return xml ? xml.toString('utf8') : null
|
|
289
373
|
}
|
|
290
374
|
|
|
@@ -355,19 +439,33 @@ function patchDocumentXml(xml, replacements) {
|
|
|
355
439
|
return `${xml.slice(0, start)}${patchBody(body, replacements)}${xml.slice(start + body.length)}`
|
|
356
440
|
}
|
|
357
441
|
|
|
442
|
+
function patchPartXml(xml, replacements) {
|
|
443
|
+
let ordinal = 0
|
|
444
|
+
return xml.replace(/<w:p\b[^>]*>[\s\S]*?<\/w:p>/gi, (paragraph) => {
|
|
445
|
+
const current = ordinal
|
|
446
|
+
ordinal += 1
|
|
447
|
+
if (!replacements.has(current)) return paragraph
|
|
448
|
+
return patchParagraph(paragraph, replacements.get(current))
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
|
|
358
452
|
/** Patch translated text into the source package, preserving its formatting. */
|
|
359
|
-
export function buildDocx(template, replacements) {
|
|
453
|
+
export function buildDocx(template, replacements, partReplacements = null) {
|
|
360
454
|
const entries = readZipEntries(template)
|
|
361
455
|
const documentEntry = entries.find((entry) => entry.name === 'word/document.xml')
|
|
362
456
|
if (!documentEntry) throw new Error('DOCX 中缺少 word/document.xml')
|
|
363
|
-
const
|
|
364
|
-
|
|
457
|
+
const byPart = partReplacements instanceof Map
|
|
458
|
+
? partReplacements
|
|
459
|
+
: new Map([['word/document.xml', replacements]])
|
|
365
460
|
return writeZip(
|
|
366
|
-
entries.map((entry) =>
|
|
367
|
-
entry.name
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
461
|
+
entries.map((entry) => {
|
|
462
|
+
if (!byPart.has(entry.name)) return entry
|
|
463
|
+
const xml = entry.data.toString('utf8')
|
|
464
|
+
const patched = partReplacements instanceof Map
|
|
465
|
+
? patchPartXml(xml, byPart.get(entry.name))
|
|
466
|
+
: patchDocumentXml(xml, replacements)
|
|
467
|
+
return { name: entry.name, data: Buffer.from(patched, 'utf8') }
|
|
468
|
+
}),
|
|
371
469
|
)
|
|
372
470
|
}
|
|
373
471
|
|
|
@@ -389,4 +487,35 @@ export function buildDocxFromText(paragraphs) {
|
|
|
389
487
|
])
|
|
390
488
|
}
|
|
391
489
|
|
|
490
|
+
export function buildBilingualDocx(segments, { includeStatus = false, title = '' } = {}) {
|
|
491
|
+
const columns = includeStatus ? 3 : 2
|
|
492
|
+
const width = Math.floor(9000 / columns)
|
|
493
|
+
const cell = (text, shaded = false) => {
|
|
494
|
+
const paragraphs = String(text ?? '')
|
|
495
|
+
.split(/\r?\n/)
|
|
496
|
+
.map((line) => `<w:p><w:r><w:t xml:space="preserve">${escapeXml(line)}</w:t></w:r></w:p>`)
|
|
497
|
+
.join('')
|
|
498
|
+
const shade = shaded ? '<w:shd w:val="clear" w:color="auto" w:fill="F2F2F2"/>' : ''
|
|
499
|
+
return `<w:tc><w:tcPr><w:tcW w:w="${width}" w:type="dxa"/>${shade}</w:tcPr>${paragraphs}</w:tc>`
|
|
500
|
+
}
|
|
501
|
+
const header = `<w:tr><w:trPr><w:tblHeader/></w:trPr>${cell('Source', true)}${cell('Target', true)}${includeStatus ? cell('Status', true) : ''}</w:tr>`
|
|
502
|
+
const body = segments
|
|
503
|
+
.map(
|
|
504
|
+
(segment) =>
|
|
505
|
+
`<w:tr>${cell(segment.source)}${cell(segment.target || '')}${includeStatus ? cell(segment.status || '') : ''}</w:tr>`,
|
|
506
|
+
)
|
|
507
|
+
.join('')
|
|
508
|
+
const titleXml = title
|
|
509
|
+
? `<w:p><w:r><w:rPr><w:b/><w:sz w:val="28"/></w:rPr><w:t xml:space="preserve">${escapeXml(title)}</w:t></w:r></w:p>`
|
|
510
|
+
: ''
|
|
511
|
+
const document = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body>${titleXml}<w:tbl><w:tblPr><w:tblW w:w="0" w:type="auto"/><w:tblBorders><w:top w:val="single" w:sz="4"/><w:left w:val="single" w:sz="4"/><w:bottom w:val="single" w:sz="4"/><w:right w:val="single" w:sz="4"/><w:insideH w:val="single" w:sz="4"/><w:insideV w:val="single" w:sz="4"/></w:tblBorders></w:tblPr><w:tblGrid>${Array.from({ length: columns }, () => `<w:gridCol w:w="${width}"/>`).join('')}</w:tblGrid>${header}${body}</w:tbl><w:sectPr><w:pgSz w:w="11906" w:h="16838"/><w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440"/></w:sectPr></w:body></w:document>`
|
|
512
|
+
const contentTypes = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>`
|
|
513
|
+
const rels = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/></Relationships>`
|
|
514
|
+
return writeZip([
|
|
515
|
+
{ name: '[Content_Types].xml', data: Buffer.from(contentTypes, 'utf8') },
|
|
516
|
+
{ name: '_rels/.rels', data: Buffer.from(rels, 'utf8') },
|
|
517
|
+
{ name: 'word/document.xml', data: Buffer.from(document, 'utf8') },
|
|
518
|
+
])
|
|
519
|
+
}
|
|
520
|
+
|
|
392
521
|
export { readZipEntries, writeZip }
|
package/lib/formats.js
CHANGED
|
@@ -577,6 +577,9 @@ function parseXliffUnit(body, attrs, format, tokens) {
|
|
|
577
577
|
const targetMatch = body.match(/<(?:[\w.-]+:)?target\b[^>]*>([\s\S]*?)<\/(?:[\w.-]+:)?target>/i)
|
|
578
578
|
?? body.match(/<(?:[\w.-]+:)?target\b[^>]*\/>/i)
|
|
579
579
|
const target = targetMatch ? extractInline(targetMatch[1] ?? '').text : ''
|
|
580
|
+
const targetTag = body.match(/<(?:[\w.-]+:)?target\b([^>]*)>/i)
|
|
581
|
+
const targetState = targetTag?.[1] ? readXmlAttr(targetTag[1], 'state') : ''
|
|
582
|
+
const caitUid = readXmlAttr(attrs, 'x-cait-uid')
|
|
580
583
|
tokens.push(...source.tokens)
|
|
581
584
|
return {
|
|
582
585
|
source: source.text,
|
|
@@ -585,6 +588,8 @@ function parseXliffUnit(body, attrs, format, tokens) {
|
|
|
585
588
|
meta: {
|
|
586
589
|
format,
|
|
587
590
|
xliffId: readXmlAttr(attrs, 'id'),
|
|
591
|
+
...(caitUid ? { caitUid } : {}),
|
|
592
|
+
...(targetState ? { targetState } : {}),
|
|
588
593
|
...(source.tokens.length ? { inline: source.tokens } : {}),
|
|
589
594
|
},
|
|
590
595
|
}
|
|
@@ -661,6 +666,38 @@ export function buildXliff(doc) {
|
|
|
661
666
|
].join('\n')
|
|
662
667
|
}
|
|
663
668
|
|
|
669
|
+
export function buildXliffReview(doc) {
|
|
670
|
+
const units = doc.segments.map((segment, index) => {
|
|
671
|
+
const id = String(segment.uid || segment.meta?.xliffId || index + 1)
|
|
672
|
+
const tokens = Array.isArray(segment.meta?.inline) ? segment.meta.inline : []
|
|
673
|
+
const target = completeInline(segment.target || '', tokens)
|
|
674
|
+
const state = segment.confirmedAt ? 'final' : segment.target ? 'translated' : 'new'
|
|
675
|
+
const note = segment.confirmedAt
|
|
676
|
+
? 'confirmed'
|
|
677
|
+
: segment.origin
|
|
678
|
+
? `origin: ${segment.origin}`
|
|
679
|
+
: ''
|
|
680
|
+
return [
|
|
681
|
+
` <trans-unit id="${escapeXml(id)}" x-cait-uid="${escapeXml(segment.uid || id)}">`,
|
|
682
|
+
` <source xml:lang="${escapeXml(doc.sourceLang)}">${renderInline(segment.source, tokens)}</source>`,
|
|
683
|
+
` <target xml:lang="${escapeXml(doc.targetLang)}" state="${state}">${renderInline(target, tokens)}</target>`,
|
|
684
|
+
...(note ? [` <note from="cait">${escapeXml(note)}</note>`] : []),
|
|
685
|
+
' </trans-unit>',
|
|
686
|
+
].join('\n')
|
|
687
|
+
})
|
|
688
|
+
return [
|
|
689
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
690
|
+
'<xliff version="1.2">',
|
|
691
|
+
` <file source-language="${escapeXml(doc.sourceLang)}" target-language="${escapeXml(doc.targetLang)}" datatype="plaintext" original="${escapeXml(doc.name)}">`,
|
|
692
|
+
' <body>',
|
|
693
|
+
...units,
|
|
694
|
+
' </body>',
|
|
695
|
+
' </file>',
|
|
696
|
+
'</xliff>',
|
|
697
|
+
'',
|
|
698
|
+
].join('\n')
|
|
699
|
+
}
|
|
700
|
+
|
|
664
701
|
export function buildXliff2(doc) {
|
|
665
702
|
const units = doc.segments.map((segment, index) => {
|
|
666
703
|
const id = String(segment.meta?.xliffId || index + 1)
|
package/lib/glossary.js
CHANGED
|
@@ -3,13 +3,16 @@ import { DEFAULT_GLOSSARY_ID, DEFAULT_GLOSSARY_NAME, MAX_SEGMENTS, canonicalLang
|
|
|
3
3
|
import { normalizeText } from './similarity.js'
|
|
4
4
|
|
|
5
5
|
export const glossaryMethods = {
|
|
6
|
-
findGlossaryHits(text, sourceLang) {
|
|
6
|
+
findGlossaryHits(text, sourceLang, documentId) {
|
|
7
7
|
const haystack = String(text ?? '')
|
|
8
8
|
if (!haystack) return []
|
|
9
9
|
const lower = haystack.toLowerCase()
|
|
10
10
|
const lang = sourceLang ? canonicalLanguage(sourceLang) : ''
|
|
11
11
|
const hits = []
|
|
12
|
-
const
|
|
12
|
+
const scopedDoc = documentId ? this.requireDocument(documentId) : null
|
|
13
|
+
const mountedGlossary = new Set(
|
|
14
|
+
scopedDoc ? this.effectiveGlossaryIds(scopedDoc) : this.mounts.glossaryIds,
|
|
15
|
+
)
|
|
13
16
|
|
|
14
17
|
for (const entry of this.glossary) {
|
|
15
18
|
if (!mountedGlossary.has(entry.glossaryId || DEFAULT_GLOSSARY_ID)) continue
|
|
@@ -29,6 +32,8 @@ export const glossaryMethods = {
|
|
|
29
32
|
term: entry.source,
|
|
30
33
|
target: entry.target,
|
|
31
34
|
note: entry.note || '',
|
|
35
|
+
forbidden: Boolean(entry.forbidden),
|
|
36
|
+
preferred: Boolean(entry.preferred),
|
|
32
37
|
count,
|
|
33
38
|
})
|
|
34
39
|
}
|
|
@@ -36,7 +41,7 @@ export const glossaryMethods = {
|
|
|
36
41
|
return hits.sort((a, b) => b.term.length - a.term.length)
|
|
37
42
|
}
|
|
38
43
|
,
|
|
39
|
-
addGlossary({ source, target, note, sourceLang, caseSensitive, glossaryId } = {}) {
|
|
44
|
+
addGlossary({ source, target, note, sourceLang, caseSensitive, forbidden, preferred, glossaryId } = {}) {
|
|
40
45
|
const term = cleanText(source, '术语')
|
|
41
46
|
const translation = cleanText(target, '术语译文')
|
|
42
47
|
if (!term || !translation) throw new CatError('EMPTY_TERM', '术语和译文都不能为空。')
|
|
@@ -53,6 +58,8 @@ export const glossaryMethods = {
|
|
|
53
58
|
existing.target = translation
|
|
54
59
|
existing.note = String(note ?? '').trim()
|
|
55
60
|
existing.caseSensitive = Boolean(caseSensitive)
|
|
61
|
+
existing.forbidden = Boolean(forbidden)
|
|
62
|
+
existing.preferred = Boolean(preferred)
|
|
56
63
|
existing.updatedAt = now()
|
|
57
64
|
this.touch()
|
|
58
65
|
this.persist()
|
|
@@ -66,6 +73,8 @@ export const glossaryMethods = {
|
|
|
66
73
|
note: String(note ?? '').trim(),
|
|
67
74
|
sourceLang: lang,
|
|
68
75
|
caseSensitive: Boolean(caseSensitive),
|
|
76
|
+
forbidden: Boolean(forbidden),
|
|
77
|
+
preferred: Boolean(preferred),
|
|
69
78
|
createdAt: now(),
|
|
70
79
|
updatedAt: now(),
|
|
71
80
|
}
|
|
@@ -75,7 +84,7 @@ export const glossaryMethods = {
|
|
|
75
84
|
return { entry: { ...entry }, created: true }
|
|
76
85
|
}
|
|
77
86
|
,
|
|
78
|
-
updateGlossary({ id, source, target, note, caseSensitive } = {}) {
|
|
87
|
+
updateGlossary({ id, source, target, note, caseSensitive, forbidden, preferred } = {}) {
|
|
79
88
|
const entry = this.glossary.find((item) => item.id === id)
|
|
80
89
|
if (!entry) throw new CatError('TERM_NOT_FOUND', '术语不存在。', 404)
|
|
81
90
|
// The library page manages every library, mounted or not; only the target
|
|
@@ -93,6 +102,9 @@ export const glossaryMethods = {
|
|
|
93
102
|
}
|
|
94
103
|
if (note !== undefined) entry.note = String(note ?? '').trim()
|
|
95
104
|
if (caseSensitive !== undefined) entry.caseSensitive = Boolean(caseSensitive)
|
|
105
|
+
if (forbidden !== undefined) entry.forbidden = Boolean(forbidden)
|
|
106
|
+
if (preferred !== undefined) entry.preferred = Boolean(preferred)
|
|
107
|
+
if (entry.forbidden) entry.preferred = false
|
|
96
108
|
entry.updatedAt = now()
|
|
97
109
|
this.touch()
|
|
98
110
|
this.persist()
|
|
@@ -129,6 +141,8 @@ export const glossaryMethods = {
|
|
|
129
141
|
if (existing) {
|
|
130
142
|
existing.target = String(item.target).trim()
|
|
131
143
|
existing.note = String(item.note ?? '').trim()
|
|
144
|
+
existing.forbidden = Boolean(item.forbidden)
|
|
145
|
+
existing.preferred = Boolean(item.preferred) && !existing.forbidden
|
|
132
146
|
existing.updatedAt = now()
|
|
133
147
|
updated += 1
|
|
134
148
|
} else {
|
|
@@ -140,6 +154,8 @@ export const glossaryMethods = {
|
|
|
140
154
|
note: String(item.note ?? '').trim(),
|
|
141
155
|
sourceLang: lang,
|
|
142
156
|
caseSensitive: Boolean(item.caseSensitive),
|
|
157
|
+
forbidden: Boolean(item.forbidden),
|
|
158
|
+
preferred: Boolean(item.preferred) && !item.forbidden,
|
|
143
159
|
createdAt: now(),
|
|
144
160
|
updatedAt: now(),
|
|
145
161
|
})
|