dsh-cait 1.1.3 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/README.ja.md +51 -14
- package/README.md +137 -30
- package/README.zh-CN.md +51 -14
- package/client/components.js +2033 -48
- package/client/entry.js +2 -0
- package/client/helpers.js +12 -0
- package/client/locales.js +658 -8
- package/client/styles.js +111 -4
- package/client.js +2814 -58
- package/lib/aligner.js +6 -6
- package/lib/analysis.js +88 -0
- package/lib/api.js +46 -0
- package/lib/brief.js +275 -0
- package/lib/docx.js +155 -26
- package/lib/formats.js +37 -0
- package/lib/glossary.js +20 -4
- package/lib/import-export.js +212 -13
- package/lib/libraries.js +156 -0
- package/lib/library-export.js +7 -1
- package/lib/localization.js +1098 -0
- package/lib/lock.js +30 -0
- package/lib/notes.js +144 -0
- package/lib/persistence.js +54 -7
- package/lib/pptx.js +33 -1
- package/lib/pre-translate.js +62 -45
- package/lib/propagate.js +3 -0
- package/lib/qa.js +94 -4
- package/lib/replace.js +243 -0
- package/lib/review.js +131 -0
- package/lib/segment-edit.js +199 -0
- package/lib/store-utils.js +33 -6
- package/lib/store.js +507 -47
- package/lib/subtitles.js +213 -0
- package/lib/term-extract.js +105 -3
- package/lib/terms.js +4 -2
- package/lib/tm-maintenance.js +157 -0
- package/lib/tm.js +4 -1
- package/lib/tools.js +294 -0
- package/lib/xlsx.js +176 -66
- package/package.json +1 -1
package/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,9 +761,21 @@
|
|
|
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, onMergeCue, onSplitCue, onShiftSubtitles, 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
|
|
767
|
+
const isSubtitle = doc.format === 'srt' || doc.format === 'vtt'
|
|
768
|
+
const [shiftSeconds, setShiftSeconds] = useState('1')
|
|
769
|
+
const cueStart = Number(segment.meta?.start)
|
|
770
|
+
const cueEnd = Number(segment.meta?.end)
|
|
771
|
+
const formatCue = (value) => {
|
|
772
|
+
if (!Number.isFinite(value)) return '--:--.-'
|
|
773
|
+
const total = Math.max(0, Math.round(value))
|
|
774
|
+
const minutes = Math.floor(total / 60000)
|
|
775
|
+
const seconds = Math.floor((total % 60000) / 1000)
|
|
776
|
+
const millis = total % 1000
|
|
777
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(millis).padStart(3, '0')}`
|
|
778
|
+
}
|
|
726
779
|
const progress = doc.progress
|
|
727
780
|
const remaining = progress.total - progress.confirmed
|
|
728
781
|
const statusLabel =
|
|
@@ -737,9 +790,31 @@
|
|
|
737
790
|
// against. It is stored in `suggestion`, which adoption already preserves.
|
|
738
791
|
const baselineText = segment.suggestion || ''
|
|
739
792
|
const baselineOrigin = segment.origin || 'agent'
|
|
793
|
+
const canMerge = !segment.locked && index < doc.segments.length - 1
|
|
794
|
+
const canSplit = !segment.locked && String(segment.source || '').length > 1
|
|
795
|
+
const splitAtCursor = () => {
|
|
796
|
+
const sourceLength = String(segment.source || '').length
|
|
797
|
+
const targetLength = String(draft || '').length
|
|
798
|
+
const cursor = textareaRef?.current?.selectionStart
|
|
799
|
+
const ratio = targetLength > 0 && Number.isInteger(cursor) ? cursor / targetLength : 0.5
|
|
800
|
+
const at = Math.max(1, Math.min(sourceLength - 1, Math.round(sourceLength * ratio)))
|
|
801
|
+
onSplit(at)
|
|
802
|
+
}
|
|
740
803
|
|
|
741
|
-
const onKeyDown = (event) => {
|
|
742
|
-
|
|
804
|
+
const onKeyDown = (event) => {
|
|
805
|
+
const mod = event.metaKey || event.ctrlKey
|
|
806
|
+
if (mod && event.key.toLowerCase() === 'z') {
|
|
807
|
+
if (canUndo || canRedo) {
|
|
808
|
+
event.preventDefault()
|
|
809
|
+
if (event.shiftKey) onRedo()
|
|
810
|
+
else onUndo()
|
|
811
|
+
}
|
|
812
|
+
} else if (mod && event.key.toLowerCase() === 'y') {
|
|
813
|
+
if (canRedo) {
|
|
814
|
+
event.preventDefault()
|
|
815
|
+
onRedo()
|
|
816
|
+
}
|
|
817
|
+
} else if (mod && event.key === 'Enter') {
|
|
743
818
|
event.preventDefault()
|
|
744
819
|
onConfirm()
|
|
745
820
|
} else if (mod && event.key.toLowerCase() === 's') {
|
|
@@ -771,6 +846,37 @@
|
|
|
771
846
|
h(Icon, { name: 'arrowRight', size: 11 }),
|
|
772
847
|
h('span', { className: 'cait-langchip' }, doc.targetLang),
|
|
773
848
|
h('span', null, `· ${doc.segments.length} ${t('doc.segments')}`),
|
|
849
|
+
h(
|
|
850
|
+
'button',
|
|
851
|
+
{
|
|
852
|
+
className: 'cait-linkbtn cait-brieflink',
|
|
853
|
+
onClick: onOpenBrief,
|
|
854
|
+
title: doc.brief ? t('panel.briefHint') : t('brief.empty'),
|
|
855
|
+
},
|
|
856
|
+
h(Icon, { name: 'file', size: 11 }),
|
|
857
|
+
t('editor.brief'),
|
|
858
|
+
doc.brief ? h('span', { className: 'cait-briefdot' }) : null,
|
|
859
|
+
),
|
|
860
|
+
h(
|
|
861
|
+
'button',
|
|
862
|
+
{
|
|
863
|
+
className: 'cait-linkbtn',
|
|
864
|
+
onClick: onOpenResources,
|
|
865
|
+
title: t('resources.hint'),
|
|
866
|
+
},
|
|
867
|
+
h(Icon, { name: 'database', size: 11 }),
|
|
868
|
+
t('editor.resources'),
|
|
869
|
+
),
|
|
870
|
+
h(
|
|
871
|
+
'button',
|
|
872
|
+
{
|
|
873
|
+
className: 'cait-linkbtn',
|
|
874
|
+
onClick: onOpenAnalysis,
|
|
875
|
+
title: t('analysis.hint'),
|
|
876
|
+
},
|
|
877
|
+
h(Icon, { name: 'chart', size: 11 }),
|
|
878
|
+
t('editor.analysis'),
|
|
879
|
+
),
|
|
774
880
|
),
|
|
775
881
|
),
|
|
776
882
|
h(
|
|
@@ -785,7 +891,40 @@
|
|
|
785
891
|
'div',
|
|
786
892
|
{ className: 'cait-seg-head' },
|
|
787
893
|
h('span', { className: 'cait-seg-label' }, `${t('editor.segment')} ${index + 1} / ${doc.segments.length}`),
|
|
788
|
-
h(
|
|
894
|
+
h(
|
|
895
|
+
'span',
|
|
896
|
+
{ className: 'cait-seg-flags' },
|
|
897
|
+
h(
|
|
898
|
+
'button',
|
|
899
|
+
{
|
|
900
|
+
className: `cait-linkbtn ${segment.locked ? 'active' : ''}`,
|
|
901
|
+
onClick: onToggleLock,
|
|
902
|
+
title: segment.locked ? t('editor.unlock') : t('editor.lock'),
|
|
903
|
+
'aria-pressed': Boolean(segment.locked),
|
|
904
|
+
},
|
|
905
|
+
h(Icon, { name: segment.locked ? 'unlock' : 'lock', size: 11 }),
|
|
906
|
+
segment.locked ? t('editor.locked') : t('editor.lock'),
|
|
907
|
+
),
|
|
908
|
+
h(
|
|
909
|
+
'button',
|
|
910
|
+
{
|
|
911
|
+
className: 'cait-linkbtn cait-notelink',
|
|
912
|
+
onClick: onOpenNotes,
|
|
913
|
+
title: t('notes.hint'),
|
|
914
|
+
},
|
|
915
|
+
h(Icon, { name: 'message', size: 11 }),
|
|
916
|
+
t('editor.notes'),
|
|
917
|
+
noteCount ? h('span', { className: 'cait-notecount' }, String(noteCount)) : null,
|
|
918
|
+
),
|
|
919
|
+
qaRisk
|
|
920
|
+
? h(
|
|
921
|
+
'span',
|
|
922
|
+
{ className: `cait-riskchip ${qaRisk.risk}`, title: qaRisk.codes.map((code) => t(`qa.code.${code}`)).join(' · ') },
|
|
923
|
+
`${t('qa.risk')} · ${t(`qa.${qaRisk.risk}`)}`,
|
|
924
|
+
)
|
|
925
|
+
: null,
|
|
926
|
+
h('span', { className: `cait-badge ${segment.status}` }, statusLabel),
|
|
927
|
+
),
|
|
789
928
|
),
|
|
790
929
|
h(
|
|
791
930
|
'div',
|
|
@@ -868,6 +1007,7 @@
|
|
|
868
1007
|
value: draft,
|
|
869
1008
|
onChange: (event) => setDraft(event.target.value),
|
|
870
1009
|
onKeyDown,
|
|
1010
|
+
readOnly: Boolean(segment.locked),
|
|
871
1011
|
placeholder: t('editor.targetPlaceholder'),
|
|
872
1012
|
spellCheck: false,
|
|
873
1013
|
rows: 8,
|
|
@@ -879,10 +1019,18 @@
|
|
|
879
1019
|
{ className: 'cait-actionbar' },
|
|
880
1020
|
h(
|
|
881
1021
|
'button',
|
|
882
|
-
{ className: 'cait-btn cait-btn-primary', onClick: onConfirm, disabled: Boolean(busy) || !draft.trim() },
|
|
1022
|
+
{ className: 'cait-btn cait-btn-primary', onClick: onConfirm, disabled: Boolean(busy) || Boolean(segment.locked) || !draft.trim() },
|
|
883
1023
|
busy === 'confirm' ? h(Spinner) : h(Icon, { name: 'check', size: 14 }),
|
|
884
1024
|
segment.status === 'confirmed' ? t('editor.update') : t('editor.confirm'),
|
|
885
1025
|
),
|
|
1026
|
+
segment.status === 'confirmed'
|
|
1027
|
+
? h(
|
|
1028
|
+
'button',
|
|
1029
|
+
{ className: 'cait-btn cait-btn-ghost', onClick: onUnconfirm, disabled: Boolean(busy) || Boolean(segment.locked), title: t('editor.unconfirmHint') },
|
|
1030
|
+
h(Icon, { name: 'refresh', size: 13 }),
|
|
1031
|
+
t('editor.unconfirm'),
|
|
1032
|
+
)
|
|
1033
|
+
: null,
|
|
886
1034
|
h(
|
|
887
1035
|
'button',
|
|
888
1036
|
{ className: 'cait-btn', onClick: onNext, disabled: Boolean(busy) },
|
|
@@ -895,9 +1043,60 @@
|
|
|
895
1043
|
h(Icon, { name: 'chevronLeft', size: 13 }),
|
|
896
1044
|
t('editor.prev'),
|
|
897
1045
|
),
|
|
1046
|
+
isSubtitle
|
|
1047
|
+
? h(
|
|
1048
|
+
'div',
|
|
1049
|
+
{ className: 'cait-subbar' },
|
|
1050
|
+
h('span', { className: 'cait-subtime' }, `${formatCue(cueStart)} → ${formatCue(cueEnd)}`),
|
|
1051
|
+
h('input', {
|
|
1052
|
+
className: 'cait-suboffset',
|
|
1053
|
+
type: 'number',
|
|
1054
|
+
step: '0.1',
|
|
1055
|
+
value: shiftSeconds,
|
|
1056
|
+
onChange: (event) => setShiftSeconds(event.target.value),
|
|
1057
|
+
title: t('editor.shiftHint'),
|
|
1058
|
+
}),
|
|
1059
|
+
h(
|
|
1060
|
+
'button',
|
|
1061
|
+
{ className: 'cait-btn cait-btn-ghost', onClick: () => onShiftSubtitles(Math.round(Number(shiftSeconds) * 1000)), disabled: Boolean(busy) || Boolean(segment.locked) || !Number.isFinite(Number(shiftSeconds)) },
|
|
1062
|
+
t('editor.shift'),
|
|
1063
|
+
),
|
|
1064
|
+
h(
|
|
1065
|
+
'button',
|
|
1066
|
+
{ className: 'cait-btn cait-btn-ghost', onClick: onMergeCue, disabled: Boolean(busy) || Boolean(segment.locked) || index >= doc.segments.length - 1 },
|
|
1067
|
+
t('editor.mergeCue'),
|
|
1068
|
+
),
|
|
1069
|
+
h(
|
|
1070
|
+
'button',
|
|
1071
|
+
{ className: 'cait-btn cait-btn-ghost', onClick: () => onSplitCue(Math.round((cueStart + cueEnd) / 2)), disabled: Boolean(busy) || Boolean(segment.locked) || !(cueEnd > cueStart) },
|
|
1072
|
+
t('editor.splitCue'),
|
|
1073
|
+
),
|
|
1074
|
+
)
|
|
1075
|
+
: [
|
|
1076
|
+
h(
|
|
1077
|
+
'button',
|
|
1078
|
+
{ key: 'merge', className: 'cait-btn cait-btn-ghost', onClick: onMerge, disabled: Boolean(busy) || !canMerge, title: t('editor.mergeHint') },
|
|
1079
|
+
t('editor.merge'),
|
|
1080
|
+
),
|
|
1081
|
+
h(
|
|
1082
|
+
'button',
|
|
1083
|
+
{ key: 'split', className: 'cait-btn cait-btn-ghost', onClick: splitAtCursor, disabled: Boolean(busy) || !canSplit, title: t('editor.splitHint') },
|
|
1084
|
+
t('editor.split'),
|
|
1085
|
+
),
|
|
1086
|
+
],
|
|
1087
|
+
h(
|
|
1088
|
+
'button',
|
|
1089
|
+
{ className: 'cait-iconbtn', onClick: onUndo, disabled: Boolean(busy) || !canUndo, title: t('editor.undo'), 'aria-label': t('editor.undo') },
|
|
1090
|
+
'↶',
|
|
1091
|
+
),
|
|
1092
|
+
h(
|
|
1093
|
+
'button',
|
|
1094
|
+
{ className: 'cait-iconbtn', onClick: onRedo, disabled: Boolean(busy) || !canRedo, title: t('editor.redo'), 'aria-label': t('editor.redo') },
|
|
1095
|
+
'↷',
|
|
1096
|
+
),
|
|
898
1097
|
h(
|
|
899
1098
|
'button',
|
|
900
|
-
{ className: 'cait-iconbtn cait-savebtn', onClick: onSave, disabled: Boolean(busy) || !dirty, title: t('editor.save') },
|
|
1099
|
+
{ className: 'cait-iconbtn cait-savebtn', onClick: onSave, disabled: Boolean(busy) || Boolean(segment.locked) || !dirty, title: t('editor.save') },
|
|
901
1100
|
h(Icon, { name: 'save', size: 14 }),
|
|
902
1101
|
),
|
|
903
1102
|
h('span', { className: 'cait-hint' }, t('editor.hint')),
|
|
@@ -1020,6 +1219,19 @@
|
|
|
1020
1219
|
onCopy,
|
|
1021
1220
|
onOpenSettings,
|
|
1022
1221
|
onSearchLibrary,
|
|
1222
|
+
qa,
|
|
1223
|
+
onRefreshQa,
|
|
1224
|
+
onJumpQa,
|
|
1225
|
+
onFixQa,
|
|
1226
|
+
review,
|
|
1227
|
+
reviewBusy,
|
|
1228
|
+
onReview,
|
|
1229
|
+
onApplyReview,
|
|
1230
|
+
concordance,
|
|
1231
|
+
concordanceQuery,
|
|
1232
|
+
setConcordanceQuery,
|
|
1233
|
+
onConcordance,
|
|
1234
|
+
onApplyConcordance,
|
|
1023
1235
|
busy,
|
|
1024
1236
|
}) {
|
|
1025
1237
|
const [termQuery, setTermQuery] = useState('')
|
|
@@ -1099,6 +1311,19 @@
|
|
|
1099
1311
|
h(Icon, { name: 'sparkles', size: 13 }),
|
|
1100
1312
|
t('panel.mt'),
|
|
1101
1313
|
),
|
|
1314
|
+
h(
|
|
1315
|
+
'button',
|
|
1316
|
+
{ className: `cait-tab${tab === 'qa' ? ' active' : ''}`, onClick: () => setTab('qa') },
|
|
1317
|
+
h(Icon, { name: 'alert', size: 13 }),
|
|
1318
|
+
t('panel.qa'),
|
|
1319
|
+
qa?.issueCount ? h('span', { className: 'cait-count' }, qa.issueCount) : null,
|
|
1320
|
+
),
|
|
1321
|
+
h(
|
|
1322
|
+
'button',
|
|
1323
|
+
{ className: `cait-tab${tab === 'concordance' ? ' active' : ''}`, onClick: () => setTab('concordance') },
|
|
1324
|
+
h(Icon, { name: 'search', size: 13 }),
|
|
1325
|
+
t('panel.concordance'),
|
|
1326
|
+
),
|
|
1102
1327
|
),
|
|
1103
1328
|
h(
|
|
1104
1329
|
'div',
|
|
@@ -1176,7 +1401,7 @@
|
|
|
1176
1401
|
event.target.value = ''
|
|
1177
1402
|
},
|
|
1178
1403
|
}),
|
|
1179
|
-
h('button', { className: 'cait-btn', onClick: () => tmxInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: '
|
|
1404
|
+
h('button', { className: 'cait-btn', onClick: () => tmxInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'download', size: 12 }), t('panel.importTmx')),
|
|
1180
1405
|
h('button', { className: 'cait-btn cait-btn-danger', onClick: onClearTm, disabled: Boolean(busy) || data.tm.count === 0 }, t('panel.clearTm')),
|
|
1181
1406
|
),
|
|
1182
1407
|
)
|
|
@@ -1194,7 +1419,7 @@
|
|
|
1194
1419
|
h(
|
|
1195
1420
|
'button',
|
|
1196
1421
|
{ key: hit.id, className: 'cait-chip cait-chip-action', onClick: () => onApplyTerm(hit), title: hit.note || hit.target },
|
|
1197
|
-
`${hit.term} → ${hit.target}`,
|
|
1422
|
+
`${hit.term} → ${hit.target}${hit.forbidden ? ` · ${t('panel.termForbidden')}` : hit.preferred ? ` · ${t('panel.termPreferred')}` : ''}`,
|
|
1198
1423
|
),
|
|
1199
1424
|
),
|
|
1200
1425
|
)
|
|
@@ -1227,7 +1452,7 @@
|
|
|
1227
1452
|
event.target.value = ''
|
|
1228
1453
|
},
|
|
1229
1454
|
}),
|
|
1230
|
-
h('button', { className: 'cait-btn', onClick: () => glossaryInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: '
|
|
1455
|
+
h('button', { className: 'cait-btn', onClick: () => glossaryInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'download', size: 12 }), t('panel.importCsv')),
|
|
1231
1456
|
h('button', { className: 'cait-btn cait-btn-danger', onClick: onClearGlossary, disabled: Boolean(busy) || data.glossary.length === 0 }, t('panel.clearGlossary')),
|
|
1232
1457
|
),
|
|
1233
1458
|
)
|
|
@@ -1292,6 +1517,239 @@
|
|
|
1292
1517
|
),
|
|
1293
1518
|
)
|
|
1294
1519
|
: null,
|
|
1520
|
+
tab === 'qa'
|
|
1521
|
+
? h(
|
|
1522
|
+
'div',
|
|
1523
|
+
null,
|
|
1524
|
+
h(
|
|
1525
|
+
'div',
|
|
1526
|
+
{ className: 'cait-subhead' },
|
|
1527
|
+
h('span', null, t('qa.title')),
|
|
1528
|
+
h(
|
|
1529
|
+
'div',
|
|
1530
|
+
{ className: 'cait-subhead-actions' },
|
|
1531
|
+
h(
|
|
1532
|
+
'button',
|
|
1533
|
+
{
|
|
1534
|
+
className: 'cait-linkbtn',
|
|
1535
|
+
onClick: onReview,
|
|
1536
|
+
disabled: Boolean(busy) || Boolean(reviewBusy),
|
|
1537
|
+
title: t('qa.review'),
|
|
1538
|
+
},
|
|
1539
|
+
h(Icon, { name: 'sparkles', size: 12 }),
|
|
1540
|
+
reviewBusy ? t('qa.reviewing') : t('qa.review'),
|
|
1541
|
+
),
|
|
1542
|
+
h(
|
|
1543
|
+
'button',
|
|
1544
|
+
{ className: 'cait-linkbtn', onClick: onRefreshQa, disabled: Boolean(busy) },
|
|
1545
|
+
h(Icon, { name: 'refresh', size: 12 }),
|
|
1546
|
+
t('qa.refresh'),
|
|
1547
|
+
),
|
|
1548
|
+
),
|
|
1549
|
+
),
|
|
1550
|
+
qa
|
|
1551
|
+
? h(
|
|
1552
|
+
'div',
|
|
1553
|
+
null,
|
|
1554
|
+
h(
|
|
1555
|
+
'div',
|
|
1556
|
+
{ className: 'cait-riskcounts' },
|
|
1557
|
+
['high', 'medium', 'low'].map((level) =>
|
|
1558
|
+
h(
|
|
1559
|
+
'span',
|
|
1560
|
+
{ key: level, className: `cait-riskcount ${level}` },
|
|
1561
|
+
`${t(`qa.${level}`)} ${qa.riskCounts?.[level] || 0}`,
|
|
1562
|
+
),
|
|
1563
|
+
),
|
|
1564
|
+
),
|
|
1565
|
+
(qa.risks || []).length
|
|
1566
|
+
? qa.risks.map((item) => {
|
|
1567
|
+
const segment = doc.segments[item.index]
|
|
1568
|
+
const codes = (item.codes || []).map((code) => t(`qa.code.${code}`)).join(' · ')
|
|
1569
|
+
return h(
|
|
1570
|
+
'div',
|
|
1571
|
+
{
|
|
1572
|
+
key: item.index,
|
|
1573
|
+
className: `cait-riskrow ${item.risk}`,
|
|
1574
|
+
role: 'button',
|
|
1575
|
+
tabIndex: 0,
|
|
1576
|
+
onClick: () => onJumpQa(item.index),
|
|
1577
|
+
onKeyDown: (event) => {
|
|
1578
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1579
|
+
event.preventDefault()
|
|
1580
|
+
onJumpQa(item.index)
|
|
1581
|
+
}
|
|
1582
|
+
},
|
|
1583
|
+
title: codes,
|
|
1584
|
+
},
|
|
1585
|
+
h('span', { className: 'cait-riskrow-index' }, `${item.index + 1}`),
|
|
1586
|
+
h(
|
|
1587
|
+
'div',
|
|
1588
|
+
{ className: 'cait-riskrow-body' },
|
|
1589
|
+
h('span', { className: 'cait-riskrow-source' }, segment?.source || ''),
|
|
1590
|
+
h('span', { className: 'cait-riskrow-codes' }, codes),
|
|
1591
|
+
onFixQa
|
|
1592
|
+
? h(
|
|
1593
|
+
'div',
|
|
1594
|
+
{ className: 'cait-riskactions' },
|
|
1595
|
+
h(
|
|
1596
|
+
'button',
|
|
1597
|
+
{
|
|
1598
|
+
className: 'cait-riskaction',
|
|
1599
|
+
onClick: (event) => {
|
|
1600
|
+
event.stopPropagation()
|
|
1601
|
+
onFixQa(item, 'copy-source')
|
|
1602
|
+
},
|
|
1603
|
+
disabled: Boolean(busy),
|
|
1604
|
+
},
|
|
1605
|
+
t('qa.fixCopy'),
|
|
1606
|
+
),
|
|
1607
|
+
h(
|
|
1608
|
+
'button',
|
|
1609
|
+
{
|
|
1610
|
+
className: 'cait-riskaction',
|
|
1611
|
+
onClick: (event) => {
|
|
1612
|
+
event.stopPropagation()
|
|
1613
|
+
onFixQa(item, 'clear')
|
|
1614
|
+
},
|
|
1615
|
+
disabled: Boolean(busy),
|
|
1616
|
+
},
|
|
1617
|
+
t('qa.fixClear'),
|
|
1618
|
+
),
|
|
1619
|
+
item.termTarget
|
|
1620
|
+
? h(
|
|
1621
|
+
'button',
|
|
1622
|
+
{
|
|
1623
|
+
className: 'cait-riskaction',
|
|
1624
|
+
onClick: (event) => {
|
|
1625
|
+
event.stopPropagation()
|
|
1626
|
+
onFixQa(item, 'term')
|
|
1627
|
+
},
|
|
1628
|
+
disabled: Boolean(busy),
|
|
1629
|
+
},
|
|
1630
|
+
t('qa.fixTerm'),
|
|
1631
|
+
)
|
|
1632
|
+
: null,
|
|
1633
|
+
)
|
|
1634
|
+
: null,
|
|
1635
|
+
),
|
|
1636
|
+
h(Icon, { name: 'chevronRight', size: 12 }),
|
|
1637
|
+
)
|
|
1638
|
+
})
|
|
1639
|
+
: h('p', { className: 'cait-empty-note' }, t('qa.empty')),
|
|
1640
|
+
)
|
|
1641
|
+
: h('p', { className: 'cait-empty-note' }, t('qa.loading')),
|
|
1642
|
+
review
|
|
1643
|
+
? h(
|
|
1644
|
+
'div',
|
|
1645
|
+
{ className: 'cait-review' },
|
|
1646
|
+
h('div', { className: 'cait-listhead' }, t('qa.reviewTitle')),
|
|
1647
|
+
(review.issues || []).length
|
|
1648
|
+
? review.issues.map((issue, issueIndex) => {
|
|
1649
|
+
const severity = ['high', 'medium', 'low'].includes(issue.severity)
|
|
1650
|
+
? issue.severity
|
|
1651
|
+
: 'medium'
|
|
1652
|
+
return h(
|
|
1653
|
+
'div',
|
|
1654
|
+
{
|
|
1655
|
+
key: `${issue.index}-${issueIndex}`,
|
|
1656
|
+
className: 'cait-reviewrow',
|
|
1657
|
+
role: 'button',
|
|
1658
|
+
tabIndex: 0,
|
|
1659
|
+
onClick: () => onJumpQa(issue.index),
|
|
1660
|
+
onKeyDown: (event) => {
|
|
1661
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1662
|
+
event.preventDefault()
|
|
1663
|
+
onJumpQa(issue.index)
|
|
1664
|
+
}
|
|
1665
|
+
},
|
|
1666
|
+
},
|
|
1667
|
+
h('span', { className: 'cait-reviewrow-index' }, `${issue.index + 1}`),
|
|
1668
|
+
h(
|
|
1669
|
+
'div',
|
|
1670
|
+
{ className: 'cait-reviewrow-body' },
|
|
1671
|
+
h(
|
|
1672
|
+
'span',
|
|
1673
|
+
{ className: `cait-reviewrow-msg ${severity}` },
|
|
1674
|
+
`[${t(`qa.${severity}`)}] ${issue.message}`,
|
|
1675
|
+
),
|
|
1676
|
+
issue.suggestion
|
|
1677
|
+
? h(
|
|
1678
|
+
'button',
|
|
1679
|
+
{
|
|
1680
|
+
className: 'cait-riskaction',
|
|
1681
|
+
onClick: (event) => {
|
|
1682
|
+
event.stopPropagation()
|
|
1683
|
+
onApplyReview(issue)
|
|
1684
|
+
},
|
|
1685
|
+
},
|
|
1686
|
+
t('qa.reviewApply'),
|
|
1687
|
+
)
|
|
1688
|
+
: null,
|
|
1689
|
+
),
|
|
1690
|
+
)
|
|
1691
|
+
})
|
|
1692
|
+
: h('p', { className: 'cait-empty-note' }, t('qa.reviewEmpty')),
|
|
1693
|
+
)
|
|
1694
|
+
: null,
|
|
1695
|
+
)
|
|
1696
|
+
: null,
|
|
1697
|
+
tab === 'concordance'
|
|
1698
|
+
? h(
|
|
1699
|
+
'div',
|
|
1700
|
+
null,
|
|
1701
|
+
h(
|
|
1702
|
+
'form',
|
|
1703
|
+
{
|
|
1704
|
+
className: 'cait-search',
|
|
1705
|
+
onSubmit: (event) => {
|
|
1706
|
+
event.preventDefault()
|
|
1707
|
+
onConcordance()
|
|
1708
|
+
},
|
|
1709
|
+
},
|
|
1710
|
+
h(Icon, { name: 'search', size: 13 }),
|
|
1711
|
+
h('input', {
|
|
1712
|
+
value: concordanceQuery,
|
|
1713
|
+
onChange: (event) => setConcordanceQuery(event.target.value),
|
|
1714
|
+
placeholder: t('panel.concordancePlaceholder'),
|
|
1715
|
+
'aria-label': t('panel.concordancePlaceholder'),
|
|
1716
|
+
}),
|
|
1717
|
+
h(
|
|
1718
|
+
'button',
|
|
1719
|
+
{ className: 'cait-iconbtn', type: 'submit', title: t('panel.searchConcordance') },
|
|
1720
|
+
h(Icon, { name: 'arrowRight', size: 13 }),
|
|
1721
|
+
),
|
|
1722
|
+
),
|
|
1723
|
+
h('p', { className: 'cait-mt-hint' }, t('panel.concordanceHint')),
|
|
1724
|
+
concordance
|
|
1725
|
+
? h(
|
|
1726
|
+
'div',
|
|
1727
|
+
null,
|
|
1728
|
+
h(
|
|
1729
|
+
'div',
|
|
1730
|
+
{ className: 'cait-listhead' },
|
|
1731
|
+
h('span', null, t('panel.concordance')),
|
|
1732
|
+
h('span', { className: 'cait-count' }, concordance.total ?? concordance.entries?.length ?? 0),
|
|
1733
|
+
),
|
|
1734
|
+
(concordance.entries || []).length
|
|
1735
|
+
? concordance.entries.map((entry, i) =>
|
|
1736
|
+
h(
|
|
1737
|
+
'button',
|
|
1738
|
+
{
|
|
1739
|
+
key: `${entry.tmId || entry.library || 'c'}-${i}`,
|
|
1740
|
+
className: 'cait-concordance-row',
|
|
1741
|
+
onClick: () => onApplyConcordance(entry),
|
|
1742
|
+
},
|
|
1743
|
+
h('span', { className: 'cait-concordance-source' }, entry.source),
|
|
1744
|
+
h('span', { className: 'cait-concordance-target' }, entry.target),
|
|
1745
|
+
h('span', { className: 'cait-concordance-meta' }, entry.library || ''),
|
|
1746
|
+
),
|
|
1747
|
+
)
|
|
1748
|
+
: h('p', { className: 'cait-empty-note' }, t('panel.noConcordance')),
|
|
1749
|
+
)
|
|
1750
|
+
: null,
|
|
1751
|
+
)
|
|
1752
|
+
: null,
|
|
1295
1753
|
),
|
|
1296
1754
|
)
|
|
1297
1755
|
}
|
|
@@ -1484,6 +1942,7 @@
|
|
|
1484
1942
|
const sourceRef = useRef(null)
|
|
1485
1943
|
const targetRef = useRef(null)
|
|
1486
1944
|
const libraries = data?.tmLibraries || []
|
|
1945
|
+
const languageOptions = ['zh-CN', 'zh-TW', 'en', 'ja', 'ko', 'fr', 'de', 'es', 'pt-BR', 'ru', 'it', 'ar']
|
|
1487
1946
|
|
|
1488
1947
|
useEffect(() => {
|
|
1489
1948
|
if (!tmId && libraries.length) setTmId(libraries[0].id)
|
|
@@ -1584,7 +2043,12 @@
|
|
|
1584
2043
|
action: async () => {
|
|
1585
2044
|
setWorking(true)
|
|
1586
2045
|
try {
|
|
1587
|
-
const written = await rpc('writeAlignment', {
|
|
2046
|
+
const written = await rpc('writeAlignment', {
|
|
2047
|
+
pairs,
|
|
2048
|
+
tmId: tmId || undefined,
|
|
2049
|
+
sourceLang: sourceLang || undefined,
|
|
2050
|
+
targetLang: targetLang || undefined,
|
|
2051
|
+
})
|
|
1588
2052
|
notify(t('align.written', { written: written.written, skipped: written.skipped }))
|
|
1589
2053
|
} catch (error) {
|
|
1590
2054
|
report(error)
|
|
@@ -1632,15 +2096,20 @@
|
|
|
1632
2096
|
h(
|
|
1633
2097
|
'div',
|
|
1634
2098
|
{ className: 'cait-alignactions' },
|
|
2099
|
+
h('datalist', { id: 'cait-align-languages' }, languageOptions.map((code) => h('option', { key: code, value: code }))),
|
|
1635
2100
|
h('input', {
|
|
1636
2101
|
className: 'cait-libinput',
|
|
1637
2102
|
placeholder: t('align.sourceLang'),
|
|
2103
|
+
title: t('align.langHint'),
|
|
2104
|
+
list: 'cait-align-languages',
|
|
1638
2105
|
value: sourceLang,
|
|
1639
2106
|
onChange: (event) => setSourceLang(event.target.value),
|
|
1640
2107
|
}),
|
|
1641
2108
|
h('input', {
|
|
1642
2109
|
className: 'cait-libinput',
|
|
1643
2110
|
placeholder: t('align.targetLang'),
|
|
2111
|
+
title: t('align.langHint'),
|
|
2112
|
+
list: 'cait-align-languages',
|
|
1644
2113
|
value: targetLang,
|
|
1645
2114
|
onChange: (event) => setTargetLang(event.target.value),
|
|
1646
2115
|
}),
|
|
@@ -1650,8 +2119,15 @@
|
|
|
1650
2119
|
className: 'cait-select cait-liblang',
|
|
1651
2120
|
value: tmId,
|
|
1652
2121
|
onChange: (event) => setTmId(event.target.value),
|
|
2122
|
+
title: t('align.tmHint'),
|
|
2123
|
+
'aria-label': t('align.tm'),
|
|
2124
|
+
disabled: !libraries.length,
|
|
1653
2125
|
},
|
|
1654
|
-
libraries.
|
|
2126
|
+
libraries.length
|
|
2127
|
+
? libraries.map((library) =>
|
|
2128
|
+
h('option', { key: library.id, value: library.id }, t('align.tmOption', { name: library.name })),
|
|
2129
|
+
)
|
|
2130
|
+
: h('option', { value: '' }, t('align.noTm')),
|
|
1655
2131
|
),
|
|
1656
2132
|
h(
|
|
1657
2133
|
'button',
|
|
@@ -1766,6 +2242,7 @@
|
|
|
1766
2242
|
onCreateLibrary,
|
|
1767
2243
|
onRenameLibrary,
|
|
1768
2244
|
onDeleteLibrary,
|
|
2245
|
+
onSetMounts,
|
|
1769
2246
|
}) {
|
|
1770
2247
|
const storage = typeof localStorage !== 'undefined' ? localStorage : null
|
|
1771
2248
|
const [kind, setKind] = useState(seed?.kind === 'glossary' ? 'glossary' : 'tm')
|
|
@@ -1793,7 +2270,23 @@
|
|
|
1793
2270
|
const idKey = kind === 'tm' ? 'tmId' : 'glossaryId'
|
|
1794
2271
|
const labelKey = 'name'
|
|
1795
2272
|
const scopeKey = scopeIds.join(',')
|
|
1796
|
-
const
|
|
2273
|
+
const defaultWriteId = kind === 'tm' ? data?.mounts?.defaultTmId : data?.mounts?.defaultGlossaryId
|
|
2274
|
+
const targetLibraryId = libraries.some((library) => library[idKey] === defaultWriteId)
|
|
2275
|
+
? defaultWriteId
|
|
2276
|
+
: libraries[0]?.[idKey] || ''
|
|
2277
|
+
|
|
2278
|
+
const changeWriteLibrary = async (value) => {
|
|
2279
|
+
if (!value || value === targetLibraryId) return
|
|
2280
|
+
// A default must also be mounted; adding it here keeps the selector
|
|
2281
|
+
// from failing when the chosen library was only in the list view.
|
|
2282
|
+
if (kind === 'tm') {
|
|
2283
|
+
const tmIds = Array.from(new Set([...(data?.mounts?.tmIds || []), value]))
|
|
2284
|
+
await onSetMounts?.({ tmIds, defaultTmId: value })
|
|
2285
|
+
} else {
|
|
2286
|
+
const glossaryIds = Array.from(new Set([...(data?.mounts?.glossaryIds || []), value]))
|
|
2287
|
+
await onSetMounts?.({ glossaryIds, defaultGlossaryId: value })
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
1797
2290
|
|
|
1798
2291
|
useEffect(() => {
|
|
1799
2292
|
const timer = setTimeout(() => setDebouncedQuery(query), 250)
|
|
@@ -2034,6 +2527,35 @@
|
|
|
2034
2527
|
})
|
|
2035
2528
|
}
|
|
2036
2529
|
|
|
2530
|
+
const maintainTm = async (action) => {
|
|
2531
|
+
if (!targetLibraryId) return
|
|
2532
|
+
try {
|
|
2533
|
+
const preview = await rpc('tmMaintenancePreview', {
|
|
2534
|
+
action,
|
|
2535
|
+
tmId: targetLibraryId,
|
|
2536
|
+
emptyTarget: true,
|
|
2537
|
+
})
|
|
2538
|
+
const count = action === 'dedupe' ? preview.duplicates : preview.matched
|
|
2539
|
+
onConfirm({
|
|
2540
|
+
title: t(`tm.maintain.${action}`),
|
|
2541
|
+
message: t(`tm.maintain.${action}Hint`).replace('{count}', String(count)),
|
|
2542
|
+
confirmLabel: t(`tm.maintain.${action}`),
|
|
2543
|
+
action: async () => {
|
|
2544
|
+
const result = await rpc(action === 'dedupe' ? 'dedupeTmLibrary' : 'pruneTmLibrary', {
|
|
2545
|
+
tmId: targetLibraryId,
|
|
2546
|
+
emptyTarget: true,
|
|
2547
|
+
confirm: true,
|
|
2548
|
+
})
|
|
2549
|
+
if (result?.state) onApplyState(result.state)
|
|
2550
|
+
await loadRows()
|
|
2551
|
+
notify(t('tm.maintain.done').replace('{count}', String(count)))
|
|
2552
|
+
},
|
|
2553
|
+
})
|
|
2554
|
+
} catch (error) {
|
|
2555
|
+
report(error)
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2037
2559
|
const exportLibrary = async () => {
|
|
2038
2560
|
if (!targetLibraryId) return
|
|
2039
2561
|
setWorking('export')
|
|
@@ -2306,6 +2828,28 @@
|
|
|
2306
2828
|
['tbx', 'csv', 'json'].map((format) => h('option', { key: format, value: format }, format.toUpperCase())),
|
|
2307
2829
|
)
|
|
2308
2830
|
: null,
|
|
2831
|
+
h(
|
|
2832
|
+
'label',
|
|
2833
|
+
{ className: 'cait-libwrite', title: t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary') },
|
|
2834
|
+
h(
|
|
2835
|
+
'span',
|
|
2836
|
+
{ className: 'cait-libwritelabel' },
|
|
2837
|
+
t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
|
|
2838
|
+
),
|
|
2839
|
+
h(
|
|
2840
|
+
'select',
|
|
2841
|
+
{
|
|
2842
|
+
className: 'cait-liblang',
|
|
2843
|
+
value: targetLibraryId,
|
|
2844
|
+
onChange: (event) => changeWriteLibrary(event.target.value),
|
|
2845
|
+
disabled: Boolean(busy) || !libraries.length,
|
|
2846
|
+
'aria-label': t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
|
|
2847
|
+
},
|
|
2848
|
+
libraries.map((library) =>
|
|
2849
|
+
h('option', { key: library[idKey], value: library[idKey] }, library[labelKey]),
|
|
2850
|
+
),
|
|
2851
|
+
),
|
|
2852
|
+
),
|
|
2309
2853
|
kind === 'glossary'
|
|
2310
2854
|
? h(
|
|
2311
2855
|
'button',
|
|
@@ -2335,7 +2879,7 @@
|
|
|
2335
2879
|
disabled: Boolean(busy) || !targetLibraryId,
|
|
2336
2880
|
title: t('library.import'),
|
|
2337
2881
|
},
|
|
2338
|
-
h(Icon, { name: '
|
|
2882
|
+
h(Icon, { name: 'download', size: 13 }),
|
|
2339
2883
|
h('span', { className: 'cait-btn-label' }, t('library.import')),
|
|
2340
2884
|
),
|
|
2341
2885
|
h(
|
|
@@ -2347,9 +2891,37 @@
|
|
|
2347
2891
|
disabled: Boolean(busy) || working === 'export' || !targetLibraryId,
|
|
2348
2892
|
title: t('library.export'),
|
|
2349
2893
|
},
|
|
2350
|
-
h(Icon, { name: '
|
|
2894
|
+
h(Icon, { name: 'upload', size: 13 }),
|
|
2351
2895
|
h('span', { className: 'cait-btn-label' }, kind === 'tm' ? 'TMX' : exportFormat.toUpperCase()),
|
|
2352
2896
|
),
|
|
2897
|
+
kind === 'tm'
|
|
2898
|
+
? h(
|
|
2899
|
+
'button',
|
|
2900
|
+
{
|
|
2901
|
+
type: 'button',
|
|
2902
|
+
className: 'cait-btn',
|
|
2903
|
+
onClick: () => maintainTm('dedupe'),
|
|
2904
|
+
disabled: Boolean(busy) || !targetLibraryId,
|
|
2905
|
+
title: t('tm.maintain.dedupe'),
|
|
2906
|
+
},
|
|
2907
|
+
h(Icon, { name: 'database', size: 13 }),
|
|
2908
|
+
h('span', { className: 'cait-btn-label' }, t('tm.maintain.dedupe')),
|
|
2909
|
+
)
|
|
2910
|
+
: null,
|
|
2911
|
+
kind === 'tm'
|
|
2912
|
+
? h(
|
|
2913
|
+
'button',
|
|
2914
|
+
{
|
|
2915
|
+
type: 'button',
|
|
2916
|
+
className: 'cait-btn',
|
|
2917
|
+
onClick: () => maintainTm('prune'),
|
|
2918
|
+
disabled: Boolean(busy) || !targetLibraryId,
|
|
2919
|
+
title: t('tm.maintain.prune'),
|
|
2920
|
+
},
|
|
2921
|
+
h(Icon, { name: 'trash', size: 13 }),
|
|
2922
|
+
h('span', { className: 'cait-btn-label' }, t('tm.maintain.prune')),
|
|
2923
|
+
)
|
|
2924
|
+
: null,
|
|
2353
2925
|
h(
|
|
2354
2926
|
'button',
|
|
2355
2927
|
{
|
|
@@ -2360,6 +2932,7 @@
|
|
|
2360
2932
|
title: t('library.clear'),
|
|
2361
2933
|
},
|
|
2362
2934
|
h(Icon, { name: 'trash', size: 13 }),
|
|
2935
|
+
h('span', { className: 'cait-btn-label' }, t('library.clear')),
|
|
2363
2936
|
),
|
|
2364
2937
|
h('input', {
|
|
2365
2938
|
ref: fileRef,
|
|
@@ -2612,6 +3185,7 @@
|
|
|
2612
3185
|
onChange: () => toggleTerm(at),
|
|
2613
3186
|
}),
|
|
2614
3187
|
h('span', { className: 'cait-termname' }, candidate.term),
|
|
3188
|
+
candidate.proper ? h('span', { className: 'cait-originchip' }, t('term.proper')) : null,
|
|
2615
3189
|
h('span', { className: 'cait-termcount' }, `×${candidate.count}`),
|
|
2616
3190
|
candidate.inLibrary ? h('span', { className: 'cait-originchip' }, t('term.inLibrary')) : null,
|
|
2617
3191
|
h('input', {
|
|
@@ -2629,7 +3203,7 @@
|
|
|
2629
3203
|
)
|
|
2630
3204
|
}
|
|
2631
3205
|
|
|
2632
|
-
function ExportModal({ t, busy, onClose, onExport, qa }) {
|
|
3206
|
+
function ExportModal({ t, busy, onClose, onExport, onImportReview, qa }) {
|
|
2633
3207
|
const issues = qa?.issueCount || 0
|
|
2634
3208
|
const items = [
|
|
2635
3209
|
['bilingual-md', t('modal.export.bilingualMd'), t('modal.export.bilingual'), 'file'],
|
|
@@ -2638,11 +3212,20 @@
|
|
|
2638
3212
|
['target-txt', t('modal.export.targetTxt'), t('modal.export.target'), 'download'],
|
|
2639
3213
|
['target-md', t('modal.export.targetMd'), t('modal.export.target'), 'download'],
|
|
2640
3214
|
['docx', t('modal.export.docx'), t('modal.export.docxGroup'), 'file'],
|
|
3215
|
+
['bilingual-docx', t('modal.export.bilingualDocx'), t('modal.export.bilingual'), 'file'],
|
|
2641
3216
|
['epub', t('modal.export.epub'), t('modal.export.epubGroup'), 'file'],
|
|
2642
3217
|
['srt', t('modal.export.srt'), t('modal.export.subtitleGroup'), 'file'],
|
|
2643
3218
|
['vtt', t('modal.export.vtt'), t('modal.export.subtitleGroup'), 'file'],
|
|
2644
3219
|
['po', t('modal.export.po'), t('modal.export.localizationGroup'), 'file'],
|
|
3220
|
+
['apple-strings', t('modal.export.appleStrings'), t('modal.export.localizationGroup'), 'file'],
|
|
3221
|
+
['apple-stringsdict', t('modal.export.appleStringsdict'), t('modal.export.localizationGroup'), 'file'],
|
|
3222
|
+
['android', t('modal.export.android'), t('modal.export.localizationGroup'), 'file'],
|
|
3223
|
+
['resx', t('modal.export.resx'), t('modal.export.localizationGroup'), 'file'],
|
|
3224
|
+
['arb', t('modal.export.arb'), t('modal.export.localizationGroup'), 'file'],
|
|
3225
|
+
['yaml', t('modal.export.yaml'), t('modal.export.localizationGroup'), 'file'],
|
|
3226
|
+
['properties', t('modal.export.properties'), t('modal.export.localizationGroup'), 'file'],
|
|
2645
3227
|
['xliff', t('modal.export.xliff'), t('modal.export.localizationGroup'), 'file'],
|
|
3228
|
+
['xliff-review', t('modal.export.xliffReview'), t('modal.export.localizationGroup'), 'file'],
|
|
2646
3229
|
['xliff2', t('modal.export.xliff2'), t('modal.export.localizationGroup'), 'file'],
|
|
2647
3230
|
['sdlxliff', t('modal.export.sdlxliff'), t('modal.export.localizationGroup'), 'file'],
|
|
2648
3231
|
['html', t('modal.export.html'), t('modal.export.webGroup'), 'file'],
|
|
@@ -2685,6 +3268,30 @@
|
|
|
2685
3268
|
),
|
|
2686
3269
|
),
|
|
2687
3270
|
),
|
|
3271
|
+
onImportReview
|
|
3272
|
+
? h(
|
|
3273
|
+
'div',
|
|
3274
|
+
{ className: 'cait-review-import' },
|
|
3275
|
+
h('p', { className: 'cait-libnote' }, t('modal.export.reviewImportHint')),
|
|
3276
|
+
h(
|
|
3277
|
+
'label',
|
|
3278
|
+
{ className: 'cait-btn' },
|
|
3279
|
+
h(Icon, { name: 'upload', size: 13 }),
|
|
3280
|
+
t('modal.export.reviewImport'),
|
|
3281
|
+
h('input', {
|
|
3282
|
+
type: 'file',
|
|
3283
|
+
accept: '.xliff,.xlf,application/x-xliff+xml',
|
|
3284
|
+
style: { display: 'none' },
|
|
3285
|
+
disabled: Boolean(busy),
|
|
3286
|
+
onChange: (event) => {
|
|
3287
|
+
const file = event.target.files?.[0]
|
|
3288
|
+
if (file) onImportReview(file)
|
|
3289
|
+
event.target.value = ''
|
|
3290
|
+
},
|
|
3291
|
+
}),
|
|
3292
|
+
),
|
|
3293
|
+
)
|
|
3294
|
+
: null,
|
|
2688
3295
|
)
|
|
2689
3296
|
}
|
|
2690
3297
|
|
|
@@ -2705,13 +3312,23 @@
|
|
|
2705
3312
|
model: settings.mt.model,
|
|
2706
3313
|
temperature: settings.mt.temperature,
|
|
2707
3314
|
},
|
|
3315
|
+
review: {
|
|
3316
|
+
provider: settings.review?.provider || '',
|
|
3317
|
+
model: settings.review?.model || '',
|
|
3318
|
+
temperature: settings.review?.temperature ?? 0,
|
|
3319
|
+
},
|
|
2708
3320
|
})
|
|
2709
3321
|
const routes = Array.isArray(mtRoutes?.providers) ? mtRoutes.providers : []
|
|
2710
3322
|
const route = routes.find((item) => item.id === form.mt.provider)
|
|
2711
3323
|
const models = Array.isArray(route?.models) ? route.models : []
|
|
2712
3324
|
const routeMissing = form.mt.provider !== 'mock' && !route
|
|
3325
|
+
const reviewRoute = routes.find((item) => item.id === form.review.provider)
|
|
3326
|
+
const reviewModels = Array.isArray(reviewRoute?.models) ? reviewRoute.models : []
|
|
3327
|
+
const reviewInherited = !form.review.provider
|
|
2713
3328
|
|
|
2714
3329
|
const setMt = (patch) => setForm((current) => ({ ...current, mt: { ...current.mt, ...patch } }))
|
|
3330
|
+
const setReview = (patch) =>
|
|
3331
|
+
setForm((current) => ({ ...current, review: { ...current.review, ...patch } }))
|
|
2715
3332
|
const setPre = (patch) =>
|
|
2716
3333
|
setForm((current) => ({ ...current, preprocess: { ...current.preprocess, ...patch } }))
|
|
2717
3334
|
const selectProvider = (provider) => {
|
|
@@ -2729,6 +3346,18 @@
|
|
|
2729
3346
|
}))
|
|
2730
3347
|
}
|
|
2731
3348
|
|
|
3349
|
+
const selectReviewProvider = (provider) => {
|
|
3350
|
+
const nextRoute = routes.find((item) => item.id === provider)
|
|
3351
|
+
setForm((current) => ({
|
|
3352
|
+
...current,
|
|
3353
|
+
review: {
|
|
3354
|
+
...current.review,
|
|
3355
|
+
provider,
|
|
3356
|
+
model: nextRoute?.models?.[0]?.id || '',
|
|
3357
|
+
},
|
|
3358
|
+
}))
|
|
3359
|
+
}
|
|
3360
|
+
|
|
2732
3361
|
const submit = (event) => {
|
|
2733
3362
|
event.preventDefault()
|
|
2734
3363
|
const patch = {
|
|
@@ -2747,6 +3376,11 @@
|
|
|
2747
3376
|
model: form.mt.model,
|
|
2748
3377
|
temperature: Number(form.mt.temperature),
|
|
2749
3378
|
},
|
|
3379
|
+
review: {
|
|
3380
|
+
provider: form.review.provider,
|
|
3381
|
+
model: form.review.model,
|
|
3382
|
+
temperature: Number(form.review.temperature),
|
|
3383
|
+
},
|
|
2750
3384
|
}
|
|
2751
3385
|
onSave(patch)
|
|
2752
3386
|
}
|
|
@@ -2831,9 +3465,26 @@
|
|
|
2831
3465
|
{ className: 'cait-settings-section' },
|
|
2832
3466
|
h('h4', { className: 'cait-settings-title' }, t('modal.settings.tm')),
|
|
2833
3467
|
h(
|
|
2834
|
-
'
|
|
3468
|
+
'div',
|
|
2835
3469
|
{ className: 'cait-field' },
|
|
2836
|
-
h(
|
|
3470
|
+
h(
|
|
3471
|
+
'div',
|
|
3472
|
+
{ className: 'cait-fieldhead' },
|
|
3473
|
+
h('span', null, `${t('modal.settings.fuzzy')} · ${fuzzyPercent(form.fuzzyThreshold)}%`),
|
|
3474
|
+
// Offered only while the slider is away from the default, so the
|
|
3475
|
+
// control never takes space in the usual case.
|
|
3476
|
+
fuzzyPercent(form.fuzzyThreshold) === fuzzyPercent(DEFAULT_FUZZY_THRESHOLD)
|
|
3477
|
+
? null
|
|
3478
|
+
: h(
|
|
3479
|
+
'button',
|
|
3480
|
+
{
|
|
3481
|
+
className: 'cait-linkbtn cait-fieldreset',
|
|
3482
|
+
type: 'button',
|
|
3483
|
+
onClick: () => setForm({ ...form, fuzzyThreshold: DEFAULT_FUZZY_THRESHOLD }),
|
|
3484
|
+
},
|
|
3485
|
+
t('modal.settings.resetDefault'),
|
|
3486
|
+
),
|
|
3487
|
+
),
|
|
2837
3488
|
h('input', {
|
|
2838
3489
|
className: 'cait-range',
|
|
2839
3490
|
type: 'range',
|
|
@@ -2843,6 +3494,7 @@
|
|
|
2843
3494
|
value: form.fuzzyThreshold,
|
|
2844
3495
|
onChange: (event) => setForm({ ...form, fuzzyThreshold: event.target.value }),
|
|
2845
3496
|
}),
|
|
3497
|
+
h('p', { className: 'cait-fieldhint' }, t('modal.settings.fuzzyHint')),
|
|
2846
3498
|
),
|
|
2847
3499
|
h(
|
|
2848
3500
|
'label',
|
|
@@ -2938,22 +3590,114 @@
|
|
|
2938
3590
|
),
|
|
2939
3591
|
h(
|
|
2940
3592
|
'div',
|
|
2941
|
-
{ className: 'cait-
|
|
2942
|
-
h('
|
|
2943
|
-
h(
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
3593
|
+
{ className: 'cait-settings-section' },
|
|
3594
|
+
h('h4', { className: 'cait-settings-title' }, t('modal.settings.review')),
|
|
3595
|
+
h(
|
|
3596
|
+
'label',
|
|
3597
|
+
{ className: 'cait-checkline' },
|
|
3598
|
+
h('input', {
|
|
3599
|
+
type: 'checkbox',
|
|
3600
|
+
checked: reviewInherited,
|
|
3601
|
+
onChange: (event) => {
|
|
3602
|
+
if (event.target.checked) {
|
|
3603
|
+
setForm((current) => ({
|
|
3604
|
+
...current,
|
|
3605
|
+
review: { ...current.review, provider: '', model: '' },
|
|
3606
|
+
}))
|
|
3607
|
+
return
|
|
3608
|
+
}
|
|
3609
|
+
const first = routes.find((item) => item.id !== 'mock' && item.hasModels !== false) || routes[0]
|
|
3610
|
+
setForm((current) => ({
|
|
3611
|
+
...current,
|
|
3612
|
+
review: {
|
|
3613
|
+
...current.review,
|
|
3614
|
+
provider: first?.id || '',
|
|
3615
|
+
model: first?.models?.[0]?.id || '',
|
|
3616
|
+
},
|
|
3617
|
+
}))
|
|
3618
|
+
},
|
|
3619
|
+
}),
|
|
3620
|
+
t('modal.settings.reviewInherit'),
|
|
3621
|
+
),
|
|
3622
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('modal.settings.reviewHint')),
|
|
3623
|
+
reviewInherited
|
|
3624
|
+
? null
|
|
3625
|
+
: h(
|
|
3626
|
+
'div',
|
|
3627
|
+
{ className: 'cait-grid2' },
|
|
3628
|
+
h(
|
|
3629
|
+
'label',
|
|
3630
|
+
{ className: 'cait-field' },
|
|
3631
|
+
h('span', null, t('modal.settings.reviewProvider')),
|
|
3632
|
+
h(
|
|
3633
|
+
'select',
|
|
3634
|
+
{
|
|
3635
|
+
className: 'cait-input',
|
|
3636
|
+
value: form.review.provider,
|
|
3637
|
+
onChange: (event) => selectReviewProvider(event.target.value),
|
|
3638
|
+
},
|
|
3639
|
+
routes.map((item) =>
|
|
3640
|
+
h(
|
|
3641
|
+
'option',
|
|
3642
|
+
{ key: item.id, value: item.id, disabled: item.hasModels === false },
|
|
3643
|
+
item.hasModels === false
|
|
3644
|
+
? `${item.name} (${item.id}) - ${t('modal.settings.noModels')}`
|
|
3645
|
+
: `${item.name} (${item.id})`,
|
|
3646
|
+
),
|
|
3647
|
+
),
|
|
3648
|
+
form.review.provider && !reviewRoute
|
|
3649
|
+
? h('option', { value: form.review.provider }, t('modal.settings.routeMissing'))
|
|
3650
|
+
: null,
|
|
3651
|
+
),
|
|
3652
|
+
),
|
|
3653
|
+
h(
|
|
3654
|
+
'label',
|
|
3655
|
+
{ className: 'cait-field' },
|
|
3656
|
+
h('span', null, t('modal.settings.model')),
|
|
3657
|
+
reviewModels.length
|
|
3658
|
+
? h(
|
|
3659
|
+
'select',
|
|
3660
|
+
{
|
|
3661
|
+
className: 'cait-input',
|
|
3662
|
+
value: form.review.model,
|
|
3663
|
+
onChange: (event) => setReview({ model: event.target.value }),
|
|
3664
|
+
},
|
|
3665
|
+
reviewModels.map((model) =>
|
|
3666
|
+
h('option', { key: model.id, value: model.id }, `${model.name} (${model.id})`),
|
|
3667
|
+
),
|
|
3668
|
+
)
|
|
3669
|
+
: h('input', {
|
|
3670
|
+
className: 'cait-input',
|
|
3671
|
+
value: form.review.model,
|
|
3672
|
+
readOnly: true,
|
|
3673
|
+
placeholder: t('modal.settings.noModels'),
|
|
3674
|
+
}),
|
|
3675
|
+
),
|
|
3676
|
+
),
|
|
3677
|
+
h(
|
|
3678
|
+
'p',
|
|
3679
|
+
{ className: 'cait-mt-hint', style: { marginTop: 0 } },
|
|
3680
|
+
reviewInherited ? t('modal.settings.reviewInheritedHint') : t('modal.settings.reviewCustomHint'),
|
|
3681
|
+
),
|
|
3682
|
+
),
|
|
3683
|
+
h(
|
|
3684
|
+
'div',
|
|
3685
|
+
{ className: 'cait-modal-actions' },
|
|
3686
|
+
h('button', { type: 'button', className: 'cait-btn cait-btn-ghost', onClick: onClose }, t('modal.settings.cancel')),
|
|
3687
|
+
h('button', { type: 'submit', className: 'cait-btn cait-btn-primary', disabled: Boolean(busy) }, busy === 'settings' ? h(Spinner) : null, t('modal.settings.save')),
|
|
3688
|
+
),
|
|
3689
|
+
),
|
|
3690
|
+
)
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
function ConfirmModal({ t, state, busy, onClose, onConfirm }) {
|
|
3694
|
+
return h(
|
|
3695
|
+
Modal,
|
|
3696
|
+
{ title: state.title || t('modal.confirm.title'), onClose },
|
|
3697
|
+
h('p', { className: 'cait-confirm-text' }, state.message),
|
|
3698
|
+
h(
|
|
3699
|
+
'div',
|
|
3700
|
+
{ className: 'cait-modal-actions' },
|
|
2957
3701
|
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
2958
3702
|
h('button', { className: 'cait-btn cait-btn-danger', onClick: onConfirm, disabled: Boolean(busy) }, busy ? h(Spinner) : null, state.confirmLabel || t('modal.confirm.ok')),
|
|
2959
3703
|
),
|
|
@@ -2988,6 +3732,614 @@
|
|
|
2988
3732
|
)
|
|
2989
3733
|
}
|
|
2990
3734
|
|
|
3735
|
+
function BriefModal({ t, doc, busy, onClose, onSave }) {
|
|
3736
|
+
const initial = doc.brief || {}
|
|
3737
|
+
const [form, setForm] = useState({
|
|
3738
|
+
domain: initial.domain || '',
|
|
3739
|
+
audience: initial.audience || '',
|
|
3740
|
+
tone: initial.tone || '',
|
|
3741
|
+
notes: initial.notes || '',
|
|
3742
|
+
doNotTranslate: initial.doNotTranslate || '',
|
|
3743
|
+
styleSample: initial.styleSample || '',
|
|
3744
|
+
})
|
|
3745
|
+
const field = (key, label, placeholder, rows) =>
|
|
3746
|
+
h(
|
|
3747
|
+
'label',
|
|
3748
|
+
{ className: 'cait-field' },
|
|
3749
|
+
h('span', null, t(label)),
|
|
3750
|
+
rows
|
|
3751
|
+
? h('textarea', {
|
|
3752
|
+
className: 'cait-textarea',
|
|
3753
|
+
rows,
|
|
3754
|
+
value: form[key],
|
|
3755
|
+
placeholder: t(placeholder),
|
|
3756
|
+
onChange: (event) => setForm({ ...form, [key]: event.target.value }),
|
|
3757
|
+
})
|
|
3758
|
+
: h('input', {
|
|
3759
|
+
className: 'cait-input',
|
|
3760
|
+
value: form[key],
|
|
3761
|
+
placeholder: t(placeholder),
|
|
3762
|
+
onChange: (event) => setForm({ ...form, [key]: event.target.value }),
|
|
3763
|
+
}),
|
|
3764
|
+
)
|
|
3765
|
+
return h(
|
|
3766
|
+
Modal,
|
|
3767
|
+
{ title: t('brief.title'), onClose, wide: true },
|
|
3768
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('brief.hint')),
|
|
3769
|
+
h(
|
|
3770
|
+
'div',
|
|
3771
|
+
{ className: 'cait-briefform' },
|
|
3772
|
+
h('div', { className: 'cait-grid2' }, field('domain', 'brief.domain', 'brief.placeholder.domain'), field('audience', 'brief.audience', 'brief.placeholder.audience')),
|
|
3773
|
+
field('tone', 'brief.tone', 'brief.placeholder.tone'),
|
|
3774
|
+
field('notes', 'brief.notes', 'brief.placeholder.notes', 3),
|
|
3775
|
+
field('doNotTranslate', 'brief.doNotTranslate', 'brief.placeholder.doNotTranslate', 3),
|
|
3776
|
+
field('styleSample', 'brief.styleSample', 'brief.placeholder.styleSample', 4),
|
|
3777
|
+
),
|
|
3778
|
+
h(
|
|
3779
|
+
'div',
|
|
3780
|
+
{ className: 'cait-modal-actions' },
|
|
3781
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
3782
|
+
h(
|
|
3783
|
+
'button',
|
|
3784
|
+
{ className: 'cait-btn cait-btn-primary', onClick: () => onSave(form), disabled: Boolean(busy) },
|
|
3785
|
+
busy ? h(Spinner) : null,
|
|
3786
|
+
t('brief.save'),
|
|
3787
|
+
),
|
|
3788
|
+
),
|
|
3789
|
+
)
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
function NoteModal({ t, doc, index, segment, notes, busy, onAdd, onUpdate, onDelete, onClose }) {
|
|
3793
|
+
const [text, setText] = useState('')
|
|
3794
|
+
const [editingId, setEditingId] = useState('')
|
|
3795
|
+
const [editingText, setEditingText] = useState('')
|
|
3796
|
+
const list = Array.isArray(notes) ? notes : []
|
|
3797
|
+
const submit = () => {
|
|
3798
|
+
const value = text.trim()
|
|
3799
|
+
if (!value || busy) return
|
|
3800
|
+
onAdd(value)
|
|
3801
|
+
setText('')
|
|
3802
|
+
}
|
|
3803
|
+
return h(
|
|
3804
|
+
Modal,
|
|
3805
|
+
{ title: `${t('notes.title')} · #${index + 1}`, onClose, wide: true },
|
|
3806
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('notes.hint')),
|
|
3807
|
+
h('div', { className: 'cait-notelist' }, segment?.source || ''),
|
|
3808
|
+
list.length
|
|
3809
|
+
? h(
|
|
3810
|
+
'div',
|
|
3811
|
+
{ className: 'cait-notelist' },
|
|
3812
|
+
list.map((note) =>
|
|
3813
|
+
h(
|
|
3814
|
+
'div',
|
|
3815
|
+
{ className: 'cait-noteitem', key: note.id },
|
|
3816
|
+
h(
|
|
3817
|
+
'div',
|
|
3818
|
+
{ className: 'cait-notehead' },
|
|
3819
|
+
h('span', { className: 'cait-noteauthor' }, note.author === 'agent' ? 'Agent' : t('notes.user')),
|
|
3820
|
+
h('span', { className: 'cait-notetime' }, String(note.createdAt || '').replace('T', ' ').slice(0, 16)),
|
|
3821
|
+
),
|
|
3822
|
+
editingId === note.id
|
|
3823
|
+
? h(
|
|
3824
|
+
'div',
|
|
3825
|
+
null,
|
|
3826
|
+
h('textarea', {
|
|
3827
|
+
className: 'cait-input cait-textarea',
|
|
3828
|
+
value: editingText,
|
|
3829
|
+
rows: 3,
|
|
3830
|
+
onChange: (event) => setEditingText(event.target.value),
|
|
3831
|
+
}),
|
|
3832
|
+
h(
|
|
3833
|
+
'div',
|
|
3834
|
+
{ className: 'cait-noteactions' },
|
|
3835
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: () => setEditingId('') }, t('notes.cancel')),
|
|
3836
|
+
h(
|
|
3837
|
+
'button',
|
|
3838
|
+
{
|
|
3839
|
+
className: 'cait-btn cait-btn-primary',
|
|
3840
|
+
disabled: Boolean(busy) || !editingText.trim(),
|
|
3841
|
+
onClick: () => {
|
|
3842
|
+
onUpdate(note.id, editingText)
|
|
3843
|
+
setEditingId('')
|
|
3844
|
+
},
|
|
3845
|
+
},
|
|
3846
|
+
t('notes.save'),
|
|
3847
|
+
),
|
|
3848
|
+
),
|
|
3849
|
+
)
|
|
3850
|
+
: h(
|
|
3851
|
+
'div',
|
|
3852
|
+
null,
|
|
3853
|
+
h('div', { className: 'cait-notetext' }, note.text),
|
|
3854
|
+
h(
|
|
3855
|
+
'div',
|
|
3856
|
+
{ className: 'cait-noteactions' },
|
|
3857
|
+
h(
|
|
3858
|
+
'button',
|
|
3859
|
+
{
|
|
3860
|
+
className: 'cait-minibtn',
|
|
3861
|
+
onClick: () => {
|
|
3862
|
+
setEditingId(note.id)
|
|
3863
|
+
setEditingText(note.text)
|
|
3864
|
+
},
|
|
3865
|
+
},
|
|
3866
|
+
t('notes.edit'),
|
|
3867
|
+
),
|
|
3868
|
+
h(
|
|
3869
|
+
'button',
|
|
3870
|
+
{ className: 'cait-minibtn cait-danger', onClick: () => onDelete(note.id) },
|
|
3871
|
+
t('notes.delete'),
|
|
3872
|
+
),
|
|
3873
|
+
),
|
|
3874
|
+
),
|
|
3875
|
+
),
|
|
3876
|
+
),
|
|
3877
|
+
)
|
|
3878
|
+
: h('p', { className: 'cait-mt-hint' }, t('notes.empty')),
|
|
3879
|
+
h(
|
|
3880
|
+
'div',
|
|
3881
|
+
{ className: 'cait-noteadd' },
|
|
3882
|
+
h('textarea', {
|
|
3883
|
+
className: 'cait-input cait-textarea',
|
|
3884
|
+
value: text,
|
|
3885
|
+
rows: 3,
|
|
3886
|
+
placeholder: t('notes.placeholder'),
|
|
3887
|
+
onChange: (event) => setText(event.target.value),
|
|
3888
|
+
}),
|
|
3889
|
+
h(
|
|
3890
|
+
'div',
|
|
3891
|
+
{ className: 'cait-noteactions' },
|
|
3892
|
+
h('button', { className: 'cait-btn cait-btn-primary', disabled: Boolean(busy) || !text.trim(), onClick: submit }, busy ? h(Spinner) : null, t('notes.add')),
|
|
3893
|
+
),
|
|
3894
|
+
),
|
|
3895
|
+
)
|
|
3896
|
+
}
|
|
3897
|
+
|
|
3898
|
+
function ReplaceModal({ t, doc, busy, preview, undoCount, onPreview, onApply, onUndo, onClose }) {
|
|
3899
|
+
const queryRef = useRef(null)
|
|
3900
|
+
const [form, setForm] = useState({
|
|
3901
|
+
query: '',
|
|
3902
|
+
replacement: '',
|
|
3903
|
+
scope: 'target',
|
|
3904
|
+
matchCase: false,
|
|
3905
|
+
regex: false,
|
|
3906
|
+
})
|
|
3907
|
+
useEffect(() => {
|
|
3908
|
+
if (queryRef.current) queryRef.current.focus()
|
|
3909
|
+
}, [])
|
|
3910
|
+
const set = (key, value) => setForm((current) => ({ ...current, [key]: value }))
|
|
3911
|
+
return h(
|
|
3912
|
+
Modal,
|
|
3913
|
+
{ title: t('replace.title'), onClose, wide: true },
|
|
3914
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('replace.hint')),
|
|
3915
|
+
h(
|
|
3916
|
+
'div',
|
|
3917
|
+
{ className: 'cait-replaceform' },
|
|
3918
|
+
h(
|
|
3919
|
+
'label',
|
|
3920
|
+
{ className: 'cait-field' },
|
|
3921
|
+
h('span', null, t('replace.query')),
|
|
3922
|
+
h('input', {
|
|
3923
|
+
ref: queryRef,
|
|
3924
|
+
className: 'cait-input',
|
|
3925
|
+
value: form.query,
|
|
3926
|
+
placeholder: t('replace.queryPlaceholder'),
|
|
3927
|
+
onChange: (event) => set('query', event.target.value),
|
|
3928
|
+
}),
|
|
3929
|
+
),
|
|
3930
|
+
h(
|
|
3931
|
+
'label',
|
|
3932
|
+
{ className: 'cait-field' },
|
|
3933
|
+
h('span', null, t('replace.replacement')),
|
|
3934
|
+
h('input', {
|
|
3935
|
+
className: 'cait-input',
|
|
3936
|
+
value: form.replacement,
|
|
3937
|
+
placeholder: t('replace.replacementPlaceholder'),
|
|
3938
|
+
onChange: (event) => set('replacement', event.target.value),
|
|
3939
|
+
}),
|
|
3940
|
+
),
|
|
3941
|
+
h(
|
|
3942
|
+
'div',
|
|
3943
|
+
{ className: 'cait-replaceoptions' },
|
|
3944
|
+
h(
|
|
3945
|
+
'label',
|
|
3946
|
+
{ className: 'cait-field' },
|
|
3947
|
+
h('span', null, t('replace.scope')),
|
|
3948
|
+
h(
|
|
3949
|
+
'select',
|
|
3950
|
+
{
|
|
3951
|
+
className: 'cait-input',
|
|
3952
|
+
value: form.scope,
|
|
3953
|
+
onChange: (event) => set('scope', event.target.value),
|
|
3954
|
+
},
|
|
3955
|
+
h('option', { value: 'target' }, t('replace.scope.target')),
|
|
3956
|
+
h('option', { value: 'source' }, t('replace.scope.source')),
|
|
3957
|
+
h('option', { value: 'both' }, t('replace.scope.both')),
|
|
3958
|
+
),
|
|
3959
|
+
),
|
|
3960
|
+
h(
|
|
3961
|
+
'label',
|
|
3962
|
+
{ className: 'cait-check' },
|
|
3963
|
+
h('input', {
|
|
3964
|
+
type: 'checkbox',
|
|
3965
|
+
checked: form.matchCase,
|
|
3966
|
+
onChange: (event) => set('matchCase', event.target.checked),
|
|
3967
|
+
}),
|
|
3968
|
+
t('replace.matchCase'),
|
|
3969
|
+
),
|
|
3970
|
+
h(
|
|
3971
|
+
'label',
|
|
3972
|
+
{ className: 'cait-check' },
|
|
3973
|
+
h('input', {
|
|
3974
|
+
type: 'checkbox',
|
|
3975
|
+
checked: form.regex,
|
|
3976
|
+
onChange: (event) => set('regex', event.target.checked),
|
|
3977
|
+
}),
|
|
3978
|
+
t('replace.regex'),
|
|
3979
|
+
),
|
|
3980
|
+
),
|
|
3981
|
+
),
|
|
3982
|
+
preview
|
|
3983
|
+
? h(
|
|
3984
|
+
'div',
|
|
3985
|
+
{ className: 'cait-replaceresults' },
|
|
3986
|
+
h(
|
|
3987
|
+
'div',
|
|
3988
|
+
{ className: 'cait-replacecounts' },
|
|
3989
|
+
t('replace.results')
|
|
3990
|
+
.replace('{count}', String(preview.total))
|
|
3991
|
+
.replace('{segments}', String(preview.matchedSegments || 0)),
|
|
3992
|
+
),
|
|
3993
|
+
preview.truncated ? h('p', { className: 'cait-mt-hint' }, t('replace.truncated')) : null,
|
|
3994
|
+
preview.matches?.length
|
|
3995
|
+
? h(
|
|
3996
|
+
'div',
|
|
3997
|
+
{ className: 'cait-replacelist' },
|
|
3998
|
+
preview.matches.slice(0, 50).map((match, index) =>
|
|
3999
|
+
h(
|
|
4000
|
+
'div',
|
|
4001
|
+
{ className: 'cait-replaceitem', key: `${match.uid}-${match.field}-${index}` },
|
|
4002
|
+
h(
|
|
4003
|
+
'div',
|
|
4004
|
+
{ className: 'cait-replacehead' },
|
|
4005
|
+
h('span', null, `#${match.index + 1}`),
|
|
4006
|
+
h('span', { className: 'cait-replacefield' }, t(`replace.field.${match.field}`)),
|
|
4007
|
+
h('span', { className: 'cait-replacecount' }, `×${match.count}`),
|
|
4008
|
+
),
|
|
4009
|
+
h('div', { className: 'cait-replacebefore' }, match.before),
|
|
4010
|
+
h('div', { className: 'cait-replaceafter' }, match.after),
|
|
4011
|
+
),
|
|
4012
|
+
),
|
|
4013
|
+
)
|
|
4014
|
+
: h('p', { className: 'cait-mt-hint' }, t('replace.noResults')),
|
|
4015
|
+
)
|
|
4016
|
+
: null,
|
|
4017
|
+
h(
|
|
4018
|
+
'div',
|
|
4019
|
+
{ className: 'cait-modal-actions' },
|
|
4020
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
4021
|
+
undoCount
|
|
4022
|
+
? h('button', { className: 'cait-btn', onClick: onUndo, disabled: Boolean(busy) }, t('replace.undo'))
|
|
4023
|
+
: null,
|
|
4024
|
+
h('button', { className: 'cait-btn', onClick: () => onPreview(form), disabled: Boolean(busy) || !form.query }, t('replace.preview')),
|
|
4025
|
+
h(
|
|
4026
|
+
'button',
|
|
4027
|
+
{ className: 'cait-btn cait-btn-primary', onClick: () => onApply(form), disabled: Boolean(busy) || !form.query || !preview?.total },
|
|
4028
|
+
busy ? h(Spinner) : null,
|
|
4029
|
+
t('replace.apply'),
|
|
4030
|
+
),
|
|
4031
|
+
),
|
|
4032
|
+
)
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
function CommandPalette({ t, items, onClose, onRun }) {
|
|
4036
|
+
const [query, setQuery] = useState('')
|
|
4037
|
+
const [active, setActive] = useState(0)
|
|
4038
|
+
const inputRef = useRef(null)
|
|
4039
|
+
useEffect(() => {
|
|
4040
|
+
if (inputRef.current) inputRef.current.focus()
|
|
4041
|
+
}, [])
|
|
4042
|
+
const needle = query.trim().toLowerCase()
|
|
4043
|
+
const filtered = items.filter((item) => {
|
|
4044
|
+
if (!needle) return true
|
|
4045
|
+
return `${item.title} ${item.hint || ''}`.toLowerCase().includes(needle)
|
|
4046
|
+
})
|
|
4047
|
+
useEffect(() => {
|
|
4048
|
+
setActive(0)
|
|
4049
|
+
}, [query])
|
|
4050
|
+
const choose = (item) => {
|
|
4051
|
+
if (item) onRun(item)
|
|
4052
|
+
}
|
|
4053
|
+
return h(
|
|
4054
|
+
Modal,
|
|
4055
|
+
{ title: t('palette.title'), onClose, wide: true },
|
|
4056
|
+
h('input', {
|
|
4057
|
+
ref: inputRef,
|
|
4058
|
+
className: 'cait-input cait-palette-input',
|
|
4059
|
+
value: query,
|
|
4060
|
+
placeholder: t('palette.placeholder'),
|
|
4061
|
+
onChange: (event) => setQuery(event.target.value),
|
|
4062
|
+
onKeyDown: (event) => {
|
|
4063
|
+
if (event.key === 'ArrowDown') {
|
|
4064
|
+
event.preventDefault()
|
|
4065
|
+
setActive((value) => Math.min(value + 1, Math.max(0, filtered.length - 1)))
|
|
4066
|
+
} else if (event.key === 'ArrowUp') {
|
|
4067
|
+
event.preventDefault()
|
|
4068
|
+
setActive((value) => Math.max(value - 1, 0))
|
|
4069
|
+
} else if (event.key === 'Enter') {
|
|
4070
|
+
event.preventDefault()
|
|
4071
|
+
choose(filtered[active])
|
|
4072
|
+
}
|
|
4073
|
+
},
|
|
4074
|
+
}),
|
|
4075
|
+
h(
|
|
4076
|
+
'div',
|
|
4077
|
+
{ className: 'cait-palette-list' },
|
|
4078
|
+
filtered.length
|
|
4079
|
+
? filtered.map((item, index) =>
|
|
4080
|
+
h(
|
|
4081
|
+
'button',
|
|
4082
|
+
{
|
|
4083
|
+
type: 'button',
|
|
4084
|
+
key: item.id,
|
|
4085
|
+
className: `cait-palette-item${index === active ? ' active' : ''}`,
|
|
4086
|
+
onMouseEnter: () => setActive(index),
|
|
4087
|
+
onClick: () => choose(item),
|
|
4088
|
+
},
|
|
4089
|
+
h('span', { className: 'cait-palette-label' }, item.title),
|
|
4090
|
+
item.hint ? h('span', { className: 'cait-palette-hint' }, item.hint) : null,
|
|
4091
|
+
),
|
|
4092
|
+
)
|
|
4093
|
+
: h('p', { className: 'cait-mt-hint' }, t('palette.empty')),
|
|
4094
|
+
),
|
|
4095
|
+
h('p', { className: 'cait-palette-foot' }, t('palette.footer')),
|
|
4096
|
+
)
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
function ResourceModal({ t, doc, libraries, mounts, busy, onClose, onSave }) {
|
|
4100
|
+
const resources = doc.resources || {}
|
|
4101
|
+
const [inheritTm, setInheritTm] = useState(resources.inheritsTm !== false)
|
|
4102
|
+
const [inheritGlossary, setInheritGlossary] = useState(resources.inheritsGlossary !== false)
|
|
4103
|
+
const [tmIds, setTmIds] = useState(
|
|
4104
|
+
() => new Set(resources.tmIds ?? resources.effectiveTmIds ?? []),
|
|
4105
|
+
)
|
|
4106
|
+
const [glossaryIds, setGlossaryIds] = useState(
|
|
4107
|
+
() => new Set(resources.glossaryIds ?? resources.effectiveGlossaryIds ?? []),
|
|
4108
|
+
)
|
|
4109
|
+
const [defaultWriteTmId, setDefaultWriteTmId] = useState(resources.defaultWriteTmId || '')
|
|
4110
|
+
|
|
4111
|
+
const allTm = libraries?.tm || []
|
|
4112
|
+
const allGlossary = libraries?.glossary || []
|
|
4113
|
+
const selectedTm = inheritTm ? mounts?.tmIds || [] : Array.from(tmIds)
|
|
4114
|
+
const selectedGlossary = inheritGlossary ? mounts?.glossaryIds || [] : Array.from(glossaryIds)
|
|
4115
|
+
const tmChoices = allTm.filter((library) => selectedTm.includes(library.tmId))
|
|
4116
|
+
const checkedTmIds = new Set(selectedTm)
|
|
4117
|
+
const checkedGlossaryIds = new Set(selectedGlossary)
|
|
4118
|
+
const defaultTm = tmChoices.some((library) => library.tmId === defaultWriteTmId)
|
|
4119
|
+
? defaultWriteTmId
|
|
4120
|
+
: ''
|
|
4121
|
+
|
|
4122
|
+
const toggleId = (setter, id) => {
|
|
4123
|
+
setter((current) => {
|
|
4124
|
+
const next = new Set(current)
|
|
4125
|
+
if (next.has(id)) next.delete(id)
|
|
4126
|
+
else next.add(id)
|
|
4127
|
+
return next
|
|
4128
|
+
})
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
const librarySection = (label, all, idKey, checkedIds, inherit, setInherit, toggle, inheritLabel) =>
|
|
4132
|
+
h(
|
|
4133
|
+
'div',
|
|
4134
|
+
{ className: 'cait-resource-section' },
|
|
4135
|
+
h(
|
|
4136
|
+
'div',
|
|
4137
|
+
{ className: 'cait-resource-head' },
|
|
4138
|
+
h('strong', null, t(label)),
|
|
4139
|
+
h(
|
|
4140
|
+
'label',
|
|
4141
|
+
{ className: 'cait-checkline' },
|
|
4142
|
+
h('input', {
|
|
4143
|
+
type: 'checkbox',
|
|
4144
|
+
checked: inherit,
|
|
4145
|
+
onChange: (event) => setInherit(event.target.checked),
|
|
4146
|
+
}),
|
|
4147
|
+
t(inheritLabel),
|
|
4148
|
+
),
|
|
4149
|
+
),
|
|
4150
|
+
h(
|
|
4151
|
+
'div',
|
|
4152
|
+
{ className: 'cait-resource-list' },
|
|
4153
|
+
all.length === 0
|
|
4154
|
+
? h('p', { className: 'cait-meta' }, t('resources.empty'))
|
|
4155
|
+
: all.map((library) =>
|
|
4156
|
+
h(
|
|
4157
|
+
'label',
|
|
4158
|
+
{ key: library[idKey], className: 'cait-resource-row' },
|
|
4159
|
+
h('input', {
|
|
4160
|
+
type: 'checkbox',
|
|
4161
|
+
checked: checkedIds.has(library[idKey]),
|
|
4162
|
+
disabled: inherit,
|
|
4163
|
+
onChange: () => toggle(library[idKey]),
|
|
4164
|
+
}),
|
|
4165
|
+
h('span', { className: 'cait-resource-name' }, library.name),
|
|
4166
|
+
h('span', { className: 'cait-meta' }, library[idKey]),
|
|
4167
|
+
),
|
|
4168
|
+
),
|
|
4169
|
+
),
|
|
4170
|
+
)
|
|
4171
|
+
|
|
4172
|
+
return h(
|
|
4173
|
+
Modal,
|
|
4174
|
+
{ title: t('resources.title'), onClose, wide: true },
|
|
4175
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('resources.hint')),
|
|
4176
|
+
h(
|
|
4177
|
+
'div',
|
|
4178
|
+
{ className: 'cait-briefform' },
|
|
4179
|
+
librarySection(
|
|
4180
|
+
'resources.tm',
|
|
4181
|
+
allTm,
|
|
4182
|
+
'tmId',
|
|
4183
|
+
checkedTmIds,
|
|
4184
|
+
inheritTm,
|
|
4185
|
+
setInheritTm,
|
|
4186
|
+
(id) => toggleId(setTmIds, id),
|
|
4187
|
+
'resources.inheritTm',
|
|
4188
|
+
),
|
|
4189
|
+
librarySection(
|
|
4190
|
+
'resources.glossary',
|
|
4191
|
+
allGlossary,
|
|
4192
|
+
'glossaryId',
|
|
4193
|
+
checkedGlossaryIds,
|
|
4194
|
+
inheritGlossary,
|
|
4195
|
+
setInheritGlossary,
|
|
4196
|
+
(id) => toggleId(setGlossaryIds, id),
|
|
4197
|
+
'resources.inheritGlossary',
|
|
4198
|
+
),
|
|
4199
|
+
h(
|
|
4200
|
+
'label',
|
|
4201
|
+
{ className: 'cait-field' },
|
|
4202
|
+
h('span', null, t('resources.defaultWrite')),
|
|
4203
|
+
h(
|
|
4204
|
+
'select',
|
|
4205
|
+
{
|
|
4206
|
+
className: 'cait-input',
|
|
4207
|
+
value: defaultTm,
|
|
4208
|
+
onChange: (event) => setDefaultWriteTmId(event.target.value),
|
|
4209
|
+
},
|
|
4210
|
+
h('option', { value: '' }, t(inheritTm ? 'resources.followDefault' : 'resources.firstBound')),
|
|
4211
|
+
tmChoices.map((library) => h('option', { key: library.tmId, value: library.tmId }, library.name)),
|
|
4212
|
+
),
|
|
4213
|
+
),
|
|
4214
|
+
),
|
|
4215
|
+
h(
|
|
4216
|
+
'div',
|
|
4217
|
+
{ className: 'cait-modal-actions' },
|
|
4218
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
|
|
4219
|
+
h(
|
|
4220
|
+
'button',
|
|
4221
|
+
{
|
|
4222
|
+
className: 'cait-btn cait-btn-primary',
|
|
4223
|
+
disabled: Boolean(busy),
|
|
4224
|
+
onClick: () =>
|
|
4225
|
+
onSave({
|
|
4226
|
+
tmIds: inheritTm ? null : Array.from(tmIds),
|
|
4227
|
+
glossaryIds: inheritGlossary ? null : Array.from(glossaryIds),
|
|
4228
|
+
defaultWriteTmId: defaultTm || null,
|
|
4229
|
+
}),
|
|
4230
|
+
},
|
|
4231
|
+
busy ? h(Spinner) : null,
|
|
4232
|
+
t('resources.save'),
|
|
4233
|
+
),
|
|
4234
|
+
),
|
|
4235
|
+
)
|
|
4236
|
+
}
|
|
4237
|
+
|
|
4238
|
+
function AnalysisModal({ t, doc, analysis, busy, onClose }) {
|
|
4239
|
+
const download = (format) => {
|
|
4240
|
+
if (!analysis) return
|
|
4241
|
+
const pairs = [
|
|
4242
|
+
['segments', analysis.segments],
|
|
4243
|
+
['words', analysis.words],
|
|
4244
|
+
['characters', analysis.characters],
|
|
4245
|
+
['duplicates.groups', analysis.duplicates.groups],
|
|
4246
|
+
['duplicates.segments', analysis.duplicates.segments],
|
|
4247
|
+
['translated', analysis.translated],
|
|
4248
|
+
['confirmed', analysis.confirmed],
|
|
4249
|
+
['ice', analysis.buckets.ice],
|
|
4250
|
+
['exact', analysis.buckets.exact],
|
|
4251
|
+
['fuzzy85', analysis.buckets.fuzzy85],
|
|
4252
|
+
['fuzzy70', analysis.buckets.fuzzy70],
|
|
4253
|
+
['new', analysis.buckets.new],
|
|
4254
|
+
['matched.segments', analysis.matched.segments],
|
|
4255
|
+
['matched.percent', analysis.matched.percent],
|
|
4256
|
+
['leverage.percent', analysis.leverage.percent],
|
|
4257
|
+
['generatedAt', analysis.generatedAt],
|
|
4258
|
+
]
|
|
4259
|
+
const content =
|
|
4260
|
+
format === 'json'
|
|
4261
|
+
? JSON.stringify(analysis, null, 2)
|
|
4262
|
+
: ['metric,value', ...pairs.map(([key, value]) => `${key},${value}`)].join('\n')
|
|
4263
|
+
const type = format === 'json' ? 'application/json' : 'text/csv'
|
|
4264
|
+
const blob = new Blob([content], { type })
|
|
4265
|
+
const url = URL.createObjectURL(blob)
|
|
4266
|
+
const anchor = document.createElement('a')
|
|
4267
|
+
anchor.href = url
|
|
4268
|
+
anchor.download = `${analysis.name || 'document'}-analysis.${format}`
|
|
4269
|
+
anchor.click()
|
|
4270
|
+
URL.revokeObjectURL(url)
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
const rows = analysis
|
|
4274
|
+
? [
|
|
4275
|
+
['analysis.segments', analysis.segments],
|
|
4276
|
+
['analysis.words', analysis.words],
|
|
4277
|
+
['analysis.characters', analysis.characters],
|
|
4278
|
+
['analysis.duplicates', `${analysis.duplicates.segments} / ${analysis.duplicates.groups}`],
|
|
4279
|
+
['analysis.translated', analysis.translated],
|
|
4280
|
+
['analysis.confirmed', analysis.confirmed],
|
|
4281
|
+
]
|
|
4282
|
+
: []
|
|
4283
|
+
const buckets = analysis
|
|
4284
|
+
? [
|
|
4285
|
+
['analysis.ice', analysis.buckets.ice],
|
|
4286
|
+
['analysis.exact', analysis.buckets.exact],
|
|
4287
|
+
['analysis.fuzzy85', analysis.buckets.fuzzy85],
|
|
4288
|
+
['analysis.fuzzy70', analysis.buckets.fuzzy70],
|
|
4289
|
+
['analysis.new', analysis.buckets.new],
|
|
4290
|
+
]
|
|
4291
|
+
: []
|
|
4292
|
+
|
|
4293
|
+
return h(
|
|
4294
|
+
Modal,
|
|
4295
|
+
{ title: t('analysis.title'), onClose, wide: true },
|
|
4296
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('analysis.hint')),
|
|
4297
|
+
busy || !analysis
|
|
4298
|
+
? h('div', { className: 'cait-empty' }, h(Spinner))
|
|
4299
|
+
: h(
|
|
4300
|
+
'div',
|
|
4301
|
+
{ className: 'cait-analysis' },
|
|
4302
|
+
h(
|
|
4303
|
+
'div',
|
|
4304
|
+
{ className: 'cait-analysis-grid' },
|
|
4305
|
+
rows.map(([label, value]) =>
|
|
4306
|
+
h(
|
|
4307
|
+
'div',
|
|
4308
|
+
{ key: label, className: 'cait-analysis-stat' },
|
|
4309
|
+
h('span', null, t(label)),
|
|
4310
|
+
h('strong', null, String(value)),
|
|
4311
|
+
),
|
|
4312
|
+
),
|
|
4313
|
+
),
|
|
4314
|
+
h(
|
|
4315
|
+
'div',
|
|
4316
|
+
{ className: 'cait-analysis-grid' },
|
|
4317
|
+
buckets.map(([label, value]) =>
|
|
4318
|
+
h(
|
|
4319
|
+
'div',
|
|
4320
|
+
{ key: label, className: 'cait-analysis-stat' },
|
|
4321
|
+
h('span', null, t(label)),
|
|
4322
|
+
h('strong', null, String(value)),
|
|
4323
|
+
),
|
|
4324
|
+
),
|
|
4325
|
+
),
|
|
4326
|
+
h(
|
|
4327
|
+
'div',
|
|
4328
|
+
{ className: 'cait-analysis-total' },
|
|
4329
|
+
h('span', null, `${t('analysis.matchRate')} ${analysis.matched.percent}%`),
|
|
4330
|
+
h('span', null, `${t('analysis.leverage')} ${analysis.leverage.percent}%`),
|
|
4331
|
+
),
|
|
4332
|
+
),
|
|
4333
|
+
h(
|
|
4334
|
+
'div',
|
|
4335
|
+
{ className: 'cait-modal-actions' },
|
|
4336
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('analysis.close')),
|
|
4337
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: () => download('csv'), disabled: Boolean(busy) || !analysis }, t('analysis.exportCsv')),
|
|
4338
|
+
h('button', { className: 'cait-btn cait-btn-primary', onClick: () => download('json'), disabled: Boolean(busy) || !analysis }, t('analysis.exportJson')),
|
|
4339
|
+
),
|
|
4340
|
+
)
|
|
4341
|
+
}
|
|
4342
|
+
|
|
2991
4343
|
function Workbench({ i18n, translate }) {
|
|
2992
4344
|
const [data, setData] = useState(null)
|
|
2993
4345
|
const [loading, setLoading] = useState(true)
|
|
@@ -3011,6 +4363,18 @@
|
|
|
3011
4363
|
}
|
|
3012
4364
|
const [pre, setPre] = useState({ running: false, done: 0, total: 0, filled: 0, failed: 0 })
|
|
3013
4365
|
const [qa, setQa] = useState(null)
|
|
4366
|
+
const [review, setReview] = useState(null)
|
|
4367
|
+
const [reviewBusy, setReviewBusy] = useState(false)
|
|
4368
|
+
const [analysis, setAnalysis] = useState(null)
|
|
4369
|
+
const [analysisBusy, setAnalysisBusy] = useState(false)
|
|
4370
|
+
const [replacePreview, setReplacePreview] = useState(null)
|
|
4371
|
+
const [replaceUndo, setReplaceUndo] = useState(null)
|
|
4372
|
+
const [replaceBusy, setReplaceBusy] = useState(false)
|
|
4373
|
+
const [notes, setNotes] = useState(null)
|
|
4374
|
+
const [notesBusy, setNotesBusy] = useState(false)
|
|
4375
|
+
const [concordance, setConcordance] = useState(null)
|
|
4376
|
+
const [concordanceQuery, setConcordanceQuery] = useState('')
|
|
4377
|
+
const [concordanceBusy, setConcordanceBusy] = useState(false)
|
|
3014
4378
|
const [tmQuery, setTmQuery] = useState('')
|
|
3015
4379
|
const [tmResults, setTmResults] = useState(null)
|
|
3016
4380
|
const [tmRefresh, setTmRefresh] = useState(0)
|
|
@@ -3027,16 +4391,20 @@
|
|
|
3027
4391
|
const busyRef = useRef(false)
|
|
3028
4392
|
const rootRef = useRef(null)
|
|
3029
4393
|
const unitRef = useRef(0)
|
|
4394
|
+
const draftHistoryRef = useRef({ index: -1, stack: [], cursor: 0, lastAt: 0 })
|
|
3030
4395
|
|
|
3031
4396
|
const doc = data?.activeDocument || null
|
|
3032
4397
|
const segments = doc?.segments || []
|
|
3033
4398
|
const index = doc ? Math.min(selected, Math.max(0, segments.length - 1)) : 0
|
|
3034
4399
|
const segment = segments[index] || null
|
|
3035
4400
|
const detail = doc?.current?.index === index ? doc.current : null
|
|
4401
|
+
useEffect(() => {
|
|
4402
|
+
draftHistoryRef.current = { index, stack: [draft], cursor: 0, lastAt: 0 }
|
|
4403
|
+
}, [doc?.id, index])
|
|
3036
4404
|
const settings = data?.settings || {
|
|
3037
4405
|
sourceLang: 'en',
|
|
3038
4406
|
targetLang: 'zh-CN',
|
|
3039
|
-
fuzzyThreshold:
|
|
4407
|
+
fuzzyThreshold: DEFAULT_FUZZY_THRESHOLD,
|
|
3040
4408
|
tmLimit: 5,
|
|
3041
4409
|
uiLang: 'auto',
|
|
3042
4410
|
mt: { provider: 'mock', model: 'cait-mock', temperature: 0.2 },
|
|
@@ -3259,6 +4627,10 @@
|
|
|
3259
4627
|
}
|
|
3260
4628
|
}, [data?.activeDocument?.id, data?.revision])
|
|
3261
4629
|
|
|
4630
|
+
useEffect(() => {
|
|
4631
|
+
setReview(null)
|
|
4632
|
+
}, [data?.activeDocument?.id])
|
|
4633
|
+
|
|
3262
4634
|
useEffect(() => {
|
|
3263
4635
|
let alive = true
|
|
3264
4636
|
rpc('listMtRoutes')
|
|
@@ -3284,6 +4656,11 @@
|
|
|
3284
4656
|
setTmQuery('')
|
|
3285
4657
|
setTmResults(null)
|
|
3286
4658
|
setTmError('')
|
|
4659
|
+
setConcordance(null)
|
|
4660
|
+
setConcordanceQuery('')
|
|
4661
|
+
setReplacePreview(null)
|
|
4662
|
+
setReplaceUndo(null)
|
|
4663
|
+
setNotes(null)
|
|
3287
4664
|
}, [doc?.id])
|
|
3288
4665
|
|
|
3289
4666
|
useEffect(() => {
|
|
@@ -3321,7 +4698,8 @@
|
|
|
3321
4698
|
const saveDraft = async (silent = true) => {
|
|
3322
4699
|
if (!doc || !dirty) return true
|
|
3323
4700
|
try {
|
|
3324
|
-
await rpc('saveDraft', { documentId: doc.id, index: selected, target: draft })
|
|
4701
|
+
const result = await rpc('saveDraft', { documentId: doc.id, index: selected, target: draft })
|
|
4702
|
+
if (result.state) adoptState(result.state)
|
|
3325
4703
|
setDirty(false)
|
|
3326
4704
|
if (!silent) notify(t('toast.draftSaved'))
|
|
3327
4705
|
return true
|
|
@@ -3348,6 +4726,49 @@
|
|
|
3348
4726
|
}
|
|
3349
4727
|
}
|
|
3350
4728
|
|
|
4729
|
+
const changeDraft = (value) => {
|
|
4730
|
+
const history = draftHistoryRef.current
|
|
4731
|
+
if (history.index !== index) {
|
|
4732
|
+
history.index = index
|
|
4733
|
+
history.stack = [draft]
|
|
4734
|
+
history.cursor = 0
|
|
4735
|
+
}
|
|
4736
|
+
if (history.stack[history.cursor] !== value) {
|
|
4737
|
+
const now = Date.now()
|
|
4738
|
+
if (now - history.lastAt < 650 && history.cursor > 0) {
|
|
4739
|
+
history.stack[history.cursor] = value
|
|
4740
|
+
} else {
|
|
4741
|
+
history.stack = history.stack.slice(0, history.cursor + 1)
|
|
4742
|
+
history.stack.push(value)
|
|
4743
|
+
if (history.stack.length > 200) history.stack.shift()
|
|
4744
|
+
history.cursor = history.stack.length - 1
|
|
4745
|
+
}
|
|
4746
|
+
history.lastAt = now
|
|
4747
|
+
}
|
|
4748
|
+
setDraft(value)
|
|
4749
|
+
setDirty(true)
|
|
4750
|
+
}
|
|
4751
|
+
|
|
4752
|
+
const undoDraft = () => {
|
|
4753
|
+
const history = draftHistoryRef.current
|
|
4754
|
+
if (history.cursor <= 0) return
|
|
4755
|
+
history.cursor -= 1
|
|
4756
|
+
history.lastAt = 0
|
|
4757
|
+
const value = history.stack[history.cursor]
|
|
4758
|
+
setDraft(value)
|
|
4759
|
+
setDirty(value !== (segment?.target || ''))
|
|
4760
|
+
}
|
|
4761
|
+
|
|
4762
|
+
const redoDraft = () => {
|
|
4763
|
+
const history = draftHistoryRef.current
|
|
4764
|
+
if (history.cursor >= history.stack.length - 1) return
|
|
4765
|
+
history.cursor += 1
|
|
4766
|
+
history.lastAt = 0
|
|
4767
|
+
const value = history.stack[history.cursor]
|
|
4768
|
+
setDraft(value)
|
|
4769
|
+
setDirty(value !== (segment?.target || ''))
|
|
4770
|
+
}
|
|
4771
|
+
|
|
3351
4772
|
const confirmSegment = async () => {
|
|
3352
4773
|
if (!doc || busy) return
|
|
3353
4774
|
if (!draft.trim()) {
|
|
@@ -3502,6 +4923,10 @@
|
|
|
3502
4923
|
}
|
|
3503
4924
|
|
|
3504
4925
|
const applyTerm = (term) => {
|
|
4926
|
+
if (term?.forbidden) {
|
|
4927
|
+
notify(t('toast.termForbidden'), 'error')
|
|
4928
|
+
return
|
|
4929
|
+
}
|
|
3505
4930
|
const separator = /^(zh|ja|ko)/i.test(doc?.targetLang || '') ? '' : ' '
|
|
3506
4931
|
setDraft((current) => {
|
|
3507
4932
|
const value = current.replace(/\s+$/, '')
|
|
@@ -3511,6 +4936,368 @@
|
|
|
3511
4936
|
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
3512
4937
|
}
|
|
3513
4938
|
|
|
4939
|
+
const unconfirmCurrent = async () => {
|
|
4940
|
+
if (!doc || busy) return
|
|
4941
|
+
setBusy('unconfirm')
|
|
4942
|
+
try {
|
|
4943
|
+
if (dirty) {
|
|
4944
|
+
const saved = await saveDraft(true)
|
|
4945
|
+
if (!saved) return
|
|
4946
|
+
}
|
|
4947
|
+
const result = await rpc('unconfirmSegment', { documentId: doc.id, index: selected })
|
|
4948
|
+
adoptState(result.state)
|
|
4949
|
+
notify(t('toast.unconfirmed'))
|
|
4950
|
+
} catch (error) {
|
|
4951
|
+
// Saving a dirty confirmed segment already revokes the confirmation.
|
|
4952
|
+
// Treat the second call as idempotent instead of showing an error.
|
|
4953
|
+
if (error?.code === 'NOT_CONFIRMED') notify(t('toast.unconfirmed'))
|
|
4954
|
+
else report(error)
|
|
4955
|
+
} finally {
|
|
4956
|
+
setBusy('')
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
|
|
4960
|
+
const toggleLock = async () => {
|
|
4961
|
+
if (!doc || busy || !segment) return
|
|
4962
|
+
setBusy('lock')
|
|
4963
|
+
try {
|
|
4964
|
+
await rpc('setSegmentLocked', { documentId: doc.id, index: selected, locked: !segment.locked })
|
|
4965
|
+
const next = await rpc('bootstrap')
|
|
4966
|
+
adoptState(next, selected)
|
|
4967
|
+
notify(t(segment.locked ? 'toast.unlocked' : 'toast.locked'))
|
|
4968
|
+
} catch (error) {
|
|
4969
|
+
report(error)
|
|
4970
|
+
} finally {
|
|
4971
|
+
setBusy('')
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4975
|
+
const mergeCurrent = () => {
|
|
4976
|
+
if (!doc || !segment || busy) return
|
|
4977
|
+
openConfirm({
|
|
4978
|
+
title: t('modal.confirm.mergeTitle'),
|
|
4979
|
+
message: t('modal.confirm.merge'),
|
|
4980
|
+
confirmLabel: t('editor.merge'),
|
|
4981
|
+
action: async () => {
|
|
4982
|
+
const result = await rpc('mergeSegments', { documentId: doc.id, index: selected, count: 2 })
|
|
4983
|
+
adoptState(result.state, result.index)
|
|
4984
|
+
notify(t('toast.merged'))
|
|
4985
|
+
},
|
|
4986
|
+
})
|
|
4987
|
+
}
|
|
4988
|
+
|
|
4989
|
+
const splitCurrent = (at) => {
|
|
4990
|
+
if (!doc || !segment || busy) return
|
|
4991
|
+
openConfirm({
|
|
4992
|
+
title: t('modal.confirm.splitTitle'),
|
|
4993
|
+
message: t('modal.confirm.split'),
|
|
4994
|
+
confirmLabel: t('editor.split'),
|
|
4995
|
+
action: async () => {
|
|
4996
|
+
const result = await rpc('splitSegment', { documentId: doc.id, index: selected, at })
|
|
4997
|
+
adoptState(result.state, result.index)
|
|
4998
|
+
notify(t('toast.split'))
|
|
4999
|
+
},
|
|
5000
|
+
})
|
|
5001
|
+
}
|
|
5002
|
+
|
|
5003
|
+
const mergeCue = () => {
|
|
5004
|
+
if (!doc || !segment || busy) return
|
|
5005
|
+
openConfirm({
|
|
5006
|
+
title: t('modal.confirm.mergeTitle'),
|
|
5007
|
+
message: t('modal.confirm.mergeCue'),
|
|
5008
|
+
confirmLabel: t('editor.mergeCue'),
|
|
5009
|
+
action: async () => {
|
|
5010
|
+
const result = await rpc('mergeCues', { documentId: doc.id, index: selected, count: 2 })
|
|
5011
|
+
adoptState(result.state, result.index)
|
|
5012
|
+
notify(t('toast.cuesMerged'))
|
|
5013
|
+
},
|
|
5014
|
+
})
|
|
5015
|
+
}
|
|
5016
|
+
|
|
5017
|
+
const splitCueAt = (at) => {
|
|
5018
|
+
if (!doc || !segment || busy) return
|
|
5019
|
+
openConfirm({
|
|
5020
|
+
title: t('modal.confirm.splitTitle'),
|
|
5021
|
+
message: t('modal.confirm.splitCue'),
|
|
5022
|
+
confirmLabel: t('editor.splitCue'),
|
|
5023
|
+
action: async () => {
|
|
5024
|
+
const result = await rpc('splitCue', { documentId: doc.id, index: selected, at })
|
|
5025
|
+
adoptState(result.state, result.index)
|
|
5026
|
+
notify(t('toast.cueSplit'))
|
|
5027
|
+
},
|
|
5028
|
+
})
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
const shiftSubtitles = (offsetMs) => {
|
|
5032
|
+
if (!doc || !offsetMs || busy) return
|
|
5033
|
+
openConfirm({
|
|
5034
|
+
title: t('modal.confirm.shiftTitle'),
|
|
5035
|
+
message: t('modal.confirm.shiftCue'),
|
|
5036
|
+
confirmLabel: t('editor.shift'),
|
|
5037
|
+
action: async () => {
|
|
5038
|
+
const result = await rpc('shiftSubtitles', { documentId: doc.id, offsetMs })
|
|
5039
|
+
adoptState(result.state)
|
|
5040
|
+
notify(t('toast.subtitlesShifted'))
|
|
5041
|
+
},
|
|
5042
|
+
})
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
const saveBrief = async (brief) => {
|
|
5046
|
+
if (!doc || busy) return false
|
|
5047
|
+
setBusy('brief')
|
|
5048
|
+
try {
|
|
5049
|
+
const result = await rpc('setBrief', { documentId: doc.id, brief })
|
|
5050
|
+
adoptState(result.state)
|
|
5051
|
+
setModal(null)
|
|
5052
|
+
notify(t('toast.briefSaved'))
|
|
5053
|
+
return true
|
|
5054
|
+
} catch (error) {
|
|
5055
|
+
report(error)
|
|
5056
|
+
return false
|
|
5057
|
+
} finally {
|
|
5058
|
+
setBusy('')
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
5061
|
+
|
|
5062
|
+
const saveResources = async (resources) => {
|
|
5063
|
+
if (!doc || busy) return false
|
|
5064
|
+
setBusy('resources')
|
|
5065
|
+
try {
|
|
5066
|
+
const result = await rpc('setDocumentResources', { documentId: doc.id, ...resources })
|
|
5067
|
+
adoptState(result.state)
|
|
5068
|
+
setModal(null)
|
|
5069
|
+
notify(t('toast.resourcesSaved'))
|
|
5070
|
+
return true
|
|
5071
|
+
} catch (error) {
|
|
5072
|
+
report(error)
|
|
5073
|
+
return false
|
|
5074
|
+
} finally {
|
|
5075
|
+
setBusy('')
|
|
5076
|
+
}
|
|
5077
|
+
}
|
|
5078
|
+
|
|
5079
|
+
const openAnalysis = async () => {
|
|
5080
|
+
if (!doc) return
|
|
5081
|
+
setAnalysis(null)
|
|
5082
|
+
setModal({ type: 'analysis' })
|
|
5083
|
+
setAnalysisBusy(true)
|
|
5084
|
+
try {
|
|
5085
|
+
setAnalysis(await rpc('analyzeDocument', { documentId: doc.id }))
|
|
5086
|
+
} catch (error) {
|
|
5087
|
+
report(error)
|
|
5088
|
+
} finally {
|
|
5089
|
+
setAnalysisBusy(false)
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
|
|
5093
|
+
const openReplace = () => {
|
|
5094
|
+
if (!doc) return
|
|
5095
|
+
setReplacePreview(null)
|
|
5096
|
+
setModal({ type: 'replace' })
|
|
5097
|
+
}
|
|
5098
|
+
|
|
5099
|
+
const previewReplace = async (form) => {
|
|
5100
|
+
if (!doc || replaceBusy) return
|
|
5101
|
+
setReplaceBusy(true)
|
|
5102
|
+
try {
|
|
5103
|
+
setReplacePreview(await rpc('replaceInDocument', { documentId: doc.id, ...form }))
|
|
5104
|
+
} catch (error) {
|
|
5105
|
+
report(error)
|
|
5106
|
+
} finally {
|
|
5107
|
+
setReplaceBusy(false)
|
|
5108
|
+
}
|
|
5109
|
+
}
|
|
5110
|
+
|
|
5111
|
+
const applyReplace = async (form) => {
|
|
5112
|
+
if (!doc || replaceBusy) return
|
|
5113
|
+
setReplaceBusy(true)
|
|
5114
|
+
try {
|
|
5115
|
+
const result = await rpc('replaceInDocument', { documentId: doc.id, ...form, apply: true })
|
|
5116
|
+
setReplaceUndo(Array.isArray(result.undo) && result.undo.length ? result.undo : null)
|
|
5117
|
+
setReplacePreview(null)
|
|
5118
|
+
adoptState(result.state, result.undo?.[0]?.index)
|
|
5119
|
+
notify(t('toast.replaced').replace('{count}', String(result.applied ?? 0)))
|
|
5120
|
+
} catch (error) {
|
|
5121
|
+
report(error)
|
|
5122
|
+
} finally {
|
|
5123
|
+
setReplaceBusy(false)
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
|
|
5127
|
+
const undoReplace = async () => {
|
|
5128
|
+
if (!doc || !replaceUndo || replaceBusy) return
|
|
5129
|
+
setReplaceBusy(true)
|
|
5130
|
+
try {
|
|
5131
|
+
const result = await rpc('undoReplace', { documentId: doc.id, undo: replaceUndo })
|
|
5132
|
+
adoptState(result.state, replaceUndo[0]?.index)
|
|
5133
|
+
setReplaceUndo(null)
|
|
5134
|
+
setReplacePreview(null)
|
|
5135
|
+
notify(t('toast.replaceUndone'))
|
|
5136
|
+
} catch (error) {
|
|
5137
|
+
report(error)
|
|
5138
|
+
} finally {
|
|
5139
|
+
setReplaceBusy(false)
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
|
|
5143
|
+
const openNotes = async () => {
|
|
5144
|
+
if (!doc) return
|
|
5145
|
+
setNotes(null)
|
|
5146
|
+
setNotesBusy(true)
|
|
5147
|
+
setModal({ type: 'notes' })
|
|
5148
|
+
try {
|
|
5149
|
+
const result = await rpc('notesForSegment', { documentId: doc.id, index })
|
|
5150
|
+
setNotes(result.notes)
|
|
5151
|
+
} catch (error) {
|
|
5152
|
+
report(error)
|
|
5153
|
+
} finally {
|
|
5154
|
+
setNotesBusy(false)
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
|
|
5158
|
+
const addNote = async (text) => {
|
|
5159
|
+
if (!doc || notesBusy) return
|
|
5160
|
+
setNotesBusy(true)
|
|
5161
|
+
try {
|
|
5162
|
+
const result = await rpc('addSegmentNote', { documentId: doc.id, index, text })
|
|
5163
|
+
setNotes(result.notes)
|
|
5164
|
+
adoptState(result.state, index)
|
|
5165
|
+
notify(t('toast.noteSaved'))
|
|
5166
|
+
} catch (error) {
|
|
5167
|
+
report(error)
|
|
5168
|
+
} finally {
|
|
5169
|
+
setNotesBusy(false)
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
|
|
5173
|
+
const updateNote = async (noteId, text) => {
|
|
5174
|
+
if (!doc || notesBusy) return
|
|
5175
|
+
setNotesBusy(true)
|
|
5176
|
+
try {
|
|
5177
|
+
const result = await rpc('updateSegmentNote', { documentId: doc.id, index, noteId, text })
|
|
5178
|
+
setNotes(result.notes)
|
|
5179
|
+
adoptState(result.state, index)
|
|
5180
|
+
notify(t('toast.noteSaved'))
|
|
5181
|
+
} catch (error) {
|
|
5182
|
+
report(error)
|
|
5183
|
+
} finally {
|
|
5184
|
+
setNotesBusy(false)
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
|
|
5188
|
+
const deleteNote = async (noteId) => {
|
|
5189
|
+
if (!doc || notesBusy) return
|
|
5190
|
+
setNotesBusy(true)
|
|
5191
|
+
try {
|
|
5192
|
+
const result = await rpc('deleteSegmentNote', { documentId: doc.id, index, noteId })
|
|
5193
|
+
setNotes(result.notes)
|
|
5194
|
+
adoptState(result.state, index)
|
|
5195
|
+
notify(t('toast.noteDeleted'))
|
|
5196
|
+
} catch (error) {
|
|
5197
|
+
report(error)
|
|
5198
|
+
} finally {
|
|
5199
|
+
setNotesBusy(false)
|
|
5200
|
+
}
|
|
5201
|
+
}
|
|
5202
|
+
|
|
5203
|
+
const refreshQa = async () => { if (!doc) return
|
|
5204
|
+
setQa(null)
|
|
5205
|
+
try {
|
|
5206
|
+
setQa(await rpc('getQaReport', { documentId: doc.id }))
|
|
5207
|
+
} catch {
|
|
5208
|
+
// QA is advisory; a failure must not disturb the editor.
|
|
5209
|
+
}
|
|
5210
|
+
}
|
|
5211
|
+
|
|
5212
|
+
const jumpQa = (nextIndex) => {
|
|
5213
|
+
if (Number.isInteger(nextIndex)) chooseSegment(nextIndex)
|
|
5214
|
+
}
|
|
5215
|
+
|
|
5216
|
+
const runReview = async () => {
|
|
5217
|
+
if (!doc || reviewBusy) return
|
|
5218
|
+
setReviewBusy(true)
|
|
5219
|
+
try {
|
|
5220
|
+
const result = await rpc('reviewDocument', {
|
|
5221
|
+
documentId: doc.id,
|
|
5222
|
+
from: 0,
|
|
5223
|
+
count: 20,
|
|
5224
|
+
})
|
|
5225
|
+
setReview(result || { issues: [] })
|
|
5226
|
+
} catch (error) {
|
|
5227
|
+
setReview(null)
|
|
5228
|
+
report(error)
|
|
5229
|
+
} finally {
|
|
5230
|
+
setReviewBusy(false)
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
|
|
5234
|
+
const applyReview = (issue) => {
|
|
5235
|
+
if (!doc || !issue || !Number.isInteger(issue.index)) return
|
|
5236
|
+
chooseSegment(issue.index)
|
|
5237
|
+
if (issue.suggestion) {
|
|
5238
|
+
setDraft(issue.suggestion)
|
|
5239
|
+
setDirty(true)
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
const applyQaFix = async (item, action) => {
|
|
5244
|
+
if (!doc || busy || !item) return
|
|
5245
|
+
const segment = doc.segments[item.index]
|
|
5246
|
+
if (!segment) return
|
|
5247
|
+
let target = segment.target || ''
|
|
5248
|
+
if (action === 'copy-source') {
|
|
5249
|
+
target = segment.source
|
|
5250
|
+
} else if (action === 'clear') {
|
|
5251
|
+
target = ''
|
|
5252
|
+
} else if (action === 'term') {
|
|
5253
|
+
const term = String(item.termTarget || '')
|
|
5254
|
+
if (!term || target.includes(term)) return
|
|
5255
|
+
const separator = /[A-Za-z0-9]$/.test(target) && /^[A-Za-z0-9]/.test(term) ? ' ' : ''
|
|
5256
|
+
target = `${target}${separator}${term}`
|
|
5257
|
+
} else {
|
|
5258
|
+
return
|
|
5259
|
+
}
|
|
5260
|
+
setBusy('qa-fix')
|
|
5261
|
+
try {
|
|
5262
|
+
const result = await rpc('saveDraft', {
|
|
5263
|
+
documentId: doc.id,
|
|
5264
|
+
index: item.index,
|
|
5265
|
+
target,
|
|
5266
|
+
})
|
|
5267
|
+
if (result.state) adoptState(result.state, item.index)
|
|
5268
|
+
setQa(await rpc('getQaReport', { documentId: doc.id }))
|
|
5269
|
+
notify(t('toast.qaFixed'))
|
|
5270
|
+
} catch (error) {
|
|
5271
|
+
report(error)
|
|
5272
|
+
} finally {
|
|
5273
|
+
setBusy('')
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
|
|
5277
|
+
const runConcordance = async () => {
|
|
5278
|
+
const query = concordanceQuery.trim()
|
|
5279
|
+
if (!doc || !query) {
|
|
5280
|
+
setConcordance(null)
|
|
5281
|
+
return
|
|
5282
|
+
}
|
|
5283
|
+
setConcordanceBusy(true)
|
|
5284
|
+
try {
|
|
5285
|
+
setConcordance(await rpc('concordance', { documentId: doc.id, query, limit: 50 }))
|
|
5286
|
+
} catch (error) {
|
|
5287
|
+
report(error)
|
|
5288
|
+
} finally {
|
|
5289
|
+
setConcordanceBusy(false)
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
const applyConcordance = (entry) => {
|
|
5294
|
+
if (!entry?.target) return
|
|
5295
|
+
setDraft(entry.target)
|
|
5296
|
+
setDirty(true)
|
|
5297
|
+
setMt(null)
|
|
5298
|
+
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
5299
|
+
}
|
|
5300
|
+
|
|
3514
5301
|
const editTm = async ({ id, target }) => {
|
|
3515
5302
|
setBusy('tm-edit')
|
|
3516
5303
|
try {
|
|
@@ -3576,6 +5363,40 @@
|
|
|
3576
5363
|
})
|
|
3577
5364
|
}
|
|
3578
5365
|
|
|
5366
|
+
const clearPretranslated = async () => {
|
|
5367
|
+
const active = data?.activeDocument
|
|
5368
|
+
if (!active) return
|
|
5369
|
+
try {
|
|
5370
|
+
const preview = await rpc('getClearUnconfirmedPreview', { documentId: active.id })
|
|
5371
|
+
const counts = preview?.counts || {}
|
|
5372
|
+
const total = counts.total || 0
|
|
5373
|
+
if (!total) {
|
|
5374
|
+
notify(t('toast.nothingToClear'), 'info')
|
|
5375
|
+
return
|
|
5376
|
+
}
|
|
5377
|
+
setModal({
|
|
5378
|
+
type: 'confirm',
|
|
5379
|
+
title: t('modal.confirm.clearPretranslatedTitle'),
|
|
5380
|
+
message: t('modal.confirm.clearPretranslatedMessage')
|
|
5381
|
+
.replace('{count}', String(total))
|
|
5382
|
+
.replace('{mt}', String(counts.mt || 0))
|
|
5383
|
+
.replace('{tm}', String(counts.tm || 0))
|
|
5384
|
+
.replace('{auto}', String(counts.auto || 0)),
|
|
5385
|
+
confirmLabel: t('modal.confirm.clearPretranslatedConfirm').replace('{count}', String(total)),
|
|
5386
|
+
action: async () => {
|
|
5387
|
+
const result = await rpc('clearUnconfirmedTranslations', {
|
|
5388
|
+
documentId: active.id,
|
|
5389
|
+
confirm: true,
|
|
5390
|
+
})
|
|
5391
|
+
adoptState(result.state, result.cursor)
|
|
5392
|
+
notify(t('toast.pretranslatedCleared').replace('{count}', String(result.cleared)))
|
|
5393
|
+
},
|
|
5394
|
+
})
|
|
5395
|
+
} catch (error) {
|
|
5396
|
+
report(error)
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
|
|
3579
5400
|
const addTerm = async (form) => { setBusy('term')
|
|
3580
5401
|
try {
|
|
3581
5402
|
const result = await rpc('addGlossary', {
|
|
@@ -3765,6 +5586,32 @@
|
|
|
3765
5586
|
}
|
|
3766
5587
|
}
|
|
3767
5588
|
|
|
5589
|
+
const importReview = (file) => {
|
|
5590
|
+
if (!doc || !file) return
|
|
5591
|
+
const reader = new FileReader()
|
|
5592
|
+
reader.onload = async () => {
|
|
5593
|
+
setBusy('review')
|
|
5594
|
+
try {
|
|
5595
|
+
const result = await rpc('applyXliffReview', {
|
|
5596
|
+
documentId: doc.id,
|
|
5597
|
+
text: String(reader.result || ''),
|
|
5598
|
+
})
|
|
5599
|
+
adoptState(result.state)
|
|
5600
|
+
notify(
|
|
5601
|
+
t('toast.reviewApplied')
|
|
5602
|
+
.replace('{applied}', String(result.applied))
|
|
5603
|
+
.replace('{unmatched}', String(result.unmatched + result.sourceMismatch)),
|
|
5604
|
+
)
|
|
5605
|
+
setModal(null)
|
|
5606
|
+
} catch (error) {
|
|
5607
|
+
report(error)
|
|
5608
|
+
} finally {
|
|
5609
|
+
setBusy('')
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
5612
|
+
reader.readAsText(file, 'utf-8')
|
|
5613
|
+
}
|
|
5614
|
+
|
|
3768
5615
|
const openConfirm = (state) => setModal({ type: 'confirm', ...state })
|
|
3769
5616
|
|
|
3770
5617
|
const clearTm = () =>
|
|
@@ -3819,6 +5666,7 @@
|
|
|
3819
5666
|
|
|
3820
5667
|
const openIds = new Set((data?.openDocuments || []).map((item) => item.id))
|
|
3821
5668
|
const closedDocuments = (data?.documents || []).filter((item) => !openIds.has(item.id))
|
|
5669
|
+
const qaRisk = (qa?.risks || []).find((item) => item.index === index) || null
|
|
3822
5670
|
|
|
3823
5671
|
const deleteClosedDocument = (document) =>
|
|
3824
5672
|
openConfirm({
|
|
@@ -3898,6 +5746,23 @@
|
|
|
3898
5746
|
}
|
|
3899
5747
|
}, [])
|
|
3900
5748
|
|
|
5749
|
+
useEffect(() => {
|
|
5750
|
+
const onKey = (event) => {
|
|
5751
|
+
const mod = event.metaKey || event.ctrlKey
|
|
5752
|
+
if (!mod) return
|
|
5753
|
+
const key = event.key.toLowerCase()
|
|
5754
|
+
if (key === 'k') {
|
|
5755
|
+
event.preventDefault()
|
|
5756
|
+
setModal((current) => (current?.type === 'palette' ? null : { type: 'palette' }))
|
|
5757
|
+
} else if (key === 'f' && doc && !modal) {
|
|
5758
|
+
event.preventDefault()
|
|
5759
|
+
openReplace()
|
|
5760
|
+
}
|
|
5761
|
+
}
|
|
5762
|
+
window.addEventListener('keydown', onKey)
|
|
5763
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
5764
|
+
}, [doc?.id, modal])
|
|
5765
|
+
|
|
3901
5766
|
if (loading) {
|
|
3902
5767
|
return h(
|
|
3903
5768
|
'div',
|
|
@@ -3925,6 +5790,44 @@
|
|
|
3925
5790
|
)
|
|
3926
5791
|
}
|
|
3927
5792
|
|
|
5793
|
+
const commands = [
|
|
5794
|
+
{ id: 'new', title: t('cmd.new'), hint: t('cmd.newHint'), run: () => setModal({ type: 'new', defaultMode: 'text' }) },
|
|
5795
|
+
{ id: 'import', title: t('cmd.import'), hint: '', run: () => setModal({ type: 'new', defaultMode: 'file' }) },
|
|
5796
|
+
{ id: 'export', title: t('cmd.export'), hint: '', docOnly: true, run: () => setModal({ type: 'export' }) },
|
|
5797
|
+
{ id: 'pretranslate', title: t('cmd.pretranslate'), hint: '', docOnly: true, run: () => runPreTranslate() },
|
|
5798
|
+
{ id: 'replace', title: t('cmd.replace'), hint: 'Ctrl+F', docOnly: true, run: () => openReplace() },
|
|
5799
|
+
{ id: 'brief', title: t('cmd.brief'), hint: '', docOnly: true, run: () => setModal({ type: 'brief' }) },
|
|
5800
|
+
{ id: 'resources', title: t('cmd.resources'), hint: '', docOnly: true, run: () => setModal({ type: 'resources' }) },
|
|
5801
|
+
{ id: 'analysis', title: t('cmd.analysis'), hint: '', docOnly: true, run: () => openAnalysis() },
|
|
5802
|
+
{ id: 'notes', title: t('cmd.notes'), hint: '', docOnly: true, run: () => openNotes() },
|
|
5803
|
+
{
|
|
5804
|
+
id: 'review',
|
|
5805
|
+
title: t('cmd.review'),
|
|
5806
|
+
hint: '',
|
|
5807
|
+
docOnly: true,
|
|
5808
|
+
run: () => {
|
|
5809
|
+
setTab('qa')
|
|
5810
|
+
runReview()
|
|
5811
|
+
},
|
|
5812
|
+
},
|
|
5813
|
+
{
|
|
5814
|
+
id: 'clear-pretranslated',
|
|
5815
|
+
title: t('cmd.clearPretranslated'),
|
|
5816
|
+
hint: '',
|
|
5817
|
+
docOnly: true,
|
|
5818
|
+
run: () => clearPretranslated(),
|
|
5819
|
+
},
|
|
5820
|
+
{ id: 'documents', title: t('cmd.documents'), hint: '', run: () => setView('documents') },
|
|
5821
|
+
{ id: 'libraries', title: t('cmd.libraries'), hint: '', run: () => setView('libraries') },
|
|
5822
|
+
{ id: 'align', title: t('cmd.align'), hint: '', run: () => setView('align') },
|
|
5823
|
+
{ id: 'settings', title: t('cmd.settings'), hint: '', run: () => setModal({ type: 'settings' }) },
|
|
5824
|
+
].filter((item) => !item.docOnly || doc)
|
|
5825
|
+
|
|
5826
|
+
const runCommand = (item) => {
|
|
5827
|
+
setModal(null)
|
|
5828
|
+
setTimeout(() => item.run(), 0)
|
|
5829
|
+
}
|
|
5830
|
+
|
|
3928
5831
|
return h(
|
|
3929
5832
|
'div',
|
|
3930
5833
|
{ className: 'cait-root', ref: rootRef },
|
|
@@ -3945,6 +5848,7 @@
|
|
|
3945
5848
|
pre,
|
|
3946
5849
|
onPreTranslate: runPreTranslate,
|
|
3947
5850
|
onCancelPreTranslate: cancelPreTranslate,
|
|
5851
|
+
onReplace: openReplace,
|
|
3948
5852
|
view,
|
|
3949
5853
|
onView: setView,
|
|
3950
5854
|
}),
|
|
@@ -3963,6 +5867,7 @@
|
|
|
3963
5867
|
onCreateLibrary: createLibrary,
|
|
3964
5868
|
onRenameLibrary: renameLibrary,
|
|
3965
5869
|
onDeleteLibrary: deleteLibrary,
|
|
5870
|
+
onSetMounts: setMounts,
|
|
3966
5871
|
})
|
|
3967
5872
|
: null,
|
|
3968
5873
|
view === 'align'
|
|
@@ -3980,7 +5885,7 @@
|
|
|
3980
5885
|
? h(
|
|
3981
5886
|
'div',
|
|
3982
5887
|
{ 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,
|
|
5888
|
+
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
5889
|
h(
|
|
3985
5890
|
'div',
|
|
3986
5891
|
{ className: `cait-main${doc ? '' : ' cait-main-empty'}` },
|
|
@@ -3992,18 +5897,32 @@
|
|
|
3992
5897
|
segment,
|
|
3993
5898
|
detail,
|
|
3994
5899
|
draft,
|
|
3995
|
-
setDraft:
|
|
3996
|
-
setDraft(value)
|
|
3997
|
-
setDirty(true)
|
|
3998
|
-
},
|
|
5900
|
+
setDraft: changeDraft,
|
|
3999
5901
|
dirty,
|
|
4000
5902
|
busy,
|
|
5903
|
+
canUndo: draftHistoryRef.current.cursor > 0,
|
|
5904
|
+
canRedo: draftHistoryRef.current.cursor < draftHistoryRef.current.stack.length - 1,
|
|
5905
|
+
onUndo: undoDraft,
|
|
5906
|
+
onRedo: redoDraft,
|
|
4001
5907
|
onConfirm: confirmSegment,
|
|
5908
|
+
onUnconfirm: unconfirmCurrent,
|
|
5909
|
+
onToggleLock: toggleLock,
|
|
5910
|
+
onMerge: mergeCurrent,
|
|
5911
|
+
onSplit: splitCurrent,
|
|
5912
|
+
onMergeCue: mergeCue,
|
|
5913
|
+
onSplitCue: splitCueAt,
|
|
5914
|
+
onShiftSubtitles: shiftSubtitles,
|
|
4002
5915
|
onNext: () => moveSegment(1),
|
|
4003
5916
|
onPrev: () => moveSegment(-1),
|
|
4004
5917
|
onSave: () => saveDraft(false),
|
|
4005
5918
|
onAdopt: adoptSuggestion,
|
|
4006
5919
|
onApplyTerm: applyTerm,
|
|
5920
|
+
onOpenBrief: () => setModal({ type: 'brief' }),
|
|
5921
|
+
onOpenResources: () => setModal({ type: 'resources' }),
|
|
5922
|
+
onOpenAnalysis: openAnalysis,
|
|
5923
|
+
onOpenNotes: openNotes,
|
|
5924
|
+
noteCount: segment?.notes?.length || 0,
|
|
5925
|
+
qaRisk,
|
|
4007
5926
|
textareaRef,
|
|
4008
5927
|
})
|
|
4009
5928
|
: h(EmptyState, { t, settings, busy, onCreate: createFromForm, onDemo: loadDemo }),
|
|
@@ -4044,7 +5963,20 @@
|
|
|
4044
5963
|
onCopy: copy,
|
|
4045
5964
|
onOpenSettings: () => setModal({ type: 'settings' }),
|
|
4046
5965
|
onSearchLibrary: openLibrary,
|
|
4047
|
-
|
|
5966
|
+
qa,
|
|
5967
|
+
onRefreshQa: refreshQa,
|
|
5968
|
+
onJumpQa: jumpQa,
|
|
5969
|
+
onFixQa: applyQaFix,
|
|
5970
|
+
review,
|
|
5971
|
+
reviewBusy,
|
|
5972
|
+
onReview: runReview,
|
|
5973
|
+
onApplyReview: applyReview,
|
|
5974
|
+
concordance,
|
|
5975
|
+
concordanceQuery,
|
|
5976
|
+
setConcordanceQuery,
|
|
5977
|
+
onConcordance: runConcordance,
|
|
5978
|
+
onApplyConcordance: applyConcordance,
|
|
5979
|
+
busy: busy || (concordanceBusy ? 'concordance' : ''),
|
|
4048
5980
|
})
|
|
4049
5981
|
: null,
|
|
4050
5982
|
),
|
|
@@ -4061,7 +5993,60 @@
|
|
|
4061
5993
|
})
|
|
4062
5994
|
: null,
|
|
4063
5995
|
modal?.type === 'export'
|
|
4064
|
-
? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs })
|
|
5996
|
+
? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs, onImportReview: importReview })
|
|
5997
|
+
: null,
|
|
5998
|
+
modal?.type === 'brief'
|
|
5999
|
+
? h(BriefModal, { t, doc, busy, onClose: () => setModal(null), onSave: saveBrief })
|
|
6000
|
+
: null,
|
|
6001
|
+
modal?.type === 'resources'
|
|
6002
|
+
? h(ResourceModal, {
|
|
6003
|
+
t,
|
|
6004
|
+
doc,
|
|
6005
|
+
libraries: data?.libraries,
|
|
6006
|
+
mounts: data?.mounts,
|
|
6007
|
+
busy,
|
|
6008
|
+
onClose: () => setModal(null),
|
|
6009
|
+
onSave: saveResources,
|
|
6010
|
+
})
|
|
6011
|
+
: null,
|
|
6012
|
+
modal?.type === 'analysis'
|
|
6013
|
+
? h(AnalysisModal, {
|
|
6014
|
+
t,
|
|
6015
|
+
doc,
|
|
6016
|
+
analysis,
|
|
6017
|
+
busy: analysisBusy,
|
|
6018
|
+
onClose: () => setModal(null),
|
|
6019
|
+
})
|
|
6020
|
+
: null,
|
|
6021
|
+
modal?.type === 'replace'
|
|
6022
|
+
? h(ReplaceModal, {
|
|
6023
|
+
t,
|
|
6024
|
+
doc,
|
|
6025
|
+
busy: replaceBusy,
|
|
6026
|
+
preview: replacePreview,
|
|
6027
|
+
undoCount: replaceUndo ? replaceUndo.length : 0,
|
|
6028
|
+
onPreview: previewReplace,
|
|
6029
|
+
onApply: applyReplace,
|
|
6030
|
+
onUndo: undoReplace,
|
|
6031
|
+
onClose: () => setModal(null),
|
|
6032
|
+
})
|
|
6033
|
+
: null,
|
|
6034
|
+
modal?.type === 'notes'
|
|
6035
|
+
? h(NoteModal, {
|
|
6036
|
+
t,
|
|
6037
|
+
doc,
|
|
6038
|
+
index,
|
|
6039
|
+
segment,
|
|
6040
|
+
notes,
|
|
6041
|
+
busy: notesBusy,
|
|
6042
|
+
onAdd: addNote,
|
|
6043
|
+
onUpdate: updateNote,
|
|
6044
|
+
onDelete: deleteNote,
|
|
6045
|
+
onClose: () => setModal(null),
|
|
6046
|
+
})
|
|
6047
|
+
: null,
|
|
6048
|
+
modal?.type === 'palette'
|
|
6049
|
+
? h(CommandPalette, { t, items: commands, onClose: () => setModal(null), onRun: runCommand })
|
|
4065
6050
|
: null,
|
|
4066
6051
|
modal?.type === 'settings'
|
|
4067
6052
|
? h(SettingsModal, { t, settings, mtRoutes, busy, onClose: () => setModal(null), onSave: saveSettings })
|