dsh-cait 1.1.3 → 1.1.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.
- package/CHANGELOG.md +143 -0
- package/README.ja.md +34 -7
- package/README.md +169 -81
- package/README.zh-CN.md +34 -7
- package/client/components.js +1934 -62
- package/client/entry.js +2 -0
- package/client/helpers.js +12 -0
- package/client/locales.js +584 -6
- package/client/styles.js +105 -2
- package/client.js +2635 -68
- package/lib/analysis.js +88 -0
- package/lib/api.js +40 -0
- package/lib/brief.js +275 -0
- package/lib/docx.js +31 -0
- package/lib/formats.js +37 -0
- package/lib/glossary.js +20 -4
- package/lib/import-export.js +97 -3
- package/lib/libraries.js +156 -0
- package/lib/library-export.js +7 -1
- package/lib/lock.js +30 -0
- package/lib/notes.js +144 -0
- package/lib/persistence.js +40 -7
- package/lib/pre-translate.js +62 -45
- package/lib/propagate.js +3 -0
- package/lib/qa.js +76 -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 +26 -4
- package/lib/store.js +494 -46
- 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/package.json +1 -1
package/client/components.js
CHANGED
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
check: ['M3 8.5l3.2 3.2L13 4.3'],
|
|
38
38
|
align: ['M2 5h8', 'M2 11h5', 'M12 3v10', 'M10 11l2 2 2-2'],
|
|
39
39
|
folder: ['M2 4h4l1.5 2H14v6.5H2z'],
|
|
40
|
+
chart: ['M3 13V8', 'M8 13V3', 'M13 13V6', 'M2 14h12'],
|
|
41
|
+
message: ['M2 3h12v8H8l-3 3v-3H2z'],
|
|
42
|
+
lock: ['M4 7h8v6H4z', 'M6 7V5a2 2 0 0 1 4 0v2'],
|
|
43
|
+
unlock: ['M4 7h8v6H4z', 'M6 7V5a2 2 0 0 1 3.8-1'],
|
|
40
44
|
}
|
|
41
45
|
const shape = paths[name] || paths.file
|
|
42
46
|
return h(
|
|
@@ -302,6 +306,7 @@
|
|
|
302
306
|
pre,
|
|
303
307
|
onPreTranslate,
|
|
304
308
|
onCancelPreTranslate,
|
|
309
|
+
onReplace,
|
|
305
310
|
view,
|
|
306
311
|
onView,
|
|
307
312
|
}) {
|
|
@@ -407,6 +412,19 @@
|
|
|
407
412
|
h('span', { className: 'cait-btn-label' }, t('pretranslate.start')),
|
|
408
413
|
)
|
|
409
414
|
: null,
|
|
415
|
+
documentsView
|
|
416
|
+
? h(
|
|
417
|
+
'button',
|
|
418
|
+
{
|
|
419
|
+
className: 'cait-btn',
|
|
420
|
+
onClick: onReplace,
|
|
421
|
+
disabled: Boolean(busy) || !doc,
|
|
422
|
+
title: t('replace.open'),
|
|
423
|
+
},
|
|
424
|
+
h(Icon, { name: 'search', size: 14 }),
|
|
425
|
+
h('span', { className: 'cait-btn-label' }, t('replace.open')),
|
|
426
|
+
)
|
|
427
|
+
: null,
|
|
410
428
|
documentsView
|
|
411
429
|
? h(
|
|
412
430
|
'button',
|
|
@@ -419,7 +437,7 @@
|
|
|
419
437
|
? h(
|
|
420
438
|
'button',
|
|
421
439
|
{ className: 'cait-btn', onClick: onImport, disabled: Boolean(busy), title: t('nav.import') },
|
|
422
|
-
h(Icon, { name: '
|
|
440
|
+
h(Icon, { name: 'download', size: 14 }),
|
|
423
441
|
h('span', { className: 'cait-btn-label' }, t('nav.import')),
|
|
424
442
|
)
|
|
425
443
|
: null,
|
|
@@ -427,7 +445,7 @@
|
|
|
427
445
|
? h(
|
|
428
446
|
'button',
|
|
429
447
|
{ className: 'cait-btn', onClick: onExport, disabled: Boolean(busy) || !doc, title: t('nav.export') },
|
|
430
|
-
h(Icon, { name: '
|
|
448
|
+
h(Icon, { name: 'upload', size: 14 }),
|
|
431
449
|
h('span', { className: 'cait-btn-label' }, t('nav.export')),
|
|
432
450
|
)
|
|
433
451
|
: null,
|
|
@@ -436,9 +454,9 @@
|
|
|
436
454
|
)
|
|
437
455
|
}
|
|
438
456
|
|
|
439
|
-
const SEGMENT_FILTERS = ['all', 'pending', 'translated', 'confirmed']
|
|
457
|
+
const SEGMENT_FILTERS = ['all', 'pending', 'translated', 'confirmed', 'risk']
|
|
440
458
|
|
|
441
|
-
function SegmentRail({ t, doc, selected, query, setQuery, filter, setFilter, onSelect, onConfirmAll, onPropagate, busy, qa }) {
|
|
459
|
+
function SegmentRail({ t, doc, selected, query, setQuery, filter, setFilter, onSelect, onConfirmAll, onPropagate, onClearPretranslated, busy, qa }) {
|
|
442
460
|
const activeRef = useRef(null)
|
|
443
461
|
const storage = typeof localStorage !== 'undefined' ? localStorage : null
|
|
444
462
|
const [pageSize, setPageSize] = useState(() => readPageSize(storage))
|
|
@@ -458,12 +476,15 @@
|
|
|
458
476
|
list.push(issue.code)
|
|
459
477
|
warns.set(issue.index, list)
|
|
460
478
|
}
|
|
479
|
+
const riskByIndex = new Map()
|
|
480
|
+
for (const item of qa?.risks || []) riskByIndex.set(item.index, item)
|
|
461
481
|
|
|
462
482
|
const counts = {
|
|
463
483
|
all: doc.progress.total,
|
|
464
484
|
pending: doc.progress.pending,
|
|
465
485
|
translated: doc.progress.translated,
|
|
466
486
|
confirmed: doc.progress.confirmed,
|
|
487
|
+
risk: riskByIndex.size,
|
|
467
488
|
}
|
|
468
489
|
|
|
469
490
|
// How many empty segments could be filled from a sibling with the same
|
|
@@ -482,10 +503,17 @@
|
|
|
482
503
|
if (list.length < 2 || !list.some((segment) => segment.target)) continue
|
|
483
504
|
duplicated += list.filter((segment) => !segment.target).length
|
|
484
505
|
}
|
|
506
|
+
const pretranslated = doc.segments.filter((segment) => (
|
|
507
|
+
segment.target &&
|
|
508
|
+
!segment.confirmedAt &&
|
|
509
|
+
['tm', 'mt', 'auto'].includes(segment.origin)
|
|
510
|
+
)).length
|
|
485
511
|
const needle = query.trim().toLowerCase()
|
|
486
512
|
const filtered = doc.segments
|
|
487
513
|
.map((segment, index) => ({ segment, index }))
|
|
488
|
-
.filter(({ segment }) =>
|
|
514
|
+
.filter(({ segment, index }) =>
|
|
515
|
+
filter === 'all' || (filter === 'risk' ? riskByIndex.has(index) : segment.status === filter),
|
|
516
|
+
)
|
|
489
517
|
.filter(({ segment }) => {
|
|
490
518
|
if (!needle) return true
|
|
491
519
|
return (
|
|
@@ -602,6 +630,19 @@
|
|
|
602
630
|
`${t('rail.propagate')} · ${duplicated}`,
|
|
603
631
|
)
|
|
604
632
|
: null,
|
|
633
|
+
pretranslated > 0
|
|
634
|
+
? h(
|
|
635
|
+
'button',
|
|
636
|
+
{
|
|
637
|
+
className: 'cait-batchbtn cait-batchbtn-danger',
|
|
638
|
+
onClick: onClearPretranslated,
|
|
639
|
+
disabled: Boolean(busy),
|
|
640
|
+
title: t('rail.clearPretranslatedHint'),
|
|
641
|
+
},
|
|
642
|
+
h(Icon, { name: 'trash', size: 11 }),
|
|
643
|
+
`${t('rail.clearPretranslated')} · ${pretranslated}`,
|
|
644
|
+
)
|
|
645
|
+
: null,
|
|
605
646
|
h(
|
|
606
647
|
'div',
|
|
607
648
|
{ className: 'cait-rail-goto' },
|
|
@@ -664,7 +705,7 @@
|
|
|
664
705
|
h('span', { className: `cait-dot ${segment.status}` }),
|
|
665
706
|
warns.has(index)
|
|
666
707
|
? h('span', {
|
|
667
|
-
className:
|
|
708
|
+
className: `cait-seg-warn ${riskByIndex.get(index)?.risk || 'low'}`,
|
|
668
709
|
title: (warns.get(index) || []).map((code) => t(`qa.code.${code}`)).join(' · '),
|
|
669
710
|
'aria-label': t('qa.warn'),
|
|
670
711
|
})
|
|
@@ -720,7 +761,7 @@
|
|
|
720
761
|
)
|
|
721
762
|
}
|
|
722
763
|
|
|
723
|
-
function EditorPane({ t, doc, index, segment, detail, draft, setDraft, dirty, busy, onConfirm, onNext, onPrev, onSave, onAdopt, onApplyTerm, textareaRef }) {
|
|
764
|
+
function EditorPane({ t, doc, index, segment, detail, draft, setDraft, dirty, busy, onConfirm, onUnconfirm, onNext, onPrev, onSave, onUndo, onRedo, canUndo, canRedo, onToggleLock, onMerge, onSplit, onAdopt, onApplyTerm, onOpenBrief, onOpenResources, onOpenAnalysis, onOpenNotes, noteCount, qaRisk, textareaRef }) {
|
|
724
765
|
const hits = detail?.index === index ? detail.glossaryHits : []
|
|
725
766
|
const charCount = Array.from(draft || '').length
|
|
726
767
|
const progress = doc.progress
|
|
@@ -737,9 +778,31 @@
|
|
|
737
778
|
// against. It is stored in `suggestion`, which adoption already preserves.
|
|
738
779
|
const baselineText = segment.suggestion || ''
|
|
739
780
|
const baselineOrigin = segment.origin || 'agent'
|
|
781
|
+
const canMerge = !segment.locked && index < doc.segments.length - 1
|
|
782
|
+
const canSplit = !segment.locked && String(segment.source || '').length > 1
|
|
783
|
+
const splitAtCursor = () => {
|
|
784
|
+
const sourceLength = String(segment.source || '').length
|
|
785
|
+
const targetLength = String(draft || '').length
|
|
786
|
+
const cursor = textareaRef?.current?.selectionStart
|
|
787
|
+
const ratio = targetLength > 0 && Number.isInteger(cursor) ? cursor / targetLength : 0.5
|
|
788
|
+
const at = Math.max(1, Math.min(sourceLength - 1, Math.round(sourceLength * ratio)))
|
|
789
|
+
onSplit(at)
|
|
790
|
+
}
|
|
740
791
|
|
|
741
|
-
const onKeyDown = (event) => {
|
|
742
|
-
|
|
792
|
+
const onKeyDown = (event) => {
|
|
793
|
+
const mod = event.metaKey || event.ctrlKey
|
|
794
|
+
if (mod && event.key.toLowerCase() === 'z') {
|
|
795
|
+
if (canUndo || canRedo) {
|
|
796
|
+
event.preventDefault()
|
|
797
|
+
if (event.shiftKey) onRedo()
|
|
798
|
+
else onUndo()
|
|
799
|
+
}
|
|
800
|
+
} else if (mod && event.key.toLowerCase() === 'y') {
|
|
801
|
+
if (canRedo) {
|
|
802
|
+
event.preventDefault()
|
|
803
|
+
onRedo()
|
|
804
|
+
}
|
|
805
|
+
} else if (mod && event.key === 'Enter') {
|
|
743
806
|
event.preventDefault()
|
|
744
807
|
onConfirm()
|
|
745
808
|
} else if (mod && event.key.toLowerCase() === 's') {
|
|
@@ -771,6 +834,37 @@
|
|
|
771
834
|
h(Icon, { name: 'arrowRight', size: 11 }),
|
|
772
835
|
h('span', { className: 'cait-langchip' }, doc.targetLang),
|
|
773
836
|
h('span', null, `· ${doc.segments.length} ${t('doc.segments')}`),
|
|
837
|
+
h(
|
|
838
|
+
'button',
|
|
839
|
+
{
|
|
840
|
+
className: 'cait-linkbtn cait-brieflink',
|
|
841
|
+
onClick: onOpenBrief,
|
|
842
|
+
title: doc.brief ? t('panel.briefHint') : t('brief.empty'),
|
|
843
|
+
},
|
|
844
|
+
h(Icon, { name: 'file', size: 11 }),
|
|
845
|
+
t('editor.brief'),
|
|
846
|
+
doc.brief ? h('span', { className: 'cait-briefdot' }) : null,
|
|
847
|
+
),
|
|
848
|
+
h(
|
|
849
|
+
'button',
|
|
850
|
+
{
|
|
851
|
+
className: 'cait-linkbtn',
|
|
852
|
+
onClick: onOpenResources,
|
|
853
|
+
title: t('resources.hint'),
|
|
854
|
+
},
|
|
855
|
+
h(Icon, { name: 'database', size: 11 }),
|
|
856
|
+
t('editor.resources'),
|
|
857
|
+
),
|
|
858
|
+
h(
|
|
859
|
+
'button',
|
|
860
|
+
{
|
|
861
|
+
className: 'cait-linkbtn',
|
|
862
|
+
onClick: onOpenAnalysis,
|
|
863
|
+
title: t('analysis.hint'),
|
|
864
|
+
},
|
|
865
|
+
h(Icon, { name: 'chart', size: 11 }),
|
|
866
|
+
t('editor.analysis'),
|
|
867
|
+
),
|
|
774
868
|
),
|
|
775
869
|
),
|
|
776
870
|
h(
|
|
@@ -785,7 +879,40 @@
|
|
|
785
879
|
'div',
|
|
786
880
|
{ className: 'cait-seg-head' },
|
|
787
881
|
h('span', { className: 'cait-seg-label' }, `${t('editor.segment')} ${index + 1} / ${doc.segments.length}`),
|
|
788
|
-
h(
|
|
882
|
+
h(
|
|
883
|
+
'span',
|
|
884
|
+
{ className: 'cait-seg-flags' },
|
|
885
|
+
h(
|
|
886
|
+
'button',
|
|
887
|
+
{
|
|
888
|
+
className: `cait-linkbtn ${segment.locked ? 'active' : ''}`,
|
|
889
|
+
onClick: onToggleLock,
|
|
890
|
+
title: segment.locked ? t('editor.unlock') : t('editor.lock'),
|
|
891
|
+
'aria-pressed': Boolean(segment.locked),
|
|
892
|
+
},
|
|
893
|
+
h(Icon, { name: segment.locked ? 'unlock' : 'lock', size: 11 }),
|
|
894
|
+
segment.locked ? t('editor.locked') : t('editor.lock'),
|
|
895
|
+
),
|
|
896
|
+
h(
|
|
897
|
+
'button',
|
|
898
|
+
{
|
|
899
|
+
className: 'cait-linkbtn cait-notelink',
|
|
900
|
+
onClick: onOpenNotes,
|
|
901
|
+
title: t('notes.hint'),
|
|
902
|
+
},
|
|
903
|
+
h(Icon, { name: 'message', size: 11 }),
|
|
904
|
+
t('editor.notes'),
|
|
905
|
+
noteCount ? h('span', { className: 'cait-notecount' }, String(noteCount)) : null,
|
|
906
|
+
),
|
|
907
|
+
qaRisk
|
|
908
|
+
? h(
|
|
909
|
+
'span',
|
|
910
|
+
{ className: `cait-riskchip ${qaRisk.risk}`, title: qaRisk.codes.map((code) => t(`qa.code.${code}`)).join(' · ') },
|
|
911
|
+
`${t('qa.risk')} · ${t(`qa.${qaRisk.risk}`)}`,
|
|
912
|
+
)
|
|
913
|
+
: null,
|
|
914
|
+
h('span', { className: `cait-badge ${segment.status}` }, statusLabel),
|
|
915
|
+
),
|
|
789
916
|
),
|
|
790
917
|
h(
|
|
791
918
|
'div',
|
|
@@ -868,6 +995,7 @@
|
|
|
868
995
|
value: draft,
|
|
869
996
|
onChange: (event) => setDraft(event.target.value),
|
|
870
997
|
onKeyDown,
|
|
998
|
+
readOnly: Boolean(segment.locked),
|
|
871
999
|
placeholder: t('editor.targetPlaceholder'),
|
|
872
1000
|
spellCheck: false,
|
|
873
1001
|
rows: 8,
|
|
@@ -879,10 +1007,18 @@
|
|
|
879
1007
|
{ className: 'cait-actionbar' },
|
|
880
1008
|
h(
|
|
881
1009
|
'button',
|
|
882
|
-
{ className: 'cait-btn cait-btn-primary', onClick: onConfirm, disabled: Boolean(busy) || !draft.trim() },
|
|
1010
|
+
{ className: 'cait-btn cait-btn-primary', onClick: onConfirm, disabled: Boolean(busy) || Boolean(segment.locked) || !draft.trim() },
|
|
883
1011
|
busy === 'confirm' ? h(Spinner) : h(Icon, { name: 'check', size: 14 }),
|
|
884
1012
|
segment.status === 'confirmed' ? t('editor.update') : t('editor.confirm'),
|
|
885
1013
|
),
|
|
1014
|
+
segment.status === 'confirmed'
|
|
1015
|
+
? h(
|
|
1016
|
+
'button',
|
|
1017
|
+
{ className: 'cait-btn cait-btn-ghost', onClick: onUnconfirm, disabled: Boolean(busy) || Boolean(segment.locked), title: t('editor.unconfirmHint') },
|
|
1018
|
+
h(Icon, { name: 'refresh', size: 13 }),
|
|
1019
|
+
t('editor.unconfirm'),
|
|
1020
|
+
)
|
|
1021
|
+
: null,
|
|
886
1022
|
h(
|
|
887
1023
|
'button',
|
|
888
1024
|
{ className: 'cait-btn', onClick: onNext, disabled: Boolean(busy) },
|
|
@@ -897,7 +1033,27 @@
|
|
|
897
1033
|
),
|
|
898
1034
|
h(
|
|
899
1035
|
'button',
|
|
900
|
-
{ className: 'cait-
|
|
1036
|
+
{ className: 'cait-btn cait-btn-ghost', onClick: onMerge, disabled: Boolean(busy) || !canMerge, title: t('editor.mergeHint') },
|
|
1037
|
+
t('editor.merge'),
|
|
1038
|
+
),
|
|
1039
|
+
h(
|
|
1040
|
+
'button',
|
|
1041
|
+
{ className: 'cait-btn cait-btn-ghost', onClick: splitAtCursor, disabled: Boolean(busy) || !canSplit, title: t('editor.splitHint') },
|
|
1042
|
+
t('editor.split'),
|
|
1043
|
+
),
|
|
1044
|
+
h(
|
|
1045
|
+
'button',
|
|
1046
|
+
{ className: 'cait-iconbtn', onClick: onUndo, disabled: Boolean(busy) || !canUndo, title: t('editor.undo'), 'aria-label': t('editor.undo') },
|
|
1047
|
+
'↶',
|
|
1048
|
+
),
|
|
1049
|
+
h(
|
|
1050
|
+
'button',
|
|
1051
|
+
{ className: 'cait-iconbtn', onClick: onRedo, disabled: Boolean(busy) || !canRedo, title: t('editor.redo'), 'aria-label': t('editor.redo') },
|
|
1052
|
+
'↷',
|
|
1053
|
+
),
|
|
1054
|
+
h(
|
|
1055
|
+
'button',
|
|
1056
|
+
{ className: 'cait-iconbtn cait-savebtn', onClick: onSave, disabled: Boolean(busy) || Boolean(segment.locked) || !dirty, title: t('editor.save') },
|
|
901
1057
|
h(Icon, { name: 'save', size: 14 }),
|
|
902
1058
|
),
|
|
903
1059
|
h('span', { className: 'cait-hint' }, t('editor.hint')),
|
|
@@ -1020,6 +1176,19 @@
|
|
|
1020
1176
|
onCopy,
|
|
1021
1177
|
onOpenSettings,
|
|
1022
1178
|
onSearchLibrary,
|
|
1179
|
+
qa,
|
|
1180
|
+
onRefreshQa,
|
|
1181
|
+
onJumpQa,
|
|
1182
|
+
onFixQa,
|
|
1183
|
+
review,
|
|
1184
|
+
reviewBusy,
|
|
1185
|
+
onReview,
|
|
1186
|
+
onApplyReview,
|
|
1187
|
+
concordance,
|
|
1188
|
+
concordanceQuery,
|
|
1189
|
+
setConcordanceQuery,
|
|
1190
|
+
onConcordance,
|
|
1191
|
+
onApplyConcordance,
|
|
1023
1192
|
busy,
|
|
1024
1193
|
}) {
|
|
1025
1194
|
const [termQuery, setTermQuery] = useState('')
|
|
@@ -1099,6 +1268,19 @@
|
|
|
1099
1268
|
h(Icon, { name: 'sparkles', size: 13 }),
|
|
1100
1269
|
t('panel.mt'),
|
|
1101
1270
|
),
|
|
1271
|
+
h(
|
|
1272
|
+
'button',
|
|
1273
|
+
{ className: `cait-tab${tab === 'qa' ? ' active' : ''}`, onClick: () => setTab('qa') },
|
|
1274
|
+
h(Icon, { name: 'alert', size: 13 }),
|
|
1275
|
+
t('panel.qa'),
|
|
1276
|
+
qa?.issueCount ? h('span', { className: 'cait-count' }, qa.issueCount) : null,
|
|
1277
|
+
),
|
|
1278
|
+
h(
|
|
1279
|
+
'button',
|
|
1280
|
+
{ className: `cait-tab${tab === 'concordance' ? ' active' : ''}`, onClick: () => setTab('concordance') },
|
|
1281
|
+
h(Icon, { name: 'search', size: 13 }),
|
|
1282
|
+
t('panel.concordance'),
|
|
1283
|
+
),
|
|
1102
1284
|
),
|
|
1103
1285
|
h(
|
|
1104
1286
|
'div',
|
|
@@ -1176,7 +1358,7 @@
|
|
|
1176
1358
|
event.target.value = ''
|
|
1177
1359
|
},
|
|
1178
1360
|
}),
|
|
1179
|
-
h('button', { className: 'cait-btn', onClick: () => tmxInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: '
|
|
1361
|
+
h('button', { className: 'cait-btn', onClick: () => tmxInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'download', size: 12 }), t('panel.importTmx')),
|
|
1180
1362
|
h('button', { className: 'cait-btn cait-btn-danger', onClick: onClearTm, disabled: Boolean(busy) || data.tm.count === 0 }, t('panel.clearTm')),
|
|
1181
1363
|
),
|
|
1182
1364
|
)
|
|
@@ -1194,7 +1376,7 @@
|
|
|
1194
1376
|
h(
|
|
1195
1377
|
'button',
|
|
1196
1378
|
{ key: hit.id, className: 'cait-chip cait-chip-action', onClick: () => onApplyTerm(hit), title: hit.note || hit.target },
|
|
1197
|
-
`${hit.term} → ${hit.target}`,
|
|
1379
|
+
`${hit.term} → ${hit.target}${hit.forbidden ? ` · ${t('panel.termForbidden')}` : hit.preferred ? ` · ${t('panel.termPreferred')}` : ''}`,
|
|
1198
1380
|
),
|
|
1199
1381
|
),
|
|
1200
1382
|
)
|
|
@@ -1227,7 +1409,7 @@
|
|
|
1227
1409
|
event.target.value = ''
|
|
1228
1410
|
},
|
|
1229
1411
|
}),
|
|
1230
|
-
h('button', { className: 'cait-btn', onClick: () => glossaryInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: '
|
|
1412
|
+
h('button', { className: 'cait-btn', onClick: () => glossaryInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'download', size: 12 }), t('panel.importCsv')),
|
|
1231
1413
|
h('button', { className: 'cait-btn cait-btn-danger', onClick: onClearGlossary, disabled: Boolean(busy) || data.glossary.length === 0 }, t('panel.clearGlossary')),
|
|
1232
1414
|
),
|
|
1233
1415
|
)
|
|
@@ -1292,6 +1474,239 @@
|
|
|
1292
1474
|
),
|
|
1293
1475
|
)
|
|
1294
1476
|
: null,
|
|
1477
|
+
tab === 'qa'
|
|
1478
|
+
? h(
|
|
1479
|
+
'div',
|
|
1480
|
+
null,
|
|
1481
|
+
h(
|
|
1482
|
+
'div',
|
|
1483
|
+
{ className: 'cait-subhead' },
|
|
1484
|
+
h('span', null, t('qa.title')),
|
|
1485
|
+
h(
|
|
1486
|
+
'div',
|
|
1487
|
+
{ className: 'cait-subhead-actions' },
|
|
1488
|
+
h(
|
|
1489
|
+
'button',
|
|
1490
|
+
{
|
|
1491
|
+
className: 'cait-linkbtn',
|
|
1492
|
+
onClick: onReview,
|
|
1493
|
+
disabled: Boolean(busy) || Boolean(reviewBusy),
|
|
1494
|
+
title: t('qa.review'),
|
|
1495
|
+
},
|
|
1496
|
+
h(Icon, { name: 'sparkles', size: 12 }),
|
|
1497
|
+
reviewBusy ? t('qa.reviewing') : t('qa.review'),
|
|
1498
|
+
),
|
|
1499
|
+
h(
|
|
1500
|
+
'button',
|
|
1501
|
+
{ className: 'cait-linkbtn', onClick: onRefreshQa, disabled: Boolean(busy) },
|
|
1502
|
+
h(Icon, { name: 'refresh', size: 12 }),
|
|
1503
|
+
t('qa.refresh'),
|
|
1504
|
+
),
|
|
1505
|
+
),
|
|
1506
|
+
),
|
|
1507
|
+
qa
|
|
1508
|
+
? h(
|
|
1509
|
+
'div',
|
|
1510
|
+
null,
|
|
1511
|
+
h(
|
|
1512
|
+
'div',
|
|
1513
|
+
{ className: 'cait-riskcounts' },
|
|
1514
|
+
['high', 'medium', 'low'].map((level) =>
|
|
1515
|
+
h(
|
|
1516
|
+
'span',
|
|
1517
|
+
{ key: level, className: `cait-riskcount ${level}` },
|
|
1518
|
+
`${t(`qa.${level}`)} ${qa.riskCounts?.[level] || 0}`,
|
|
1519
|
+
),
|
|
1520
|
+
),
|
|
1521
|
+
),
|
|
1522
|
+
(qa.risks || []).length
|
|
1523
|
+
? qa.risks.map((item) => {
|
|
1524
|
+
const segment = doc.segments[item.index]
|
|
1525
|
+
const codes = (item.codes || []).map((code) => t(`qa.code.${code}`)).join(' · ')
|
|
1526
|
+
return h(
|
|
1527
|
+
'div',
|
|
1528
|
+
{
|
|
1529
|
+
key: item.index,
|
|
1530
|
+
className: `cait-riskrow ${item.risk}`,
|
|
1531
|
+
role: 'button',
|
|
1532
|
+
tabIndex: 0,
|
|
1533
|
+
onClick: () => onJumpQa(item.index),
|
|
1534
|
+
onKeyDown: (event) => {
|
|
1535
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1536
|
+
event.preventDefault()
|
|
1537
|
+
onJumpQa(item.index)
|
|
1538
|
+
}
|
|
1539
|
+
},
|
|
1540
|
+
title: codes,
|
|
1541
|
+
},
|
|
1542
|
+
h('span', { className: 'cait-riskrow-index' }, `${item.index + 1}`),
|
|
1543
|
+
h(
|
|
1544
|
+
'div',
|
|
1545
|
+
{ className: 'cait-riskrow-body' },
|
|
1546
|
+
h('span', { className: 'cait-riskrow-source' }, segment?.source || ''),
|
|
1547
|
+
h('span', { className: 'cait-riskrow-codes' }, codes),
|
|
1548
|
+
onFixQa
|
|
1549
|
+
? h(
|
|
1550
|
+
'div',
|
|
1551
|
+
{ className: 'cait-riskactions' },
|
|
1552
|
+
h(
|
|
1553
|
+
'button',
|
|
1554
|
+
{
|
|
1555
|
+
className: 'cait-riskaction',
|
|
1556
|
+
onClick: (event) => {
|
|
1557
|
+
event.stopPropagation()
|
|
1558
|
+
onFixQa(item, 'copy-source')
|
|
1559
|
+
},
|
|
1560
|
+
disabled: Boolean(busy),
|
|
1561
|
+
},
|
|
1562
|
+
t('qa.fixCopy'),
|
|
1563
|
+
),
|
|
1564
|
+
h(
|
|
1565
|
+
'button',
|
|
1566
|
+
{
|
|
1567
|
+
className: 'cait-riskaction',
|
|
1568
|
+
onClick: (event) => {
|
|
1569
|
+
event.stopPropagation()
|
|
1570
|
+
onFixQa(item, 'clear')
|
|
1571
|
+
},
|
|
1572
|
+
disabled: Boolean(busy),
|
|
1573
|
+
},
|
|
1574
|
+
t('qa.fixClear'),
|
|
1575
|
+
),
|
|
1576
|
+
item.termTarget
|
|
1577
|
+
? h(
|
|
1578
|
+
'button',
|
|
1579
|
+
{
|
|
1580
|
+
className: 'cait-riskaction',
|
|
1581
|
+
onClick: (event) => {
|
|
1582
|
+
event.stopPropagation()
|
|
1583
|
+
onFixQa(item, 'term')
|
|
1584
|
+
},
|
|
1585
|
+
disabled: Boolean(busy),
|
|
1586
|
+
},
|
|
1587
|
+
t('qa.fixTerm'),
|
|
1588
|
+
)
|
|
1589
|
+
: null,
|
|
1590
|
+
)
|
|
1591
|
+
: null,
|
|
1592
|
+
),
|
|
1593
|
+
h(Icon, { name: 'chevronRight', size: 12 }),
|
|
1594
|
+
)
|
|
1595
|
+
})
|
|
1596
|
+
: h('p', { className: 'cait-empty-note' }, t('qa.empty')),
|
|
1597
|
+
)
|
|
1598
|
+
: h('p', { className: 'cait-empty-note' }, t('qa.loading')),
|
|
1599
|
+
review
|
|
1600
|
+
? h(
|
|
1601
|
+
'div',
|
|
1602
|
+
{ className: 'cait-review' },
|
|
1603
|
+
h('div', { className: 'cait-listhead' }, t('qa.reviewTitle')),
|
|
1604
|
+
(review.issues || []).length
|
|
1605
|
+
? review.issues.map((issue, issueIndex) => {
|
|
1606
|
+
const severity = ['high', 'medium', 'low'].includes(issue.severity)
|
|
1607
|
+
? issue.severity
|
|
1608
|
+
: 'medium'
|
|
1609
|
+
return h(
|
|
1610
|
+
'div',
|
|
1611
|
+
{
|
|
1612
|
+
key: `${issue.index}-${issueIndex}`,
|
|
1613
|
+
className: 'cait-reviewrow',
|
|
1614
|
+
role: 'button',
|
|
1615
|
+
tabIndex: 0,
|
|
1616
|
+
onClick: () => onJumpQa(issue.index),
|
|
1617
|
+
onKeyDown: (event) => {
|
|
1618
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1619
|
+
event.preventDefault()
|
|
1620
|
+
onJumpQa(issue.index)
|
|
1621
|
+
}
|
|
1622
|
+
},
|
|
1623
|
+
},
|
|
1624
|
+
h('span', { className: 'cait-reviewrow-index' }, `${issue.index + 1}`),
|
|
1625
|
+
h(
|
|
1626
|
+
'div',
|
|
1627
|
+
{ className: 'cait-reviewrow-body' },
|
|
1628
|
+
h(
|
|
1629
|
+
'span',
|
|
1630
|
+
{ className: `cait-reviewrow-msg ${severity}` },
|
|
1631
|
+
`[${t(`qa.${severity}`)}] ${issue.message}`,
|
|
1632
|
+
),
|
|
1633
|
+
issue.suggestion
|
|
1634
|
+
? h(
|
|
1635
|
+
'button',
|
|
1636
|
+
{
|
|
1637
|
+
className: 'cait-riskaction',
|
|
1638
|
+
onClick: (event) => {
|
|
1639
|
+
event.stopPropagation()
|
|
1640
|
+
onApplyReview(issue)
|
|
1641
|
+
},
|
|
1642
|
+
},
|
|
1643
|
+
t('qa.reviewApply'),
|
|
1644
|
+
)
|
|
1645
|
+
: null,
|
|
1646
|
+
),
|
|
1647
|
+
)
|
|
1648
|
+
})
|
|
1649
|
+
: h('p', { className: 'cait-empty-note' }, t('qa.reviewEmpty')),
|
|
1650
|
+
)
|
|
1651
|
+
: null,
|
|
1652
|
+
)
|
|
1653
|
+
: null,
|
|
1654
|
+
tab === 'concordance'
|
|
1655
|
+
? h(
|
|
1656
|
+
'div',
|
|
1657
|
+
null,
|
|
1658
|
+
h(
|
|
1659
|
+
'form',
|
|
1660
|
+
{
|
|
1661
|
+
className: 'cait-search',
|
|
1662
|
+
onSubmit: (event) => {
|
|
1663
|
+
event.preventDefault()
|
|
1664
|
+
onConcordance()
|
|
1665
|
+
},
|
|
1666
|
+
},
|
|
1667
|
+
h(Icon, { name: 'search', size: 13 }),
|
|
1668
|
+
h('input', {
|
|
1669
|
+
value: concordanceQuery,
|
|
1670
|
+
onChange: (event) => setConcordanceQuery(event.target.value),
|
|
1671
|
+
placeholder: t('panel.concordancePlaceholder'),
|
|
1672
|
+
'aria-label': t('panel.concordancePlaceholder'),
|
|
1673
|
+
}),
|
|
1674
|
+
h(
|
|
1675
|
+
'button',
|
|
1676
|
+
{ className: 'cait-iconbtn', type: 'submit', title: t('panel.searchConcordance') },
|
|
1677
|
+
h(Icon, { name: 'arrowRight', size: 13 }),
|
|
1678
|
+
),
|
|
1679
|
+
),
|
|
1680
|
+
h('p', { className: 'cait-mt-hint' }, t('panel.concordanceHint')),
|
|
1681
|
+
concordance
|
|
1682
|
+
? h(
|
|
1683
|
+
'div',
|
|
1684
|
+
null,
|
|
1685
|
+
h(
|
|
1686
|
+
'div',
|
|
1687
|
+
{ className: 'cait-listhead' },
|
|
1688
|
+
h('span', null, t('panel.concordance')),
|
|
1689
|
+
h('span', { className: 'cait-count' }, concordance.total ?? concordance.entries?.length ?? 0),
|
|
1690
|
+
),
|
|
1691
|
+
(concordance.entries || []).length
|
|
1692
|
+
? concordance.entries.map((entry, i) =>
|
|
1693
|
+
h(
|
|
1694
|
+
'button',
|
|
1695
|
+
{
|
|
1696
|
+
key: `${entry.tmId || entry.library || 'c'}-${i}`,
|
|
1697
|
+
className: 'cait-concordance-row',
|
|
1698
|
+
onClick: () => onApplyConcordance(entry),
|
|
1699
|
+
},
|
|
1700
|
+
h('span', { className: 'cait-concordance-source' }, entry.source),
|
|
1701
|
+
h('span', { className: 'cait-concordance-target' }, entry.target),
|
|
1702
|
+
h('span', { className: 'cait-concordance-meta' }, entry.library || ''),
|
|
1703
|
+
),
|
|
1704
|
+
)
|
|
1705
|
+
: h('p', { className: 'cait-empty-note' }, t('panel.noConcordance')),
|
|
1706
|
+
)
|
|
1707
|
+
: null,
|
|
1708
|
+
)
|
|
1709
|
+
: null,
|
|
1295
1710
|
),
|
|
1296
1711
|
)
|
|
1297
1712
|
}
|
|
@@ -1766,6 +2181,7 @@
|
|
|
1766
2181
|
onCreateLibrary,
|
|
1767
2182
|
onRenameLibrary,
|
|
1768
2183
|
onDeleteLibrary,
|
|
2184
|
+
onSetMounts,
|
|
1769
2185
|
}) {
|
|
1770
2186
|
const storage = typeof localStorage !== 'undefined' ? localStorage : null
|
|
1771
2187
|
const [kind, setKind] = useState(seed?.kind === 'glossary' ? 'glossary' : 'tm')
|
|
@@ -1793,7 +2209,23 @@
|
|
|
1793
2209
|
const idKey = kind === 'tm' ? 'tmId' : 'glossaryId'
|
|
1794
2210
|
const labelKey = 'name'
|
|
1795
2211
|
const scopeKey = scopeIds.join(',')
|
|
1796
|
-
const
|
|
2212
|
+
const defaultWriteId = kind === 'tm' ? data?.mounts?.defaultTmId : data?.mounts?.defaultGlossaryId
|
|
2213
|
+
const targetLibraryId = libraries.some((library) => library[idKey] === defaultWriteId)
|
|
2214
|
+
? defaultWriteId
|
|
2215
|
+
: libraries[0]?.[idKey] || ''
|
|
2216
|
+
|
|
2217
|
+
const changeWriteLibrary = async (value) => {
|
|
2218
|
+
if (!value || value === targetLibraryId) return
|
|
2219
|
+
// A default must also be mounted; adding it here keeps the selector
|
|
2220
|
+
// from failing when the chosen library was only in the list view.
|
|
2221
|
+
if (kind === 'tm') {
|
|
2222
|
+
const tmIds = Array.from(new Set([...(data?.mounts?.tmIds || []), value]))
|
|
2223
|
+
await onSetMounts?.({ tmIds, defaultTmId: value })
|
|
2224
|
+
} else {
|
|
2225
|
+
const glossaryIds = Array.from(new Set([...(data?.mounts?.glossaryIds || []), value]))
|
|
2226
|
+
await onSetMounts?.({ glossaryIds, defaultGlossaryId: value })
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
1797
2229
|
|
|
1798
2230
|
useEffect(() => {
|
|
1799
2231
|
const timer = setTimeout(() => setDebouncedQuery(query), 250)
|
|
@@ -2034,6 +2466,35 @@
|
|
|
2034
2466
|
})
|
|
2035
2467
|
}
|
|
2036
2468
|
|
|
2469
|
+
const maintainTm = async (action) => {
|
|
2470
|
+
if (!targetLibraryId) return
|
|
2471
|
+
try {
|
|
2472
|
+
const preview = await rpc('tmMaintenancePreview', {
|
|
2473
|
+
action,
|
|
2474
|
+
tmId: targetLibraryId,
|
|
2475
|
+
emptyTarget: true,
|
|
2476
|
+
})
|
|
2477
|
+
const count = action === 'dedupe' ? preview.duplicates : preview.matched
|
|
2478
|
+
onConfirm({
|
|
2479
|
+
title: t(`tm.maintain.${action}`),
|
|
2480
|
+
message: t(`tm.maintain.${action}Hint`).replace('{count}', String(count)),
|
|
2481
|
+
confirmLabel: t(`tm.maintain.${action}`),
|
|
2482
|
+
action: async () => {
|
|
2483
|
+
const result = await rpc(action === 'dedupe' ? 'dedupeTmLibrary' : 'pruneTmLibrary', {
|
|
2484
|
+
tmId: targetLibraryId,
|
|
2485
|
+
emptyTarget: true,
|
|
2486
|
+
confirm: true,
|
|
2487
|
+
})
|
|
2488
|
+
if (result?.state) onApplyState(result.state)
|
|
2489
|
+
await loadRows()
|
|
2490
|
+
notify(t('tm.maintain.done').replace('{count}', String(count)))
|
|
2491
|
+
},
|
|
2492
|
+
})
|
|
2493
|
+
} catch (error) {
|
|
2494
|
+
report(error)
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2037
2498
|
const exportLibrary = async () => {
|
|
2038
2499
|
if (!targetLibraryId) return
|
|
2039
2500
|
setWorking('export')
|
|
@@ -2306,6 +2767,28 @@
|
|
|
2306
2767
|
['tbx', 'csv', 'json'].map((format) => h('option', { key: format, value: format }, format.toUpperCase())),
|
|
2307
2768
|
)
|
|
2308
2769
|
: null,
|
|
2770
|
+
h(
|
|
2771
|
+
'label',
|
|
2772
|
+
{ className: 'cait-libwrite', title: t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary') },
|
|
2773
|
+
h(
|
|
2774
|
+
'span',
|
|
2775
|
+
{ className: 'cait-libwritelabel' },
|
|
2776
|
+
t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
|
|
2777
|
+
),
|
|
2778
|
+
h(
|
|
2779
|
+
'select',
|
|
2780
|
+
{
|
|
2781
|
+
className: 'cait-liblang',
|
|
2782
|
+
value: targetLibraryId,
|
|
2783
|
+
onChange: (event) => changeWriteLibrary(event.target.value),
|
|
2784
|
+
disabled: Boolean(busy) || !libraries.length,
|
|
2785
|
+
'aria-label': t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
|
|
2786
|
+
},
|
|
2787
|
+
libraries.map((library) =>
|
|
2788
|
+
h('option', { key: library[idKey], value: library[idKey] }, library[labelKey]),
|
|
2789
|
+
),
|
|
2790
|
+
),
|
|
2791
|
+
),
|
|
2309
2792
|
kind === 'glossary'
|
|
2310
2793
|
? h(
|
|
2311
2794
|
'button',
|
|
@@ -2335,7 +2818,7 @@
|
|
|
2335
2818
|
disabled: Boolean(busy) || !targetLibraryId,
|
|
2336
2819
|
title: t('library.import'),
|
|
2337
2820
|
},
|
|
2338
|
-
h(Icon, { name: '
|
|
2821
|
+
h(Icon, { name: 'download', size: 13 }),
|
|
2339
2822
|
h('span', { className: 'cait-btn-label' }, t('library.import')),
|
|
2340
2823
|
),
|
|
2341
2824
|
h(
|
|
@@ -2347,9 +2830,37 @@
|
|
|
2347
2830
|
disabled: Boolean(busy) || working === 'export' || !targetLibraryId,
|
|
2348
2831
|
title: t('library.export'),
|
|
2349
2832
|
},
|
|
2350
|
-
h(Icon, { name: '
|
|
2833
|
+
h(Icon, { name: 'upload', size: 13 }),
|
|
2351
2834
|
h('span', { className: 'cait-btn-label' }, kind === 'tm' ? 'TMX' : exportFormat.toUpperCase()),
|
|
2352
2835
|
),
|
|
2836
|
+
kind === 'tm'
|
|
2837
|
+
? h(
|
|
2838
|
+
'button',
|
|
2839
|
+
{
|
|
2840
|
+
type: 'button',
|
|
2841
|
+
className: 'cait-btn',
|
|
2842
|
+
onClick: () => maintainTm('dedupe'),
|
|
2843
|
+
disabled: Boolean(busy) || !targetLibraryId,
|
|
2844
|
+
title: t('tm.maintain.dedupe'),
|
|
2845
|
+
},
|
|
2846
|
+
h(Icon, { name: 'database', size: 13 }),
|
|
2847
|
+
h('span', { className: 'cait-btn-label' }, t('tm.maintain.dedupe')),
|
|
2848
|
+
)
|
|
2849
|
+
: null,
|
|
2850
|
+
kind === 'tm'
|
|
2851
|
+
? h(
|
|
2852
|
+
'button',
|
|
2853
|
+
{
|
|
2854
|
+
type: 'button',
|
|
2855
|
+
className: 'cait-btn',
|
|
2856
|
+
onClick: () => maintainTm('prune'),
|
|
2857
|
+
disabled: Boolean(busy) || !targetLibraryId,
|
|
2858
|
+
title: t('tm.maintain.prune'),
|
|
2859
|
+
},
|
|
2860
|
+
h(Icon, { name: 'trash', size: 13 }),
|
|
2861
|
+
h('span', { className: 'cait-btn-label' }, t('tm.maintain.prune')),
|
|
2862
|
+
)
|
|
2863
|
+
: null,
|
|
2353
2864
|
h(
|
|
2354
2865
|
'button',
|
|
2355
2866
|
{
|
|
@@ -2360,6 +2871,7 @@
|
|
|
2360
2871
|
title: t('library.clear'),
|
|
2361
2872
|
},
|
|
2362
2873
|
h(Icon, { name: 'trash', size: 13 }),
|
|
2874
|
+
h('span', { className: 'cait-btn-label' }, t('library.clear')),
|
|
2363
2875
|
),
|
|
2364
2876
|
h('input', {
|
|
2365
2877
|
ref: fileRef,
|
|
@@ -2612,6 +3124,7 @@
|
|
|
2612
3124
|
onChange: () => toggleTerm(at),
|
|
2613
3125
|
}),
|
|
2614
3126
|
h('span', { className: 'cait-termname' }, candidate.term),
|
|
3127
|
+
candidate.proper ? h('span', { className: 'cait-originchip' }, t('term.proper')) : null,
|
|
2615
3128
|
h('span', { className: 'cait-termcount' }, `×${candidate.count}`),
|
|
2616
3129
|
candidate.inLibrary ? h('span', { className: 'cait-originchip' }, t('term.inLibrary')) : null,
|
|
2617
3130
|
h('input', {
|
|
@@ -2629,7 +3142,7 @@
|
|
|
2629
3142
|
)
|
|
2630
3143
|
}
|
|
2631
3144
|
|
|
2632
|
-
function ExportModal({ t, busy, onClose, onExport, qa }) {
|
|
3145
|
+
function ExportModal({ t, busy, onClose, onExport, onImportReview, qa }) {
|
|
2633
3146
|
const issues = qa?.issueCount || 0
|
|
2634
3147
|
const items = [
|
|
2635
3148
|
['bilingual-md', t('modal.export.bilingualMd'), t('modal.export.bilingual'), 'file'],
|
|
@@ -2638,11 +3151,13 @@
|
|
|
2638
3151
|
['target-txt', t('modal.export.targetTxt'), t('modal.export.target'), 'download'],
|
|
2639
3152
|
['target-md', t('modal.export.targetMd'), t('modal.export.target'), 'download'],
|
|
2640
3153
|
['docx', t('modal.export.docx'), t('modal.export.docxGroup'), 'file'],
|
|
3154
|
+
['bilingual-docx', t('modal.export.bilingualDocx'), t('modal.export.bilingual'), 'file'],
|
|
2641
3155
|
['epub', t('modal.export.epub'), t('modal.export.epubGroup'), 'file'],
|
|
2642
3156
|
['srt', t('modal.export.srt'), t('modal.export.subtitleGroup'), 'file'],
|
|
2643
3157
|
['vtt', t('modal.export.vtt'), t('modal.export.subtitleGroup'), 'file'],
|
|
2644
3158
|
['po', t('modal.export.po'), t('modal.export.localizationGroup'), 'file'],
|
|
2645
3159
|
['xliff', t('modal.export.xliff'), t('modal.export.localizationGroup'), 'file'],
|
|
3160
|
+
['xliff-review', t('modal.export.xliffReview'), t('modal.export.localizationGroup'), 'file'],
|
|
2646
3161
|
['xliff2', t('modal.export.xliff2'), t('modal.export.localizationGroup'), 'file'],
|
|
2647
3162
|
['sdlxliff', t('modal.export.sdlxliff'), t('modal.export.localizationGroup'), 'file'],
|
|
2648
3163
|
['html', t('modal.export.html'), t('modal.export.webGroup'), 'file'],
|
|
@@ -2685,6 +3200,30 @@
|
|
|
2685
3200
|
),
|
|
2686
3201
|
),
|
|
2687
3202
|
),
|
|
3203
|
+
onImportReview
|
|
3204
|
+
? h(
|
|
3205
|
+
'div',
|
|
3206
|
+
{ className: 'cait-review-import' },
|
|
3207
|
+
h('p', { className: 'cait-libnote' }, t('modal.export.reviewImportHint')),
|
|
3208
|
+
h(
|
|
3209
|
+
'label',
|
|
3210
|
+
{ className: 'cait-btn' },
|
|
3211
|
+
h(Icon, { name: 'upload', size: 13 }),
|
|
3212
|
+
t('modal.export.reviewImport'),
|
|
3213
|
+
h('input', {
|
|
3214
|
+
type: 'file',
|
|
3215
|
+
accept: '.xliff,.xlf,application/x-xliff+xml',
|
|
3216
|
+
style: { display: 'none' },
|
|
3217
|
+
disabled: Boolean(busy),
|
|
3218
|
+
onChange: (event) => {
|
|
3219
|
+
const file = event.target.files?.[0]
|
|
3220
|
+
if (file) onImportReview(file)
|
|
3221
|
+
event.target.value = ''
|
|
3222
|
+
},
|
|
3223
|
+
}),
|
|
3224
|
+
),
|
|
3225
|
+
)
|
|
3226
|
+
: null,
|
|
2688
3227
|
)
|
|
2689
3228
|
}
|
|
2690
3229
|
|
|
@@ -2705,13 +3244,23 @@
|
|
|
2705
3244
|
model: settings.mt.model,
|
|
2706
3245
|
temperature: settings.mt.temperature,
|
|
2707
3246
|
},
|
|
3247
|
+
review: {
|
|
3248
|
+
provider: settings.review?.provider || '',
|
|
3249
|
+
model: settings.review?.model || '',
|
|
3250
|
+
temperature: settings.review?.temperature ?? 0,
|
|
3251
|
+
},
|
|
2708
3252
|
})
|
|
2709
3253
|
const routes = Array.isArray(mtRoutes?.providers) ? mtRoutes.providers : []
|
|
2710
3254
|
const route = routes.find((item) => item.id === form.mt.provider)
|
|
2711
3255
|
const models = Array.isArray(route?.models) ? route.models : []
|
|
2712
3256
|
const routeMissing = form.mt.provider !== 'mock' && !route
|
|
3257
|
+
const reviewRoute = routes.find((item) => item.id === form.review.provider)
|
|
3258
|
+
const reviewModels = Array.isArray(reviewRoute?.models) ? reviewRoute.models : []
|
|
3259
|
+
const reviewInherited = !form.review.provider
|
|
2713
3260
|
|
|
2714
3261
|
const setMt = (patch) => setForm((current) => ({ ...current, mt: { ...current.mt, ...patch } }))
|
|
3262
|
+
const setReview = (patch) =>
|
|
3263
|
+
setForm((current) => ({ ...current, review: { ...current.review, ...patch } }))
|
|
2715
3264
|
const setPre = (patch) =>
|
|
2716
3265
|
setForm((current) => ({ ...current, preprocess: { ...current.preprocess, ...patch } }))
|
|
2717
3266
|
const selectProvider = (provider) => {
|
|
@@ -2729,6 +3278,18 @@
|
|
|
2729
3278
|
}))
|
|
2730
3279
|
}
|
|
2731
3280
|
|
|
3281
|
+
const selectReviewProvider = (provider) => {
|
|
3282
|
+
const nextRoute = routes.find((item) => item.id === provider)
|
|
3283
|
+
setForm((current) => ({
|
|
3284
|
+
...current,
|
|
3285
|
+
review: {
|
|
3286
|
+
...current.review,
|
|
3287
|
+
provider,
|
|
3288
|
+
model: nextRoute?.models?.[0]?.id || '',
|
|
3289
|
+
},
|
|
3290
|
+
}))
|
|
3291
|
+
}
|
|
3292
|
+
|
|
2732
3293
|
const submit = (event) => {
|
|
2733
3294
|
event.preventDefault()
|
|
2734
3295
|
const patch = {
|
|
@@ -2747,6 +3308,11 @@
|
|
|
2747
3308
|
model: form.mt.model,
|
|
2748
3309
|
temperature: Number(form.mt.temperature),
|
|
2749
3310
|
},
|
|
3311
|
+
review: {
|
|
3312
|
+
provider: form.review.provider,
|
|
3313
|
+
model: form.review.model,
|
|
3314
|
+
temperature: Number(form.review.temperature),
|
|
3315
|
+
},
|
|
2750
3316
|
}
|
|
2751
3317
|
onSave(patch)
|
|
2752
3318
|
}
|
|
@@ -2831,9 +3397,26 @@
|
|
|
2831
3397
|
{ className: 'cait-settings-section' },
|
|
2832
3398
|
h('h4', { className: 'cait-settings-title' }, t('modal.settings.tm')),
|
|
2833
3399
|
h(
|
|
2834
|
-
'
|
|
3400
|
+
'div',
|
|
2835
3401
|
{ className: 'cait-field' },
|
|
2836
|
-
h(
|
|
3402
|
+
h(
|
|
3403
|
+
'div',
|
|
3404
|
+
{ className: 'cait-fieldhead' },
|
|
3405
|
+
h('span', null, `${t('modal.settings.fuzzy')} · ${fuzzyPercent(form.fuzzyThreshold)}%`),
|
|
3406
|
+
// Offered only while the slider is away from the default, so the
|
|
3407
|
+
// control never takes space in the usual case.
|
|
3408
|
+
fuzzyPercent(form.fuzzyThreshold) === fuzzyPercent(DEFAULT_FUZZY_THRESHOLD)
|
|
3409
|
+
? null
|
|
3410
|
+
: h(
|
|
3411
|
+
'button',
|
|
3412
|
+
{
|
|
3413
|
+
className: 'cait-linkbtn cait-fieldreset',
|
|
3414
|
+
type: 'button',
|
|
3415
|
+
onClick: () => setForm({ ...form, fuzzyThreshold: DEFAULT_FUZZY_THRESHOLD }),
|
|
3416
|
+
},
|
|
3417
|
+
t('modal.settings.resetDefault'),
|
|
3418
|
+
),
|
|
3419
|
+
),
|
|
2837
3420
|
h('input', {
|
|
2838
3421
|
className: 'cait-range',
|
|
2839
3422
|
type: 'range',
|
|
@@ -2843,6 +3426,7 @@
|
|
|
2843
3426
|
value: form.fuzzyThreshold,
|
|
2844
3427
|
onChange: (event) => setForm({ ...form, fuzzyThreshold: event.target.value }),
|
|
2845
3428
|
}),
|
|
3429
|
+
h('p', { className: 'cait-fieldhint' }, t('modal.settings.fuzzyHint')),
|
|
2846
3430
|
),
|
|
2847
3431
|
h(
|
|
2848
3432
|
'label',
|
|
@@ -2938,38 +3522,130 @@
|
|
|
2938
3522
|
),
|
|
2939
3523
|
h(
|
|
2940
3524
|
'div',
|
|
2941
|
-
{ className: 'cait-
|
|
2942
|
-
h('
|
|
2943
|
-
h(
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3525
|
+
{ className: 'cait-settings-section' },
|
|
3526
|
+
h('h4', { className: 'cait-settings-title' }, t('modal.settings.review')),
|
|
3527
|
+
h(
|
|
3528
|
+
'label',
|
|
3529
|
+
{ className: 'cait-checkline' },
|
|
3530
|
+
h('input', {
|
|
3531
|
+
type: 'checkbox',
|
|
3532
|
+
checked: reviewInherited,
|
|
3533
|
+
onChange: (event) => {
|
|
3534
|
+
if (event.target.checked) {
|
|
3535
|
+
setForm((current) => ({
|
|
3536
|
+
...current,
|
|
3537
|
+
review: { ...current.review, provider: '', model: '' },
|
|
3538
|
+
}))
|
|
3539
|
+
return
|
|
3540
|
+
}
|
|
3541
|
+
const first = routes.find((item) => item.id !== 'mock' && item.hasModels !== false) || routes[0]
|
|
3542
|
+
setForm((current) => ({
|
|
3543
|
+
...current,
|
|
3544
|
+
review: {
|
|
3545
|
+
...current.review,
|
|
3546
|
+
provider: first?.id || '',
|
|
3547
|
+
model: first?.models?.[0]?.id || '',
|
|
3548
|
+
},
|
|
3549
|
+
}))
|
|
3550
|
+
},
|
|
3551
|
+
}),
|
|
3552
|
+
t('modal.settings.reviewInherit'),
|
|
3553
|
+
),
|
|
3554
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('modal.settings.reviewHint')),
|
|
3555
|
+
reviewInherited
|
|
3556
|
+
? null
|
|
3557
|
+
: h(
|
|
3558
|
+
'div',
|
|
3559
|
+
{ className: 'cait-grid2' },
|
|
3560
|
+
h(
|
|
3561
|
+
'label',
|
|
3562
|
+
{ className: 'cait-field' },
|
|
3563
|
+
h('span', null, t('modal.settings.reviewProvider')),
|
|
3564
|
+
h(
|
|
3565
|
+
'select',
|
|
3566
|
+
{
|
|
3567
|
+
className: 'cait-input',
|
|
3568
|
+
value: form.review.provider,
|
|
3569
|
+
onChange: (event) => selectReviewProvider(event.target.value),
|
|
3570
|
+
},
|
|
3571
|
+
routes.map((item) =>
|
|
3572
|
+
h(
|
|
3573
|
+
'option',
|
|
3574
|
+
{ key: item.id, value: item.id, disabled: item.hasModels === false },
|
|
3575
|
+
item.hasModels === false
|
|
3576
|
+
? `${item.name} (${item.id}) - ${t('modal.settings.noModels')}`
|
|
3577
|
+
: `${item.name} (${item.id})`,
|
|
3578
|
+
),
|
|
3579
|
+
),
|
|
3580
|
+
form.review.provider && !reviewRoute
|
|
3581
|
+
? h('option', { value: form.review.provider }, t('modal.settings.routeMissing'))
|
|
3582
|
+
: null,
|
|
3583
|
+
),
|
|
3584
|
+
),
|
|
3585
|
+
h(
|
|
3586
|
+
'label',
|
|
3587
|
+
{ className: 'cait-field' },
|
|
3588
|
+
h('span', null, t('modal.settings.model')),
|
|
3589
|
+
reviewModels.length
|
|
3590
|
+
? h(
|
|
3591
|
+
'select',
|
|
3592
|
+
{
|
|
3593
|
+
className: 'cait-input',
|
|
3594
|
+
value: form.review.model,
|
|
3595
|
+
onChange: (event) => setReview({ model: event.target.value }),
|
|
3596
|
+
},
|
|
3597
|
+
reviewModels.map((model) =>
|
|
3598
|
+
h('option', { key: model.id, value: model.id }, `${model.name} (${model.id})`),
|
|
3599
|
+
),
|
|
3600
|
+
)
|
|
3601
|
+
: h('input', {
|
|
3602
|
+
className: 'cait-input',
|
|
3603
|
+
value: form.review.model,
|
|
3604
|
+
readOnly: true,
|
|
3605
|
+
placeholder: t('modal.settings.noModels'),
|
|
3606
|
+
}),
|
|
3607
|
+
),
|
|
3608
|
+
),
|
|
3609
|
+
h(
|
|
3610
|
+
'p',
|
|
3611
|
+
{ className: 'cait-mt-hint', style: { marginTop: 0 } },
|
|
3612
|
+
reviewInherited ? t('modal.settings.reviewInheritedHint') : t('modal.settings.reviewCustomHint'),
|
|
3613
|
+
),
|
|
3614
|
+
),
|
|
3615
|
+
h(
|
|
3616
|
+
'div',
|
|
3617
|
+
{ className: 'cait-modal-actions' },
|
|
3618
|
+
h('button', { type: 'button', className: 'cait-btn cait-btn-ghost', onClick: onClose }, t('modal.settings.cancel')),
|
|
3619
|
+
h('button', { type: 'submit', className: 'cait-btn cait-btn-primary', disabled: Boolean(busy) }, busy === 'settings' ? h(Spinner) : null, t('modal.settings.save')),
|
|
3620
|
+
),
|
|
3621
|
+
),
|
|
3622
|
+
)
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
function ConfirmModal({ t, state, busy, onClose, onConfirm }) {
|
|
3626
|
+
return h(
|
|
3627
|
+
Modal,
|
|
3628
|
+
{ title: state.title || t('modal.confirm.title'), onClose },
|
|
3629
|
+
h('p', { className: 'cait-confirm-text' }, state.message),
|
|
3630
|
+
h(
|
|
3631
|
+
'div',
|
|
3632
|
+
{ className: 'cait-modal-actions' },
|
|
3633
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
3634
|
+
h('button', { className: 'cait-btn cait-btn-danger', onClick: onConfirm, disabled: Boolean(busy) }, busy ? h(Spinner) : null, state.confirmLabel || t('modal.confirm.ok')),
|
|
3635
|
+
),
|
|
3636
|
+
)
|
|
3637
|
+
}
|
|
3638
|
+
|
|
3639
|
+
function EmptyState({ t, settings, busy, onCreate, onDemo }) {
|
|
3640
|
+
return h(
|
|
3641
|
+
'div',
|
|
3642
|
+
{ className: 'cait-empty' },
|
|
3643
|
+
h(
|
|
3644
|
+
'div',
|
|
3645
|
+
{ className: 'cait-empty-inner' },
|
|
3646
|
+
h('span', { className: 'cait-kicker' }, 'CAIT · CAT WORKBENCH'),
|
|
3647
|
+
h('h1', null, t('empty.title')),
|
|
3648
|
+
h('p', { className: 'cait-lede' }, t('empty.desc')),
|
|
2973
3649
|
h(
|
|
2974
3650
|
'div',
|
|
2975
3651
|
{ className: 'cait-features' },
|
|
@@ -2988,6 +3664,614 @@
|
|
|
2988
3664
|
)
|
|
2989
3665
|
}
|
|
2990
3666
|
|
|
3667
|
+
function BriefModal({ t, doc, busy, onClose, onSave }) {
|
|
3668
|
+
const initial = doc.brief || {}
|
|
3669
|
+
const [form, setForm] = useState({
|
|
3670
|
+
domain: initial.domain || '',
|
|
3671
|
+
audience: initial.audience || '',
|
|
3672
|
+
tone: initial.tone || '',
|
|
3673
|
+
notes: initial.notes || '',
|
|
3674
|
+
doNotTranslate: initial.doNotTranslate || '',
|
|
3675
|
+
styleSample: initial.styleSample || '',
|
|
3676
|
+
})
|
|
3677
|
+
const field = (key, label, placeholder, rows) =>
|
|
3678
|
+
h(
|
|
3679
|
+
'label',
|
|
3680
|
+
{ className: 'cait-field' },
|
|
3681
|
+
h('span', null, t(label)),
|
|
3682
|
+
rows
|
|
3683
|
+
? h('textarea', {
|
|
3684
|
+
className: 'cait-textarea',
|
|
3685
|
+
rows,
|
|
3686
|
+
value: form[key],
|
|
3687
|
+
placeholder: t(placeholder),
|
|
3688
|
+
onChange: (event) => setForm({ ...form, [key]: event.target.value }),
|
|
3689
|
+
})
|
|
3690
|
+
: h('input', {
|
|
3691
|
+
className: 'cait-input',
|
|
3692
|
+
value: form[key],
|
|
3693
|
+
placeholder: t(placeholder),
|
|
3694
|
+
onChange: (event) => setForm({ ...form, [key]: event.target.value }),
|
|
3695
|
+
}),
|
|
3696
|
+
)
|
|
3697
|
+
return h(
|
|
3698
|
+
Modal,
|
|
3699
|
+
{ title: t('brief.title'), onClose, wide: true },
|
|
3700
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('brief.hint')),
|
|
3701
|
+
h(
|
|
3702
|
+
'div',
|
|
3703
|
+
{ className: 'cait-briefform' },
|
|
3704
|
+
h('div', { className: 'cait-grid2' }, field('domain', 'brief.domain', 'brief.placeholder.domain'), field('audience', 'brief.audience', 'brief.placeholder.audience')),
|
|
3705
|
+
field('tone', 'brief.tone', 'brief.placeholder.tone'),
|
|
3706
|
+
field('notes', 'brief.notes', 'brief.placeholder.notes', 3),
|
|
3707
|
+
field('doNotTranslate', 'brief.doNotTranslate', 'brief.placeholder.doNotTranslate', 3),
|
|
3708
|
+
field('styleSample', 'brief.styleSample', 'brief.placeholder.styleSample', 4),
|
|
3709
|
+
),
|
|
3710
|
+
h(
|
|
3711
|
+
'div',
|
|
3712
|
+
{ className: 'cait-modal-actions' },
|
|
3713
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
3714
|
+
h(
|
|
3715
|
+
'button',
|
|
3716
|
+
{ className: 'cait-btn cait-btn-primary', onClick: () => onSave(form), disabled: Boolean(busy) },
|
|
3717
|
+
busy ? h(Spinner) : null,
|
|
3718
|
+
t('brief.save'),
|
|
3719
|
+
),
|
|
3720
|
+
),
|
|
3721
|
+
)
|
|
3722
|
+
}
|
|
3723
|
+
|
|
3724
|
+
function NoteModal({ t, doc, index, segment, notes, busy, onAdd, onUpdate, onDelete, onClose }) {
|
|
3725
|
+
const [text, setText] = useState('')
|
|
3726
|
+
const [editingId, setEditingId] = useState('')
|
|
3727
|
+
const [editingText, setEditingText] = useState('')
|
|
3728
|
+
const list = Array.isArray(notes) ? notes : []
|
|
3729
|
+
const submit = () => {
|
|
3730
|
+
const value = text.trim()
|
|
3731
|
+
if (!value || busy) return
|
|
3732
|
+
onAdd(value)
|
|
3733
|
+
setText('')
|
|
3734
|
+
}
|
|
3735
|
+
return h(
|
|
3736
|
+
Modal,
|
|
3737
|
+
{ title: `${t('notes.title')} · #${index + 1}`, onClose, wide: true },
|
|
3738
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('notes.hint')),
|
|
3739
|
+
h('div', { className: 'cait-notelist' }, segment?.source || ''),
|
|
3740
|
+
list.length
|
|
3741
|
+
? h(
|
|
3742
|
+
'div',
|
|
3743
|
+
{ className: 'cait-notelist' },
|
|
3744
|
+
list.map((note) =>
|
|
3745
|
+
h(
|
|
3746
|
+
'div',
|
|
3747
|
+
{ className: 'cait-noteitem', key: note.id },
|
|
3748
|
+
h(
|
|
3749
|
+
'div',
|
|
3750
|
+
{ className: 'cait-notehead' },
|
|
3751
|
+
h('span', { className: 'cait-noteauthor' }, note.author === 'agent' ? 'Agent' : t('notes.user')),
|
|
3752
|
+
h('span', { className: 'cait-notetime' }, String(note.createdAt || '').replace('T', ' ').slice(0, 16)),
|
|
3753
|
+
),
|
|
3754
|
+
editingId === note.id
|
|
3755
|
+
? h(
|
|
3756
|
+
'div',
|
|
3757
|
+
null,
|
|
3758
|
+
h('textarea', {
|
|
3759
|
+
className: 'cait-input cait-textarea',
|
|
3760
|
+
value: editingText,
|
|
3761
|
+
rows: 3,
|
|
3762
|
+
onChange: (event) => setEditingText(event.target.value),
|
|
3763
|
+
}),
|
|
3764
|
+
h(
|
|
3765
|
+
'div',
|
|
3766
|
+
{ className: 'cait-noteactions' },
|
|
3767
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: () => setEditingId('') }, t('notes.cancel')),
|
|
3768
|
+
h(
|
|
3769
|
+
'button',
|
|
3770
|
+
{
|
|
3771
|
+
className: 'cait-btn cait-btn-primary',
|
|
3772
|
+
disabled: Boolean(busy) || !editingText.trim(),
|
|
3773
|
+
onClick: () => {
|
|
3774
|
+
onUpdate(note.id, editingText)
|
|
3775
|
+
setEditingId('')
|
|
3776
|
+
},
|
|
3777
|
+
},
|
|
3778
|
+
t('notes.save'),
|
|
3779
|
+
),
|
|
3780
|
+
),
|
|
3781
|
+
)
|
|
3782
|
+
: h(
|
|
3783
|
+
'div',
|
|
3784
|
+
null,
|
|
3785
|
+
h('div', { className: 'cait-notetext' }, note.text),
|
|
3786
|
+
h(
|
|
3787
|
+
'div',
|
|
3788
|
+
{ className: 'cait-noteactions' },
|
|
3789
|
+
h(
|
|
3790
|
+
'button',
|
|
3791
|
+
{
|
|
3792
|
+
className: 'cait-minibtn',
|
|
3793
|
+
onClick: () => {
|
|
3794
|
+
setEditingId(note.id)
|
|
3795
|
+
setEditingText(note.text)
|
|
3796
|
+
},
|
|
3797
|
+
},
|
|
3798
|
+
t('notes.edit'),
|
|
3799
|
+
),
|
|
3800
|
+
h(
|
|
3801
|
+
'button',
|
|
3802
|
+
{ className: 'cait-minibtn cait-danger', onClick: () => onDelete(note.id) },
|
|
3803
|
+
t('notes.delete'),
|
|
3804
|
+
),
|
|
3805
|
+
),
|
|
3806
|
+
),
|
|
3807
|
+
),
|
|
3808
|
+
),
|
|
3809
|
+
)
|
|
3810
|
+
: h('p', { className: 'cait-mt-hint' }, t('notes.empty')),
|
|
3811
|
+
h(
|
|
3812
|
+
'div',
|
|
3813
|
+
{ className: 'cait-noteadd' },
|
|
3814
|
+
h('textarea', {
|
|
3815
|
+
className: 'cait-input cait-textarea',
|
|
3816
|
+
value: text,
|
|
3817
|
+
rows: 3,
|
|
3818
|
+
placeholder: t('notes.placeholder'),
|
|
3819
|
+
onChange: (event) => setText(event.target.value),
|
|
3820
|
+
}),
|
|
3821
|
+
h(
|
|
3822
|
+
'div',
|
|
3823
|
+
{ className: 'cait-noteactions' },
|
|
3824
|
+
h('button', { className: 'cait-btn cait-btn-primary', disabled: Boolean(busy) || !text.trim(), onClick: submit }, busy ? h(Spinner) : null, t('notes.add')),
|
|
3825
|
+
),
|
|
3826
|
+
),
|
|
3827
|
+
)
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
function ReplaceModal({ t, doc, busy, preview, undoCount, onPreview, onApply, onUndo, onClose }) {
|
|
3831
|
+
const queryRef = useRef(null)
|
|
3832
|
+
const [form, setForm] = useState({
|
|
3833
|
+
query: '',
|
|
3834
|
+
replacement: '',
|
|
3835
|
+
scope: 'target',
|
|
3836
|
+
matchCase: false,
|
|
3837
|
+
regex: false,
|
|
3838
|
+
})
|
|
3839
|
+
useEffect(() => {
|
|
3840
|
+
if (queryRef.current) queryRef.current.focus()
|
|
3841
|
+
}, [])
|
|
3842
|
+
const set = (key, value) => setForm((current) => ({ ...current, [key]: value }))
|
|
3843
|
+
return h(
|
|
3844
|
+
Modal,
|
|
3845
|
+
{ title: t('replace.title'), onClose, wide: true },
|
|
3846
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('replace.hint')),
|
|
3847
|
+
h(
|
|
3848
|
+
'div',
|
|
3849
|
+
{ className: 'cait-replaceform' },
|
|
3850
|
+
h(
|
|
3851
|
+
'label',
|
|
3852
|
+
{ className: 'cait-field' },
|
|
3853
|
+
h('span', null, t('replace.query')),
|
|
3854
|
+
h('input', {
|
|
3855
|
+
ref: queryRef,
|
|
3856
|
+
className: 'cait-input',
|
|
3857
|
+
value: form.query,
|
|
3858
|
+
placeholder: t('replace.queryPlaceholder'),
|
|
3859
|
+
onChange: (event) => set('query', event.target.value),
|
|
3860
|
+
}),
|
|
3861
|
+
),
|
|
3862
|
+
h(
|
|
3863
|
+
'label',
|
|
3864
|
+
{ className: 'cait-field' },
|
|
3865
|
+
h('span', null, t('replace.replacement')),
|
|
3866
|
+
h('input', {
|
|
3867
|
+
className: 'cait-input',
|
|
3868
|
+
value: form.replacement,
|
|
3869
|
+
placeholder: t('replace.replacementPlaceholder'),
|
|
3870
|
+
onChange: (event) => set('replacement', event.target.value),
|
|
3871
|
+
}),
|
|
3872
|
+
),
|
|
3873
|
+
h(
|
|
3874
|
+
'div',
|
|
3875
|
+
{ className: 'cait-replaceoptions' },
|
|
3876
|
+
h(
|
|
3877
|
+
'label',
|
|
3878
|
+
{ className: 'cait-field' },
|
|
3879
|
+
h('span', null, t('replace.scope')),
|
|
3880
|
+
h(
|
|
3881
|
+
'select',
|
|
3882
|
+
{
|
|
3883
|
+
className: 'cait-input',
|
|
3884
|
+
value: form.scope,
|
|
3885
|
+
onChange: (event) => set('scope', event.target.value),
|
|
3886
|
+
},
|
|
3887
|
+
h('option', { value: 'target' }, t('replace.scope.target')),
|
|
3888
|
+
h('option', { value: 'source' }, t('replace.scope.source')),
|
|
3889
|
+
h('option', { value: 'both' }, t('replace.scope.both')),
|
|
3890
|
+
),
|
|
3891
|
+
),
|
|
3892
|
+
h(
|
|
3893
|
+
'label',
|
|
3894
|
+
{ className: 'cait-check' },
|
|
3895
|
+
h('input', {
|
|
3896
|
+
type: 'checkbox',
|
|
3897
|
+
checked: form.matchCase,
|
|
3898
|
+
onChange: (event) => set('matchCase', event.target.checked),
|
|
3899
|
+
}),
|
|
3900
|
+
t('replace.matchCase'),
|
|
3901
|
+
),
|
|
3902
|
+
h(
|
|
3903
|
+
'label',
|
|
3904
|
+
{ className: 'cait-check' },
|
|
3905
|
+
h('input', {
|
|
3906
|
+
type: 'checkbox',
|
|
3907
|
+
checked: form.regex,
|
|
3908
|
+
onChange: (event) => set('regex', event.target.checked),
|
|
3909
|
+
}),
|
|
3910
|
+
t('replace.regex'),
|
|
3911
|
+
),
|
|
3912
|
+
),
|
|
3913
|
+
),
|
|
3914
|
+
preview
|
|
3915
|
+
? h(
|
|
3916
|
+
'div',
|
|
3917
|
+
{ className: 'cait-replaceresults' },
|
|
3918
|
+
h(
|
|
3919
|
+
'div',
|
|
3920
|
+
{ className: 'cait-replacecounts' },
|
|
3921
|
+
t('replace.results')
|
|
3922
|
+
.replace('{count}', String(preview.total))
|
|
3923
|
+
.replace('{segments}', String(preview.matchedSegments || 0)),
|
|
3924
|
+
),
|
|
3925
|
+
preview.truncated ? h('p', { className: 'cait-mt-hint' }, t('replace.truncated')) : null,
|
|
3926
|
+
preview.matches?.length
|
|
3927
|
+
? h(
|
|
3928
|
+
'div',
|
|
3929
|
+
{ className: 'cait-replacelist' },
|
|
3930
|
+
preview.matches.slice(0, 50).map((match, index) =>
|
|
3931
|
+
h(
|
|
3932
|
+
'div',
|
|
3933
|
+
{ className: 'cait-replaceitem', key: `${match.uid}-${match.field}-${index}` },
|
|
3934
|
+
h(
|
|
3935
|
+
'div',
|
|
3936
|
+
{ className: 'cait-replacehead' },
|
|
3937
|
+
h('span', null, `#${match.index + 1}`),
|
|
3938
|
+
h('span', { className: 'cait-replacefield' }, t(`replace.field.${match.field}`)),
|
|
3939
|
+
h('span', { className: 'cait-replacecount' }, `×${match.count}`),
|
|
3940
|
+
),
|
|
3941
|
+
h('div', { className: 'cait-replacebefore' }, match.before),
|
|
3942
|
+
h('div', { className: 'cait-replaceafter' }, match.after),
|
|
3943
|
+
),
|
|
3944
|
+
),
|
|
3945
|
+
)
|
|
3946
|
+
: h('p', { className: 'cait-mt-hint' }, t('replace.noResults')),
|
|
3947
|
+
)
|
|
3948
|
+
: null,
|
|
3949
|
+
h(
|
|
3950
|
+
'div',
|
|
3951
|
+
{ className: 'cait-modal-actions' },
|
|
3952
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
3953
|
+
undoCount
|
|
3954
|
+
? h('button', { className: 'cait-btn', onClick: onUndo, disabled: Boolean(busy) }, t('replace.undo'))
|
|
3955
|
+
: null,
|
|
3956
|
+
h('button', { className: 'cait-btn', onClick: () => onPreview(form), disabled: Boolean(busy) || !form.query }, t('replace.preview')),
|
|
3957
|
+
h(
|
|
3958
|
+
'button',
|
|
3959
|
+
{ className: 'cait-btn cait-btn-primary', onClick: () => onApply(form), disabled: Boolean(busy) || !form.query || !preview?.total },
|
|
3960
|
+
busy ? h(Spinner) : null,
|
|
3961
|
+
t('replace.apply'),
|
|
3962
|
+
),
|
|
3963
|
+
),
|
|
3964
|
+
)
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function CommandPalette({ t, items, onClose, onRun }) {
|
|
3968
|
+
const [query, setQuery] = useState('')
|
|
3969
|
+
const [active, setActive] = useState(0)
|
|
3970
|
+
const inputRef = useRef(null)
|
|
3971
|
+
useEffect(() => {
|
|
3972
|
+
if (inputRef.current) inputRef.current.focus()
|
|
3973
|
+
}, [])
|
|
3974
|
+
const needle = query.trim().toLowerCase()
|
|
3975
|
+
const filtered = items.filter((item) => {
|
|
3976
|
+
if (!needle) return true
|
|
3977
|
+
return `${item.title} ${item.hint || ''}`.toLowerCase().includes(needle)
|
|
3978
|
+
})
|
|
3979
|
+
useEffect(() => {
|
|
3980
|
+
setActive(0)
|
|
3981
|
+
}, [query])
|
|
3982
|
+
const choose = (item) => {
|
|
3983
|
+
if (item) onRun(item)
|
|
3984
|
+
}
|
|
3985
|
+
return h(
|
|
3986
|
+
Modal,
|
|
3987
|
+
{ title: t('palette.title'), onClose, wide: true },
|
|
3988
|
+
h('input', {
|
|
3989
|
+
ref: inputRef,
|
|
3990
|
+
className: 'cait-input cait-palette-input',
|
|
3991
|
+
value: query,
|
|
3992
|
+
placeholder: t('palette.placeholder'),
|
|
3993
|
+
onChange: (event) => setQuery(event.target.value),
|
|
3994
|
+
onKeyDown: (event) => {
|
|
3995
|
+
if (event.key === 'ArrowDown') {
|
|
3996
|
+
event.preventDefault()
|
|
3997
|
+
setActive((value) => Math.min(value + 1, Math.max(0, filtered.length - 1)))
|
|
3998
|
+
} else if (event.key === 'ArrowUp') {
|
|
3999
|
+
event.preventDefault()
|
|
4000
|
+
setActive((value) => Math.max(value - 1, 0))
|
|
4001
|
+
} else if (event.key === 'Enter') {
|
|
4002
|
+
event.preventDefault()
|
|
4003
|
+
choose(filtered[active])
|
|
4004
|
+
}
|
|
4005
|
+
},
|
|
4006
|
+
}),
|
|
4007
|
+
h(
|
|
4008
|
+
'div',
|
|
4009
|
+
{ className: 'cait-palette-list' },
|
|
4010
|
+
filtered.length
|
|
4011
|
+
? filtered.map((item, index) =>
|
|
4012
|
+
h(
|
|
4013
|
+
'button',
|
|
4014
|
+
{
|
|
4015
|
+
type: 'button',
|
|
4016
|
+
key: item.id,
|
|
4017
|
+
className: `cait-palette-item${index === active ? ' active' : ''}`,
|
|
4018
|
+
onMouseEnter: () => setActive(index),
|
|
4019
|
+
onClick: () => choose(item),
|
|
4020
|
+
},
|
|
4021
|
+
h('span', { className: 'cait-palette-label' }, item.title),
|
|
4022
|
+
item.hint ? h('span', { className: 'cait-palette-hint' }, item.hint) : null,
|
|
4023
|
+
),
|
|
4024
|
+
)
|
|
4025
|
+
: h('p', { className: 'cait-mt-hint' }, t('palette.empty')),
|
|
4026
|
+
),
|
|
4027
|
+
h('p', { className: 'cait-palette-foot' }, t('palette.footer')),
|
|
4028
|
+
)
|
|
4029
|
+
}
|
|
4030
|
+
|
|
4031
|
+
function ResourceModal({ t, doc, libraries, mounts, busy, onClose, onSave }) {
|
|
4032
|
+
const resources = doc.resources || {}
|
|
4033
|
+
const [inheritTm, setInheritTm] = useState(resources.inheritsTm !== false)
|
|
4034
|
+
const [inheritGlossary, setInheritGlossary] = useState(resources.inheritsGlossary !== false)
|
|
4035
|
+
const [tmIds, setTmIds] = useState(
|
|
4036
|
+
() => new Set(resources.tmIds ?? resources.effectiveTmIds ?? []),
|
|
4037
|
+
)
|
|
4038
|
+
const [glossaryIds, setGlossaryIds] = useState(
|
|
4039
|
+
() => new Set(resources.glossaryIds ?? resources.effectiveGlossaryIds ?? []),
|
|
4040
|
+
)
|
|
4041
|
+
const [defaultWriteTmId, setDefaultWriteTmId] = useState(resources.defaultWriteTmId || '')
|
|
4042
|
+
|
|
4043
|
+
const allTm = libraries?.tm || []
|
|
4044
|
+
const allGlossary = libraries?.glossary || []
|
|
4045
|
+
const selectedTm = inheritTm ? mounts?.tmIds || [] : Array.from(tmIds)
|
|
4046
|
+
const selectedGlossary = inheritGlossary ? mounts?.glossaryIds || [] : Array.from(glossaryIds)
|
|
4047
|
+
const tmChoices = allTm.filter((library) => selectedTm.includes(library.tmId))
|
|
4048
|
+
const checkedTmIds = new Set(selectedTm)
|
|
4049
|
+
const checkedGlossaryIds = new Set(selectedGlossary)
|
|
4050
|
+
const defaultTm = tmChoices.some((library) => library.tmId === defaultWriteTmId)
|
|
4051
|
+
? defaultWriteTmId
|
|
4052
|
+
: ''
|
|
4053
|
+
|
|
4054
|
+
const toggleId = (setter, id) => {
|
|
4055
|
+
setter((current) => {
|
|
4056
|
+
const next = new Set(current)
|
|
4057
|
+
if (next.has(id)) next.delete(id)
|
|
4058
|
+
else next.add(id)
|
|
4059
|
+
return next
|
|
4060
|
+
})
|
|
4061
|
+
}
|
|
4062
|
+
|
|
4063
|
+
const librarySection = (label, all, idKey, checkedIds, inherit, setInherit, toggle, inheritLabel) =>
|
|
4064
|
+
h(
|
|
4065
|
+
'div',
|
|
4066
|
+
{ className: 'cait-resource-section' },
|
|
4067
|
+
h(
|
|
4068
|
+
'div',
|
|
4069
|
+
{ className: 'cait-resource-head' },
|
|
4070
|
+
h('strong', null, t(label)),
|
|
4071
|
+
h(
|
|
4072
|
+
'label',
|
|
4073
|
+
{ className: 'cait-checkline' },
|
|
4074
|
+
h('input', {
|
|
4075
|
+
type: 'checkbox',
|
|
4076
|
+
checked: inherit,
|
|
4077
|
+
onChange: (event) => setInherit(event.target.checked),
|
|
4078
|
+
}),
|
|
4079
|
+
t(inheritLabel),
|
|
4080
|
+
),
|
|
4081
|
+
),
|
|
4082
|
+
h(
|
|
4083
|
+
'div',
|
|
4084
|
+
{ className: 'cait-resource-list' },
|
|
4085
|
+
all.length === 0
|
|
4086
|
+
? h('p', { className: 'cait-meta' }, t('resources.empty'))
|
|
4087
|
+
: all.map((library) =>
|
|
4088
|
+
h(
|
|
4089
|
+
'label',
|
|
4090
|
+
{ key: library[idKey], className: 'cait-resource-row' },
|
|
4091
|
+
h('input', {
|
|
4092
|
+
type: 'checkbox',
|
|
4093
|
+
checked: checkedIds.has(library[idKey]),
|
|
4094
|
+
disabled: inherit,
|
|
4095
|
+
onChange: () => toggle(library[idKey]),
|
|
4096
|
+
}),
|
|
4097
|
+
h('span', { className: 'cait-resource-name' }, library.name),
|
|
4098
|
+
h('span', { className: 'cait-meta' }, library[idKey]),
|
|
4099
|
+
),
|
|
4100
|
+
),
|
|
4101
|
+
),
|
|
4102
|
+
)
|
|
4103
|
+
|
|
4104
|
+
return h(
|
|
4105
|
+
Modal,
|
|
4106
|
+
{ title: t('resources.title'), onClose, wide: true },
|
|
4107
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('resources.hint')),
|
|
4108
|
+
h(
|
|
4109
|
+
'div',
|
|
4110
|
+
{ className: 'cait-briefform' },
|
|
4111
|
+
librarySection(
|
|
4112
|
+
'resources.tm',
|
|
4113
|
+
allTm,
|
|
4114
|
+
'tmId',
|
|
4115
|
+
checkedTmIds,
|
|
4116
|
+
inheritTm,
|
|
4117
|
+
setInheritTm,
|
|
4118
|
+
(id) => toggleId(setTmIds, id),
|
|
4119
|
+
'resources.inheritTm',
|
|
4120
|
+
),
|
|
4121
|
+
librarySection(
|
|
4122
|
+
'resources.glossary',
|
|
4123
|
+
allGlossary,
|
|
4124
|
+
'glossaryId',
|
|
4125
|
+
checkedGlossaryIds,
|
|
4126
|
+
inheritGlossary,
|
|
4127
|
+
setInheritGlossary,
|
|
4128
|
+
(id) => toggleId(setGlossaryIds, id),
|
|
4129
|
+
'resources.inheritGlossary',
|
|
4130
|
+
),
|
|
4131
|
+
h(
|
|
4132
|
+
'label',
|
|
4133
|
+
{ className: 'cait-field' },
|
|
4134
|
+
h('span', null, t('resources.defaultWrite')),
|
|
4135
|
+
h(
|
|
4136
|
+
'select',
|
|
4137
|
+
{
|
|
4138
|
+
className: 'cait-input',
|
|
4139
|
+
value: defaultTm,
|
|
4140
|
+
onChange: (event) => setDefaultWriteTmId(event.target.value),
|
|
4141
|
+
},
|
|
4142
|
+
h('option', { value: '' }, t(inheritTm ? 'resources.followDefault' : 'resources.firstBound')),
|
|
4143
|
+
tmChoices.map((library) => h('option', { key: library.tmId, value: library.tmId }, library.name)),
|
|
4144
|
+
),
|
|
4145
|
+
),
|
|
4146
|
+
),
|
|
4147
|
+
h(
|
|
4148
|
+
'div',
|
|
4149
|
+
{ className: 'cait-modal-actions' },
|
|
4150
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
4151
|
+
h(
|
|
4152
|
+
'button',
|
|
4153
|
+
{
|
|
4154
|
+
className: 'cait-btn cait-btn-primary',
|
|
4155
|
+
disabled: Boolean(busy),
|
|
4156
|
+
onClick: () =>
|
|
4157
|
+
onSave({
|
|
4158
|
+
tmIds: inheritTm ? null : Array.from(tmIds),
|
|
4159
|
+
glossaryIds: inheritGlossary ? null : Array.from(glossaryIds),
|
|
4160
|
+
defaultWriteTmId: defaultTm || null,
|
|
4161
|
+
}),
|
|
4162
|
+
},
|
|
4163
|
+
busy ? h(Spinner) : null,
|
|
4164
|
+
t('resources.save'),
|
|
4165
|
+
),
|
|
4166
|
+
),
|
|
4167
|
+
)
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
function AnalysisModal({ t, doc, analysis, busy, onClose }) {
|
|
4171
|
+
const download = (format) => {
|
|
4172
|
+
if (!analysis) return
|
|
4173
|
+
const pairs = [
|
|
4174
|
+
['segments', analysis.segments],
|
|
4175
|
+
['words', analysis.words],
|
|
4176
|
+
['characters', analysis.characters],
|
|
4177
|
+
['duplicates.groups', analysis.duplicates.groups],
|
|
4178
|
+
['duplicates.segments', analysis.duplicates.segments],
|
|
4179
|
+
['translated', analysis.translated],
|
|
4180
|
+
['confirmed', analysis.confirmed],
|
|
4181
|
+
['ice', analysis.buckets.ice],
|
|
4182
|
+
['exact', analysis.buckets.exact],
|
|
4183
|
+
['fuzzy85', analysis.buckets.fuzzy85],
|
|
4184
|
+
['fuzzy70', analysis.buckets.fuzzy70],
|
|
4185
|
+
['new', analysis.buckets.new],
|
|
4186
|
+
['matched.segments', analysis.matched.segments],
|
|
4187
|
+
['matched.percent', analysis.matched.percent],
|
|
4188
|
+
['leverage.percent', analysis.leverage.percent],
|
|
4189
|
+
['generatedAt', analysis.generatedAt],
|
|
4190
|
+
]
|
|
4191
|
+
const content =
|
|
4192
|
+
format === 'json'
|
|
4193
|
+
? JSON.stringify(analysis, null, 2)
|
|
4194
|
+
: ['metric,value', ...pairs.map(([key, value]) => `${key},${value}`)].join('\n')
|
|
4195
|
+
const type = format === 'json' ? 'application/json' : 'text/csv'
|
|
4196
|
+
const blob = new Blob([content], { type })
|
|
4197
|
+
const url = URL.createObjectURL(blob)
|
|
4198
|
+
const anchor = document.createElement('a')
|
|
4199
|
+
anchor.href = url
|
|
4200
|
+
anchor.download = `${analysis.name || 'document'}-analysis.${format}`
|
|
4201
|
+
anchor.click()
|
|
4202
|
+
URL.revokeObjectURL(url)
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
const rows = analysis
|
|
4206
|
+
? [
|
|
4207
|
+
['analysis.segments', analysis.segments],
|
|
4208
|
+
['analysis.words', analysis.words],
|
|
4209
|
+
['analysis.characters', analysis.characters],
|
|
4210
|
+
['analysis.duplicates', `${analysis.duplicates.segments} / ${analysis.duplicates.groups}`],
|
|
4211
|
+
['analysis.translated', analysis.translated],
|
|
4212
|
+
['analysis.confirmed', analysis.confirmed],
|
|
4213
|
+
]
|
|
4214
|
+
: []
|
|
4215
|
+
const buckets = analysis
|
|
4216
|
+
? [
|
|
4217
|
+
['analysis.ice', analysis.buckets.ice],
|
|
4218
|
+
['analysis.exact', analysis.buckets.exact],
|
|
4219
|
+
['analysis.fuzzy85', analysis.buckets.fuzzy85],
|
|
4220
|
+
['analysis.fuzzy70', analysis.buckets.fuzzy70],
|
|
4221
|
+
['analysis.new', analysis.buckets.new],
|
|
4222
|
+
]
|
|
4223
|
+
: []
|
|
4224
|
+
|
|
4225
|
+
return h(
|
|
4226
|
+
Modal,
|
|
4227
|
+
{ title: t('analysis.title'), onClose, wide: true },
|
|
4228
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('analysis.hint')),
|
|
4229
|
+
busy || !analysis
|
|
4230
|
+
? h('div', { className: 'cait-empty' }, h(Spinner))
|
|
4231
|
+
: h(
|
|
4232
|
+
'div',
|
|
4233
|
+
{ className: 'cait-analysis' },
|
|
4234
|
+
h(
|
|
4235
|
+
'div',
|
|
4236
|
+
{ className: 'cait-analysis-grid' },
|
|
4237
|
+
rows.map(([label, value]) =>
|
|
4238
|
+
h(
|
|
4239
|
+
'div',
|
|
4240
|
+
{ key: label, className: 'cait-analysis-stat' },
|
|
4241
|
+
h('span', null, t(label)),
|
|
4242
|
+
h('strong', null, String(value)),
|
|
4243
|
+
),
|
|
4244
|
+
),
|
|
4245
|
+
),
|
|
4246
|
+
h(
|
|
4247
|
+
'div',
|
|
4248
|
+
{ className: 'cait-analysis-grid' },
|
|
4249
|
+
buckets.map(([label, value]) =>
|
|
4250
|
+
h(
|
|
4251
|
+
'div',
|
|
4252
|
+
{ key: label, className: 'cait-analysis-stat' },
|
|
4253
|
+
h('span', null, t(label)),
|
|
4254
|
+
h('strong', null, String(value)),
|
|
4255
|
+
),
|
|
4256
|
+
),
|
|
4257
|
+
),
|
|
4258
|
+
h(
|
|
4259
|
+
'div',
|
|
4260
|
+
{ className: 'cait-analysis-total' },
|
|
4261
|
+
h('span', null, `${t('analysis.matchRate')} ${analysis.matched.percent}%`),
|
|
4262
|
+
h('span', null, `${t('analysis.leverage')} ${analysis.leverage.percent}%`),
|
|
4263
|
+
),
|
|
4264
|
+
),
|
|
4265
|
+
h(
|
|
4266
|
+
'div',
|
|
4267
|
+
{ className: 'cait-modal-actions' },
|
|
4268
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('analysis.close')),
|
|
4269
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: () => download('csv'), disabled: Boolean(busy) || !analysis }, t('analysis.exportCsv')),
|
|
4270
|
+
h('button', { className: 'cait-btn cait-btn-primary', onClick: () => download('json'), disabled: Boolean(busy) || !analysis }, t('analysis.exportJson')),
|
|
4271
|
+
),
|
|
4272
|
+
)
|
|
4273
|
+
}
|
|
4274
|
+
|
|
2991
4275
|
function Workbench({ i18n, translate }) {
|
|
2992
4276
|
const [data, setData] = useState(null)
|
|
2993
4277
|
const [loading, setLoading] = useState(true)
|
|
@@ -3011,6 +4295,18 @@
|
|
|
3011
4295
|
}
|
|
3012
4296
|
const [pre, setPre] = useState({ running: false, done: 0, total: 0, filled: 0, failed: 0 })
|
|
3013
4297
|
const [qa, setQa] = useState(null)
|
|
4298
|
+
const [review, setReview] = useState(null)
|
|
4299
|
+
const [reviewBusy, setReviewBusy] = useState(false)
|
|
4300
|
+
const [analysis, setAnalysis] = useState(null)
|
|
4301
|
+
const [analysisBusy, setAnalysisBusy] = useState(false)
|
|
4302
|
+
const [replacePreview, setReplacePreview] = useState(null)
|
|
4303
|
+
const [replaceUndo, setReplaceUndo] = useState(null)
|
|
4304
|
+
const [replaceBusy, setReplaceBusy] = useState(false)
|
|
4305
|
+
const [notes, setNotes] = useState(null)
|
|
4306
|
+
const [notesBusy, setNotesBusy] = useState(false)
|
|
4307
|
+
const [concordance, setConcordance] = useState(null)
|
|
4308
|
+
const [concordanceQuery, setConcordanceQuery] = useState('')
|
|
4309
|
+
const [concordanceBusy, setConcordanceBusy] = useState(false)
|
|
3014
4310
|
const [tmQuery, setTmQuery] = useState('')
|
|
3015
4311
|
const [tmResults, setTmResults] = useState(null)
|
|
3016
4312
|
const [tmRefresh, setTmRefresh] = useState(0)
|
|
@@ -3027,16 +4323,20 @@
|
|
|
3027
4323
|
const busyRef = useRef(false)
|
|
3028
4324
|
const rootRef = useRef(null)
|
|
3029
4325
|
const unitRef = useRef(0)
|
|
4326
|
+
const draftHistoryRef = useRef({ index: -1, stack: [], cursor: 0, lastAt: 0 })
|
|
3030
4327
|
|
|
3031
4328
|
const doc = data?.activeDocument || null
|
|
3032
4329
|
const segments = doc?.segments || []
|
|
3033
4330
|
const index = doc ? Math.min(selected, Math.max(0, segments.length - 1)) : 0
|
|
3034
4331
|
const segment = segments[index] || null
|
|
3035
4332
|
const detail = doc?.current?.index === index ? doc.current : null
|
|
4333
|
+
useEffect(() => {
|
|
4334
|
+
draftHistoryRef.current = { index, stack: [draft], cursor: 0, lastAt: 0 }
|
|
4335
|
+
}, [doc?.id, index])
|
|
3036
4336
|
const settings = data?.settings || {
|
|
3037
4337
|
sourceLang: 'en',
|
|
3038
4338
|
targetLang: 'zh-CN',
|
|
3039
|
-
fuzzyThreshold:
|
|
4339
|
+
fuzzyThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
3040
4340
|
tmLimit: 5,
|
|
3041
4341
|
uiLang: 'auto',
|
|
3042
4342
|
mt: { provider: 'mock', model: 'cait-mock', temperature: 0.2 },
|
|
@@ -3259,6 +4559,10 @@
|
|
|
3259
4559
|
}
|
|
3260
4560
|
}, [data?.activeDocument?.id, data?.revision])
|
|
3261
4561
|
|
|
4562
|
+
useEffect(() => {
|
|
4563
|
+
setReview(null)
|
|
4564
|
+
}, [data?.activeDocument?.id])
|
|
4565
|
+
|
|
3262
4566
|
useEffect(() => {
|
|
3263
4567
|
let alive = true
|
|
3264
4568
|
rpc('listMtRoutes')
|
|
@@ -3284,6 +4588,11 @@
|
|
|
3284
4588
|
setTmQuery('')
|
|
3285
4589
|
setTmResults(null)
|
|
3286
4590
|
setTmError('')
|
|
4591
|
+
setConcordance(null)
|
|
4592
|
+
setConcordanceQuery('')
|
|
4593
|
+
setReplacePreview(null)
|
|
4594
|
+
setReplaceUndo(null)
|
|
4595
|
+
setNotes(null)
|
|
3287
4596
|
}, [doc?.id])
|
|
3288
4597
|
|
|
3289
4598
|
useEffect(() => {
|
|
@@ -3321,7 +4630,8 @@
|
|
|
3321
4630
|
const saveDraft = async (silent = true) => {
|
|
3322
4631
|
if (!doc || !dirty) return true
|
|
3323
4632
|
try {
|
|
3324
|
-
await rpc('saveDraft', { documentId: doc.id, index: selected, target: draft })
|
|
4633
|
+
const result = await rpc('saveDraft', { documentId: doc.id, index: selected, target: draft })
|
|
4634
|
+
if (result.state) adoptState(result.state)
|
|
3325
4635
|
setDirty(false)
|
|
3326
4636
|
if (!silent) notify(t('toast.draftSaved'))
|
|
3327
4637
|
return true
|
|
@@ -3348,6 +4658,49 @@
|
|
|
3348
4658
|
}
|
|
3349
4659
|
}
|
|
3350
4660
|
|
|
4661
|
+
const changeDraft = (value) => {
|
|
4662
|
+
const history = draftHistoryRef.current
|
|
4663
|
+
if (history.index !== index) {
|
|
4664
|
+
history.index = index
|
|
4665
|
+
history.stack = [draft]
|
|
4666
|
+
history.cursor = 0
|
|
4667
|
+
}
|
|
4668
|
+
if (history.stack[history.cursor] !== value) {
|
|
4669
|
+
const now = Date.now()
|
|
4670
|
+
if (now - history.lastAt < 650 && history.cursor > 0) {
|
|
4671
|
+
history.stack[history.cursor] = value
|
|
4672
|
+
} else {
|
|
4673
|
+
history.stack = history.stack.slice(0, history.cursor + 1)
|
|
4674
|
+
history.stack.push(value)
|
|
4675
|
+
if (history.stack.length > 200) history.stack.shift()
|
|
4676
|
+
history.cursor = history.stack.length - 1
|
|
4677
|
+
}
|
|
4678
|
+
history.lastAt = now
|
|
4679
|
+
}
|
|
4680
|
+
setDraft(value)
|
|
4681
|
+
setDirty(true)
|
|
4682
|
+
}
|
|
4683
|
+
|
|
4684
|
+
const undoDraft = () => {
|
|
4685
|
+
const history = draftHistoryRef.current
|
|
4686
|
+
if (history.cursor <= 0) return
|
|
4687
|
+
history.cursor -= 1
|
|
4688
|
+
history.lastAt = 0
|
|
4689
|
+
const value = history.stack[history.cursor]
|
|
4690
|
+
setDraft(value)
|
|
4691
|
+
setDirty(value !== (segment?.target || ''))
|
|
4692
|
+
}
|
|
4693
|
+
|
|
4694
|
+
const redoDraft = () => {
|
|
4695
|
+
const history = draftHistoryRef.current
|
|
4696
|
+
if (history.cursor >= history.stack.length - 1) return
|
|
4697
|
+
history.cursor += 1
|
|
4698
|
+
history.lastAt = 0
|
|
4699
|
+
const value = history.stack[history.cursor]
|
|
4700
|
+
setDraft(value)
|
|
4701
|
+
setDirty(value !== (segment?.target || ''))
|
|
4702
|
+
}
|
|
4703
|
+
|
|
3351
4704
|
const confirmSegment = async () => {
|
|
3352
4705
|
if (!doc || busy) return
|
|
3353
4706
|
if (!draft.trim()) {
|
|
@@ -3502,6 +4855,10 @@
|
|
|
3502
4855
|
}
|
|
3503
4856
|
|
|
3504
4857
|
const applyTerm = (term) => {
|
|
4858
|
+
if (term?.forbidden) {
|
|
4859
|
+
notify(t('toast.termForbidden'), 'error')
|
|
4860
|
+
return
|
|
4861
|
+
}
|
|
3505
4862
|
const separator = /^(zh|ja|ko)/i.test(doc?.targetLang || '') ? '' : ' '
|
|
3506
4863
|
setDraft((current) => {
|
|
3507
4864
|
const value = current.replace(/\s+$/, '')
|
|
@@ -3511,6 +4868,326 @@
|
|
|
3511
4868
|
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
3512
4869
|
}
|
|
3513
4870
|
|
|
4871
|
+
const unconfirmCurrent = async () => {
|
|
4872
|
+
if (!doc || busy) return
|
|
4873
|
+
setBusy('unconfirm')
|
|
4874
|
+
try {
|
|
4875
|
+
if (dirty) {
|
|
4876
|
+
const saved = await saveDraft(true)
|
|
4877
|
+
if (!saved) return
|
|
4878
|
+
}
|
|
4879
|
+
const result = await rpc('unconfirmSegment', { documentId: doc.id, index: selected })
|
|
4880
|
+
adoptState(result.state)
|
|
4881
|
+
notify(t('toast.unconfirmed'))
|
|
4882
|
+
} catch (error) {
|
|
4883
|
+
// Saving a dirty confirmed segment already revokes the confirmation.
|
|
4884
|
+
// Treat the second call as idempotent instead of showing an error.
|
|
4885
|
+
if (error?.code === 'NOT_CONFIRMED') notify(t('toast.unconfirmed'))
|
|
4886
|
+
else report(error)
|
|
4887
|
+
} finally {
|
|
4888
|
+
setBusy('')
|
|
4889
|
+
}
|
|
4890
|
+
}
|
|
4891
|
+
|
|
4892
|
+
const toggleLock = async () => {
|
|
4893
|
+
if (!doc || busy || !segment) return
|
|
4894
|
+
setBusy('lock')
|
|
4895
|
+
try {
|
|
4896
|
+
await rpc('setSegmentLocked', { documentId: doc.id, index: selected, locked: !segment.locked })
|
|
4897
|
+
const next = await rpc('bootstrap')
|
|
4898
|
+
adoptState(next, selected)
|
|
4899
|
+
notify(t(segment.locked ? 'toast.unlocked' : 'toast.locked'))
|
|
4900
|
+
} catch (error) {
|
|
4901
|
+
report(error)
|
|
4902
|
+
} finally {
|
|
4903
|
+
setBusy('')
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4907
|
+
const mergeCurrent = () => {
|
|
4908
|
+
if (!doc || !segment || busy) return
|
|
4909
|
+
openConfirm({
|
|
4910
|
+
title: t('modal.confirm.mergeTitle'),
|
|
4911
|
+
message: t('modal.confirm.merge'),
|
|
4912
|
+
confirmLabel: t('editor.merge'),
|
|
4913
|
+
action: async () => {
|
|
4914
|
+
const result = await rpc('mergeSegments', { documentId: doc.id, index: selected, count: 2 })
|
|
4915
|
+
adoptState(result.state, result.index)
|
|
4916
|
+
notify(t('toast.merged'))
|
|
4917
|
+
},
|
|
4918
|
+
})
|
|
4919
|
+
}
|
|
4920
|
+
|
|
4921
|
+
const splitCurrent = (at) => {
|
|
4922
|
+
if (!doc || !segment || busy) return
|
|
4923
|
+
openConfirm({
|
|
4924
|
+
title: t('modal.confirm.splitTitle'),
|
|
4925
|
+
message: t('modal.confirm.split'),
|
|
4926
|
+
confirmLabel: t('editor.split'),
|
|
4927
|
+
action: async () => {
|
|
4928
|
+
const result = await rpc('splitSegment', { documentId: doc.id, index: selected, at })
|
|
4929
|
+
adoptState(result.state, result.index)
|
|
4930
|
+
notify(t('toast.split'))
|
|
4931
|
+
},
|
|
4932
|
+
})
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
const saveBrief = async (brief) => {
|
|
4936
|
+
if (!doc || busy) return false
|
|
4937
|
+
setBusy('brief')
|
|
4938
|
+
try {
|
|
4939
|
+
const result = await rpc('setBrief', { documentId: doc.id, brief })
|
|
4940
|
+
adoptState(result.state)
|
|
4941
|
+
setModal(null)
|
|
4942
|
+
notify(t('toast.briefSaved'))
|
|
4943
|
+
return true
|
|
4944
|
+
} catch (error) {
|
|
4945
|
+
report(error)
|
|
4946
|
+
return false
|
|
4947
|
+
} finally {
|
|
4948
|
+
setBusy('')
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
const saveResources = async (resources) => {
|
|
4953
|
+
if (!doc || busy) return false
|
|
4954
|
+
setBusy('resources')
|
|
4955
|
+
try {
|
|
4956
|
+
const result = await rpc('setDocumentResources', { documentId: doc.id, ...resources })
|
|
4957
|
+
adoptState(result.state)
|
|
4958
|
+
setModal(null)
|
|
4959
|
+
notify(t('toast.resourcesSaved'))
|
|
4960
|
+
return true
|
|
4961
|
+
} catch (error) {
|
|
4962
|
+
report(error)
|
|
4963
|
+
return false
|
|
4964
|
+
} finally {
|
|
4965
|
+
setBusy('')
|
|
4966
|
+
}
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
const openAnalysis = async () => {
|
|
4970
|
+
if (!doc) return
|
|
4971
|
+
setAnalysis(null)
|
|
4972
|
+
setModal({ type: 'analysis' })
|
|
4973
|
+
setAnalysisBusy(true)
|
|
4974
|
+
try {
|
|
4975
|
+
setAnalysis(await rpc('analyzeDocument', { documentId: doc.id }))
|
|
4976
|
+
} catch (error) {
|
|
4977
|
+
report(error)
|
|
4978
|
+
} finally {
|
|
4979
|
+
setAnalysisBusy(false)
|
|
4980
|
+
}
|
|
4981
|
+
}
|
|
4982
|
+
|
|
4983
|
+
const openReplace = () => {
|
|
4984
|
+
if (!doc) return
|
|
4985
|
+
setReplacePreview(null)
|
|
4986
|
+
setModal({ type: 'replace' })
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
const previewReplace = async (form) => {
|
|
4990
|
+
if (!doc || replaceBusy) return
|
|
4991
|
+
setReplaceBusy(true)
|
|
4992
|
+
try {
|
|
4993
|
+
setReplacePreview(await rpc('replaceInDocument', { documentId: doc.id, ...form }))
|
|
4994
|
+
} catch (error) {
|
|
4995
|
+
report(error)
|
|
4996
|
+
} finally {
|
|
4997
|
+
setReplaceBusy(false)
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
const applyReplace = async (form) => {
|
|
5002
|
+
if (!doc || replaceBusy) return
|
|
5003
|
+
setReplaceBusy(true)
|
|
5004
|
+
try {
|
|
5005
|
+
const result = await rpc('replaceInDocument', { documentId: doc.id, ...form, apply: true })
|
|
5006
|
+
setReplaceUndo(Array.isArray(result.undo) && result.undo.length ? result.undo : null)
|
|
5007
|
+
setReplacePreview(null)
|
|
5008
|
+
adoptState(result.state, result.undo?.[0]?.index)
|
|
5009
|
+
notify(t('toast.replaced').replace('{count}', String(result.applied ?? 0)))
|
|
5010
|
+
} catch (error) {
|
|
5011
|
+
report(error)
|
|
5012
|
+
} finally {
|
|
5013
|
+
setReplaceBusy(false)
|
|
5014
|
+
}
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
const undoReplace = async () => {
|
|
5018
|
+
if (!doc || !replaceUndo || replaceBusy) return
|
|
5019
|
+
setReplaceBusy(true)
|
|
5020
|
+
try {
|
|
5021
|
+
const result = await rpc('undoReplace', { documentId: doc.id, undo: replaceUndo })
|
|
5022
|
+
adoptState(result.state, replaceUndo[0]?.index)
|
|
5023
|
+
setReplaceUndo(null)
|
|
5024
|
+
setReplacePreview(null)
|
|
5025
|
+
notify(t('toast.replaceUndone'))
|
|
5026
|
+
} catch (error) {
|
|
5027
|
+
report(error)
|
|
5028
|
+
} finally {
|
|
5029
|
+
setReplaceBusy(false)
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
|
|
5033
|
+
const openNotes = async () => {
|
|
5034
|
+
if (!doc) return
|
|
5035
|
+
setNotes(null)
|
|
5036
|
+
setNotesBusy(true)
|
|
5037
|
+
setModal({ type: 'notes' })
|
|
5038
|
+
try {
|
|
5039
|
+
const result = await rpc('notesForSegment', { documentId: doc.id, index })
|
|
5040
|
+
setNotes(result.notes)
|
|
5041
|
+
} catch (error) {
|
|
5042
|
+
report(error)
|
|
5043
|
+
} finally {
|
|
5044
|
+
setNotesBusy(false)
|
|
5045
|
+
}
|
|
5046
|
+
}
|
|
5047
|
+
|
|
5048
|
+
const addNote = async (text) => {
|
|
5049
|
+
if (!doc || notesBusy) return
|
|
5050
|
+
setNotesBusy(true)
|
|
5051
|
+
try {
|
|
5052
|
+
const result = await rpc('addSegmentNote', { documentId: doc.id, index, text })
|
|
5053
|
+
setNotes(result.notes)
|
|
5054
|
+
adoptState(result.state, index)
|
|
5055
|
+
notify(t('toast.noteSaved'))
|
|
5056
|
+
} catch (error) {
|
|
5057
|
+
report(error)
|
|
5058
|
+
} finally {
|
|
5059
|
+
setNotesBusy(false)
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
|
|
5063
|
+
const updateNote = async (noteId, text) => {
|
|
5064
|
+
if (!doc || notesBusy) return
|
|
5065
|
+
setNotesBusy(true)
|
|
5066
|
+
try {
|
|
5067
|
+
const result = await rpc('updateSegmentNote', { documentId: doc.id, index, noteId, text })
|
|
5068
|
+
setNotes(result.notes)
|
|
5069
|
+
adoptState(result.state, index)
|
|
5070
|
+
notify(t('toast.noteSaved'))
|
|
5071
|
+
} catch (error) {
|
|
5072
|
+
report(error)
|
|
5073
|
+
} finally {
|
|
5074
|
+
setNotesBusy(false)
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
const deleteNote = async (noteId) => {
|
|
5079
|
+
if (!doc || notesBusy) return
|
|
5080
|
+
setNotesBusy(true)
|
|
5081
|
+
try {
|
|
5082
|
+
const result = await rpc('deleteSegmentNote', { documentId: doc.id, index, noteId })
|
|
5083
|
+
setNotes(result.notes)
|
|
5084
|
+
adoptState(result.state, index)
|
|
5085
|
+
notify(t('toast.noteDeleted'))
|
|
5086
|
+
} catch (error) {
|
|
5087
|
+
report(error)
|
|
5088
|
+
} finally {
|
|
5089
|
+
setNotesBusy(false)
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
|
|
5093
|
+
const refreshQa = async () => { if (!doc) return
|
|
5094
|
+
setQa(null)
|
|
5095
|
+
try {
|
|
5096
|
+
setQa(await rpc('getQaReport', { documentId: doc.id }))
|
|
5097
|
+
} catch {
|
|
5098
|
+
// QA is advisory; a failure must not disturb the editor.
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
|
|
5102
|
+
const jumpQa = (nextIndex) => {
|
|
5103
|
+
if (Number.isInteger(nextIndex)) chooseSegment(nextIndex)
|
|
5104
|
+
}
|
|
5105
|
+
|
|
5106
|
+
const runReview = async () => {
|
|
5107
|
+
if (!doc || reviewBusy) return
|
|
5108
|
+
setReviewBusy(true)
|
|
5109
|
+
try {
|
|
5110
|
+
const result = await rpc('reviewDocument', {
|
|
5111
|
+
documentId: doc.id,
|
|
5112
|
+
from: 0,
|
|
5113
|
+
count: 20,
|
|
5114
|
+
})
|
|
5115
|
+
setReview(result || { issues: [] })
|
|
5116
|
+
} catch (error) {
|
|
5117
|
+
setReview(null)
|
|
5118
|
+
report(error)
|
|
5119
|
+
} finally {
|
|
5120
|
+
setReviewBusy(false)
|
|
5121
|
+
}
|
|
5122
|
+
}
|
|
5123
|
+
|
|
5124
|
+
const applyReview = (issue) => {
|
|
5125
|
+
if (!doc || !issue || !Number.isInteger(issue.index)) return
|
|
5126
|
+
chooseSegment(issue.index)
|
|
5127
|
+
if (issue.suggestion) {
|
|
5128
|
+
setDraft(issue.suggestion)
|
|
5129
|
+
setDirty(true)
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
|
|
5133
|
+
const applyQaFix = async (item, action) => {
|
|
5134
|
+
if (!doc || busy || !item) return
|
|
5135
|
+
const segment = doc.segments[item.index]
|
|
5136
|
+
if (!segment) return
|
|
5137
|
+
let target = segment.target || ''
|
|
5138
|
+
if (action === 'copy-source') {
|
|
5139
|
+
target = segment.source
|
|
5140
|
+
} else if (action === 'clear') {
|
|
5141
|
+
target = ''
|
|
5142
|
+
} else if (action === 'term') {
|
|
5143
|
+
const term = String(item.termTarget || '')
|
|
5144
|
+
if (!term || target.includes(term)) return
|
|
5145
|
+
const separator = /[A-Za-z0-9]$/.test(target) && /^[A-Za-z0-9]/.test(term) ? ' ' : ''
|
|
5146
|
+
target = `${target}${separator}${term}`
|
|
5147
|
+
} else {
|
|
5148
|
+
return
|
|
5149
|
+
}
|
|
5150
|
+
setBusy('qa-fix')
|
|
5151
|
+
try {
|
|
5152
|
+
const result = await rpc('saveDraft', {
|
|
5153
|
+
documentId: doc.id,
|
|
5154
|
+
index: item.index,
|
|
5155
|
+
target,
|
|
5156
|
+
})
|
|
5157
|
+
if (result.state) adoptState(result.state, item.index)
|
|
5158
|
+
setQa(await rpc('getQaReport', { documentId: doc.id }))
|
|
5159
|
+
notify(t('toast.qaFixed'))
|
|
5160
|
+
} catch (error) {
|
|
5161
|
+
report(error)
|
|
5162
|
+
} finally {
|
|
5163
|
+
setBusy('')
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
|
|
5167
|
+
const runConcordance = async () => {
|
|
5168
|
+
const query = concordanceQuery.trim()
|
|
5169
|
+
if (!doc || !query) {
|
|
5170
|
+
setConcordance(null)
|
|
5171
|
+
return
|
|
5172
|
+
}
|
|
5173
|
+
setConcordanceBusy(true)
|
|
5174
|
+
try {
|
|
5175
|
+
setConcordance(await rpc('concordance', { documentId: doc.id, query, limit: 50 }))
|
|
5176
|
+
} catch (error) {
|
|
5177
|
+
report(error)
|
|
5178
|
+
} finally {
|
|
5179
|
+
setConcordanceBusy(false)
|
|
5180
|
+
}
|
|
5181
|
+
}
|
|
5182
|
+
|
|
5183
|
+
const applyConcordance = (entry) => {
|
|
5184
|
+
if (!entry?.target) return
|
|
5185
|
+
setDraft(entry.target)
|
|
5186
|
+
setDirty(true)
|
|
5187
|
+
setMt(null)
|
|
5188
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
5189
|
+
}
|
|
5190
|
+
|
|
3514
5191
|
const editTm = async ({ id, target }) => {
|
|
3515
5192
|
setBusy('tm-edit')
|
|
3516
5193
|
try {
|
|
@@ -3576,6 +5253,40 @@
|
|
|
3576
5253
|
})
|
|
3577
5254
|
}
|
|
3578
5255
|
|
|
5256
|
+
const clearPretranslated = async () => {
|
|
5257
|
+
const active = data?.activeDocument
|
|
5258
|
+
if (!active) return
|
|
5259
|
+
try {
|
|
5260
|
+
const preview = await rpc('getClearUnconfirmedPreview', { documentId: active.id })
|
|
5261
|
+
const counts = preview?.counts || {}
|
|
5262
|
+
const total = counts.total || 0
|
|
5263
|
+
if (!total) {
|
|
5264
|
+
notify(t('toast.nothingToClear'), 'info')
|
|
5265
|
+
return
|
|
5266
|
+
}
|
|
5267
|
+
setModal({
|
|
5268
|
+
type: 'confirm',
|
|
5269
|
+
title: t('modal.confirm.clearPretranslatedTitle'),
|
|
5270
|
+
message: t('modal.confirm.clearPretranslatedMessage')
|
|
5271
|
+
.replace('{count}', String(total))
|
|
5272
|
+
.replace('{mt}', String(counts.mt || 0))
|
|
5273
|
+
.replace('{tm}', String(counts.tm || 0))
|
|
5274
|
+
.replace('{auto}', String(counts.auto || 0)),
|
|
5275
|
+
confirmLabel: t('modal.confirm.clearPretranslatedConfirm').replace('{count}', String(total)),
|
|
5276
|
+
action: async () => {
|
|
5277
|
+
const result = await rpc('clearUnconfirmedTranslations', {
|
|
5278
|
+
documentId: active.id,
|
|
5279
|
+
confirm: true,
|
|
5280
|
+
})
|
|
5281
|
+
adoptState(result.state, result.cursor)
|
|
5282
|
+
notify(t('toast.pretranslatedCleared').replace('{count}', String(result.cleared)))
|
|
5283
|
+
},
|
|
5284
|
+
})
|
|
5285
|
+
} catch (error) {
|
|
5286
|
+
report(error)
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
|
|
3579
5290
|
const addTerm = async (form) => { setBusy('term')
|
|
3580
5291
|
try {
|
|
3581
5292
|
const result = await rpc('addGlossary', {
|
|
@@ -3765,6 +5476,32 @@
|
|
|
3765
5476
|
}
|
|
3766
5477
|
}
|
|
3767
5478
|
|
|
5479
|
+
const importReview = (file) => {
|
|
5480
|
+
if (!doc || !file) return
|
|
5481
|
+
const reader = new FileReader()
|
|
5482
|
+
reader.onload = async () => {
|
|
5483
|
+
setBusy('review')
|
|
5484
|
+
try {
|
|
5485
|
+
const result = await rpc('applyXliffReview', {
|
|
5486
|
+
documentId: doc.id,
|
|
5487
|
+
text: String(reader.result || ''),
|
|
5488
|
+
})
|
|
5489
|
+
adoptState(result.state)
|
|
5490
|
+
notify(
|
|
5491
|
+
t('toast.reviewApplied')
|
|
5492
|
+
.replace('{applied}', String(result.applied))
|
|
5493
|
+
.replace('{unmatched}', String(result.unmatched + result.sourceMismatch)),
|
|
5494
|
+
)
|
|
5495
|
+
setModal(null)
|
|
5496
|
+
} catch (error) {
|
|
5497
|
+
report(error)
|
|
5498
|
+
} finally {
|
|
5499
|
+
setBusy('')
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
reader.readAsText(file, 'utf-8')
|
|
5503
|
+
}
|
|
5504
|
+
|
|
3768
5505
|
const openConfirm = (state) => setModal({ type: 'confirm', ...state })
|
|
3769
5506
|
|
|
3770
5507
|
const clearTm = () =>
|
|
@@ -3819,6 +5556,7 @@
|
|
|
3819
5556
|
|
|
3820
5557
|
const openIds = new Set((data?.openDocuments || []).map((item) => item.id))
|
|
3821
5558
|
const closedDocuments = (data?.documents || []).filter((item) => !openIds.has(item.id))
|
|
5559
|
+
const qaRisk = (qa?.risks || []).find((item) => item.index === index) || null
|
|
3822
5560
|
|
|
3823
5561
|
const deleteClosedDocument = (document) =>
|
|
3824
5562
|
openConfirm({
|
|
@@ -3898,6 +5636,23 @@
|
|
|
3898
5636
|
}
|
|
3899
5637
|
}, [])
|
|
3900
5638
|
|
|
5639
|
+
useEffect(() => {
|
|
5640
|
+
const onKey = (event) => {
|
|
5641
|
+
const mod = event.metaKey || event.ctrlKey
|
|
5642
|
+
if (!mod) return
|
|
5643
|
+
const key = event.key.toLowerCase()
|
|
5644
|
+
if (key === 'k') {
|
|
5645
|
+
event.preventDefault()
|
|
5646
|
+
setModal((current) => (current?.type === 'palette' ? null : { type: 'palette' }))
|
|
5647
|
+
} else if (key === 'f' && doc && !modal) {
|
|
5648
|
+
event.preventDefault()
|
|
5649
|
+
openReplace()
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
window.addEventListener('keydown', onKey)
|
|
5653
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
5654
|
+
}, [doc?.id, modal])
|
|
5655
|
+
|
|
3901
5656
|
if (loading) {
|
|
3902
5657
|
return h(
|
|
3903
5658
|
'div',
|
|
@@ -3925,6 +5680,44 @@
|
|
|
3925
5680
|
)
|
|
3926
5681
|
}
|
|
3927
5682
|
|
|
5683
|
+
const commands = [
|
|
5684
|
+
{ id: 'new', title: t('cmd.new'), hint: t('cmd.newHint'), run: () => setModal({ type: 'new', defaultMode: 'text' }) },
|
|
5685
|
+
{ id: 'import', title: t('cmd.import'), hint: '', run: () => setModal({ type: 'new', defaultMode: 'file' }) },
|
|
5686
|
+
{ id: 'export', title: t('cmd.export'), hint: '', docOnly: true, run: () => setModal({ type: 'export' }) },
|
|
5687
|
+
{ id: 'pretranslate', title: t('cmd.pretranslate'), hint: '', docOnly: true, run: () => runPreTranslate() },
|
|
5688
|
+
{ id: 'replace', title: t('cmd.replace'), hint: 'Ctrl+F', docOnly: true, run: () => openReplace() },
|
|
5689
|
+
{ id: 'brief', title: t('cmd.brief'), hint: '', docOnly: true, run: () => setModal({ type: 'brief' }) },
|
|
5690
|
+
{ id: 'resources', title: t('cmd.resources'), hint: '', docOnly: true, run: () => setModal({ type: 'resources' }) },
|
|
5691
|
+
{ id: 'analysis', title: t('cmd.analysis'), hint: '', docOnly: true, run: () => openAnalysis() },
|
|
5692
|
+
{ id: 'notes', title: t('cmd.notes'), hint: '', docOnly: true, run: () => openNotes() },
|
|
5693
|
+
{
|
|
5694
|
+
id: 'review',
|
|
5695
|
+
title: t('cmd.review'),
|
|
5696
|
+
hint: '',
|
|
5697
|
+
docOnly: true,
|
|
5698
|
+
run: () => {
|
|
5699
|
+
setTab('qa')
|
|
5700
|
+
runReview()
|
|
5701
|
+
},
|
|
5702
|
+
},
|
|
5703
|
+
{
|
|
5704
|
+
id: 'clear-pretranslated',
|
|
5705
|
+
title: t('cmd.clearPretranslated'),
|
|
5706
|
+
hint: '',
|
|
5707
|
+
docOnly: true,
|
|
5708
|
+
run: () => clearPretranslated(),
|
|
5709
|
+
},
|
|
5710
|
+
{ id: 'documents', title: t('cmd.documents'), hint: '', run: () => setView('documents') },
|
|
5711
|
+
{ id: 'libraries', title: t('cmd.libraries'), hint: '', run: () => setView('libraries') },
|
|
5712
|
+
{ id: 'align', title: t('cmd.align'), hint: '', run: () => setView('align') },
|
|
5713
|
+
{ id: 'settings', title: t('cmd.settings'), hint: '', run: () => setModal({ type: 'settings' }) },
|
|
5714
|
+
].filter((item) => !item.docOnly || doc)
|
|
5715
|
+
|
|
5716
|
+
const runCommand = (item) => {
|
|
5717
|
+
setModal(null)
|
|
5718
|
+
setTimeout(() => item.run(), 0)
|
|
5719
|
+
}
|
|
5720
|
+
|
|
3928
5721
|
return h(
|
|
3929
5722
|
'div',
|
|
3930
5723
|
{ className: 'cait-root', ref: rootRef },
|
|
@@ -3945,6 +5738,7 @@
|
|
|
3945
5738
|
pre,
|
|
3946
5739
|
onPreTranslate: runPreTranslate,
|
|
3947
5740
|
onCancelPreTranslate: cancelPreTranslate,
|
|
5741
|
+
onReplace: openReplace,
|
|
3948
5742
|
view,
|
|
3949
5743
|
onView: setView,
|
|
3950
5744
|
}),
|
|
@@ -3963,6 +5757,7 @@
|
|
|
3963
5757
|
onCreateLibrary: createLibrary,
|
|
3964
5758
|
onRenameLibrary: renameLibrary,
|
|
3965
5759
|
onDeleteLibrary: deleteLibrary,
|
|
5760
|
+
onSetMounts: setMounts,
|
|
3966
5761
|
})
|
|
3967
5762
|
: null,
|
|
3968
5763
|
view === 'align'
|
|
@@ -3980,7 +5775,7 @@
|
|
|
3980
5775
|
? h(
|
|
3981
5776
|
'div',
|
|
3982
5777
|
{ className: `cait-body${doc ? '' : ' cait-no-doc'}` },
|
|
3983
|
-
doc ? h(SegmentRail, { t, doc, selected: index, query: railQuery, setQuery: setRailQuery, filter: railFilter, setFilter: setRailFilter, onSelect: chooseSegment, onConfirmAll: confirmAllTranslated, onPropagate: propagateRepeats, busy, qa }) : null,
|
|
5778
|
+
doc ? h(SegmentRail, { t, doc, selected: index, query: railQuery, setQuery: setRailQuery, filter: railFilter, setFilter: setRailFilter, onSelect: chooseSegment, onConfirmAll: confirmAllTranslated, onPropagate: propagateRepeats, onClearPretranslated: clearPretranslated, busy, qa }) : null,
|
|
3984
5779
|
h(
|
|
3985
5780
|
'div',
|
|
3986
5781
|
{ className: `cait-main${doc ? '' : ' cait-main-empty'}` },
|
|
@@ -3992,18 +5787,29 @@
|
|
|
3992
5787
|
segment,
|
|
3993
5788
|
detail,
|
|
3994
5789
|
draft,
|
|
3995
|
-
setDraft:
|
|
3996
|
-
setDraft(value)
|
|
3997
|
-
setDirty(true)
|
|
3998
|
-
},
|
|
5790
|
+
setDraft: changeDraft,
|
|
3999
5791
|
dirty,
|
|
4000
5792
|
busy,
|
|
5793
|
+
canUndo: draftHistoryRef.current.cursor > 0,
|
|
5794
|
+
canRedo: draftHistoryRef.current.cursor < draftHistoryRef.current.stack.length - 1,
|
|
5795
|
+
onUndo: undoDraft,
|
|
5796
|
+
onRedo: redoDraft,
|
|
4001
5797
|
onConfirm: confirmSegment,
|
|
5798
|
+
onUnconfirm: unconfirmCurrent,
|
|
5799
|
+
onToggleLock: toggleLock,
|
|
5800
|
+
onMerge: mergeCurrent,
|
|
5801
|
+
onSplit: splitCurrent,
|
|
4002
5802
|
onNext: () => moveSegment(1),
|
|
4003
5803
|
onPrev: () => moveSegment(-1),
|
|
4004
5804
|
onSave: () => saveDraft(false),
|
|
4005
5805
|
onAdopt: adoptSuggestion,
|
|
4006
5806
|
onApplyTerm: applyTerm,
|
|
5807
|
+
onOpenBrief: () => setModal({ type: 'brief' }),
|
|
5808
|
+
onOpenResources: () => setModal({ type: 'resources' }),
|
|
5809
|
+
onOpenAnalysis: openAnalysis,
|
|
5810
|
+
onOpenNotes: openNotes,
|
|
5811
|
+
noteCount: segment?.notes?.length || 0,
|
|
5812
|
+
qaRisk,
|
|
4007
5813
|
textareaRef,
|
|
4008
5814
|
})
|
|
4009
5815
|
: h(EmptyState, { t, settings, busy, onCreate: createFromForm, onDemo: loadDemo }),
|
|
@@ -4044,7 +5850,20 @@
|
|
|
4044
5850
|
onCopy: copy,
|
|
4045
5851
|
onOpenSettings: () => setModal({ type: 'settings' }),
|
|
4046
5852
|
onSearchLibrary: openLibrary,
|
|
4047
|
-
|
|
5853
|
+
qa,
|
|
5854
|
+
onRefreshQa: refreshQa,
|
|
5855
|
+
onJumpQa: jumpQa,
|
|
5856
|
+
onFixQa: applyQaFix,
|
|
5857
|
+
review,
|
|
5858
|
+
reviewBusy,
|
|
5859
|
+
onReview: runReview,
|
|
5860
|
+
onApplyReview: applyReview,
|
|
5861
|
+
concordance,
|
|
5862
|
+
concordanceQuery,
|
|
5863
|
+
setConcordanceQuery,
|
|
5864
|
+
onConcordance: runConcordance,
|
|
5865
|
+
onApplyConcordance: applyConcordance,
|
|
5866
|
+
busy: busy || (concordanceBusy ? 'concordance' : ''),
|
|
4048
5867
|
})
|
|
4049
5868
|
: null,
|
|
4050
5869
|
),
|
|
@@ -4061,7 +5880,60 @@
|
|
|
4061
5880
|
})
|
|
4062
5881
|
: null,
|
|
4063
5882
|
modal?.type === 'export'
|
|
4064
|
-
? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs })
|
|
5883
|
+
? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs, onImportReview: importReview })
|
|
5884
|
+
: null,
|
|
5885
|
+
modal?.type === 'brief'
|
|
5886
|
+
? h(BriefModal, { t, doc, busy, onClose: () => setModal(null), onSave: saveBrief })
|
|
5887
|
+
: null,
|
|
5888
|
+
modal?.type === 'resources'
|
|
5889
|
+
? h(ResourceModal, {
|
|
5890
|
+
t,
|
|
5891
|
+
doc,
|
|
5892
|
+
libraries: data?.libraries,
|
|
5893
|
+
mounts: data?.mounts,
|
|
5894
|
+
busy,
|
|
5895
|
+
onClose: () => setModal(null),
|
|
5896
|
+
onSave: saveResources,
|
|
5897
|
+
})
|
|
5898
|
+
: null,
|
|
5899
|
+
modal?.type === 'analysis'
|
|
5900
|
+
? h(AnalysisModal, {
|
|
5901
|
+
t,
|
|
5902
|
+
doc,
|
|
5903
|
+
analysis,
|
|
5904
|
+
busy: analysisBusy,
|
|
5905
|
+
onClose: () => setModal(null),
|
|
5906
|
+
})
|
|
5907
|
+
: null,
|
|
5908
|
+
modal?.type === 'replace'
|
|
5909
|
+
? h(ReplaceModal, {
|
|
5910
|
+
t,
|
|
5911
|
+
doc,
|
|
5912
|
+
busy: replaceBusy,
|
|
5913
|
+
preview: replacePreview,
|
|
5914
|
+
undoCount: replaceUndo ? replaceUndo.length : 0,
|
|
5915
|
+
onPreview: previewReplace,
|
|
5916
|
+
onApply: applyReplace,
|
|
5917
|
+
onUndo: undoReplace,
|
|
5918
|
+
onClose: () => setModal(null),
|
|
5919
|
+
})
|
|
5920
|
+
: null,
|
|
5921
|
+
modal?.type === 'notes'
|
|
5922
|
+
? h(NoteModal, {
|
|
5923
|
+
t,
|
|
5924
|
+
doc,
|
|
5925
|
+
index,
|
|
5926
|
+
segment,
|
|
5927
|
+
notes,
|
|
5928
|
+
busy: notesBusy,
|
|
5929
|
+
onAdd: addNote,
|
|
5930
|
+
onUpdate: updateNote,
|
|
5931
|
+
onDelete: deleteNote,
|
|
5932
|
+
onClose: () => setModal(null),
|
|
5933
|
+
})
|
|
5934
|
+
: null,
|
|
5935
|
+
modal?.type === 'palette'
|
|
5936
|
+
? h(CommandPalette, { t, items: commands, onClose: () => setModal(null), onRun: runCommand })
|
|
4065
5937
|
: null,
|
|
4066
5938
|
modal?.type === 'settings'
|
|
4067
5939
|
? h(SettingsModal, { t, settings, mtRoutes, busy, onClose: () => setModal(null), onSave: saveSettings })
|