dsh-cait 1.1.3 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/client.js CHANGED
@@ -212,6 +212,18 @@ window.__ModuleLoader__.load({
212
212
  return number - 1
213
213
  }
214
214
 
215
+ // --- settings defaults ---------------------------------------------------
216
+ // Below ~70% a fuzzy match usually shares little more than function words,
217
+ // so it costs review time and saves none. The settings modal offers a reset
218
+ // button only while the slider sits away from this value.
219
+ const DEFAULT_FUZZY_THRESHOLD = 0.7
220
+
221
+ function fuzzyPercent(value) {
222
+ const number = Number(value)
223
+ if (!Number.isFinite(number)) return Math.round(DEFAULT_FUZZY_THRESHOLD * 100)
224
+ return Math.round(Math.min(1, Math.max(0, number)) * 100)
225
+ }
226
+
215
227
  const CSS = `
216
228
  .cait-root{--cait-u:1;position:relative;display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-family:inherit;font-size:13px;line-height:1.5;color-scheme:light}
217
229
  .cait-root[data-cait-theme="dark"]{color-scheme:dark}
@@ -297,6 +309,90 @@ window.__ModuleLoader__.load({
297
309
  .cait-dot.confirmed{background:var(--dsw-alias-state-success-primary)}
298
310
  .cait-dot.translated{background:var(--dsw-alias-button-primary-fill)}
299
311
  .cait-seg-warn{width:5px;height:5px;border-radius:50%;background:var(--dsw-alias-state-warn-primary);flex:0 0 auto}
312
+ .cait-seg-warn.high{background:var(--dsw-alias-state-error-primary)}
313
+ .cait-seg-warn.medium{background:var(--dsw-alias-state-warn-primary)}
314
+ .cait-seg-warn.low{background:var(--dsw-alias-label-secondary)}
315
+ .cait-seg-flags{display:flex;align-items:center;gap:5px;flex:0 0 auto}
316
+ .cait-seg-flags .cait-linkbtn{display:inline-flex;align-items:center;justify-content:center;gap:3px;height:21px;padding:0 5px;border-radius:6px;font-size:10px;line-height:1;white-space:nowrap;text-decoration:none}
317
+ .cait-seg-flags .cait-linkbtn:hover{background:var(--dsw-alias-bg-layer-1);text-decoration:none}
318
+ .cait-seg-flags .cait-linkbtn.active{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent);color:var(--dsw-alias-brand-primary)}
319
+ .cait-seg-flags .cait-linkbtn svg{flex:0 0 auto}
320
+ .cait-seg-flags .cait-badge{height:21px;padding:0 9px;line-height:1;font-size:10px}
321
+ .cait-seg-flags .cait-riskchip{height:18px}
322
+ .cait-seg-flags .cait-notecount{height:15px;min-width:15px;line-height:1}
323
+ .cait-riskchip{display:inline-flex;align-items:center;height:17px;padding:0 6px;border:1px solid transparent;border-radius:999px;font-size:9.5px;font-weight:650;letter-spacing:.01em}
324
+ .cait-riskchip.high{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 9%,transparent)}
325
+ .cait-riskchip.medium{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 38%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent)}
326
+ .cait-riskchip.low{color:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1)}
327
+ .cait-brieflink{height:20px;padding:0 4px;gap:3px;font-size:10px}
328
+ .cait-notelink{height:20px;padding:0 4px;gap:3px;font-size:10px}
329
+ .cait-notecount{display:inline-grid;place-items:center;min-width:14px;height:14px;padding:0 3px;border-radius:7px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 16%,transparent);color:var(--dsw-alias-brand-primary);font-size:9px}
330
+ .cait-notelist{display:flex;flex-direction:column;gap:7px;margin:10px 0 12px;font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-secondary)}
331
+ .cait-noteitem{padding:8px 9px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
332
+ .cait-notehead{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:4px}
333
+ .cait-noteauthor{font-size:10px;font-weight:650;color:var(--dsw-alias-label-primary)}
334
+ .cait-notetime{font-size:9.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
335
+ .cait-notetext{font-size:11.5px;line-height:1.6;color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere}
336
+ .cait-noteactions{display:flex;align-items:center;justify-content:flex-end;gap:5px;margin-top:5px}
337
+ .cait-noteactions .cait-minibtn{margin-left:0}
338
+ .cait-noteadd{display:flex;flex-direction:column;gap:7px;margin-top:8px}
339
+ .cait-noteadd .cait-textarea,.cait-noteitem .cait-textarea{min-height:64px;padding:7px 8px;border-radius:7px;font-size:11.5px;line-height:1.55}
340
+ .cait-minibtn.cait-danger:hover{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 32%,var(--dsw-alias-border-l1))}
341
+ .cait-briefdot{width:4px;height:4px;border-radius:50%;background:var(--dsw-alias-brand-primary)}
342
+ .cait-briefform{display:flex;flex-direction:column;gap:11px;margin:12px 0 4px}
343
+ .cait-resource-section{display:flex;flex-direction:column;gap:6px}
344
+ .cait-resource-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
345
+ .cait-resource-list{max-height:132px;overflow:auto;border:1px solid var(--dsw-alias-border-secondary);border-radius:6px;padding:4px 6px}
346
+ .cait-resource-row{display:flex;align-items:center;gap:7px;padding:3px 0;font-size:10.5px;cursor:pointer}
347
+ .cait-resource-row input{flex:0 0 auto}
348
+ .cait-resource-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-primary)}
349
+ .cait-analysis{display:flex;flex-direction:column;gap:10px;margin:8px 0}
350
+ .cait-analysis-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px}
351
+ .cait-analysis-stat{display:flex;flex-direction:column;gap:2px;padding:7px 9px;border:1px solid var(--dsw-alias-border-secondary);border-radius:6px;font-size:10.5px;color:var(--dsw-alias-label-secondary)}
352
+ .cait-analysis-stat strong{font-size:15px;color:var(--dsw-alias-label-primary);font-variant-numeric:tabular-nums}
353
+ .cait-analysis-total{display:flex;gap:14px;font-size:11px;color:var(--dsw-alias-label-secondary)}
354
+ .cait-replaceform{display:flex;flex-direction:column;gap:10px;margin:12px 0}
355
+ .cait-replaceoptions{display:flex;align-items:flex-end;flex-wrap:wrap;gap:14px}
356
+ .cait-replaceresults{margin-top:12px}
357
+ .cait-replacecounts{font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary)}
358
+ .cait-replacelist{max-height:220px;overflow:auto;margin-top:7px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px}
359
+ .cait-replaceitem{padding:7px 9px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:10.5px}
360
+ .cait-replaceitem:last-child{border-bottom:0}
361
+ .cait-replacehead{display:flex;align-items:center;gap:8px;margin-bottom:3px;color:var(--dsw-alias-label-secondary)}
362
+ .cait-replacefield{font-weight:600;color:var(--dsw-alias-label-primary)}
363
+ .cait-replacecount{margin-left:auto;font-variant-numeric:tabular-nums}
364
+ .cait-replacebefore,.cait-replaceafter{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--dsw-font-mono,monospace);line-height:1.5}
365
+ .cait-replacebefore{color:var(--dsw-alias-label-secondary);text-decoration:line-through}
366
+ .cait-replaceafter{color:var(--dsw-alias-label-primary)}
367
+ .cait-palette-input{margin:10px 0 7px}
368
+ .cait-palette-list{max-height:280px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:7px}
369
+ .cait-palette-item{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;padding:8px 10px;border:0;border-bottom:1px solid var(--dsw-alias-border-l1);background:transparent;color:var(--dsw-alias-label-primary);text-align:left;font-size:11px}
370
+ .cait-palette-item:last-child{border-bottom:0}
371
+ .cait-palette-item.active{background:var(--dsw-alias-bg-layer-1)}
372
+ .cait-palette-label{font-weight:600}
373
+ .cait-palette-hint{color:var(--dsw-alias-label-secondary);font-size:10px}
374
+ .cait-palette-foot{margin:8px 0 0;font-size:10px;color:var(--dsw-alias-label-secondary);text-align:right}
375
+ .cait-riskcounts{display:flex;flex-wrap:wrap;gap:6px;margin:2px 0 9px}
376
+ .cait-riskcount{display:inline-flex;align-items:center;height:20px;padding:0 7px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;font-size:10px;font-weight:600;color:var(--dsw-alias-label-secondary)}
377
+ .cait-riskcount.high{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent)}
378
+ .cait-riskcount.medium{color:var(--dsw-alias-state-warn-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 38%,transparent)}
379
+ .cait-riskrow{display:flex;align-items:flex-start;gap:8px;width:100%;margin-bottom:6px;padding:8px 8px;border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-label-secondary);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);text-align:left}
380
+ .cait-riskrow:hover{background:var(--dsw-alias-bg-layer-2)}
381
+ .cait-riskrow.high{border-left-color:var(--dsw-alias-state-error-primary)}
382
+ .cait-riskrow.medium{border-left-color:var(--dsw-alias-state-warn-primary)}
383
+ .cait-riskrow.low{border-left-color:var(--dsw-alias-label-secondary)}
384
+ .cait-riskrow-index{min-width:18px;font-size:10px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);padding-top:2px}
385
+ .cait-riskrow-body{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1}
386
+ .cait-riskrow-source{font-size:11px;line-height:1.4;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
387
+ .cait-riskrow-codes{font-size:9.5px;color:var(--dsw-alias-label-secondary)}
388
+ .cait-riskactions{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}
389
+ .cait-riskaction{padding:2px 6px;border:1px solid var(--dsw-alias-border-l1);border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:9.5px;cursor:pointer}
390
+ .cait-riskaction:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}
391
+ .cait-concordance-row{display:flex;flex-direction:column;gap:3px;width:100%;margin-bottom:6px;padding:8px 9px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);text-align:left}
392
+ .cait-concordance-row:hover{background:var(--dsw-alias-bg-layer-2)}
393
+ .cait-concordance-source{font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary)}
394
+ .cait-concordance-target{font-size:11.5px;line-height:1.45}
395
+ .cait-concordance-meta{font-size:9.5px;color:var(--dsw-alias-label-secondary)}
300
396
  .cait-qa-banner{display:flex;align-items:flex-start;gap:6px;margin-bottom:10px;padding:7px 9px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 45%,transparent);border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 10%,transparent);color:var(--dsw-alias-label-primary);font-size:11px;line-height:1.5}
301
397
  .cait-seg-text{display:-webkit-box;overflow:hidden;-webkit-line-clamp:2;-webkit-box-orient:vertical;font-size:11.5px;line-height:1.42;color:var(--dsw-alias-label-primary)}
302
398
  .cait-segitem.active .cait-seg-text{font-weight:550}
@@ -379,8 +475,8 @@ window.__ModuleLoader__.load({
379
475
  .cait-libcreate{display:flex;align-items:center;gap:5px;margin-top:8px}
380
476
  .cait-libcreate .cait-input{height:27px;min-width:0;flex:1}
381
477
  .cait-libcreate .cait-btn{height:27px;padding:0 8px;font-size:10px;flex:0 0 auto}
382
- .cait-tabs{display:flex;gap:3px;padding:7px 9px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:0 0 auto}
383
- .cait-tab{display:flex;align-items:center;justify-content:center;gap:5px;flex:1;height:29px;padding:0 6px;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:500}
478
+ .cait-tabs{display:flex;flex-wrap:wrap;gap:4px;padding:7px 9px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:0 0 auto}
479
+ .cait-tab{display:inline-flex;align-items:center;justify-content:center;gap:5px;flex:0 0 auto;white-space:nowrap;height:29px;padding:0 8px;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:10.5px;font-weight:500}
384
480
  .cait-tab:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent)}
385
481
  .cait-tab.active{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 11%,var(--dsw-alias-bg-layer-1));border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,var(--dsw-alias-border-l1));color:var(--dsw-alias-brand-primary);font-weight:650}
386
482
  .cait-tab .cait-count{color:inherit;opacity:.72}
@@ -414,6 +510,16 @@ window.__ModuleLoader__.load({
414
510
  .cait-panel-foot{display:flex;align-items:center;gap:5px;margin-top:10px;padding-top:9px;border-top:1px solid var(--dsw-alias-border-l1);flex-wrap:wrap}
415
511
  .cait-panel-foot .cait-btn{height:26px;padding:0 7px;font-size:10px}
416
512
  .cait-subhead{display:flex;align-items:center;justify-content:space-between;margin:2px 1px 7px;font-size:10px;font-weight:650;letter-spacing:.05em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}
513
+ .cait-subhead-actions{display:flex;align-items:center;gap:6px}
514
+ .cait-review{margin-top:10px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l1)}
515
+ .cait-reviewrow{display:flex;align-items:flex-start;gap:7px;width:100%;margin-bottom:6px;padding:7px 8px;border:1px solid var(--dsw-alias-border-l1);border-left:3px solid var(--dsw-alias-state-warn-primary);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer}
516
+ .cait-reviewrow:hover{background:var(--dsw-alias-bg-layer-2)}
517
+ .cait-reviewrow-index{min-width:18px;font-size:10px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);padding-top:2px}
518
+ .cait-reviewrow-body{display:flex;flex-direction:column;align-items:flex-start;gap:4px;min-width:0;flex:1}
519
+ .cait-reviewrow-msg{font-size:11px;line-height:1.45;overflow-wrap:anywhere}
520
+ .cait-reviewrow-msg.high{color:var(--dsw-alias-state-error-primary)}
521
+ .cait-reviewrow-msg.medium{color:var(--dsw-alias-state-warn-primary)}
522
+ .cait-reviewrow-msg.low{color:var(--dsw-alias-label-secondary)}
417
523
  .cait-term{padding:8px 9px;margin-bottom:6px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
418
524
  .cait-term-main{display:flex;align-items:baseline;gap:5px;min-width:0;font-size:11.5px}
419
525
  .cait-term-main strong{font-weight:650;overflow-wrap:anywhere}
@@ -451,6 +557,10 @@ window.__ModuleLoader__.load({
451
557
  .cait-segmented button{flex:1;height:27px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:550}
452
558
  .cait-segmented button.active{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 1px 3px color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent)}
453
559
  .cait-field{display:flex;flex-direction:column;gap:4px;margin-bottom:9px}
560
+ .cait-fieldhead{display:flex;align-items:baseline;justify-content:space-between;gap:8px}
561
+ .cait-fieldhead>span{font-size:10px;font-weight:600;color:var(--dsw-alias-label-secondary)}
562
+ .cait-fieldreset{font-size:10px;padding:1px 0}
563
+ .cait-fieldhint{margin:0;font-size:9.5px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
454
564
  .cait-field>span{font-size:10px;font-weight:600;color:var(--dsw-alias-label-secondary)}
455
565
  .cait-field .cait-input,.cait-field select{height:30px}
456
566
  .cait-grid2{display:grid;grid-template-columns:1fr 1fr;gap:8px}
@@ -465,6 +575,7 @@ window.__ModuleLoader__.load({
465
575
  .cait-formfoot .cait-btn{margin-left:auto}
466
576
  .cait-linkbtn{border:0;background:transparent;color:var(--dsw-alias-brand-primary);font-size:11px;font-weight:600;padding:3px 0}
467
577
  .cait-linkbtn:hover{text-decoration:underline}
578
+ .cait-linkbtn.active{color:var(--dsw-alias-label-primary);text-decoration:none}
468
579
  .cait-demo-row{display:flex;align-items:center;gap:8px;margin-top:11px;font-size:10.5px;color:var(--dsw-alias-label-secondary)}
469
580
  .cait-overlay{position:absolute;inset:0;z-index:20;display:grid;place-items:center;padding:18px;background:color-mix(in srgb,var(--dsw-alias-label-primary) 22%,transparent);backdrop-filter:blur(2px)}
470
581
  .cait-modal{width:min(530px,100%);max-height:92%;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:13px;background:var(--dsw-alias-bg-layer-2);box-shadow:0 18px 55px color-mix(in srgb,var(--dsw-alias-label-primary) 22%,transparent)}
@@ -538,6 +649,8 @@ window.__ModuleLoader__.load({
538
649
  .cait-libsearch{display:flex;align-items:center;gap:6px;min-width:150px;flex:1;height:29px;padding:0 9px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-secondary)}
539
650
  .cait-libsearch input{min-width:0;flex:1;border:0;outline:0;background:transparent;font-size:11.5px;color:var(--dsw-alias-label-primary)}
540
651
  .cait-liblang{height:29px;padding:0 5px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:var(--dsw-alias-bg-layer-1);font-size:11px;flex:0 0 auto}
652
+ .cait-libwrite{display:inline-flex;align-items:center;gap:5px;flex:0 0 auto;font-size:10.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap}
653
+ .cait-libwritelabel{font-size:10.5px;color:var(--dsw-alias-label-secondary)}
541
654
  .cait-libscope{display:flex;align-items:center;gap:4px;padding:0 12px 8px;flex-wrap:wrap;flex:0 0 auto}
542
655
  .cait-libtable-wrap{flex:1;min-height:0;overflow:auto;padding:0 12px}
543
656
  .cait-libtable{width:100%;border-collapse:collapse;table-layout:fixed}
@@ -562,6 +675,8 @@ window.__ModuleLoader__.load({
562
675
  .cait-libform-actions{display:flex;align-items:center;gap:6px;margin-top:11px}
563
676
  .cait-libform-actions .cait-btn{flex:1}
564
677
  .cait-batchbtn-plain{border-style:dashed}
678
+ .cait-batchbtn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,var(--dsw-alias-border-l1))}
679
+ .cait-batchbtn-danger:hover:not(:disabled){border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
565
680
  .cait-alignpage{display:flex;flex-direction:column;gap:10px;flex:1;min-height:0;padding:12px 14px 14px;overflow:auto}
566
681
  .cait-alignhead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
567
682
  .cait-alignhead h2{margin:0 0 3px;font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}
@@ -738,6 +853,10 @@ window.__ModuleLoader__.load({
738
853
  check: ['M3 8.5l3.2 3.2L13 4.3'],
739
854
  align: ['M2 5h8', 'M2 11h5', 'M12 3v10', 'M10 11l2 2 2-2'],
740
855
  folder: ['M2 4h4l1.5 2H14v6.5H2z'],
856
+ chart: ['M3 13V8', 'M8 13V3', 'M13 13V6', 'M2 14h12'],
857
+ message: ['M2 3h12v8H8l-3 3v-3H2z'],
858
+ lock: ['M4 7h8v6H4z', 'M6 7V5a2 2 0 0 1 4 0v2'],
859
+ unlock: ['M4 7h8v6H4z', 'M6 7V5a2 2 0 0 1 3.8-1'],
741
860
  }
742
861
  const shape = paths[name] || paths.file
743
862
  return h(
@@ -1003,6 +1122,7 @@ window.__ModuleLoader__.load({
1003
1122
  pre,
1004
1123
  onPreTranslate,
1005
1124
  onCancelPreTranslate,
1125
+ onReplace,
1006
1126
  view,
1007
1127
  onView,
1008
1128
  }) {
@@ -1108,6 +1228,19 @@ window.__ModuleLoader__.load({
1108
1228
  h('span', { className: 'cait-btn-label' }, t('pretranslate.start')),
1109
1229
  )
1110
1230
  : null,
1231
+ documentsView
1232
+ ? h(
1233
+ 'button',
1234
+ {
1235
+ className: 'cait-btn',
1236
+ onClick: onReplace,
1237
+ disabled: Boolean(busy) || !doc,
1238
+ title: t('replace.open'),
1239
+ },
1240
+ h(Icon, { name: 'search', size: 14 }),
1241
+ h('span', { className: 'cait-btn-label' }, t('replace.open')),
1242
+ )
1243
+ : null,
1111
1244
  documentsView
1112
1245
  ? h(
1113
1246
  'button',
@@ -1120,7 +1253,7 @@ window.__ModuleLoader__.load({
1120
1253
  ? h(
1121
1254
  'button',
1122
1255
  { className: 'cait-btn', onClick: onImport, disabled: Boolean(busy), title: t('nav.import') },
1123
- h(Icon, { name: 'upload', size: 14 }),
1256
+ h(Icon, { name: 'download', size: 14 }),
1124
1257
  h('span', { className: 'cait-btn-label' }, t('nav.import')),
1125
1258
  )
1126
1259
  : null,
@@ -1128,7 +1261,7 @@ window.__ModuleLoader__.load({
1128
1261
  ? h(
1129
1262
  'button',
1130
1263
  { className: 'cait-btn', onClick: onExport, disabled: Boolean(busy) || !doc, title: t('nav.export') },
1131
- h(Icon, { name: 'download', size: 14 }),
1264
+ h(Icon, { name: 'upload', size: 14 }),
1132
1265
  h('span', { className: 'cait-btn-label' }, t('nav.export')),
1133
1266
  )
1134
1267
  : null,
@@ -1137,9 +1270,9 @@ window.__ModuleLoader__.load({
1137
1270
  )
1138
1271
  }
1139
1272
 
1140
- const SEGMENT_FILTERS = ['all', 'pending', 'translated', 'confirmed']
1273
+ const SEGMENT_FILTERS = ['all', 'pending', 'translated', 'confirmed', 'risk']
1141
1274
 
1142
- function SegmentRail({ t, doc, selected, query, setQuery, filter, setFilter, onSelect, onConfirmAll, onPropagate, busy, qa }) {
1275
+ function SegmentRail({ t, doc, selected, query, setQuery, filter, setFilter, onSelect, onConfirmAll, onPropagate, onClearPretranslated, busy, qa }) {
1143
1276
  const activeRef = useRef(null)
1144
1277
  const storage = typeof localStorage !== 'undefined' ? localStorage : null
1145
1278
  const [pageSize, setPageSize] = useState(() => readPageSize(storage))
@@ -1159,12 +1292,15 @@ window.__ModuleLoader__.load({
1159
1292
  list.push(issue.code)
1160
1293
  warns.set(issue.index, list)
1161
1294
  }
1295
+ const riskByIndex = new Map()
1296
+ for (const item of qa?.risks || []) riskByIndex.set(item.index, item)
1162
1297
 
1163
1298
  const counts = {
1164
1299
  all: doc.progress.total,
1165
1300
  pending: doc.progress.pending,
1166
1301
  translated: doc.progress.translated,
1167
1302
  confirmed: doc.progress.confirmed,
1303
+ risk: riskByIndex.size,
1168
1304
  }
1169
1305
 
1170
1306
  // How many empty segments could be filled from a sibling with the same
@@ -1183,10 +1319,17 @@ window.__ModuleLoader__.load({
1183
1319
  if (list.length < 2 || !list.some((segment) => segment.target)) continue
1184
1320
  duplicated += list.filter((segment) => !segment.target).length
1185
1321
  }
1322
+ const pretranslated = doc.segments.filter((segment) => (
1323
+ segment.target &&
1324
+ !segment.confirmedAt &&
1325
+ ['tm', 'mt', 'auto'].includes(segment.origin)
1326
+ )).length
1186
1327
  const needle = query.trim().toLowerCase()
1187
1328
  const filtered = doc.segments
1188
1329
  .map((segment, index) => ({ segment, index }))
1189
- .filter(({ segment }) => filter === 'all' || segment.status === filter)
1330
+ .filter(({ segment, index }) =>
1331
+ filter === 'all' || (filter === 'risk' ? riskByIndex.has(index) : segment.status === filter),
1332
+ )
1190
1333
  .filter(({ segment }) => {
1191
1334
  if (!needle) return true
1192
1335
  return (
@@ -1303,6 +1446,19 @@ window.__ModuleLoader__.load({
1303
1446
  `${t('rail.propagate')} · ${duplicated}`,
1304
1447
  )
1305
1448
  : null,
1449
+ pretranslated > 0
1450
+ ? h(
1451
+ 'button',
1452
+ {
1453
+ className: 'cait-batchbtn cait-batchbtn-danger',
1454
+ onClick: onClearPretranslated,
1455
+ disabled: Boolean(busy),
1456
+ title: t('rail.clearPretranslatedHint'),
1457
+ },
1458
+ h(Icon, { name: 'trash', size: 11 }),
1459
+ `${t('rail.clearPretranslated')} · ${pretranslated}`,
1460
+ )
1461
+ : null,
1306
1462
  h(
1307
1463
  'div',
1308
1464
  { className: 'cait-rail-goto' },
@@ -1365,7 +1521,7 @@ window.__ModuleLoader__.load({
1365
1521
  h('span', { className: `cait-dot ${segment.status}` }),
1366
1522
  warns.has(index)
1367
1523
  ? h('span', {
1368
- className: 'cait-seg-warn',
1524
+ className: `cait-seg-warn ${riskByIndex.get(index)?.risk || 'low'}`,
1369
1525
  title: (warns.get(index) || []).map((code) => t(`qa.code.${code}`)).join(' · '),
1370
1526
  'aria-label': t('qa.warn'),
1371
1527
  })
@@ -1421,7 +1577,7 @@ window.__ModuleLoader__.load({
1421
1577
  )
1422
1578
  }
1423
1579
 
1424
- function EditorPane({ t, doc, index, segment, detail, draft, setDraft, dirty, busy, onConfirm, onNext, onPrev, onSave, onAdopt, onApplyTerm, textareaRef }) {
1580
+ function EditorPane({ t, doc, index, segment, detail, draft, setDraft, dirty, busy, onConfirm, onUnconfirm, onNext, onPrev, onSave, onUndo, onRedo, canUndo, canRedo, onToggleLock, onMerge, onSplit, onAdopt, onApplyTerm, onOpenBrief, onOpenResources, onOpenAnalysis, onOpenNotes, noteCount, qaRisk, textareaRef }) {
1425
1581
  const hits = detail?.index === index ? detail.glossaryHits : []
1426
1582
  const charCount = Array.from(draft || '').length
1427
1583
  const progress = doc.progress
@@ -1438,9 +1594,31 @@ window.__ModuleLoader__.load({
1438
1594
  // against. It is stored in `suggestion`, which adoption already preserves.
1439
1595
  const baselineText = segment.suggestion || ''
1440
1596
  const baselineOrigin = segment.origin || 'agent'
1597
+ const canMerge = !segment.locked && index < doc.segments.length - 1
1598
+ const canSplit = !segment.locked && String(segment.source || '').length > 1
1599
+ const splitAtCursor = () => {
1600
+ const sourceLength = String(segment.source || '').length
1601
+ const targetLength = String(draft || '').length
1602
+ const cursor = textareaRef?.current?.selectionStart
1603
+ const ratio = targetLength > 0 && Number.isInteger(cursor) ? cursor / targetLength : 0.5
1604
+ const at = Math.max(1, Math.min(sourceLength - 1, Math.round(sourceLength * ratio)))
1605
+ onSplit(at)
1606
+ }
1441
1607
 
1442
- const onKeyDown = (event) => { const mod = event.metaKey || event.ctrlKey
1443
- if (mod && event.key === 'Enter') {
1608
+ const onKeyDown = (event) => {
1609
+ const mod = event.metaKey || event.ctrlKey
1610
+ if (mod && event.key.toLowerCase() === 'z') {
1611
+ if (canUndo || canRedo) {
1612
+ event.preventDefault()
1613
+ if (event.shiftKey) onRedo()
1614
+ else onUndo()
1615
+ }
1616
+ } else if (mod && event.key.toLowerCase() === 'y') {
1617
+ if (canRedo) {
1618
+ event.preventDefault()
1619
+ onRedo()
1620
+ }
1621
+ } else if (mod && event.key === 'Enter') {
1444
1622
  event.preventDefault()
1445
1623
  onConfirm()
1446
1624
  } else if (mod && event.key.toLowerCase() === 's') {
@@ -1472,6 +1650,37 @@ window.__ModuleLoader__.load({
1472
1650
  h(Icon, { name: 'arrowRight', size: 11 }),
1473
1651
  h('span', { className: 'cait-langchip' }, doc.targetLang),
1474
1652
  h('span', null, `· ${doc.segments.length} ${t('doc.segments')}`),
1653
+ h(
1654
+ 'button',
1655
+ {
1656
+ className: 'cait-linkbtn cait-brieflink',
1657
+ onClick: onOpenBrief,
1658
+ title: doc.brief ? t('panel.briefHint') : t('brief.empty'),
1659
+ },
1660
+ h(Icon, { name: 'file', size: 11 }),
1661
+ t('editor.brief'),
1662
+ doc.brief ? h('span', { className: 'cait-briefdot' }) : null,
1663
+ ),
1664
+ h(
1665
+ 'button',
1666
+ {
1667
+ className: 'cait-linkbtn',
1668
+ onClick: onOpenResources,
1669
+ title: t('resources.hint'),
1670
+ },
1671
+ h(Icon, { name: 'database', size: 11 }),
1672
+ t('editor.resources'),
1673
+ ),
1674
+ h(
1675
+ 'button',
1676
+ {
1677
+ className: 'cait-linkbtn',
1678
+ onClick: onOpenAnalysis,
1679
+ title: t('analysis.hint'),
1680
+ },
1681
+ h(Icon, { name: 'chart', size: 11 }),
1682
+ t('editor.analysis'),
1683
+ ),
1475
1684
  ),
1476
1685
  ),
1477
1686
  h(
@@ -1486,7 +1695,40 @@ window.__ModuleLoader__.load({
1486
1695
  'div',
1487
1696
  { className: 'cait-seg-head' },
1488
1697
  h('span', { className: 'cait-seg-label' }, `${t('editor.segment')} ${index + 1} / ${doc.segments.length}`),
1489
- h('span', { className: `cait-badge ${segment.status}` }, statusLabel),
1698
+ h(
1699
+ 'span',
1700
+ { className: 'cait-seg-flags' },
1701
+ h(
1702
+ 'button',
1703
+ {
1704
+ className: `cait-linkbtn ${segment.locked ? 'active' : ''}`,
1705
+ onClick: onToggleLock,
1706
+ title: segment.locked ? t('editor.unlock') : t('editor.lock'),
1707
+ 'aria-pressed': Boolean(segment.locked),
1708
+ },
1709
+ h(Icon, { name: segment.locked ? 'unlock' : 'lock', size: 11 }),
1710
+ segment.locked ? t('editor.locked') : t('editor.lock'),
1711
+ ),
1712
+ h(
1713
+ 'button',
1714
+ {
1715
+ className: 'cait-linkbtn cait-notelink',
1716
+ onClick: onOpenNotes,
1717
+ title: t('notes.hint'),
1718
+ },
1719
+ h(Icon, { name: 'message', size: 11 }),
1720
+ t('editor.notes'),
1721
+ noteCount ? h('span', { className: 'cait-notecount' }, String(noteCount)) : null,
1722
+ ),
1723
+ qaRisk
1724
+ ? h(
1725
+ 'span',
1726
+ { className: `cait-riskchip ${qaRisk.risk}`, title: qaRisk.codes.map((code) => t(`qa.code.${code}`)).join(' · ') },
1727
+ `${t('qa.risk')} · ${t(`qa.${qaRisk.risk}`)}`,
1728
+ )
1729
+ : null,
1730
+ h('span', { className: `cait-badge ${segment.status}` }, statusLabel),
1731
+ ),
1490
1732
  ),
1491
1733
  h(
1492
1734
  'div',
@@ -1569,6 +1811,7 @@ window.__ModuleLoader__.load({
1569
1811
  value: draft,
1570
1812
  onChange: (event) => setDraft(event.target.value),
1571
1813
  onKeyDown,
1814
+ readOnly: Boolean(segment.locked),
1572
1815
  placeholder: t('editor.targetPlaceholder'),
1573
1816
  spellCheck: false,
1574
1817
  rows: 8,
@@ -1580,10 +1823,18 @@ window.__ModuleLoader__.load({
1580
1823
  { className: 'cait-actionbar' },
1581
1824
  h(
1582
1825
  'button',
1583
- { className: 'cait-btn cait-btn-primary', onClick: onConfirm, disabled: Boolean(busy) || !draft.trim() },
1826
+ { className: 'cait-btn cait-btn-primary', onClick: onConfirm, disabled: Boolean(busy) || Boolean(segment.locked) || !draft.trim() },
1584
1827
  busy === 'confirm' ? h(Spinner) : h(Icon, { name: 'check', size: 14 }),
1585
1828
  segment.status === 'confirmed' ? t('editor.update') : t('editor.confirm'),
1586
1829
  ),
1830
+ segment.status === 'confirmed'
1831
+ ? h(
1832
+ 'button',
1833
+ { className: 'cait-btn cait-btn-ghost', onClick: onUnconfirm, disabled: Boolean(busy) || Boolean(segment.locked), title: t('editor.unconfirmHint') },
1834
+ h(Icon, { name: 'refresh', size: 13 }),
1835
+ t('editor.unconfirm'),
1836
+ )
1837
+ : null,
1587
1838
  h(
1588
1839
  'button',
1589
1840
  { className: 'cait-btn', onClick: onNext, disabled: Boolean(busy) },
@@ -1598,7 +1849,27 @@ window.__ModuleLoader__.load({
1598
1849
  ),
1599
1850
  h(
1600
1851
  'button',
1601
- { className: 'cait-iconbtn cait-savebtn', onClick: onSave, disabled: Boolean(busy) || !dirty, title: t('editor.save') },
1852
+ { className: 'cait-btn cait-btn-ghost', onClick: onMerge, disabled: Boolean(busy) || !canMerge, title: t('editor.mergeHint') },
1853
+ t('editor.merge'),
1854
+ ),
1855
+ h(
1856
+ 'button',
1857
+ { className: 'cait-btn cait-btn-ghost', onClick: splitAtCursor, disabled: Boolean(busy) || !canSplit, title: t('editor.splitHint') },
1858
+ t('editor.split'),
1859
+ ),
1860
+ h(
1861
+ 'button',
1862
+ { className: 'cait-iconbtn', onClick: onUndo, disabled: Boolean(busy) || !canUndo, title: t('editor.undo'), 'aria-label': t('editor.undo') },
1863
+ '↶',
1864
+ ),
1865
+ h(
1866
+ 'button',
1867
+ { className: 'cait-iconbtn', onClick: onRedo, disabled: Boolean(busy) || !canRedo, title: t('editor.redo'), 'aria-label': t('editor.redo') },
1868
+ '↷',
1869
+ ),
1870
+ h(
1871
+ 'button',
1872
+ { className: 'cait-iconbtn cait-savebtn', onClick: onSave, disabled: Boolean(busy) || Boolean(segment.locked) || !dirty, title: t('editor.save') },
1602
1873
  h(Icon, { name: 'save', size: 14 }),
1603
1874
  ),
1604
1875
  h('span', { className: 'cait-hint' }, t('editor.hint')),
@@ -1721,6 +1992,19 @@ window.__ModuleLoader__.load({
1721
1992
  onCopy,
1722
1993
  onOpenSettings,
1723
1994
  onSearchLibrary,
1995
+ qa,
1996
+ onRefreshQa,
1997
+ onJumpQa,
1998
+ onFixQa,
1999
+ review,
2000
+ reviewBusy,
2001
+ onReview,
2002
+ onApplyReview,
2003
+ concordance,
2004
+ concordanceQuery,
2005
+ setConcordanceQuery,
2006
+ onConcordance,
2007
+ onApplyConcordance,
1724
2008
  busy,
1725
2009
  }) {
1726
2010
  const [termQuery, setTermQuery] = useState('')
@@ -1800,6 +2084,19 @@ window.__ModuleLoader__.load({
1800
2084
  h(Icon, { name: 'sparkles', size: 13 }),
1801
2085
  t('panel.mt'),
1802
2086
  ),
2087
+ h(
2088
+ 'button',
2089
+ { className: `cait-tab${tab === 'qa' ? ' active' : ''}`, onClick: () => setTab('qa') },
2090
+ h(Icon, { name: 'alert', size: 13 }),
2091
+ t('panel.qa'),
2092
+ qa?.issueCount ? h('span', { className: 'cait-count' }, qa.issueCount) : null,
2093
+ ),
2094
+ h(
2095
+ 'button',
2096
+ { className: `cait-tab${tab === 'concordance' ? ' active' : ''}`, onClick: () => setTab('concordance') },
2097
+ h(Icon, { name: 'search', size: 13 }),
2098
+ t('panel.concordance'),
2099
+ ),
1803
2100
  ),
1804
2101
  h(
1805
2102
  'div',
@@ -1877,7 +2174,7 @@ window.__ModuleLoader__.load({
1877
2174
  event.target.value = ''
1878
2175
  },
1879
2176
  }),
1880
- h('button', { className: 'cait-btn', onClick: () => tmxInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'upload', size: 12 }), t('panel.importTmx')),
2177
+ h('button', { className: 'cait-btn', onClick: () => tmxInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'download', size: 12 }), t('panel.importTmx')),
1881
2178
  h('button', { className: 'cait-btn cait-btn-danger', onClick: onClearTm, disabled: Boolean(busy) || data.tm.count === 0 }, t('panel.clearTm')),
1882
2179
  ),
1883
2180
  )
@@ -1895,7 +2192,7 @@ window.__ModuleLoader__.load({
1895
2192
  h(
1896
2193
  'button',
1897
2194
  { key: hit.id, className: 'cait-chip cait-chip-action', onClick: () => onApplyTerm(hit), title: hit.note || hit.target },
1898
- `${hit.term} → ${hit.target}`,
2195
+ `${hit.term} → ${hit.target}${hit.forbidden ? ` · ${t('panel.termForbidden')}` : hit.preferred ? ` · ${t('panel.termPreferred')}` : ''}`,
1899
2196
  ),
1900
2197
  ),
1901
2198
  )
@@ -1928,7 +2225,7 @@ window.__ModuleLoader__.load({
1928
2225
  event.target.value = ''
1929
2226
  },
1930
2227
  }),
1931
- h('button', { className: 'cait-btn', onClick: () => glossaryInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'upload', size: 12 }), t('panel.importCsv')),
2228
+ h('button', { className: 'cait-btn', onClick: () => glossaryInput.current?.click(), disabled: Boolean(busy) }, h(Icon, { name: 'download', size: 12 }), t('panel.importCsv')),
1932
2229
  h('button', { className: 'cait-btn cait-btn-danger', onClick: onClearGlossary, disabled: Boolean(busy) || data.glossary.length === 0 }, t('panel.clearGlossary')),
1933
2230
  ),
1934
2231
  )
@@ -1993,6 +2290,239 @@ window.__ModuleLoader__.load({
1993
2290
  ),
1994
2291
  )
1995
2292
  : null,
2293
+ tab === 'qa'
2294
+ ? h(
2295
+ 'div',
2296
+ null,
2297
+ h(
2298
+ 'div',
2299
+ { className: 'cait-subhead' },
2300
+ h('span', null, t('qa.title')),
2301
+ h(
2302
+ 'div',
2303
+ { className: 'cait-subhead-actions' },
2304
+ h(
2305
+ 'button',
2306
+ {
2307
+ className: 'cait-linkbtn',
2308
+ onClick: onReview,
2309
+ disabled: Boolean(busy) || Boolean(reviewBusy),
2310
+ title: t('qa.review'),
2311
+ },
2312
+ h(Icon, { name: 'sparkles', size: 12 }),
2313
+ reviewBusy ? t('qa.reviewing') : t('qa.review'),
2314
+ ),
2315
+ h(
2316
+ 'button',
2317
+ { className: 'cait-linkbtn', onClick: onRefreshQa, disabled: Boolean(busy) },
2318
+ h(Icon, { name: 'refresh', size: 12 }),
2319
+ t('qa.refresh'),
2320
+ ),
2321
+ ),
2322
+ ),
2323
+ qa
2324
+ ? h(
2325
+ 'div',
2326
+ null,
2327
+ h(
2328
+ 'div',
2329
+ { className: 'cait-riskcounts' },
2330
+ ['high', 'medium', 'low'].map((level) =>
2331
+ h(
2332
+ 'span',
2333
+ { key: level, className: `cait-riskcount ${level}` },
2334
+ `${t(`qa.${level}`)} ${qa.riskCounts?.[level] || 0}`,
2335
+ ),
2336
+ ),
2337
+ ),
2338
+ (qa.risks || []).length
2339
+ ? qa.risks.map((item) => {
2340
+ const segment = doc.segments[item.index]
2341
+ const codes = (item.codes || []).map((code) => t(`qa.code.${code}`)).join(' · ')
2342
+ return h(
2343
+ 'div',
2344
+ {
2345
+ key: item.index,
2346
+ className: `cait-riskrow ${item.risk}`,
2347
+ role: 'button',
2348
+ tabIndex: 0,
2349
+ onClick: () => onJumpQa(item.index),
2350
+ onKeyDown: (event) => {
2351
+ if (event.key === 'Enter' || event.key === ' ') {
2352
+ event.preventDefault()
2353
+ onJumpQa(item.index)
2354
+ }
2355
+ },
2356
+ title: codes,
2357
+ },
2358
+ h('span', { className: 'cait-riskrow-index' }, `${item.index + 1}`),
2359
+ h(
2360
+ 'div',
2361
+ { className: 'cait-riskrow-body' },
2362
+ h('span', { className: 'cait-riskrow-source' }, segment?.source || ''),
2363
+ h('span', { className: 'cait-riskrow-codes' }, codes),
2364
+ onFixQa
2365
+ ? h(
2366
+ 'div',
2367
+ { className: 'cait-riskactions' },
2368
+ h(
2369
+ 'button',
2370
+ {
2371
+ className: 'cait-riskaction',
2372
+ onClick: (event) => {
2373
+ event.stopPropagation()
2374
+ onFixQa(item, 'copy-source')
2375
+ },
2376
+ disabled: Boolean(busy),
2377
+ },
2378
+ t('qa.fixCopy'),
2379
+ ),
2380
+ h(
2381
+ 'button',
2382
+ {
2383
+ className: 'cait-riskaction',
2384
+ onClick: (event) => {
2385
+ event.stopPropagation()
2386
+ onFixQa(item, 'clear')
2387
+ },
2388
+ disabled: Boolean(busy),
2389
+ },
2390
+ t('qa.fixClear'),
2391
+ ),
2392
+ item.termTarget
2393
+ ? h(
2394
+ 'button',
2395
+ {
2396
+ className: 'cait-riskaction',
2397
+ onClick: (event) => {
2398
+ event.stopPropagation()
2399
+ onFixQa(item, 'term')
2400
+ },
2401
+ disabled: Boolean(busy),
2402
+ },
2403
+ t('qa.fixTerm'),
2404
+ )
2405
+ : null,
2406
+ )
2407
+ : null,
2408
+ ),
2409
+ h(Icon, { name: 'chevronRight', size: 12 }),
2410
+ )
2411
+ })
2412
+ : h('p', { className: 'cait-empty-note' }, t('qa.empty')),
2413
+ )
2414
+ : h('p', { className: 'cait-empty-note' }, t('qa.loading')),
2415
+ review
2416
+ ? h(
2417
+ 'div',
2418
+ { className: 'cait-review' },
2419
+ h('div', { className: 'cait-listhead' }, t('qa.reviewTitle')),
2420
+ (review.issues || []).length
2421
+ ? review.issues.map((issue, issueIndex) => {
2422
+ const severity = ['high', 'medium', 'low'].includes(issue.severity)
2423
+ ? issue.severity
2424
+ : 'medium'
2425
+ return h(
2426
+ 'div',
2427
+ {
2428
+ key: `${issue.index}-${issueIndex}`,
2429
+ className: 'cait-reviewrow',
2430
+ role: 'button',
2431
+ tabIndex: 0,
2432
+ onClick: () => onJumpQa(issue.index),
2433
+ onKeyDown: (event) => {
2434
+ if (event.key === 'Enter' || event.key === ' ') {
2435
+ event.preventDefault()
2436
+ onJumpQa(issue.index)
2437
+ }
2438
+ },
2439
+ },
2440
+ h('span', { className: 'cait-reviewrow-index' }, `${issue.index + 1}`),
2441
+ h(
2442
+ 'div',
2443
+ { className: 'cait-reviewrow-body' },
2444
+ h(
2445
+ 'span',
2446
+ { className: `cait-reviewrow-msg ${severity}` },
2447
+ `[${t(`qa.${severity}`)}] ${issue.message}`,
2448
+ ),
2449
+ issue.suggestion
2450
+ ? h(
2451
+ 'button',
2452
+ {
2453
+ className: 'cait-riskaction',
2454
+ onClick: (event) => {
2455
+ event.stopPropagation()
2456
+ onApplyReview(issue)
2457
+ },
2458
+ },
2459
+ t('qa.reviewApply'),
2460
+ )
2461
+ : null,
2462
+ ),
2463
+ )
2464
+ })
2465
+ : h('p', { className: 'cait-empty-note' }, t('qa.reviewEmpty')),
2466
+ )
2467
+ : null,
2468
+ )
2469
+ : null,
2470
+ tab === 'concordance'
2471
+ ? h(
2472
+ 'div',
2473
+ null,
2474
+ h(
2475
+ 'form',
2476
+ {
2477
+ className: 'cait-search',
2478
+ onSubmit: (event) => {
2479
+ event.preventDefault()
2480
+ onConcordance()
2481
+ },
2482
+ },
2483
+ h(Icon, { name: 'search', size: 13 }),
2484
+ h('input', {
2485
+ value: concordanceQuery,
2486
+ onChange: (event) => setConcordanceQuery(event.target.value),
2487
+ placeholder: t('panel.concordancePlaceholder'),
2488
+ 'aria-label': t('panel.concordancePlaceholder'),
2489
+ }),
2490
+ h(
2491
+ 'button',
2492
+ { className: 'cait-iconbtn', type: 'submit', title: t('panel.searchConcordance') },
2493
+ h(Icon, { name: 'arrowRight', size: 13 }),
2494
+ ),
2495
+ ),
2496
+ h('p', { className: 'cait-mt-hint' }, t('panel.concordanceHint')),
2497
+ concordance
2498
+ ? h(
2499
+ 'div',
2500
+ null,
2501
+ h(
2502
+ 'div',
2503
+ { className: 'cait-listhead' },
2504
+ h('span', null, t('panel.concordance')),
2505
+ h('span', { className: 'cait-count' }, concordance.total ?? concordance.entries?.length ?? 0),
2506
+ ),
2507
+ (concordance.entries || []).length
2508
+ ? concordance.entries.map((entry, i) =>
2509
+ h(
2510
+ 'button',
2511
+ {
2512
+ key: `${entry.tmId || entry.library || 'c'}-${i}`,
2513
+ className: 'cait-concordance-row',
2514
+ onClick: () => onApplyConcordance(entry),
2515
+ },
2516
+ h('span', { className: 'cait-concordance-source' }, entry.source),
2517
+ h('span', { className: 'cait-concordance-target' }, entry.target),
2518
+ h('span', { className: 'cait-concordance-meta' }, entry.library || ''),
2519
+ ),
2520
+ )
2521
+ : h('p', { className: 'cait-empty-note' }, t('panel.noConcordance')),
2522
+ )
2523
+ : null,
2524
+ )
2525
+ : null,
1996
2526
  ),
1997
2527
  )
1998
2528
  }
@@ -2467,6 +2997,7 @@ window.__ModuleLoader__.load({
2467
2997
  onCreateLibrary,
2468
2998
  onRenameLibrary,
2469
2999
  onDeleteLibrary,
3000
+ onSetMounts,
2470
3001
  }) {
2471
3002
  const storage = typeof localStorage !== 'undefined' ? localStorage : null
2472
3003
  const [kind, setKind] = useState(seed?.kind === 'glossary' ? 'glossary' : 'tm')
@@ -2494,7 +3025,23 @@ window.__ModuleLoader__.load({
2494
3025
  const idKey = kind === 'tm' ? 'tmId' : 'glossaryId'
2495
3026
  const labelKey = 'name'
2496
3027
  const scopeKey = scopeIds.join(',')
2497
- const targetLibraryId = scopeIds[0] || (kind === 'tm' ? data?.mounts?.defaultTmId : data?.mounts?.defaultGlossaryId) || libraries[0]?.[idKey] || ''
3028
+ const defaultWriteId = kind === 'tm' ? data?.mounts?.defaultTmId : data?.mounts?.defaultGlossaryId
3029
+ const targetLibraryId = libraries.some((library) => library[idKey] === defaultWriteId)
3030
+ ? defaultWriteId
3031
+ : libraries[0]?.[idKey] || ''
3032
+
3033
+ const changeWriteLibrary = async (value) => {
3034
+ if (!value || value === targetLibraryId) return
3035
+ // A default must also be mounted; adding it here keeps the selector
3036
+ // from failing when the chosen library was only in the list view.
3037
+ if (kind === 'tm') {
3038
+ const tmIds = Array.from(new Set([...(data?.mounts?.tmIds || []), value]))
3039
+ await onSetMounts?.({ tmIds, defaultTmId: value })
3040
+ } else {
3041
+ const glossaryIds = Array.from(new Set([...(data?.mounts?.glossaryIds || []), value]))
3042
+ await onSetMounts?.({ glossaryIds, defaultGlossaryId: value })
3043
+ }
3044
+ }
2498
3045
 
2499
3046
  useEffect(() => {
2500
3047
  const timer = setTimeout(() => setDebouncedQuery(query), 250)
@@ -2735,6 +3282,35 @@ window.__ModuleLoader__.load({
2735
3282
  })
2736
3283
  }
2737
3284
 
3285
+ const maintainTm = async (action) => {
3286
+ if (!targetLibraryId) return
3287
+ try {
3288
+ const preview = await rpc('tmMaintenancePreview', {
3289
+ action,
3290
+ tmId: targetLibraryId,
3291
+ emptyTarget: true,
3292
+ })
3293
+ const count = action === 'dedupe' ? preview.duplicates : preview.matched
3294
+ onConfirm({
3295
+ title: t(`tm.maintain.${action}`),
3296
+ message: t(`tm.maintain.${action}Hint`).replace('{count}', String(count)),
3297
+ confirmLabel: t(`tm.maintain.${action}`),
3298
+ action: async () => {
3299
+ const result = await rpc(action === 'dedupe' ? 'dedupeTmLibrary' : 'pruneTmLibrary', {
3300
+ tmId: targetLibraryId,
3301
+ emptyTarget: true,
3302
+ confirm: true,
3303
+ })
3304
+ if (result?.state) onApplyState(result.state)
3305
+ await loadRows()
3306
+ notify(t('tm.maintain.done').replace('{count}', String(count)))
3307
+ },
3308
+ })
3309
+ } catch (error) {
3310
+ report(error)
3311
+ }
3312
+ }
3313
+
2738
3314
  const exportLibrary = async () => {
2739
3315
  if (!targetLibraryId) return
2740
3316
  setWorking('export')
@@ -3007,6 +3583,28 @@ window.__ModuleLoader__.load({
3007
3583
  ['tbx', 'csv', 'json'].map((format) => h('option', { key: format, value: format }, format.toUpperCase())),
3008
3584
  )
3009
3585
  : null,
3586
+ h(
3587
+ 'label',
3588
+ { className: 'cait-libwrite', title: t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary') },
3589
+ h(
3590
+ 'span',
3591
+ { className: 'cait-libwritelabel' },
3592
+ t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
3593
+ ),
3594
+ h(
3595
+ 'select',
3596
+ {
3597
+ className: 'cait-liblang',
3598
+ value: targetLibraryId,
3599
+ onChange: (event) => changeWriteLibrary(event.target.value),
3600
+ disabled: Boolean(busy) || !libraries.length,
3601
+ 'aria-label': t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
3602
+ },
3603
+ libraries.map((library) =>
3604
+ h('option', { key: library[idKey], value: library[idKey] }, library[labelKey]),
3605
+ ),
3606
+ ),
3607
+ ),
3010
3608
  kind === 'glossary'
3011
3609
  ? h(
3012
3610
  'button',
@@ -3036,7 +3634,7 @@ window.__ModuleLoader__.load({
3036
3634
  disabled: Boolean(busy) || !targetLibraryId,
3037
3635
  title: t('library.import'),
3038
3636
  },
3039
- h(Icon, { name: 'upload', size: 13 }),
3637
+ h(Icon, { name: 'download', size: 13 }),
3040
3638
  h('span', { className: 'cait-btn-label' }, t('library.import')),
3041
3639
  ),
3042
3640
  h(
@@ -3048,9 +3646,37 @@ window.__ModuleLoader__.load({
3048
3646
  disabled: Boolean(busy) || working === 'export' || !targetLibraryId,
3049
3647
  title: t('library.export'),
3050
3648
  },
3051
- h(Icon, { name: 'download', size: 13 }),
3649
+ h(Icon, { name: 'upload', size: 13 }),
3052
3650
  h('span', { className: 'cait-btn-label' }, kind === 'tm' ? 'TMX' : exportFormat.toUpperCase()),
3053
3651
  ),
3652
+ kind === 'tm'
3653
+ ? h(
3654
+ 'button',
3655
+ {
3656
+ type: 'button',
3657
+ className: 'cait-btn',
3658
+ onClick: () => maintainTm('dedupe'),
3659
+ disabled: Boolean(busy) || !targetLibraryId,
3660
+ title: t('tm.maintain.dedupe'),
3661
+ },
3662
+ h(Icon, { name: 'database', size: 13 }),
3663
+ h('span', { className: 'cait-btn-label' }, t('tm.maintain.dedupe')),
3664
+ )
3665
+ : null,
3666
+ kind === 'tm'
3667
+ ? h(
3668
+ 'button',
3669
+ {
3670
+ type: 'button',
3671
+ className: 'cait-btn',
3672
+ onClick: () => maintainTm('prune'),
3673
+ disabled: Boolean(busy) || !targetLibraryId,
3674
+ title: t('tm.maintain.prune'),
3675
+ },
3676
+ h(Icon, { name: 'trash', size: 13 }),
3677
+ h('span', { className: 'cait-btn-label' }, t('tm.maintain.prune')),
3678
+ )
3679
+ : null,
3054
3680
  h(
3055
3681
  'button',
3056
3682
  {
@@ -3061,6 +3687,7 @@ window.__ModuleLoader__.load({
3061
3687
  title: t('library.clear'),
3062
3688
  },
3063
3689
  h(Icon, { name: 'trash', size: 13 }),
3690
+ h('span', { className: 'cait-btn-label' }, t('library.clear')),
3064
3691
  ),
3065
3692
  h('input', {
3066
3693
  ref: fileRef,
@@ -3313,6 +3940,7 @@ window.__ModuleLoader__.load({
3313
3940
  onChange: () => toggleTerm(at),
3314
3941
  }),
3315
3942
  h('span', { className: 'cait-termname' }, candidate.term),
3943
+ candidate.proper ? h('span', { className: 'cait-originchip' }, t('term.proper')) : null,
3316
3944
  h('span', { className: 'cait-termcount' }, `×${candidate.count}`),
3317
3945
  candidate.inLibrary ? h('span', { className: 'cait-originchip' }, t('term.inLibrary')) : null,
3318
3946
  h('input', {
@@ -3330,7 +3958,7 @@ window.__ModuleLoader__.load({
3330
3958
  )
3331
3959
  }
3332
3960
 
3333
- function ExportModal({ t, busy, onClose, onExport, qa }) {
3961
+ function ExportModal({ t, busy, onClose, onExport, onImportReview, qa }) {
3334
3962
  const issues = qa?.issueCount || 0
3335
3963
  const items = [
3336
3964
  ['bilingual-md', t('modal.export.bilingualMd'), t('modal.export.bilingual'), 'file'],
@@ -3339,11 +3967,13 @@ window.__ModuleLoader__.load({
3339
3967
  ['target-txt', t('modal.export.targetTxt'), t('modal.export.target'), 'download'],
3340
3968
  ['target-md', t('modal.export.targetMd'), t('modal.export.target'), 'download'],
3341
3969
  ['docx', t('modal.export.docx'), t('modal.export.docxGroup'), 'file'],
3970
+ ['bilingual-docx', t('modal.export.bilingualDocx'), t('modal.export.bilingual'), 'file'],
3342
3971
  ['epub', t('modal.export.epub'), t('modal.export.epubGroup'), 'file'],
3343
3972
  ['srt', t('modal.export.srt'), t('modal.export.subtitleGroup'), 'file'],
3344
3973
  ['vtt', t('modal.export.vtt'), t('modal.export.subtitleGroup'), 'file'],
3345
3974
  ['po', t('modal.export.po'), t('modal.export.localizationGroup'), 'file'],
3346
3975
  ['xliff', t('modal.export.xliff'), t('modal.export.localizationGroup'), 'file'],
3976
+ ['xliff-review', t('modal.export.xliffReview'), t('modal.export.localizationGroup'), 'file'],
3347
3977
  ['xliff2', t('modal.export.xliff2'), t('modal.export.localizationGroup'), 'file'],
3348
3978
  ['sdlxliff', t('modal.export.sdlxliff'), t('modal.export.localizationGroup'), 'file'],
3349
3979
  ['html', t('modal.export.html'), t('modal.export.webGroup'), 'file'],
@@ -3386,6 +4016,30 @@ window.__ModuleLoader__.load({
3386
4016
  ),
3387
4017
  ),
3388
4018
  ),
4019
+ onImportReview
4020
+ ? h(
4021
+ 'div',
4022
+ { className: 'cait-review-import' },
4023
+ h('p', { className: 'cait-libnote' }, t('modal.export.reviewImportHint')),
4024
+ h(
4025
+ 'label',
4026
+ { className: 'cait-btn' },
4027
+ h(Icon, { name: 'upload', size: 13 }),
4028
+ t('modal.export.reviewImport'),
4029
+ h('input', {
4030
+ type: 'file',
4031
+ accept: '.xliff,.xlf,application/x-xliff+xml',
4032
+ style: { display: 'none' },
4033
+ disabled: Boolean(busy),
4034
+ onChange: (event) => {
4035
+ const file = event.target.files?.[0]
4036
+ if (file) onImportReview(file)
4037
+ event.target.value = ''
4038
+ },
4039
+ }),
4040
+ ),
4041
+ )
4042
+ : null,
3389
4043
  )
3390
4044
  }
3391
4045
 
@@ -3406,13 +4060,23 @@ window.__ModuleLoader__.load({
3406
4060
  model: settings.mt.model,
3407
4061
  temperature: settings.mt.temperature,
3408
4062
  },
4063
+ review: {
4064
+ provider: settings.review?.provider || '',
4065
+ model: settings.review?.model || '',
4066
+ temperature: settings.review?.temperature ?? 0,
4067
+ },
3409
4068
  })
3410
4069
  const routes = Array.isArray(mtRoutes?.providers) ? mtRoutes.providers : []
3411
4070
  const route = routes.find((item) => item.id === form.mt.provider)
3412
4071
  const models = Array.isArray(route?.models) ? route.models : []
3413
4072
  const routeMissing = form.mt.provider !== 'mock' && !route
4073
+ const reviewRoute = routes.find((item) => item.id === form.review.provider)
4074
+ const reviewModels = Array.isArray(reviewRoute?.models) ? reviewRoute.models : []
4075
+ const reviewInherited = !form.review.provider
3414
4076
 
3415
4077
  const setMt = (patch) => setForm((current) => ({ ...current, mt: { ...current.mt, ...patch } }))
4078
+ const setReview = (patch) =>
4079
+ setForm((current) => ({ ...current, review: { ...current.review, ...patch } }))
3416
4080
  const setPre = (patch) =>
3417
4081
  setForm((current) => ({ ...current, preprocess: { ...current.preprocess, ...patch } }))
3418
4082
  const selectProvider = (provider) => {
@@ -3430,6 +4094,18 @@ window.__ModuleLoader__.load({
3430
4094
  }))
3431
4095
  }
3432
4096
 
4097
+ const selectReviewProvider = (provider) => {
4098
+ const nextRoute = routes.find((item) => item.id === provider)
4099
+ setForm((current) => ({
4100
+ ...current,
4101
+ review: {
4102
+ ...current.review,
4103
+ provider,
4104
+ model: nextRoute?.models?.[0]?.id || '',
4105
+ },
4106
+ }))
4107
+ }
4108
+
3433
4109
  const submit = (event) => {
3434
4110
  event.preventDefault()
3435
4111
  const patch = {
@@ -3448,6 +4124,11 @@ window.__ModuleLoader__.load({
3448
4124
  model: form.mt.model,
3449
4125
  temperature: Number(form.mt.temperature),
3450
4126
  },
4127
+ review: {
4128
+ provider: form.review.provider,
4129
+ model: form.review.model,
4130
+ temperature: Number(form.review.temperature),
4131
+ },
3451
4132
  }
3452
4133
  onSave(patch)
3453
4134
  }
@@ -3532,9 +4213,26 @@ window.__ModuleLoader__.load({
3532
4213
  { className: 'cait-settings-section' },
3533
4214
  h('h4', { className: 'cait-settings-title' }, t('modal.settings.tm')),
3534
4215
  h(
3535
- 'label',
4216
+ 'div',
3536
4217
  { className: 'cait-field' },
3537
- h('span', null, `${t('modal.settings.fuzzy')} · ${Math.round(Number(form.fuzzyThreshold) * 100)}%`),
4218
+ h(
4219
+ 'div',
4220
+ { className: 'cait-fieldhead' },
4221
+ h('span', null, `${t('modal.settings.fuzzy')} · ${fuzzyPercent(form.fuzzyThreshold)}%`),
4222
+ // Offered only while the slider is away from the default, so the
4223
+ // control never takes space in the usual case.
4224
+ fuzzyPercent(form.fuzzyThreshold) === fuzzyPercent(DEFAULT_FUZZY_THRESHOLD)
4225
+ ? null
4226
+ : h(
4227
+ 'button',
4228
+ {
4229
+ className: 'cait-linkbtn cait-fieldreset',
4230
+ type: 'button',
4231
+ onClick: () => setForm({ ...form, fuzzyThreshold: DEFAULT_FUZZY_THRESHOLD }),
4232
+ },
4233
+ t('modal.settings.resetDefault'),
4234
+ ),
4235
+ ),
3538
4236
  h('input', {
3539
4237
  className: 'cait-range',
3540
4238
  type: 'range',
@@ -3544,6 +4242,7 @@ window.__ModuleLoader__.load({
3544
4242
  value: form.fuzzyThreshold,
3545
4243
  onChange: (event) => setForm({ ...form, fuzzyThreshold: event.target.value }),
3546
4244
  }),
4245
+ h('p', { className: 'cait-fieldhint' }, t('modal.settings.fuzzyHint')),
3547
4246
  ),
3548
4247
  h(
3549
4248
  'label',
@@ -3639,36 +4338,128 @@ window.__ModuleLoader__.load({
3639
4338
  ),
3640
4339
  h(
3641
4340
  'div',
3642
- { className: 'cait-modal-actions' },
3643
- h('button', { type: 'button', className: 'cait-btn cait-btn-ghost', onClick: onClose }, t('modal.settings.cancel')),
3644
- h('button', { type: 'submit', className: 'cait-btn cait-btn-primary', disabled: Boolean(busy) }, busy === 'settings' ? h(Spinner) : null, t('modal.settings.save')),
3645
- ),
3646
- ),
3647
- )
3648
- }
3649
-
3650
- function ConfirmModal({ t, state, busy, onClose, onConfirm }) {
3651
- return h(
3652
- Modal,
3653
- { title: state.title || t('modal.confirm.title'), onClose },
3654
- h('p', { className: 'cait-confirm-text' }, state.message),
3655
- h(
3656
- 'div',
3657
- { className: 'cait-modal-actions' },
3658
- h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
3659
- h('button', { className: 'cait-btn cait-btn-danger', onClick: onConfirm, disabled: Boolean(busy) }, busy ? h(Spinner) : null, state.confirmLabel || t('modal.confirm.ok')),
3660
- ),
3661
- )
3662
- }
3663
-
3664
- function EmptyState({ t, settings, busy, onCreate, onDemo }) {
3665
- return h(
3666
- 'div',
3667
- { className: 'cait-empty' },
3668
- h(
3669
- 'div',
3670
- { className: 'cait-empty-inner' },
3671
- h('span', { className: 'cait-kicker' }, 'CAIT · CAT WORKBENCH'),
4341
+ { className: 'cait-settings-section' },
4342
+ h('h4', { className: 'cait-settings-title' }, t('modal.settings.review')),
4343
+ h(
4344
+ 'label',
4345
+ { className: 'cait-checkline' },
4346
+ h('input', {
4347
+ type: 'checkbox',
4348
+ checked: reviewInherited,
4349
+ onChange: (event) => {
4350
+ if (event.target.checked) {
4351
+ setForm((current) => ({
4352
+ ...current,
4353
+ review: { ...current.review, provider: '', model: '' },
4354
+ }))
4355
+ return
4356
+ }
4357
+ const first = routes.find((item) => item.id !== 'mock' && item.hasModels !== false) || routes[0]
4358
+ setForm((current) => ({
4359
+ ...current,
4360
+ review: {
4361
+ ...current.review,
4362
+ provider: first?.id || '',
4363
+ model: first?.models?.[0]?.id || '',
4364
+ },
4365
+ }))
4366
+ },
4367
+ }),
4368
+ t('modal.settings.reviewInherit'),
4369
+ ),
4370
+ h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('modal.settings.reviewHint')),
4371
+ reviewInherited
4372
+ ? null
4373
+ : h(
4374
+ 'div',
4375
+ { className: 'cait-grid2' },
4376
+ h(
4377
+ 'label',
4378
+ { className: 'cait-field' },
4379
+ h('span', null, t('modal.settings.reviewProvider')),
4380
+ h(
4381
+ 'select',
4382
+ {
4383
+ className: 'cait-input',
4384
+ value: form.review.provider,
4385
+ onChange: (event) => selectReviewProvider(event.target.value),
4386
+ },
4387
+ routes.map((item) =>
4388
+ h(
4389
+ 'option',
4390
+ { key: item.id, value: item.id, disabled: item.hasModels === false },
4391
+ item.hasModels === false
4392
+ ? `${item.name} (${item.id}) - ${t('modal.settings.noModels')}`
4393
+ : `${item.name} (${item.id})`,
4394
+ ),
4395
+ ),
4396
+ form.review.provider && !reviewRoute
4397
+ ? h('option', { value: form.review.provider }, t('modal.settings.routeMissing'))
4398
+ : null,
4399
+ ),
4400
+ ),
4401
+ h(
4402
+ 'label',
4403
+ { className: 'cait-field' },
4404
+ h('span', null, t('modal.settings.model')),
4405
+ reviewModels.length
4406
+ ? h(
4407
+ 'select',
4408
+ {
4409
+ className: 'cait-input',
4410
+ value: form.review.model,
4411
+ onChange: (event) => setReview({ model: event.target.value }),
4412
+ },
4413
+ reviewModels.map((model) =>
4414
+ h('option', { key: model.id, value: model.id }, `${model.name} (${model.id})`),
4415
+ ),
4416
+ )
4417
+ : h('input', {
4418
+ className: 'cait-input',
4419
+ value: form.review.model,
4420
+ readOnly: true,
4421
+ placeholder: t('modal.settings.noModels'),
4422
+ }),
4423
+ ),
4424
+ ),
4425
+ h(
4426
+ 'p',
4427
+ { className: 'cait-mt-hint', style: { marginTop: 0 } },
4428
+ reviewInherited ? t('modal.settings.reviewInheritedHint') : t('modal.settings.reviewCustomHint'),
4429
+ ),
4430
+ ),
4431
+ h(
4432
+ 'div',
4433
+ { className: 'cait-modal-actions' },
4434
+ h('button', { type: 'button', className: 'cait-btn cait-btn-ghost', onClick: onClose }, t('modal.settings.cancel')),
4435
+ h('button', { type: 'submit', className: 'cait-btn cait-btn-primary', disabled: Boolean(busy) }, busy === 'settings' ? h(Spinner) : null, t('modal.settings.save')),
4436
+ ),
4437
+ ),
4438
+ )
4439
+ }
4440
+
4441
+ function ConfirmModal({ t, state, busy, onClose, onConfirm }) {
4442
+ return h(
4443
+ Modal,
4444
+ { title: state.title || t('modal.confirm.title'), onClose },
4445
+ h('p', { className: 'cait-confirm-text' }, state.message),
4446
+ h(
4447
+ 'div',
4448
+ { className: 'cait-modal-actions' },
4449
+ h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
4450
+ h('button', { className: 'cait-btn cait-btn-danger', onClick: onConfirm, disabled: Boolean(busy) }, busy ? h(Spinner) : null, state.confirmLabel || t('modal.confirm.ok')),
4451
+ ),
4452
+ )
4453
+ }
4454
+
4455
+ function EmptyState({ t, settings, busy, onCreate, onDemo }) {
4456
+ return h(
4457
+ 'div',
4458
+ { className: 'cait-empty' },
4459
+ h(
4460
+ 'div',
4461
+ { className: 'cait-empty-inner' },
4462
+ h('span', { className: 'cait-kicker' }, 'CAIT · CAT WORKBENCH'),
3672
4463
  h('h1', null, t('empty.title')),
3673
4464
  h('p', { className: 'cait-lede' }, t('empty.desc')),
3674
4465
  h(
@@ -3689,6 +4480,614 @@ window.__ModuleLoader__.load({
3689
4480
  )
3690
4481
  }
3691
4482
 
4483
+ function BriefModal({ t, doc, busy, onClose, onSave }) {
4484
+ const initial = doc.brief || {}
4485
+ const [form, setForm] = useState({
4486
+ domain: initial.domain || '',
4487
+ audience: initial.audience || '',
4488
+ tone: initial.tone || '',
4489
+ notes: initial.notes || '',
4490
+ doNotTranslate: initial.doNotTranslate || '',
4491
+ styleSample: initial.styleSample || '',
4492
+ })
4493
+ const field = (key, label, placeholder, rows) =>
4494
+ h(
4495
+ 'label',
4496
+ { className: 'cait-field' },
4497
+ h('span', null, t(label)),
4498
+ rows
4499
+ ? h('textarea', {
4500
+ className: 'cait-textarea',
4501
+ rows,
4502
+ value: form[key],
4503
+ placeholder: t(placeholder),
4504
+ onChange: (event) => setForm({ ...form, [key]: event.target.value }),
4505
+ })
4506
+ : h('input', {
4507
+ className: 'cait-input',
4508
+ value: form[key],
4509
+ placeholder: t(placeholder),
4510
+ onChange: (event) => setForm({ ...form, [key]: event.target.value }),
4511
+ }),
4512
+ )
4513
+ return h(
4514
+ Modal,
4515
+ { title: t('brief.title'), onClose, wide: true },
4516
+ h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('brief.hint')),
4517
+ h(
4518
+ 'div',
4519
+ { className: 'cait-briefform' },
4520
+ h('div', { className: 'cait-grid2' }, field('domain', 'brief.domain', 'brief.placeholder.domain'), field('audience', 'brief.audience', 'brief.placeholder.audience')),
4521
+ field('tone', 'brief.tone', 'brief.placeholder.tone'),
4522
+ field('notes', 'brief.notes', 'brief.placeholder.notes', 3),
4523
+ field('doNotTranslate', 'brief.doNotTranslate', 'brief.placeholder.doNotTranslate', 3),
4524
+ field('styleSample', 'brief.styleSample', 'brief.placeholder.styleSample', 4),
4525
+ ),
4526
+ h(
4527
+ 'div',
4528
+ { className: 'cait-modal-actions' },
4529
+ h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
4530
+ h(
4531
+ 'button',
4532
+ { className: 'cait-btn cait-btn-primary', onClick: () => onSave(form), disabled: Boolean(busy) },
4533
+ busy ? h(Spinner) : null,
4534
+ t('brief.save'),
4535
+ ),
4536
+ ),
4537
+ )
4538
+ }
4539
+
4540
+ function NoteModal({ t, doc, index, segment, notes, busy, onAdd, onUpdate, onDelete, onClose }) {
4541
+ const [text, setText] = useState('')
4542
+ const [editingId, setEditingId] = useState('')
4543
+ const [editingText, setEditingText] = useState('')
4544
+ const list = Array.isArray(notes) ? notes : []
4545
+ const submit = () => {
4546
+ const value = text.trim()
4547
+ if (!value || busy) return
4548
+ onAdd(value)
4549
+ setText('')
4550
+ }
4551
+ return h(
4552
+ Modal,
4553
+ { title: `${t('notes.title')} · #${index + 1}`, onClose, wide: true },
4554
+ h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('notes.hint')),
4555
+ h('div', { className: 'cait-notelist' }, segment?.source || ''),
4556
+ list.length
4557
+ ? h(
4558
+ 'div',
4559
+ { className: 'cait-notelist' },
4560
+ list.map((note) =>
4561
+ h(
4562
+ 'div',
4563
+ { className: 'cait-noteitem', key: note.id },
4564
+ h(
4565
+ 'div',
4566
+ { className: 'cait-notehead' },
4567
+ h('span', { className: 'cait-noteauthor' }, note.author === 'agent' ? 'Agent' : t('notes.user')),
4568
+ h('span', { className: 'cait-notetime' }, String(note.createdAt || '').replace('T', ' ').slice(0, 16)),
4569
+ ),
4570
+ editingId === note.id
4571
+ ? h(
4572
+ 'div',
4573
+ null,
4574
+ h('textarea', {
4575
+ className: 'cait-input cait-textarea',
4576
+ value: editingText,
4577
+ rows: 3,
4578
+ onChange: (event) => setEditingText(event.target.value),
4579
+ }),
4580
+ h(
4581
+ 'div',
4582
+ { className: 'cait-noteactions' },
4583
+ h('button', { className: 'cait-btn cait-btn-ghost', onClick: () => setEditingId('') }, t('notes.cancel')),
4584
+ h(
4585
+ 'button',
4586
+ {
4587
+ className: 'cait-btn cait-btn-primary',
4588
+ disabled: Boolean(busy) || !editingText.trim(),
4589
+ onClick: () => {
4590
+ onUpdate(note.id, editingText)
4591
+ setEditingId('')
4592
+ },
4593
+ },
4594
+ t('notes.save'),
4595
+ ),
4596
+ ),
4597
+ )
4598
+ : h(
4599
+ 'div',
4600
+ null,
4601
+ h('div', { className: 'cait-notetext' }, note.text),
4602
+ h(
4603
+ 'div',
4604
+ { className: 'cait-noteactions' },
4605
+ h(
4606
+ 'button',
4607
+ {
4608
+ className: 'cait-minibtn',
4609
+ onClick: () => {
4610
+ setEditingId(note.id)
4611
+ setEditingText(note.text)
4612
+ },
4613
+ },
4614
+ t('notes.edit'),
4615
+ ),
4616
+ h(
4617
+ 'button',
4618
+ { className: 'cait-minibtn cait-danger', onClick: () => onDelete(note.id) },
4619
+ t('notes.delete'),
4620
+ ),
4621
+ ),
4622
+ ),
4623
+ ),
4624
+ ),
4625
+ )
4626
+ : h('p', { className: 'cait-mt-hint' }, t('notes.empty')),
4627
+ h(
4628
+ 'div',
4629
+ { className: 'cait-noteadd' },
4630
+ h('textarea', {
4631
+ className: 'cait-input cait-textarea',
4632
+ value: text,
4633
+ rows: 3,
4634
+ placeholder: t('notes.placeholder'),
4635
+ onChange: (event) => setText(event.target.value),
4636
+ }),
4637
+ h(
4638
+ 'div',
4639
+ { className: 'cait-noteactions' },
4640
+ h('button', { className: 'cait-btn cait-btn-primary', disabled: Boolean(busy) || !text.trim(), onClick: submit }, busy ? h(Spinner) : null, t('notes.add')),
4641
+ ),
4642
+ ),
4643
+ )
4644
+ }
4645
+
4646
+ function ReplaceModal({ t, doc, busy, preview, undoCount, onPreview, onApply, onUndo, onClose }) {
4647
+ const queryRef = useRef(null)
4648
+ const [form, setForm] = useState({
4649
+ query: '',
4650
+ replacement: '',
4651
+ scope: 'target',
4652
+ matchCase: false,
4653
+ regex: false,
4654
+ })
4655
+ useEffect(() => {
4656
+ if (queryRef.current) queryRef.current.focus()
4657
+ }, [])
4658
+ const set = (key, value) => setForm((current) => ({ ...current, [key]: value }))
4659
+ return h(
4660
+ Modal,
4661
+ { title: t('replace.title'), onClose, wide: true },
4662
+ h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('replace.hint')),
4663
+ h(
4664
+ 'div',
4665
+ { className: 'cait-replaceform' },
4666
+ h(
4667
+ 'label',
4668
+ { className: 'cait-field' },
4669
+ h('span', null, t('replace.query')),
4670
+ h('input', {
4671
+ ref: queryRef,
4672
+ className: 'cait-input',
4673
+ value: form.query,
4674
+ placeholder: t('replace.queryPlaceholder'),
4675
+ onChange: (event) => set('query', event.target.value),
4676
+ }),
4677
+ ),
4678
+ h(
4679
+ 'label',
4680
+ { className: 'cait-field' },
4681
+ h('span', null, t('replace.replacement')),
4682
+ h('input', {
4683
+ className: 'cait-input',
4684
+ value: form.replacement,
4685
+ placeholder: t('replace.replacementPlaceholder'),
4686
+ onChange: (event) => set('replacement', event.target.value),
4687
+ }),
4688
+ ),
4689
+ h(
4690
+ 'div',
4691
+ { className: 'cait-replaceoptions' },
4692
+ h(
4693
+ 'label',
4694
+ { className: 'cait-field' },
4695
+ h('span', null, t('replace.scope')),
4696
+ h(
4697
+ 'select',
4698
+ {
4699
+ className: 'cait-input',
4700
+ value: form.scope,
4701
+ onChange: (event) => set('scope', event.target.value),
4702
+ },
4703
+ h('option', { value: 'target' }, t('replace.scope.target')),
4704
+ h('option', { value: 'source' }, t('replace.scope.source')),
4705
+ h('option', { value: 'both' }, t('replace.scope.both')),
4706
+ ),
4707
+ ),
4708
+ h(
4709
+ 'label',
4710
+ { className: 'cait-check' },
4711
+ h('input', {
4712
+ type: 'checkbox',
4713
+ checked: form.matchCase,
4714
+ onChange: (event) => set('matchCase', event.target.checked),
4715
+ }),
4716
+ t('replace.matchCase'),
4717
+ ),
4718
+ h(
4719
+ 'label',
4720
+ { className: 'cait-check' },
4721
+ h('input', {
4722
+ type: 'checkbox',
4723
+ checked: form.regex,
4724
+ onChange: (event) => set('regex', event.target.checked),
4725
+ }),
4726
+ t('replace.regex'),
4727
+ ),
4728
+ ),
4729
+ ),
4730
+ preview
4731
+ ? h(
4732
+ 'div',
4733
+ { className: 'cait-replaceresults' },
4734
+ h(
4735
+ 'div',
4736
+ { className: 'cait-replacecounts' },
4737
+ t('replace.results')
4738
+ .replace('{count}', String(preview.total))
4739
+ .replace('{segments}', String(preview.matchedSegments || 0)),
4740
+ ),
4741
+ preview.truncated ? h('p', { className: 'cait-mt-hint' }, t('replace.truncated')) : null,
4742
+ preview.matches?.length
4743
+ ? h(
4744
+ 'div',
4745
+ { className: 'cait-replacelist' },
4746
+ preview.matches.slice(0, 50).map((match, index) =>
4747
+ h(
4748
+ 'div',
4749
+ { className: 'cait-replaceitem', key: `${match.uid}-${match.field}-${index}` },
4750
+ h(
4751
+ 'div',
4752
+ { className: 'cait-replacehead' },
4753
+ h('span', null, `#${match.index + 1}`),
4754
+ h('span', { className: 'cait-replacefield' }, t(`replace.field.${match.field}`)),
4755
+ h('span', { className: 'cait-replacecount' }, `×${match.count}`),
4756
+ ),
4757
+ h('div', { className: 'cait-replacebefore' }, match.before),
4758
+ h('div', { className: 'cait-replaceafter' }, match.after),
4759
+ ),
4760
+ ),
4761
+ )
4762
+ : h('p', { className: 'cait-mt-hint' }, t('replace.noResults')),
4763
+ )
4764
+ : null,
4765
+ h(
4766
+ 'div',
4767
+ { className: 'cait-modal-actions' },
4768
+ h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
4769
+ undoCount
4770
+ ? h('button', { className: 'cait-btn', onClick: onUndo, disabled: Boolean(busy) }, t('replace.undo'))
4771
+ : null,
4772
+ h('button', { className: 'cait-btn', onClick: () => onPreview(form), disabled: Boolean(busy) || !form.query }, t('replace.preview')),
4773
+ h(
4774
+ 'button',
4775
+ { className: 'cait-btn cait-btn-primary', onClick: () => onApply(form), disabled: Boolean(busy) || !form.query || !preview?.total },
4776
+ busy ? h(Spinner) : null,
4777
+ t('replace.apply'),
4778
+ ),
4779
+ ),
4780
+ )
4781
+ }
4782
+
4783
+ function CommandPalette({ t, items, onClose, onRun }) {
4784
+ const [query, setQuery] = useState('')
4785
+ const [active, setActive] = useState(0)
4786
+ const inputRef = useRef(null)
4787
+ useEffect(() => {
4788
+ if (inputRef.current) inputRef.current.focus()
4789
+ }, [])
4790
+ const needle = query.trim().toLowerCase()
4791
+ const filtered = items.filter((item) => {
4792
+ if (!needle) return true
4793
+ return `${item.title} ${item.hint || ''}`.toLowerCase().includes(needle)
4794
+ })
4795
+ useEffect(() => {
4796
+ setActive(0)
4797
+ }, [query])
4798
+ const choose = (item) => {
4799
+ if (item) onRun(item)
4800
+ }
4801
+ return h(
4802
+ Modal,
4803
+ { title: t('palette.title'), onClose, wide: true },
4804
+ h('input', {
4805
+ ref: inputRef,
4806
+ className: 'cait-input cait-palette-input',
4807
+ value: query,
4808
+ placeholder: t('palette.placeholder'),
4809
+ onChange: (event) => setQuery(event.target.value),
4810
+ onKeyDown: (event) => {
4811
+ if (event.key === 'ArrowDown') {
4812
+ event.preventDefault()
4813
+ setActive((value) => Math.min(value + 1, Math.max(0, filtered.length - 1)))
4814
+ } else if (event.key === 'ArrowUp') {
4815
+ event.preventDefault()
4816
+ setActive((value) => Math.max(value - 1, 0))
4817
+ } else if (event.key === 'Enter') {
4818
+ event.preventDefault()
4819
+ choose(filtered[active])
4820
+ }
4821
+ },
4822
+ }),
4823
+ h(
4824
+ 'div',
4825
+ { className: 'cait-palette-list' },
4826
+ filtered.length
4827
+ ? filtered.map((item, index) =>
4828
+ h(
4829
+ 'button',
4830
+ {
4831
+ type: 'button',
4832
+ key: item.id,
4833
+ className: `cait-palette-item${index === active ? ' active' : ''}`,
4834
+ onMouseEnter: () => setActive(index),
4835
+ onClick: () => choose(item),
4836
+ },
4837
+ h('span', { className: 'cait-palette-label' }, item.title),
4838
+ item.hint ? h('span', { className: 'cait-palette-hint' }, item.hint) : null,
4839
+ ),
4840
+ )
4841
+ : h('p', { className: 'cait-mt-hint' }, t('palette.empty')),
4842
+ ),
4843
+ h('p', { className: 'cait-palette-foot' }, t('palette.footer')),
4844
+ )
4845
+ }
4846
+
4847
+ function ResourceModal({ t, doc, libraries, mounts, busy, onClose, onSave }) {
4848
+ const resources = doc.resources || {}
4849
+ const [inheritTm, setInheritTm] = useState(resources.inheritsTm !== false)
4850
+ const [inheritGlossary, setInheritGlossary] = useState(resources.inheritsGlossary !== false)
4851
+ const [tmIds, setTmIds] = useState(
4852
+ () => new Set(resources.tmIds ?? resources.effectiveTmIds ?? []),
4853
+ )
4854
+ const [glossaryIds, setGlossaryIds] = useState(
4855
+ () => new Set(resources.glossaryIds ?? resources.effectiveGlossaryIds ?? []),
4856
+ )
4857
+ const [defaultWriteTmId, setDefaultWriteTmId] = useState(resources.defaultWriteTmId || '')
4858
+
4859
+ const allTm = libraries?.tm || []
4860
+ const allGlossary = libraries?.glossary || []
4861
+ const selectedTm = inheritTm ? mounts?.tmIds || [] : Array.from(tmIds)
4862
+ const selectedGlossary = inheritGlossary ? mounts?.glossaryIds || [] : Array.from(glossaryIds)
4863
+ const tmChoices = allTm.filter((library) => selectedTm.includes(library.tmId))
4864
+ const checkedTmIds = new Set(selectedTm)
4865
+ const checkedGlossaryIds = new Set(selectedGlossary)
4866
+ const defaultTm = tmChoices.some((library) => library.tmId === defaultWriteTmId)
4867
+ ? defaultWriteTmId
4868
+ : ''
4869
+
4870
+ const toggleId = (setter, id) => {
4871
+ setter((current) => {
4872
+ const next = new Set(current)
4873
+ if (next.has(id)) next.delete(id)
4874
+ else next.add(id)
4875
+ return next
4876
+ })
4877
+ }
4878
+
4879
+ const librarySection = (label, all, idKey, checkedIds, inherit, setInherit, toggle, inheritLabel) =>
4880
+ h(
4881
+ 'div',
4882
+ { className: 'cait-resource-section' },
4883
+ h(
4884
+ 'div',
4885
+ { className: 'cait-resource-head' },
4886
+ h('strong', null, t(label)),
4887
+ h(
4888
+ 'label',
4889
+ { className: 'cait-checkline' },
4890
+ h('input', {
4891
+ type: 'checkbox',
4892
+ checked: inherit,
4893
+ onChange: (event) => setInherit(event.target.checked),
4894
+ }),
4895
+ t(inheritLabel),
4896
+ ),
4897
+ ),
4898
+ h(
4899
+ 'div',
4900
+ { className: 'cait-resource-list' },
4901
+ all.length === 0
4902
+ ? h('p', { className: 'cait-meta' }, t('resources.empty'))
4903
+ : all.map((library) =>
4904
+ h(
4905
+ 'label',
4906
+ { key: library[idKey], className: 'cait-resource-row' },
4907
+ h('input', {
4908
+ type: 'checkbox',
4909
+ checked: checkedIds.has(library[idKey]),
4910
+ disabled: inherit,
4911
+ onChange: () => toggle(library[idKey]),
4912
+ }),
4913
+ h('span', { className: 'cait-resource-name' }, library.name),
4914
+ h('span', { className: 'cait-meta' }, library[idKey]),
4915
+ ),
4916
+ ),
4917
+ ),
4918
+ )
4919
+
4920
+ return h(
4921
+ Modal,
4922
+ { title: t('resources.title'), onClose, wide: true },
4923
+ h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('resources.hint')),
4924
+ h(
4925
+ 'div',
4926
+ { className: 'cait-briefform' },
4927
+ librarySection(
4928
+ 'resources.tm',
4929
+ allTm,
4930
+ 'tmId',
4931
+ checkedTmIds,
4932
+ inheritTm,
4933
+ setInheritTm,
4934
+ (id) => toggleId(setTmIds, id),
4935
+ 'resources.inheritTm',
4936
+ ),
4937
+ librarySection(
4938
+ 'resources.glossary',
4939
+ allGlossary,
4940
+ 'glossaryId',
4941
+ checkedGlossaryIds,
4942
+ inheritGlossary,
4943
+ setInheritGlossary,
4944
+ (id) => toggleId(setGlossaryIds, id),
4945
+ 'resources.inheritGlossary',
4946
+ ),
4947
+ h(
4948
+ 'label',
4949
+ { className: 'cait-field' },
4950
+ h('span', null, t('resources.defaultWrite')),
4951
+ h(
4952
+ 'select',
4953
+ {
4954
+ className: 'cait-input',
4955
+ value: defaultTm,
4956
+ onChange: (event) => setDefaultWriteTmId(event.target.value),
4957
+ },
4958
+ h('option', { value: '' }, t(inheritTm ? 'resources.followDefault' : 'resources.firstBound')),
4959
+ tmChoices.map((library) => h('option', { key: library.tmId, value: library.tmId }, library.name)),
4960
+ ),
4961
+ ),
4962
+ ),
4963
+ h(
4964
+ 'div',
4965
+ { className: 'cait-modal-actions' },
4966
+ h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('modal.confirm.cancel')),
4967
+ h(
4968
+ 'button',
4969
+ {
4970
+ className: 'cait-btn cait-btn-primary',
4971
+ disabled: Boolean(busy),
4972
+ onClick: () =>
4973
+ onSave({
4974
+ tmIds: inheritTm ? null : Array.from(tmIds),
4975
+ glossaryIds: inheritGlossary ? null : Array.from(glossaryIds),
4976
+ defaultWriteTmId: defaultTm || null,
4977
+ }),
4978
+ },
4979
+ busy ? h(Spinner) : null,
4980
+ t('resources.save'),
4981
+ ),
4982
+ ),
4983
+ )
4984
+ }
4985
+
4986
+ function AnalysisModal({ t, doc, analysis, busy, onClose }) {
4987
+ const download = (format) => {
4988
+ if (!analysis) return
4989
+ const pairs = [
4990
+ ['segments', analysis.segments],
4991
+ ['words', analysis.words],
4992
+ ['characters', analysis.characters],
4993
+ ['duplicates.groups', analysis.duplicates.groups],
4994
+ ['duplicates.segments', analysis.duplicates.segments],
4995
+ ['translated', analysis.translated],
4996
+ ['confirmed', analysis.confirmed],
4997
+ ['ice', analysis.buckets.ice],
4998
+ ['exact', analysis.buckets.exact],
4999
+ ['fuzzy85', analysis.buckets.fuzzy85],
5000
+ ['fuzzy70', analysis.buckets.fuzzy70],
5001
+ ['new', analysis.buckets.new],
5002
+ ['matched.segments', analysis.matched.segments],
5003
+ ['matched.percent', analysis.matched.percent],
5004
+ ['leverage.percent', analysis.leverage.percent],
5005
+ ['generatedAt', analysis.generatedAt],
5006
+ ]
5007
+ const content =
5008
+ format === 'json'
5009
+ ? JSON.stringify(analysis, null, 2)
5010
+ : ['metric,value', ...pairs.map(([key, value]) => `${key},${value}`)].join('\n')
5011
+ const type = format === 'json' ? 'application/json' : 'text/csv'
5012
+ const blob = new Blob([content], { type })
5013
+ const url = URL.createObjectURL(blob)
5014
+ const anchor = document.createElement('a')
5015
+ anchor.href = url
5016
+ anchor.download = `${analysis.name || 'document'}-analysis.${format}`
5017
+ anchor.click()
5018
+ URL.revokeObjectURL(url)
5019
+ }
5020
+
5021
+ const rows = analysis
5022
+ ? [
5023
+ ['analysis.segments', analysis.segments],
5024
+ ['analysis.words', analysis.words],
5025
+ ['analysis.characters', analysis.characters],
5026
+ ['analysis.duplicates', `${analysis.duplicates.segments} / ${analysis.duplicates.groups}`],
5027
+ ['analysis.translated', analysis.translated],
5028
+ ['analysis.confirmed', analysis.confirmed],
5029
+ ]
5030
+ : []
5031
+ const buckets = analysis
5032
+ ? [
5033
+ ['analysis.ice', analysis.buckets.ice],
5034
+ ['analysis.exact', analysis.buckets.exact],
5035
+ ['analysis.fuzzy85', analysis.buckets.fuzzy85],
5036
+ ['analysis.fuzzy70', analysis.buckets.fuzzy70],
5037
+ ['analysis.new', analysis.buckets.new],
5038
+ ]
5039
+ : []
5040
+
5041
+ return h(
5042
+ Modal,
5043
+ { title: t('analysis.title'), onClose, wide: true },
5044
+ h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('analysis.hint')),
5045
+ busy || !analysis
5046
+ ? h('div', { className: 'cait-empty' }, h(Spinner))
5047
+ : h(
5048
+ 'div',
5049
+ { className: 'cait-analysis' },
5050
+ h(
5051
+ 'div',
5052
+ { className: 'cait-analysis-grid' },
5053
+ rows.map(([label, value]) =>
5054
+ h(
5055
+ 'div',
5056
+ { key: label, className: 'cait-analysis-stat' },
5057
+ h('span', null, t(label)),
5058
+ h('strong', null, String(value)),
5059
+ ),
5060
+ ),
5061
+ ),
5062
+ h(
5063
+ 'div',
5064
+ { className: 'cait-analysis-grid' },
5065
+ buckets.map(([label, value]) =>
5066
+ h(
5067
+ 'div',
5068
+ { key: label, className: 'cait-analysis-stat' },
5069
+ h('span', null, t(label)),
5070
+ h('strong', null, String(value)),
5071
+ ),
5072
+ ),
5073
+ ),
5074
+ h(
5075
+ 'div',
5076
+ { className: 'cait-analysis-total' },
5077
+ h('span', null, `${t('analysis.matchRate')} ${analysis.matched.percent}%`),
5078
+ h('span', null, `${t('analysis.leverage')} ${analysis.leverage.percent}%`),
5079
+ ),
5080
+ ),
5081
+ h(
5082
+ 'div',
5083
+ { className: 'cait-modal-actions' },
5084
+ h('button', { className: 'cait-btn cait-btn-ghost', onClick: onClose, disabled: Boolean(busy) }, t('analysis.close')),
5085
+ h('button', { className: 'cait-btn cait-btn-ghost', onClick: () => download('csv'), disabled: Boolean(busy) || !analysis }, t('analysis.exportCsv')),
5086
+ h('button', { className: 'cait-btn cait-btn-primary', onClick: () => download('json'), disabled: Boolean(busy) || !analysis }, t('analysis.exportJson')),
5087
+ ),
5088
+ )
5089
+ }
5090
+
3692
5091
  function Workbench({ i18n, translate }) {
3693
5092
  const [data, setData] = useState(null)
3694
5093
  const [loading, setLoading] = useState(true)
@@ -3712,6 +5111,18 @@ window.__ModuleLoader__.load({
3712
5111
  }
3713
5112
  const [pre, setPre] = useState({ running: false, done: 0, total: 0, filled: 0, failed: 0 })
3714
5113
  const [qa, setQa] = useState(null)
5114
+ const [review, setReview] = useState(null)
5115
+ const [reviewBusy, setReviewBusy] = useState(false)
5116
+ const [analysis, setAnalysis] = useState(null)
5117
+ const [analysisBusy, setAnalysisBusy] = useState(false)
5118
+ const [replacePreview, setReplacePreview] = useState(null)
5119
+ const [replaceUndo, setReplaceUndo] = useState(null)
5120
+ const [replaceBusy, setReplaceBusy] = useState(false)
5121
+ const [notes, setNotes] = useState(null)
5122
+ const [notesBusy, setNotesBusy] = useState(false)
5123
+ const [concordance, setConcordance] = useState(null)
5124
+ const [concordanceQuery, setConcordanceQuery] = useState('')
5125
+ const [concordanceBusy, setConcordanceBusy] = useState(false)
3715
5126
  const [tmQuery, setTmQuery] = useState('')
3716
5127
  const [tmResults, setTmResults] = useState(null)
3717
5128
  const [tmRefresh, setTmRefresh] = useState(0)
@@ -3728,16 +5139,20 @@ window.__ModuleLoader__.load({
3728
5139
  const busyRef = useRef(false)
3729
5140
  const rootRef = useRef(null)
3730
5141
  const unitRef = useRef(0)
5142
+ const draftHistoryRef = useRef({ index: -1, stack: [], cursor: 0, lastAt: 0 })
3731
5143
 
3732
5144
  const doc = data?.activeDocument || null
3733
5145
  const segments = doc?.segments || []
3734
5146
  const index = doc ? Math.min(selected, Math.max(0, segments.length - 1)) : 0
3735
5147
  const segment = segments[index] || null
3736
5148
  const detail = doc?.current?.index === index ? doc.current : null
5149
+ useEffect(() => {
5150
+ draftHistoryRef.current = { index, stack: [draft], cursor: 0, lastAt: 0 }
5151
+ }, [doc?.id, index])
3737
5152
  const settings = data?.settings || {
3738
5153
  sourceLang: 'en',
3739
5154
  targetLang: 'zh-CN',
3740
- fuzzyThreshold: 0.55,
5155
+ fuzzyThreshold: DEFAULT_FUZZY_THRESHOLD,
3741
5156
  tmLimit: 5,
3742
5157
  uiLang: 'auto',
3743
5158
  mt: { provider: 'mock', model: 'cait-mock', temperature: 0.2 },
@@ -3960,6 +5375,10 @@ window.__ModuleLoader__.load({
3960
5375
  }
3961
5376
  }, [data?.activeDocument?.id, data?.revision])
3962
5377
 
5378
+ useEffect(() => {
5379
+ setReview(null)
5380
+ }, [data?.activeDocument?.id])
5381
+
3963
5382
  useEffect(() => {
3964
5383
  let alive = true
3965
5384
  rpc('listMtRoutes')
@@ -3985,6 +5404,11 @@ window.__ModuleLoader__.load({
3985
5404
  setTmQuery('')
3986
5405
  setTmResults(null)
3987
5406
  setTmError('')
5407
+ setConcordance(null)
5408
+ setConcordanceQuery('')
5409
+ setReplacePreview(null)
5410
+ setReplaceUndo(null)
5411
+ setNotes(null)
3988
5412
  }, [doc?.id])
3989
5413
 
3990
5414
  useEffect(() => {
@@ -4022,7 +5446,8 @@ window.__ModuleLoader__.load({
4022
5446
  const saveDraft = async (silent = true) => {
4023
5447
  if (!doc || !dirty) return true
4024
5448
  try {
4025
- await rpc('saveDraft', { documentId: doc.id, index: selected, target: draft })
5449
+ const result = await rpc('saveDraft', { documentId: doc.id, index: selected, target: draft })
5450
+ if (result.state) adoptState(result.state)
4026
5451
  setDirty(false)
4027
5452
  if (!silent) notify(t('toast.draftSaved'))
4028
5453
  return true
@@ -4049,6 +5474,49 @@ window.__ModuleLoader__.load({
4049
5474
  }
4050
5475
  }
4051
5476
 
5477
+ const changeDraft = (value) => {
5478
+ const history = draftHistoryRef.current
5479
+ if (history.index !== index) {
5480
+ history.index = index
5481
+ history.stack = [draft]
5482
+ history.cursor = 0
5483
+ }
5484
+ if (history.stack[history.cursor] !== value) {
5485
+ const now = Date.now()
5486
+ if (now - history.lastAt < 650 && history.cursor > 0) {
5487
+ history.stack[history.cursor] = value
5488
+ } else {
5489
+ history.stack = history.stack.slice(0, history.cursor + 1)
5490
+ history.stack.push(value)
5491
+ if (history.stack.length > 200) history.stack.shift()
5492
+ history.cursor = history.stack.length - 1
5493
+ }
5494
+ history.lastAt = now
5495
+ }
5496
+ setDraft(value)
5497
+ setDirty(true)
5498
+ }
5499
+
5500
+ const undoDraft = () => {
5501
+ const history = draftHistoryRef.current
5502
+ if (history.cursor <= 0) return
5503
+ history.cursor -= 1
5504
+ history.lastAt = 0
5505
+ const value = history.stack[history.cursor]
5506
+ setDraft(value)
5507
+ setDirty(value !== (segment?.target || ''))
5508
+ }
5509
+
5510
+ const redoDraft = () => {
5511
+ const history = draftHistoryRef.current
5512
+ if (history.cursor >= history.stack.length - 1) return
5513
+ history.cursor += 1
5514
+ history.lastAt = 0
5515
+ const value = history.stack[history.cursor]
5516
+ setDraft(value)
5517
+ setDirty(value !== (segment?.target || ''))
5518
+ }
5519
+
4052
5520
  const confirmSegment = async () => {
4053
5521
  if (!doc || busy) return
4054
5522
  if (!draft.trim()) {
@@ -4203,6 +5671,10 @@ window.__ModuleLoader__.load({
4203
5671
  }
4204
5672
 
4205
5673
  const applyTerm = (term) => {
5674
+ if (term?.forbidden) {
5675
+ notify(t('toast.termForbidden'), 'error')
5676
+ return
5677
+ }
4206
5678
  const separator = /^(zh|ja|ko)/i.test(doc?.targetLang || '') ? '' : ' '
4207
5679
  setDraft((current) => {
4208
5680
  const value = current.replace(/\s+$/, '')
@@ -4212,6 +5684,326 @@ window.__ModuleLoader__.load({
4212
5684
  setTimeout(() => textareaRef.current?.focus(), 0)
4213
5685
  }
4214
5686
 
5687
+ const unconfirmCurrent = async () => {
5688
+ if (!doc || busy) return
5689
+ setBusy('unconfirm')
5690
+ try {
5691
+ if (dirty) {
5692
+ const saved = await saveDraft(true)
5693
+ if (!saved) return
5694
+ }
5695
+ const result = await rpc('unconfirmSegment', { documentId: doc.id, index: selected })
5696
+ adoptState(result.state)
5697
+ notify(t('toast.unconfirmed'))
5698
+ } catch (error) {
5699
+ // Saving a dirty confirmed segment already revokes the confirmation.
5700
+ // Treat the second call as idempotent instead of showing an error.
5701
+ if (error?.code === 'NOT_CONFIRMED') notify(t('toast.unconfirmed'))
5702
+ else report(error)
5703
+ } finally {
5704
+ setBusy('')
5705
+ }
5706
+ }
5707
+
5708
+ const toggleLock = async () => {
5709
+ if (!doc || busy || !segment) return
5710
+ setBusy('lock')
5711
+ try {
5712
+ await rpc('setSegmentLocked', { documentId: doc.id, index: selected, locked: !segment.locked })
5713
+ const next = await rpc('bootstrap')
5714
+ adoptState(next, selected)
5715
+ notify(t(segment.locked ? 'toast.unlocked' : 'toast.locked'))
5716
+ } catch (error) {
5717
+ report(error)
5718
+ } finally {
5719
+ setBusy('')
5720
+ }
5721
+ }
5722
+
5723
+ const mergeCurrent = () => {
5724
+ if (!doc || !segment || busy) return
5725
+ openConfirm({
5726
+ title: t('modal.confirm.mergeTitle'),
5727
+ message: t('modal.confirm.merge'),
5728
+ confirmLabel: t('editor.merge'),
5729
+ action: async () => {
5730
+ const result = await rpc('mergeSegments', { documentId: doc.id, index: selected, count: 2 })
5731
+ adoptState(result.state, result.index)
5732
+ notify(t('toast.merged'))
5733
+ },
5734
+ })
5735
+ }
5736
+
5737
+ const splitCurrent = (at) => {
5738
+ if (!doc || !segment || busy) return
5739
+ openConfirm({
5740
+ title: t('modal.confirm.splitTitle'),
5741
+ message: t('modal.confirm.split'),
5742
+ confirmLabel: t('editor.split'),
5743
+ action: async () => {
5744
+ const result = await rpc('splitSegment', { documentId: doc.id, index: selected, at })
5745
+ adoptState(result.state, result.index)
5746
+ notify(t('toast.split'))
5747
+ },
5748
+ })
5749
+ }
5750
+
5751
+ const saveBrief = async (brief) => {
5752
+ if (!doc || busy) return false
5753
+ setBusy('brief')
5754
+ try {
5755
+ const result = await rpc('setBrief', { documentId: doc.id, brief })
5756
+ adoptState(result.state)
5757
+ setModal(null)
5758
+ notify(t('toast.briefSaved'))
5759
+ return true
5760
+ } catch (error) {
5761
+ report(error)
5762
+ return false
5763
+ } finally {
5764
+ setBusy('')
5765
+ }
5766
+ }
5767
+
5768
+ const saveResources = async (resources) => {
5769
+ if (!doc || busy) return false
5770
+ setBusy('resources')
5771
+ try {
5772
+ const result = await rpc('setDocumentResources', { documentId: doc.id, ...resources })
5773
+ adoptState(result.state)
5774
+ setModal(null)
5775
+ notify(t('toast.resourcesSaved'))
5776
+ return true
5777
+ } catch (error) {
5778
+ report(error)
5779
+ return false
5780
+ } finally {
5781
+ setBusy('')
5782
+ }
5783
+ }
5784
+
5785
+ const openAnalysis = async () => {
5786
+ if (!doc) return
5787
+ setAnalysis(null)
5788
+ setModal({ type: 'analysis' })
5789
+ setAnalysisBusy(true)
5790
+ try {
5791
+ setAnalysis(await rpc('analyzeDocument', { documentId: doc.id }))
5792
+ } catch (error) {
5793
+ report(error)
5794
+ } finally {
5795
+ setAnalysisBusy(false)
5796
+ }
5797
+ }
5798
+
5799
+ const openReplace = () => {
5800
+ if (!doc) return
5801
+ setReplacePreview(null)
5802
+ setModal({ type: 'replace' })
5803
+ }
5804
+
5805
+ const previewReplace = async (form) => {
5806
+ if (!doc || replaceBusy) return
5807
+ setReplaceBusy(true)
5808
+ try {
5809
+ setReplacePreview(await rpc('replaceInDocument', { documentId: doc.id, ...form }))
5810
+ } catch (error) {
5811
+ report(error)
5812
+ } finally {
5813
+ setReplaceBusy(false)
5814
+ }
5815
+ }
5816
+
5817
+ const applyReplace = async (form) => {
5818
+ if (!doc || replaceBusy) return
5819
+ setReplaceBusy(true)
5820
+ try {
5821
+ const result = await rpc('replaceInDocument', { documentId: doc.id, ...form, apply: true })
5822
+ setReplaceUndo(Array.isArray(result.undo) && result.undo.length ? result.undo : null)
5823
+ setReplacePreview(null)
5824
+ adoptState(result.state, result.undo?.[0]?.index)
5825
+ notify(t('toast.replaced').replace('{count}', String(result.applied ?? 0)))
5826
+ } catch (error) {
5827
+ report(error)
5828
+ } finally {
5829
+ setReplaceBusy(false)
5830
+ }
5831
+ }
5832
+
5833
+ const undoReplace = async () => {
5834
+ if (!doc || !replaceUndo || replaceBusy) return
5835
+ setReplaceBusy(true)
5836
+ try {
5837
+ const result = await rpc('undoReplace', { documentId: doc.id, undo: replaceUndo })
5838
+ adoptState(result.state, replaceUndo[0]?.index)
5839
+ setReplaceUndo(null)
5840
+ setReplacePreview(null)
5841
+ notify(t('toast.replaceUndone'))
5842
+ } catch (error) {
5843
+ report(error)
5844
+ } finally {
5845
+ setReplaceBusy(false)
5846
+ }
5847
+ }
5848
+
5849
+ const openNotes = async () => {
5850
+ if (!doc) return
5851
+ setNotes(null)
5852
+ setNotesBusy(true)
5853
+ setModal({ type: 'notes' })
5854
+ try {
5855
+ const result = await rpc('notesForSegment', { documentId: doc.id, index })
5856
+ setNotes(result.notes)
5857
+ } catch (error) {
5858
+ report(error)
5859
+ } finally {
5860
+ setNotesBusy(false)
5861
+ }
5862
+ }
5863
+
5864
+ const addNote = async (text) => {
5865
+ if (!doc || notesBusy) return
5866
+ setNotesBusy(true)
5867
+ try {
5868
+ const result = await rpc('addSegmentNote', { documentId: doc.id, index, text })
5869
+ setNotes(result.notes)
5870
+ adoptState(result.state, index)
5871
+ notify(t('toast.noteSaved'))
5872
+ } catch (error) {
5873
+ report(error)
5874
+ } finally {
5875
+ setNotesBusy(false)
5876
+ }
5877
+ }
5878
+
5879
+ const updateNote = async (noteId, text) => {
5880
+ if (!doc || notesBusy) return
5881
+ setNotesBusy(true)
5882
+ try {
5883
+ const result = await rpc('updateSegmentNote', { documentId: doc.id, index, noteId, text })
5884
+ setNotes(result.notes)
5885
+ adoptState(result.state, index)
5886
+ notify(t('toast.noteSaved'))
5887
+ } catch (error) {
5888
+ report(error)
5889
+ } finally {
5890
+ setNotesBusy(false)
5891
+ }
5892
+ }
5893
+
5894
+ const deleteNote = async (noteId) => {
5895
+ if (!doc || notesBusy) return
5896
+ setNotesBusy(true)
5897
+ try {
5898
+ const result = await rpc('deleteSegmentNote', { documentId: doc.id, index, noteId })
5899
+ setNotes(result.notes)
5900
+ adoptState(result.state, index)
5901
+ notify(t('toast.noteDeleted'))
5902
+ } catch (error) {
5903
+ report(error)
5904
+ } finally {
5905
+ setNotesBusy(false)
5906
+ }
5907
+ }
5908
+
5909
+ const refreshQa = async () => { if (!doc) return
5910
+ setQa(null)
5911
+ try {
5912
+ setQa(await rpc('getQaReport', { documentId: doc.id }))
5913
+ } catch {
5914
+ // QA is advisory; a failure must not disturb the editor.
5915
+ }
5916
+ }
5917
+
5918
+ const jumpQa = (nextIndex) => {
5919
+ if (Number.isInteger(nextIndex)) chooseSegment(nextIndex)
5920
+ }
5921
+
5922
+ const runReview = async () => {
5923
+ if (!doc || reviewBusy) return
5924
+ setReviewBusy(true)
5925
+ try {
5926
+ const result = await rpc('reviewDocument', {
5927
+ documentId: doc.id,
5928
+ from: 0,
5929
+ count: 20,
5930
+ })
5931
+ setReview(result || { issues: [] })
5932
+ } catch (error) {
5933
+ setReview(null)
5934
+ report(error)
5935
+ } finally {
5936
+ setReviewBusy(false)
5937
+ }
5938
+ }
5939
+
5940
+ const applyReview = (issue) => {
5941
+ if (!doc || !issue || !Number.isInteger(issue.index)) return
5942
+ chooseSegment(issue.index)
5943
+ if (issue.suggestion) {
5944
+ setDraft(issue.suggestion)
5945
+ setDirty(true)
5946
+ }
5947
+ }
5948
+
5949
+ const applyQaFix = async (item, action) => {
5950
+ if (!doc || busy || !item) return
5951
+ const segment = doc.segments[item.index]
5952
+ if (!segment) return
5953
+ let target = segment.target || ''
5954
+ if (action === 'copy-source') {
5955
+ target = segment.source
5956
+ } else if (action === 'clear') {
5957
+ target = ''
5958
+ } else if (action === 'term') {
5959
+ const term = String(item.termTarget || '')
5960
+ if (!term || target.includes(term)) return
5961
+ const separator = /[A-Za-z0-9]$/.test(target) && /^[A-Za-z0-9]/.test(term) ? ' ' : ''
5962
+ target = `${target}${separator}${term}`
5963
+ } else {
5964
+ return
5965
+ }
5966
+ setBusy('qa-fix')
5967
+ try {
5968
+ const result = await rpc('saveDraft', {
5969
+ documentId: doc.id,
5970
+ index: item.index,
5971
+ target,
5972
+ })
5973
+ if (result.state) adoptState(result.state, item.index)
5974
+ setQa(await rpc('getQaReport', { documentId: doc.id }))
5975
+ notify(t('toast.qaFixed'))
5976
+ } catch (error) {
5977
+ report(error)
5978
+ } finally {
5979
+ setBusy('')
5980
+ }
5981
+ }
5982
+
5983
+ const runConcordance = async () => {
5984
+ const query = concordanceQuery.trim()
5985
+ if (!doc || !query) {
5986
+ setConcordance(null)
5987
+ return
5988
+ }
5989
+ setConcordanceBusy(true)
5990
+ try {
5991
+ setConcordance(await rpc('concordance', { documentId: doc.id, query, limit: 50 }))
5992
+ } catch (error) {
5993
+ report(error)
5994
+ } finally {
5995
+ setConcordanceBusy(false)
5996
+ }
5997
+ }
5998
+
5999
+ const applyConcordance = (entry) => {
6000
+ if (!entry?.target) return
6001
+ setDraft(entry.target)
6002
+ setDirty(true)
6003
+ setMt(null)
6004
+ setTimeout(() => textareaRef.current?.focus(), 0)
6005
+ }
6006
+
4215
6007
  const editTm = async ({ id, target }) => {
4216
6008
  setBusy('tm-edit')
4217
6009
  try {
@@ -4277,6 +6069,40 @@ window.__ModuleLoader__.load({
4277
6069
  })
4278
6070
  }
4279
6071
 
6072
+ const clearPretranslated = async () => {
6073
+ const active = data?.activeDocument
6074
+ if (!active) return
6075
+ try {
6076
+ const preview = await rpc('getClearUnconfirmedPreview', { documentId: active.id })
6077
+ const counts = preview?.counts || {}
6078
+ const total = counts.total || 0
6079
+ if (!total) {
6080
+ notify(t('toast.nothingToClear'), 'info')
6081
+ return
6082
+ }
6083
+ setModal({
6084
+ type: 'confirm',
6085
+ title: t('modal.confirm.clearPretranslatedTitle'),
6086
+ message: t('modal.confirm.clearPretranslatedMessage')
6087
+ .replace('{count}', String(total))
6088
+ .replace('{mt}', String(counts.mt || 0))
6089
+ .replace('{tm}', String(counts.tm || 0))
6090
+ .replace('{auto}', String(counts.auto || 0)),
6091
+ confirmLabel: t('modal.confirm.clearPretranslatedConfirm').replace('{count}', String(total)),
6092
+ action: async () => {
6093
+ const result = await rpc('clearUnconfirmedTranslations', {
6094
+ documentId: active.id,
6095
+ confirm: true,
6096
+ })
6097
+ adoptState(result.state, result.cursor)
6098
+ notify(t('toast.pretranslatedCleared').replace('{count}', String(result.cleared)))
6099
+ },
6100
+ })
6101
+ } catch (error) {
6102
+ report(error)
6103
+ }
6104
+ }
6105
+
4280
6106
  const addTerm = async (form) => { setBusy('term')
4281
6107
  try {
4282
6108
  const result = await rpc('addGlossary', {
@@ -4466,6 +6292,32 @@ window.__ModuleLoader__.load({
4466
6292
  }
4467
6293
  }
4468
6294
 
6295
+ const importReview = (file) => {
6296
+ if (!doc || !file) return
6297
+ const reader = new FileReader()
6298
+ reader.onload = async () => {
6299
+ setBusy('review')
6300
+ try {
6301
+ const result = await rpc('applyXliffReview', {
6302
+ documentId: doc.id,
6303
+ text: String(reader.result || ''),
6304
+ })
6305
+ adoptState(result.state)
6306
+ notify(
6307
+ t('toast.reviewApplied')
6308
+ .replace('{applied}', String(result.applied))
6309
+ .replace('{unmatched}', String(result.unmatched + result.sourceMismatch)),
6310
+ )
6311
+ setModal(null)
6312
+ } catch (error) {
6313
+ report(error)
6314
+ } finally {
6315
+ setBusy('')
6316
+ }
6317
+ }
6318
+ reader.readAsText(file, 'utf-8')
6319
+ }
6320
+
4469
6321
  const openConfirm = (state) => setModal({ type: 'confirm', ...state })
4470
6322
 
4471
6323
  const clearTm = () =>
@@ -4520,6 +6372,7 @@ window.__ModuleLoader__.load({
4520
6372
 
4521
6373
  const openIds = new Set((data?.openDocuments || []).map((item) => item.id))
4522
6374
  const closedDocuments = (data?.documents || []).filter((item) => !openIds.has(item.id))
6375
+ const qaRisk = (qa?.risks || []).find((item) => item.index === index) || null
4523
6376
 
4524
6377
  const deleteClosedDocument = (document) =>
4525
6378
  openConfirm({
@@ -4599,6 +6452,23 @@ window.__ModuleLoader__.load({
4599
6452
  }
4600
6453
  }, [])
4601
6454
 
6455
+ useEffect(() => {
6456
+ const onKey = (event) => {
6457
+ const mod = event.metaKey || event.ctrlKey
6458
+ if (!mod) return
6459
+ const key = event.key.toLowerCase()
6460
+ if (key === 'k') {
6461
+ event.preventDefault()
6462
+ setModal((current) => (current?.type === 'palette' ? null : { type: 'palette' }))
6463
+ } else if (key === 'f' && doc && !modal) {
6464
+ event.preventDefault()
6465
+ openReplace()
6466
+ }
6467
+ }
6468
+ window.addEventListener('keydown', onKey)
6469
+ return () => window.removeEventListener('keydown', onKey)
6470
+ }, [doc?.id, modal])
6471
+
4602
6472
  if (loading) {
4603
6473
  return h(
4604
6474
  'div',
@@ -4626,6 +6496,44 @@ window.__ModuleLoader__.load({
4626
6496
  )
4627
6497
  }
4628
6498
 
6499
+ const commands = [
6500
+ { id: 'new', title: t('cmd.new'), hint: t('cmd.newHint'), run: () => setModal({ type: 'new', defaultMode: 'text' }) },
6501
+ { id: 'import', title: t('cmd.import'), hint: '', run: () => setModal({ type: 'new', defaultMode: 'file' }) },
6502
+ { id: 'export', title: t('cmd.export'), hint: '', docOnly: true, run: () => setModal({ type: 'export' }) },
6503
+ { id: 'pretranslate', title: t('cmd.pretranslate'), hint: '', docOnly: true, run: () => runPreTranslate() },
6504
+ { id: 'replace', title: t('cmd.replace'), hint: 'Ctrl+F', docOnly: true, run: () => openReplace() },
6505
+ { id: 'brief', title: t('cmd.brief'), hint: '', docOnly: true, run: () => setModal({ type: 'brief' }) },
6506
+ { id: 'resources', title: t('cmd.resources'), hint: '', docOnly: true, run: () => setModal({ type: 'resources' }) },
6507
+ { id: 'analysis', title: t('cmd.analysis'), hint: '', docOnly: true, run: () => openAnalysis() },
6508
+ { id: 'notes', title: t('cmd.notes'), hint: '', docOnly: true, run: () => openNotes() },
6509
+ {
6510
+ id: 'review',
6511
+ title: t('cmd.review'),
6512
+ hint: '',
6513
+ docOnly: true,
6514
+ run: () => {
6515
+ setTab('qa')
6516
+ runReview()
6517
+ },
6518
+ },
6519
+ {
6520
+ id: 'clear-pretranslated',
6521
+ title: t('cmd.clearPretranslated'),
6522
+ hint: '',
6523
+ docOnly: true,
6524
+ run: () => clearPretranslated(),
6525
+ },
6526
+ { id: 'documents', title: t('cmd.documents'), hint: '', run: () => setView('documents') },
6527
+ { id: 'libraries', title: t('cmd.libraries'), hint: '', run: () => setView('libraries') },
6528
+ { id: 'align', title: t('cmd.align'), hint: '', run: () => setView('align') },
6529
+ { id: 'settings', title: t('cmd.settings'), hint: '', run: () => setModal({ type: 'settings' }) },
6530
+ ].filter((item) => !item.docOnly || doc)
6531
+
6532
+ const runCommand = (item) => {
6533
+ setModal(null)
6534
+ setTimeout(() => item.run(), 0)
6535
+ }
6536
+
4629
6537
  return h(
4630
6538
  'div',
4631
6539
  { className: 'cait-root', ref: rootRef },
@@ -4646,6 +6554,7 @@ window.__ModuleLoader__.load({
4646
6554
  pre,
4647
6555
  onPreTranslate: runPreTranslate,
4648
6556
  onCancelPreTranslate: cancelPreTranslate,
6557
+ onReplace: openReplace,
4649
6558
  view,
4650
6559
  onView: setView,
4651
6560
  }),
@@ -4664,6 +6573,7 @@ window.__ModuleLoader__.load({
4664
6573
  onCreateLibrary: createLibrary,
4665
6574
  onRenameLibrary: renameLibrary,
4666
6575
  onDeleteLibrary: deleteLibrary,
6576
+ onSetMounts: setMounts,
4667
6577
  })
4668
6578
  : null,
4669
6579
  view === 'align'
@@ -4681,7 +6591,7 @@ window.__ModuleLoader__.load({
4681
6591
  ? h(
4682
6592
  'div',
4683
6593
  { className: `cait-body${doc ? '' : ' cait-no-doc'}` },
4684
- doc ? h(SegmentRail, { t, doc, selected: index, query: railQuery, setQuery: setRailQuery, filter: railFilter, setFilter: setRailFilter, onSelect: chooseSegment, onConfirmAll: confirmAllTranslated, onPropagate: propagateRepeats, busy, qa }) : null,
6594
+ 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,
4685
6595
  h(
4686
6596
  'div',
4687
6597
  { className: `cait-main${doc ? '' : ' cait-main-empty'}` },
@@ -4693,18 +6603,29 @@ window.__ModuleLoader__.load({
4693
6603
  segment,
4694
6604
  detail,
4695
6605
  draft,
4696
- setDraft: (value) => {
4697
- setDraft(value)
4698
- setDirty(true)
4699
- },
6606
+ setDraft: changeDraft,
4700
6607
  dirty,
4701
6608
  busy,
6609
+ canUndo: draftHistoryRef.current.cursor > 0,
6610
+ canRedo: draftHistoryRef.current.cursor < draftHistoryRef.current.stack.length - 1,
6611
+ onUndo: undoDraft,
6612
+ onRedo: redoDraft,
4702
6613
  onConfirm: confirmSegment,
6614
+ onUnconfirm: unconfirmCurrent,
6615
+ onToggleLock: toggleLock,
6616
+ onMerge: mergeCurrent,
6617
+ onSplit: splitCurrent,
4703
6618
  onNext: () => moveSegment(1),
4704
6619
  onPrev: () => moveSegment(-1),
4705
6620
  onSave: () => saveDraft(false),
4706
6621
  onAdopt: adoptSuggestion,
4707
6622
  onApplyTerm: applyTerm,
6623
+ onOpenBrief: () => setModal({ type: 'brief' }),
6624
+ onOpenResources: () => setModal({ type: 'resources' }),
6625
+ onOpenAnalysis: openAnalysis,
6626
+ onOpenNotes: openNotes,
6627
+ noteCount: segment?.notes?.length || 0,
6628
+ qaRisk,
4708
6629
  textareaRef,
4709
6630
  })
4710
6631
  : h(EmptyState, { t, settings, busy, onCreate: createFromForm, onDemo: loadDemo }),
@@ -4745,7 +6666,20 @@ window.__ModuleLoader__.load({
4745
6666
  onCopy: copy,
4746
6667
  onOpenSettings: () => setModal({ type: 'settings' }),
4747
6668
  onSearchLibrary: openLibrary,
4748
- busy,
6669
+ qa,
6670
+ onRefreshQa: refreshQa,
6671
+ onJumpQa: jumpQa,
6672
+ onFixQa: applyQaFix,
6673
+ review,
6674
+ reviewBusy,
6675
+ onReview: runReview,
6676
+ onApplyReview: applyReview,
6677
+ concordance,
6678
+ concordanceQuery,
6679
+ setConcordanceQuery,
6680
+ onConcordance: runConcordance,
6681
+ onApplyConcordance: applyConcordance,
6682
+ busy: busy || (concordanceBusy ? 'concordance' : ''),
4749
6683
  })
4750
6684
  : null,
4751
6685
  ),
@@ -4762,7 +6696,60 @@ window.__ModuleLoader__.load({
4762
6696
  })
4763
6697
  : null,
4764
6698
  modal?.type === 'export'
4765
- ? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs })
6699
+ ? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs, onImportReview: importReview })
6700
+ : null,
6701
+ modal?.type === 'brief'
6702
+ ? h(BriefModal, { t, doc, busy, onClose: () => setModal(null), onSave: saveBrief })
6703
+ : null,
6704
+ modal?.type === 'resources'
6705
+ ? h(ResourceModal, {
6706
+ t,
6707
+ doc,
6708
+ libraries: data?.libraries,
6709
+ mounts: data?.mounts,
6710
+ busy,
6711
+ onClose: () => setModal(null),
6712
+ onSave: saveResources,
6713
+ })
6714
+ : null,
6715
+ modal?.type === 'analysis'
6716
+ ? h(AnalysisModal, {
6717
+ t,
6718
+ doc,
6719
+ analysis,
6720
+ busy: analysisBusy,
6721
+ onClose: () => setModal(null),
6722
+ })
6723
+ : null,
6724
+ modal?.type === 'replace'
6725
+ ? h(ReplaceModal, {
6726
+ t,
6727
+ doc,
6728
+ busy: replaceBusy,
6729
+ preview: replacePreview,
6730
+ undoCount: replaceUndo ? replaceUndo.length : 0,
6731
+ onPreview: previewReplace,
6732
+ onApply: applyReplace,
6733
+ onUndo: undoReplace,
6734
+ onClose: () => setModal(null),
6735
+ })
6736
+ : null,
6737
+ modal?.type === 'notes'
6738
+ ? h(NoteModal, {
6739
+ t,
6740
+ doc,
6741
+ index,
6742
+ segment,
6743
+ notes,
6744
+ busy: notesBusy,
6745
+ onAdd: addNote,
6746
+ onUpdate: updateNote,
6747
+ onDelete: deleteNote,
6748
+ onClose: () => setModal(null),
6749
+ })
6750
+ : null,
6751
+ modal?.type === 'palette'
6752
+ ? h(CommandPalette, { t, items: commands, onClose: () => setModal(null), onRun: runCommand })
4766
6753
  : null,
4767
6754
  modal?.type === 'settings'
4768
6755
  ? h(SettingsModal, { t, settings, mtRoutes, busy, onClose: () => setModal(null), onSave: saveSettings })
@@ -4846,6 +6833,14 @@ window.__ModuleLoader__.load({
4846
6833
  'rail.filter.pending': '待译',
4847
6834
  'rail.filter.translated': '待审',
4848
6835
  'rail.filter.confirmed': '已确认',
6836
+ 'rail.filter.risk': '重点审',
6837
+ 'rail.clearPretranslated': '清理预翻译',
6838
+ 'rail.clearPretranslatedHint': '清空未确认的预翻译与自动填充译文,保留人工草稿和已确认句段',
6839
+ 'modal.confirm.clearPretranslatedTitle': '清理预翻译译文?',
6840
+ 'modal.confirm.clearPretranslatedMessage': '将清空当前文档中 {count} 句未确认的预翻译译文(MT {mt}、TM {tm}、自动传播 {auto})。已确认句段和人工编辑的译文不会受影响;TM 中已保存的内容也不会删除。清理后这些句段回到未翻译状态,且无法撤销。',
6841
+ 'modal.confirm.clearPretranslatedConfirm': '清理 {count} 句',
6842
+ 'toast.pretranslatedCleared': '已清理 {count} 句预翻译译文',
6843
+ 'toast.nothingToClear': '没有可清理的未确认预翻译',
4849
6844
  'rail.confirmAll': '全部确认',
4850
6845
  'rail.confirmAllHint': '把当前所有「待审」句段一次性确认并写入翻译记忆',
4851
6846
  'rail.confirmAllMessage': '将确认 {count} 个待审句段并写入翻译记忆,句段译文不会经过逐句复核。确定继续?',
@@ -4987,6 +6982,7 @@ window.__ModuleLoader__.load({
4987
6982
  'modal.export.targetTxt': '纯文本',
4988
6983
  'modal.export.targetMd': 'Markdown',
4989
6984
  'modal.export.docx': 'Word DOCX',
6985
+ 'modal.export.bilingualDocx': '双语审校稿 DOCX',
4990
6986
  'modal.export.docxGroup': '保留源格式',
4991
6987
  'modal.export.epub': 'EPUB 电子书',
4992
6988
  'modal.export.epubGroup': '保留源排版',
@@ -4997,6 +6993,10 @@ window.__ModuleLoader__.load({
4997
6993
  'modal.export.vtt': 'WebVTT 字幕',
4998
6994
  'modal.export.po': 'PO 本地化',
4999
6995
  'modal.export.xliff': 'XLIFF 1.2',
6996
+ 'modal.export.xliffReview': 'XLIFF 审校包',
6997
+ 'modal.export.reviewImport': '回填审校包',
6998
+ 'modal.export.reviewImportHint': '选择外部返回的 XLIFF 审校包,只更新能按 id 或源文对上的句段。',
6999
+ 'toast.reviewApplied': '审校包已回填 {applied} 段,{unmatched} 段未匹配',
5000
7000
  'modal.export.xliff2': 'XLIFF 2.0',
5001
7001
  'modal.export.sdlxliff': 'SDL XLIFF(基础)',
5002
7002
  'modal.export.webGroup': '网页与结构化文本',
@@ -5019,8 +7019,16 @@ window.__ModuleLoader__.load({
5019
7019
  'modal.settings.pre.collapseBlankLines': '合并连续空行',
5020
7020
  'modal.settings.tm': '翻译记忆',
5021
7021
  'modal.settings.fuzzy': '模糊匹配阈值',
7022
+ 'modal.settings.fuzzyHint': '低于 70% 的匹配通常只共享虚词,改起来比重译更慢;70% 是主流 CAT 工具的默认值。',
7023
+ 'modal.settings.resetDefault': '恢复默认',
5022
7024
  'modal.settings.limit': '返回匹配数',
5023
7025
  'modal.settings.mt': '机器翻译',
7026
+ 'modal.settings.review': 'AI 审校模型',
7027
+ 'modal.settings.reviewInherit': '跟随翻译模型(默认)',
7028
+ 'modal.settings.reviewHint': '取消勾选后,AI 审校使用独立路由;预翻译仍使用上面的机器翻译路由。',
7029
+ 'modal.settings.reviewProvider': '审校 provider 路由',
7030
+ 'modal.settings.reviewInheritedHint': '当前与预翻译共用同一个 provider 和模型。',
7031
+ 'modal.settings.reviewCustomHint': 'AI 审校使用独立路由,预翻译不受影响。',
5024
7032
  'modal.settings.provider': 'MT provider 路由',
5025
7033
  'modal.settings.routeHint': 'CAIT 只保存路由名和模型;API Key 由 DSH 凭据服务解析,CAIT 不接触。',
5026
7034
  'modal.settings.routeMissing': '已失效的路由',
@@ -5081,6 +7089,11 @@ window.__ModuleLoader__.load({
5081
7089
  'library.import': '导入',
5082
7090
  'library.export': '导出',
5083
7091
  'library.clear': '清空',
7092
+ 'tm.maintain.dedupe': '去重',
7093
+ 'tm.maintain.prune': '清理空译文',
7094
+ 'tm.maintain.dedupeHint': '将删除 {count} 条重复条目,保留最新一条。',
7095
+ 'tm.maintain.pruneHint': '将删除 {count} 条空译文条目。',
7096
+ 'tm.maintain.done': 'TM 维护完成:{count} 条',
5084
7097
  'library.newLibrary': '新建库',
5085
7098
  'library.rename': '重命名',
5086
7099
  'library.deleteLibrary': '删除库',
@@ -5142,13 +7155,14 @@ window.__ModuleLoader__.load({
5142
7155
  'rail.propagateMessage': '把已有译文填到本文档中其它相同原文的句段(已确认的不会动)。',
5143
7156
  'toast.propagated': '已铺满 {count} 个句段。',
5144
7157
  'term.title': '术语候选',
5145
- 'term.hint': '按出现次数与长度排序,点选后填入译文即可入库。',
7158
+ 'term.hint': '候选包含重复出现的术语和只出现一次的专名;专名需人工确认后才入库。',
5146
7159
  'term.extract': '从文档提取术语',
5147
- 'term.empty': '没有找到重复出现的候选术语。',
7160
+ 'term.empty': '没有找到候选术语。',
5148
7161
  'term.add': '加入术语库',
5149
7162
  'term.added': '已加入 {added} 条术语。',
5150
7163
  'term.noDoc': '先打开一个文档再提取术语。',
5151
7164
  'term.inLibrary': '已有',
7165
+ 'term.proper': '专名',
5152
7166
  }
5153
7167
 
5154
7168
  const EN = {
@@ -5213,6 +7227,14 @@ window.__ModuleLoader__.load({
5213
7227
  'rail.filter.pending': 'To translate',
5214
7228
  'rail.filter.translated': 'To review',
5215
7229
  'rail.filter.confirmed': 'Confirmed',
7230
+ 'rail.filter.risk': 'Needs review',
7231
+ 'rail.clearPretranslated': 'Clear pretranslated',
7232
+ 'rail.clearPretranslatedHint': 'Clear unconfirmed pretranslated and auto-filled targets; manual drafts and confirmed segments are kept',
7233
+ 'modal.confirm.clearPretranslatedTitle': 'Clear pretranslated targets?',
7234
+ 'modal.confirm.clearPretranslatedMessage': 'This will clear {count} unconfirmed pretranslated target(s) in the current document (MT {mt}, TM {tm}, auto-propagated {auto}). Confirmed segments and manually edited targets are not affected, and saved TM entries are not deleted. The cleared segments return to untranslated and this cannot be undone.',
7235
+ 'modal.confirm.clearPretranslatedConfirm': 'Clear {count} segment(s)',
7236
+ 'toast.pretranslatedCleared': 'Cleared {count} pretranslated target(s)',
7237
+ 'toast.nothingToClear': 'No unconfirmed pretranslated targets to clear',
5216
7238
  'rail.confirmAll': 'Confirm all',
5217
7239
  'rail.confirmAllHint': 'Confirm every "To review" segment at once and write them to translation memory',
5218
7240
  'rail.confirmAllMessage': 'This confirms {count} segment(s) awaiting review and writes them to translation memory, without reviewing them one by one. Continue?',
@@ -5354,6 +7376,7 @@ window.__ModuleLoader__.load({
5354
7376
  'modal.export.targetTxt': 'Plain text',
5355
7377
  'modal.export.targetMd': 'Markdown',
5356
7378
  'modal.export.docx': 'Word DOCX',
7379
+ 'modal.export.bilingualDocx': 'Bilingual review DOCX',
5357
7380
  'modal.export.docxGroup': 'Preserve source formatting',
5358
7381
  'modal.export.epub': 'EPUB ebook',
5359
7382
  'modal.export.epubGroup': 'Preserve source layout',
@@ -5364,6 +7387,10 @@ window.__ModuleLoader__.load({
5364
7387
  'modal.export.vtt': 'WebVTT subtitles',
5365
7388
  'modal.export.po': 'PO localization',
5366
7389
  'modal.export.xliff': 'XLIFF 1.2',
7390
+ 'modal.export.xliffReview': 'XLIFF review package',
7391
+ 'modal.export.reviewImport': 'Apply review package',
7392
+ 'modal.export.reviewImportHint': 'Choose an XLIFF review package. Only segments matched by id or source are updated.',
7393
+ 'toast.reviewApplied': 'Review applied: {applied} segments, {unmatched} unmatched',
5367
7394
  'modal.export.xliff2': 'XLIFF 2.0',
5368
7395
  'modal.export.sdlxliff': 'SDL XLIFF (basic)',
5369
7396
  'modal.export.webGroup': 'Web and structured text',
@@ -5386,8 +7413,17 @@ window.__ModuleLoader__.load({
5386
7413
  'modal.settings.pre.collapseBlankLines': 'Merge consecutive blank lines',
5387
7414
  'modal.settings.tm': 'Translation memory',
5388
7415
  'modal.settings.fuzzy': 'Fuzzy-match threshold',
7416
+ 'modal.settings.fuzzyHint':
7417
+ 'Below 70% a match usually shares only function words, so editing it takes longer than retranslating. 70% is what mainstream CAT tools default to.',
7418
+ 'modal.settings.resetDefault': 'Reset',
5389
7419
  'modal.settings.limit': 'Matches returned',
5390
7420
  'modal.settings.mt': 'Machine translation',
7421
+ 'modal.settings.review': 'AI review model',
7422
+ 'modal.settings.reviewInherit': 'Follow the translation model (default)',
7423
+ 'modal.settings.reviewHint': 'Uncheck to give AI review its own route. Pre-translation keeps using the MT route above.',
7424
+ 'modal.settings.reviewProvider': 'Review provider route',
7425
+ 'modal.settings.reviewInheritedHint': 'Review currently shares the same provider and model as pre-translation.',
7426
+ 'modal.settings.reviewCustomHint': 'AI review uses a separate route; pre-translation is unaffected.',
5391
7427
  'modal.settings.provider': 'MT provider route',
5392
7428
  'modal.settings.routeHint': 'CAIT stores only the route and model name; DSH resolves the API key, which CAIT never sees.',
5393
7429
  'modal.settings.routeMissing': 'Route no longer available',
@@ -5448,6 +7484,11 @@ window.__ModuleLoader__.load({
5448
7484
  'library.import': 'Import',
5449
7485
  'library.export': 'Export',
5450
7486
  'library.clear': 'Clear',
7487
+ 'tm.maintain.dedupe': 'Deduplicate',
7488
+ 'tm.maintain.prune': 'Prune empty targets',
7489
+ 'tm.maintain.dedupeHint': 'Delete {count} duplicate entries, keeping the newest.',
7490
+ 'tm.maintain.pruneHint': 'Delete {count} entries with an empty target.',
7491
+ 'tm.maintain.done': 'TM maintenance done: {count} entries',
5451
7492
  'library.newLibrary': 'New library',
5452
7493
  'library.rename': 'Rename',
5453
7494
  'library.deleteLibrary': 'Delete library',
@@ -5509,13 +7550,14 @@ window.__ModuleLoader__.load({
5509
7550
  'rail.propagateMessage': 'Copy the existing translation to other segments with the same source. Confirmed segments are left alone.',
5510
7551
  'toast.propagated': 'Filled {count} segment(s).',
5511
7552
  'term.title': 'Terminology candidates',
5512
- 'term.hint': 'Ranked by occurrences and length. Pick the real terms, type a translation, and add them.',
7553
+ 'term.hint': 'Candidates include repeated terms and one-off proper names; names need a human decision before they enter the glossary.',
5513
7554
  'term.extract': 'Mine terms from the document',
5514
- 'term.empty': 'No repeated candidate found.',
7555
+ 'term.empty': 'No candidate found.',
5515
7556
  'term.add': 'Add to glossary',
5516
7557
  'term.added': 'Added {added} term(s).',
5517
7558
  'term.noDoc': 'Open a document before mining terms.',
5518
7559
  'term.inLibrary': 'in library',
7560
+ 'term.proper': 'proper name',
5519
7561
  }
5520
7562
 
5521
7563
  const JA = {
@@ -5580,6 +7622,14 @@ window.__ModuleLoader__.load({
5580
7622
  'rail.filter.pending': '未翻訳',
5581
7623
  'rail.filter.translated': '要確認',
5582
7624
  'rail.filter.confirmed': '確認済み',
7625
+ 'rail.filter.risk': '重点確認',
7626
+ 'rail.clearPretranslated': '事前翻訳をクリア',
7627
+ 'rail.clearPretranslatedHint': '未確認の事前翻訳・自動反映の訳文を消去します。手動の下書きと確認済みセグメントは残します',
7628
+ 'modal.confirm.clearPretranslatedTitle': '事前翻訳をクリアしますか?',
7629
+ 'modal.confirm.clearPretranslatedMessage': '現在の文書で未確認の事前翻訳訳文 {count} 件(MT {mt}、TM {tm}、自動反映 {auto})を消去します。確認済みセグメントと手動編集の訳文は影響を受けず、TM に保存済みの内容も削除されません。消去後は未翻訳に戻り、元に戻せません。',
7630
+ 'modal.confirm.clearPretranslatedConfirm': '{count} 件をクリア',
7631
+ 'toast.pretranslatedCleared': '事前翻訳訳文を {count} 件クリアしました',
7632
+ 'toast.nothingToClear': 'クリアできる未確認の事前翻訳はありません',
5583
7633
  'rail.confirmAll': 'まとめて確認',
5584
7634
  'rail.confirmAllHint': '「要確認」の文をまとめて確認し、翻訳メモリに書き込みます',
5585
7635
  'rail.confirmAllMessage': '要確認の {count} 文をまとめて確認し、翻訳メモリに書き込みます。1 文ずつ見直すことはありません。続けますか?',
@@ -5721,6 +7771,7 @@ window.__ModuleLoader__.load({
5721
7771
  'modal.export.targetTxt': 'プレーンテキスト',
5722
7772
  'modal.export.targetMd': 'Markdown',
5723
7773
  'modal.export.docx': 'Word DOCX',
7774
+ 'modal.export.bilingualDocx': 'バイリンガル校閲 DOCX',
5724
7775
  'modal.export.docxGroup': '元の書式を保持',
5725
7776
  'modal.export.epub': 'EPUB 電子書',
5726
7777
  'modal.export.epubGroup': '元のレイアウトを保持',
@@ -5731,6 +7782,10 @@ window.__ModuleLoader__.load({
5731
7782
  'modal.export.vtt': 'WebVTT 字幕',
5732
7783
  'modal.export.po': 'PO ローカライズ',
5733
7784
  'modal.export.xliff': 'XLIFF 1.2',
7785
+ 'modal.export.xliffReview': 'XLIFF 校閲パッケージ',
7786
+ 'modal.export.reviewImport': '校閲パッケージを反映',
7787
+ 'modal.export.reviewImportHint': '外部から返却された XLIFF を選択。id または原文が一致するセグメントだけ更新します。',
7788
+ 'toast.reviewApplied': '校閲を反映: {applied} セグメント、未一致 {unmatched}',
5734
7789
  'modal.export.xliff2': 'XLIFF 2.0',
5735
7790
  'modal.export.sdlxliff': 'SDL XLIFF(基本)',
5736
7791
  'modal.export.webGroup': 'Web と構造化テキスト',
@@ -5753,8 +7808,17 @@ window.__ModuleLoader__.load({
5753
7808
  'modal.settings.pre.collapseBlankLines': '連続する空行をまとめる',
5754
7809
  'modal.settings.tm': '翻訳メモリ',
5755
7810
  'modal.settings.fuzzy': 'あいまい一致のしきい値',
7811
+ 'modal.settings.fuzzyHint':
7812
+ '70% を下回る一致は機能語だけが重なることが多く、訳し直すより遅くなります。主流の CAT ツールも 70% を既定にしています。',
7813
+ 'modal.settings.resetDefault': '既定に戻す',
5756
7814
  'modal.settings.limit': '返す一致件数',
5757
7815
  'modal.settings.mt': '機械翻訳',
7816
+ 'modal.settings.review': 'AI レビューモデル',
7817
+ 'modal.settings.reviewInherit': '翻訳モデルに従う(デフォルト)',
7818
+ 'modal.settings.reviewHint': 'チェックを外すと AI レビューが独立したルートを使います。事前翻訳は上の MT ルートのままです。',
7819
+ 'modal.settings.reviewProvider': 'レビュー用プロバイダーのルート',
7820
+ 'modal.settings.reviewInheritedHint': '現在、レビューは事前翻訳と同じプロバイダーとモデルを共有しています。',
7821
+ 'modal.settings.reviewCustomHint': 'AI レビューは独立したルートを使います。事前翻訳には影響しません。',
5758
7822
  'modal.settings.provider': 'MT プロバイダーのルート',
5759
7823
  'modal.settings.routeHint': 'CAIT はルート名とモデル名だけを保存します。API キーは DSH の認証情報サービスが解決し、CAIT は触れません。',
5760
7824
  'modal.settings.routeMissing': '利用できないルート',
@@ -5815,6 +7879,11 @@ window.__ModuleLoader__.load({
5815
7879
  'library.import': 'インポート',
5816
7880
  'library.export': 'エクスポート',
5817
7881
  'library.clear': 'クリア',
7882
+ 'tm.maintain.dedupe': '重複排除',
7883
+ 'tm.maintain.prune': '空訳を整理',
7884
+ 'tm.maintain.dedupeHint': '{count} 件の重複を削除し、最新のみ残します。',
7885
+ 'tm.maintain.pruneHint': '空の訳文を持つ {count} 件を削除します。',
7886
+ 'tm.maintain.done': 'TM メンテナンス完了: {count} 件',
5818
7887
  'library.newLibrary': '新しいライブラリ',
5819
7888
  'library.rename': '名前を変更',
5820
7889
  'library.deleteLibrary': 'ライブラリを削除',
@@ -5876,18 +7945,514 @@ window.__ModuleLoader__.load({
5876
7945
  'rail.propagateMessage': '同じ原文の他のセグメントに既存の訳文をコピーします。確定済みは変更しません。',
5877
7946
  'toast.propagated': '{count} 件のセグメントを埋めました。',
5878
7947
  'term.title': '用語候補',
5879
- 'term.hint': '出現回数と長さで並んでいます。実際の用語だけを選び、訳語を入れて登録してください。',
7948
+ 'term.hint': '繰り返し用語と一度だけ現れる固有名詞を候補にします。固有名詞は確認してから登録してください。',
5880
7949
  'term.extract': '文書から用語を抽出',
5881
- 'term.empty': '繰り返し現れる候補はありませんでした。',
7950
+ 'term.empty': '候補が見つかりませんでした。',
5882
7951
  'term.add': '用語集に追加',
5883
7952
  'term.added': '{added} 件の用語を追加しました。',
5884
7953
  'term.noDoc': '先に文書を開いてください。',
5885
7954
  'term.inLibrary': '登録済み',
7955
+ 'term.proper': '固有名詞',
5886
7956
  }
5887
7957
 
7958
+ // 1.1.4: brief, risk-aware QA, unconfirm and concordance. Kept in one place
7959
+ // so the three dictionaries cannot drift apart.
7960
+ Object.assign(ZH, {
7961
+ 'panel.qa': '检查',
7962
+ 'panel.concordance': '检索',
7963
+ 'panel.concordancePlaceholder': '在翻译记忆中检索…',
7964
+ 'panel.concordanceHint': '在已挂载的记忆库中检索源文或译文;点击结果写入当前句段的译文。',
7965
+ 'panel.noConcordance': '没有检索结果。',
7966
+ 'panel.searchConcordance': '检索',
7967
+ 'panel.brief': '文档 brief',
7968
+ 'panel.briefHint': '给译者和 agent 的领域、读者、语气与术语约束。',
7969
+ 'panel.termForbidden': '禁用',
7970
+ 'panel.termPreferred': '必用',
7971
+ 'brief.title': '文档 brief',
7972
+ 'brief.hint': '这些信息会注入预翻译和 agent 上下文;留空表示不限制。',
7973
+ 'brief.domain': '领域',
7974
+ 'brief.audience': '读者',
7975
+ 'brief.tone': '语气',
7976
+ 'brief.notes': '补充说明',
7977
+ 'brief.doNotTranslate': '不要翻译',
7978
+ 'brief.styleSample': '风格示例',
7979
+ 'brief.save': '保存 brief',
7980
+ 'brief.saved': 'brief 已保存',
7981
+ 'brief.empty': '尚未填写 brief',
7982
+ 'brief.placeholder.domain': '例如:法律、医疗、游戏',
7983
+ 'brief.placeholder.audience': '例如:终端用户、开发者',
7984
+ 'brief.placeholder.tone': '例如:正式、简洁、口语',
7985
+ 'brief.placeholder.notes': '术语偏好、格式要求、背景…',
7986
+ 'brief.placeholder.doNotTranslate': '每行一个词或短语',
7987
+ 'brief.placeholder.styleSample': '粘贴一段期望风格的译文…',
7988
+ 'editor.unconfirm': '撤销确认',
7989
+ 'editor.unconfirmHint': '回到待审状态,不改动翻译记忆',
7990
+ 'editor.undo': '撤销编辑',
7991
+ 'editor.redo': '重做编辑',
7992
+ 'editor.lock': '锁定',
7993
+ 'editor.unlock': '解锁',
7994
+ 'editor.locked': '已锁定',
7995
+ 'toast.locked': '已锁定句段',
7996
+ 'toast.unlocked': '已解锁句段',
7997
+ 'editor.brief': 'Brief',
7998
+ 'editor.risk': '风险',
7999
+ 'qa.title': '检查',
8000
+ 'qa.refresh': '重新检查',
8001
+ 'qa.empty': '没有发现问题。',
8002
+ 'qa.loading': '正在检查…',
8003
+ 'qa.review': 'AI 审校',
8004
+ 'qa.reviewing': '审校中…',
8005
+ 'qa.reviewTitle': 'AI 审校建议',
8006
+ 'qa.reviewEmpty': 'AI 没有发现需要改的句子。',
8007
+ 'qa.reviewApply': '填入建议',
8008
+ 'qa.high': '高',
8009
+ 'qa.medium': '中',
8010
+ 'qa.low': '低',
8011
+ 'qa.jump': '跳到句段',
8012
+ 'qa.risk': '风险',
8013
+ 'qa.code.TERM_FORBIDDEN': '禁用术语',
8014
+ 'qa.code.LENGTH_DRIFT': '长度异常',
8015
+ 'qa.fixCopy': '复制源文',
8016
+ 'qa.fixClear': '清空译文',
8017
+ 'qa.fixTerm': '插入术语',
8018
+ 'toast.qaFixed': '已修复该句,可继续检查',
8019
+ 'toast.unconfirmed': '已撤销确认,可继续编辑',
8020
+ 'toast.briefSaved': 'brief 已保存',
8021
+ 'toast.termForbidden': '这是禁用术语,不能插入',
8022
+ 'editor.resources': '资源',
8023
+ 'resources.title': '文档资源',
8024
+ 'resources.hint': '限定这篇文档读写哪些 TM 和术语库;不勾选“继承全局”时只使用下面选中的库。',
8025
+ 'resources.inheritTm': '继承全局 TM 挂载',
8026
+ 'resources.inheritGlossary': '继承全局术语库挂载',
8027
+ 'resources.tm': '翻译记忆',
8028
+ 'resources.glossary': '术语库',
8029
+ 'resources.defaultWrite': '默认写入',
8030
+ 'resources.followDefault': '跟随全局默认',
8031
+ 'resources.firstBound': '首个绑定库',
8032
+ 'resources.none': '未绑定(不读不写)',
8033
+ 'resources.empty': '没有可用的库。',
8034
+ 'resources.save': '保存资源',
8035
+ 'toast.resourcesSaved': '文档资源已保存',
8036
+ 'editor.analysis': '分析',
8037
+ 'analysis.title': '文档分析',
8038
+ 'analysis.hint': '按当前文档绑定的 TM 统计;匹配率用于报价参考。',
8039
+ 'analysis.words': '字数',
8040
+ 'analysis.characters': '字符',
8041
+ 'analysis.segments': '句段',
8042
+ 'analysis.duplicates': '重复句段',
8043
+ 'analysis.translated': '已有译文',
8044
+ 'analysis.confirmed': '已确认',
8045
+ 'analysis.ice': 'ICE / 上下文',
8046
+ 'analysis.exact': '完全匹配',
8047
+ 'analysis.fuzzy85': '85–99%',
8048
+ 'analysis.fuzzy70': '70–84%',
8049
+ 'analysis.new': '新句段',
8050
+ 'analysis.matchRate': '匹配率',
8051
+ 'analysis.leverage': '加权 leverage',
8052
+ 'analysis.exportCsv': '导出 CSV',
8053
+ 'analysis.exportJson': '导出 JSON',
8054
+ 'analysis.close': '关闭',
8055
+ 'replace.open': '查找替换',
8056
+ 'replace.title': '查找替换',
8057
+ 'replace.hint': '在原文或译文中查找并替换;应用前先预览影响范围。',
8058
+ 'replace.query': '查找',
8059
+ 'replace.queryPlaceholder': '要查找的文本',
8060
+ 'replace.replacement': '替换为',
8061
+ 'replace.replacementPlaceholder': '留空表示删除',
8062
+ 'replace.scope': '范围',
8063
+ 'replace.scope.target': '仅译文',
8064
+ 'replace.scope.source': '仅原文',
8065
+ 'replace.scope.both': '原文和译文',
8066
+ 'replace.matchCase': '区分大小写',
8067
+ 'replace.regex': '正则表达式',
8068
+ 'replace.preview': '预览',
8069
+ 'replace.apply': '应用替换',
8070
+ 'replace.undo': '撤销上次替换',
8071
+ 'replace.results': '命中 {count} 处,涉及 {segments} 个句段',
8072
+ 'replace.truncated': '结果较多,仅显示前 200 条。',
8073
+ 'replace.noResults': '没有命中。',
8074
+ 'replace.field.source': '原文',
8075
+ 'replace.field.target': '译文',
8076
+ 'toast.replaced': '已替换 {count} 处',
8077
+ 'toast.replaceUndone': '已撤销替换',
8078
+ 'palette.title': '命令面板',
8079
+ 'palette.placeholder': '输入命令…',
8080
+ 'palette.empty': '没有匹配的命令。',
8081
+ 'palette.footer': '↑↓ 选择 · Enter 执行 · Esc 关闭 · Ctrl+K 打开',
8082
+ 'cmd.new': '新建文档',
8083
+ 'cmd.newHint': '从文本或文件开始',
8084
+ 'cmd.import': '导入文件',
8085
+ 'cmd.export': '导出',
8086
+ 'cmd.pretranslate': '预翻译',
8087
+ 'cmd.replace': '查找替换',
8088
+ 'cmd.brief': '文档 brief',
8089
+ 'cmd.resources': '文档资源',
8090
+ 'cmd.analysis': '文档分析',
8091
+ 'cmd.clearPretranslated': '清理预翻译',
8092
+ 'cmd.notes': '句段备注',
8093
+ 'cmd.review': 'AI 审校当前文档',
8094
+ 'editor.notes': '备注',
8095
+ 'notes.title': '句段备注',
8096
+ 'notes.hint': '备注只保存在本文档,不会写入 TM。',
8097
+ 'notes.empty': '还没有备注。',
8098
+ 'notes.placeholder': '写下审校说明、术语疑问或给 agent 的提示…',
8099
+ 'notes.add': '添加备注',
8100
+ 'notes.save': '保存',
8101
+ 'notes.cancel': '取消',
8102
+ 'notes.edit': '编辑',
8103
+ 'notes.delete': '删除',
8104
+ 'notes.user': '你',
8105
+ 'toast.noteSaved': '备注已保存',
8106
+ 'toast.noteDeleted': '备注已删除',
8107
+ 'cmd.documents': '文档工作区',
8108
+ 'cmd.libraries': '库管理',
8109
+ 'cmd.align': '对齐',
8110
+ 'cmd.settings': '设置',
8111
+ })
8112
+ Object.assign(EN, {
8113
+ 'panel.qa': 'Checks',
8114
+ 'panel.concordance': 'Concordance',
8115
+ 'panel.concordancePlaceholder': 'Search translation memory…',
8116
+ 'panel.concordanceHint': 'Search source or target across mounted memories; click a result to use its target in the current segment.',
8117
+ 'panel.noConcordance': 'No matches.',
8118
+ 'panel.searchConcordance': 'Search',
8119
+ 'panel.brief': 'Document brief',
8120
+ 'panel.briefHint': 'Domain, audience, tone and terminology guidance for translators and agents.',
8121
+ 'panel.termForbidden': 'Forbidden',
8122
+ 'panel.termPreferred': 'Required',
8123
+ 'brief.title': 'Document brief',
8124
+ 'brief.hint': 'Injected into pre-translation and agent context. Empty fields mean no constraint.',
8125
+ 'brief.domain': 'Domain',
8126
+ 'brief.audience': 'Audience',
8127
+ 'brief.tone': 'Tone',
8128
+ 'brief.notes': 'Notes',
8129
+ 'brief.doNotTranslate': 'Do not translate',
8130
+ 'brief.styleSample': 'Style sample',
8131
+ 'brief.save': 'Save brief',
8132
+ 'brief.saved': 'Brief saved',
8133
+ 'brief.empty': 'No brief yet',
8134
+ 'brief.placeholder.domain': 'e.g. legal, medical, games',
8135
+ 'brief.placeholder.audience': 'e.g. end users, developers',
8136
+ 'brief.placeholder.tone': 'e.g. formal, concise, conversational',
8137
+ 'brief.placeholder.notes': 'Terminology preferences, formatting requirements, background…',
8138
+ 'brief.placeholder.doNotTranslate': 'One term or phrase per line',
8139
+ 'brief.placeholder.styleSample': 'Paste a sample of the desired style…',
8140
+ 'editor.unconfirm': 'Unconfirm',
8141
+ 'editor.unconfirmHint': 'Return to review without changing translation memory',
8142
+ 'editor.undo': 'Undo edit',
8143
+ 'editor.redo': 'Redo edit',
8144
+ 'editor.lock': 'Lock',
8145
+ 'editor.unlock': 'Unlock',
8146
+ 'editor.locked': 'Locked',
8147
+ 'toast.locked': 'Segment locked',
8148
+ 'toast.unlocked': 'Segment unlocked',
8149
+ 'editor.brief': 'Brief',
8150
+ 'editor.risk': 'Risk',
8151
+ 'qa.title': 'Checks',
8152
+ 'qa.refresh': 'Re-run checks',
8153
+ 'qa.empty': 'No issues found.',
8154
+ 'qa.loading': 'Running checks…',
8155
+ 'qa.review': 'AI review',
8156
+ 'qa.reviewing': 'Reviewing…',
8157
+ 'qa.reviewTitle': 'AI review suggestions',
8158
+ 'qa.reviewEmpty': 'The model found no sentence worth changing.',
8159
+ 'qa.reviewApply': 'Use suggestion',
8160
+ 'qa.high': 'High',
8161
+ 'qa.medium': 'Medium',
8162
+ 'qa.low': 'Low',
8163
+ 'qa.jump': 'Jump to segment',
8164
+ 'qa.risk': 'Risk',
8165
+ 'qa.code.TERM_FORBIDDEN': 'Forbidden term',
8166
+ 'qa.code.LENGTH_DRIFT': 'Length drift',
8167
+ 'qa.fixCopy': 'Copy source',
8168
+ 'qa.fixClear': 'Clear target',
8169
+ 'qa.fixTerm': 'Insert term',
8170
+ 'toast.qaFixed': 'Segment fixed; checks refreshed',
8171
+ 'toast.unconfirmed': 'Confirmation revoked; editing is enabled again',
8172
+ 'toast.briefSaved': 'Brief saved',
8173
+ 'toast.termForbidden': 'This is a forbidden term and cannot be inserted',
8174
+ 'editor.resources': 'Resources',
8175
+ 'resources.title': 'Document resources',
8176
+ 'resources.hint': 'Limit which translation memories and glossaries this document reads and writes. Without inheritance, only the selected libraries are used.',
8177
+ 'resources.inheritTm': 'Inherit global TM mounts',
8178
+ 'resources.inheritGlossary': 'Inherit global glossary mounts',
8179
+ 'resources.tm': 'Translation memories',
8180
+ 'resources.glossary': 'Glossaries',
8181
+ 'resources.defaultWrite': 'Default write',
8182
+ 'resources.followDefault': 'Follow global default',
8183
+ 'resources.firstBound': 'First bound library',
8184
+ 'resources.none': 'None (no read, no write)',
8185
+ 'resources.empty': 'No libraries available.',
8186
+ 'resources.save': 'Save resources',
8187
+ 'toast.resourcesSaved': 'Document resources saved',
8188
+ 'editor.analysis': 'Analysis',
8189
+ 'analysis.title': 'Document analysis',
8190
+ 'analysis.hint': 'Counted against the memory bound to this document; match rate is a quoting aid.',
8191
+ 'analysis.words': 'Words',
8192
+ 'analysis.characters': 'Characters',
8193
+ 'analysis.segments': 'Segments',
8194
+ 'analysis.duplicates': 'Duplicate segments',
8195
+ 'analysis.translated': 'With target',
8196
+ 'analysis.confirmed': 'Confirmed',
8197
+ 'analysis.ice': 'ICE / context',
8198
+ 'analysis.exact': 'Exact',
8199
+ 'analysis.fuzzy85': '85–99%',
8200
+ 'analysis.fuzzy70': '70–84%',
8201
+ 'analysis.new': 'New',
8202
+ 'analysis.matchRate': 'Match rate',
8203
+ 'analysis.leverage': 'Weighted leverage',
8204
+ 'analysis.exportCsv': 'Export CSV',
8205
+ 'analysis.exportJson': 'Export JSON',
8206
+ 'analysis.close': 'Close',
8207
+ 'replace.open': 'Find & replace',
8208
+ 'replace.title': 'Find & replace',
8209
+ 'replace.hint': 'Find and replace in source or target. Preview the impact before applying.',
8210
+ 'replace.query': 'Find',
8211
+ 'replace.queryPlaceholder': 'Text to find',
8212
+ 'replace.replacement': 'Replace with',
8213
+ 'replace.replacementPlaceholder': 'Leave empty to delete',
8214
+ 'replace.scope': 'Scope',
8215
+ 'replace.scope.target': 'Target only',
8216
+ 'replace.scope.source': 'Source only',
8217
+ 'replace.scope.both': 'Source and target',
8218
+ 'replace.matchCase': 'Match case',
8219
+ 'replace.regex': 'Regular expression',
8220
+ 'replace.preview': 'Preview',
8221
+ 'replace.apply': 'Apply replacement',
8222
+ 'replace.undo': 'Undo last replacement',
8223
+ 'replace.results': '{count} matches across {segments} segments',
8224
+ 'replace.truncated': 'Too many matches; showing the first 200.',
8225
+ 'replace.noResults': 'No matches.',
8226
+ 'replace.field.source': 'Source',
8227
+ 'replace.field.target': 'Target',
8228
+ 'toast.replaced': 'Replaced {count} occurrences',
8229
+ 'toast.replaceUndone': 'Replacement undone',
8230
+ 'palette.title': 'Command palette',
8231
+ 'palette.placeholder': 'Type a command…',
8232
+ 'palette.empty': 'No matching commands.',
8233
+ 'palette.footer': '↑↓ select · Enter run · Esc close · Ctrl+K open',
8234
+ 'cmd.new': 'New document',
8235
+ 'cmd.newHint': 'Start from text or a file',
8236
+ 'cmd.import': 'Import file',
8237
+ 'cmd.export': 'Export',
8238
+ 'cmd.pretranslate': 'Pre-translate',
8239
+ 'cmd.replace': 'Find & replace',
8240
+ 'cmd.brief': 'Document brief',
8241
+ 'cmd.resources': 'Document resources',
8242
+ 'cmd.analysis': 'Document analysis',
8243
+ 'cmd.clearPretranslated': 'Clear pre-translated targets',
8244
+ 'cmd.notes': 'Segment notes',
8245
+ 'cmd.review': 'AI review this document',
8246
+ 'editor.notes': 'Notes',
8247
+ 'notes.title': 'Segment notes',
8248
+ 'notes.hint': 'Notes stay in this document and are never written to TM.',
8249
+ 'notes.empty': 'No notes yet.',
8250
+ 'notes.placeholder': 'Review comments, terminology questions, or hints for the agent…',
8251
+ 'notes.add': 'Add note',
8252
+ 'notes.save': 'Save',
8253
+ 'notes.cancel': 'Cancel',
8254
+ 'notes.edit': 'Edit',
8255
+ 'notes.delete': 'Delete',
8256
+ 'notes.user': 'You',
8257
+ 'toast.noteSaved': 'Note saved',
8258
+ 'toast.noteDeleted': 'Note deleted',
8259
+ 'cmd.documents': 'Document workspace',
8260
+ 'cmd.libraries': 'Library manager',
8261
+ 'cmd.align': 'Alignment',
8262
+ 'cmd.settings': 'Settings',
8263
+ })
8264
+ Object.assign(JA, {
8265
+ 'panel.qa': 'チェック',
8266
+ 'panel.concordance': '検索',
8267
+ 'panel.concordancePlaceholder': '翻訳メモリを検索…',
8268
+ 'panel.concordanceHint': 'マウント中の TM を原文・訳文で検索し、結果をクリックすると現在のセグメントの訳文に書き込みます。',
8269
+ 'panel.noConcordance': '一致なし。',
8270
+ 'panel.searchConcordance': '検索',
8271
+ 'panel.brief': '文書ブリーフ',
8272
+ 'panel.briefHint': '译者と agent のための分野・読者・トーン・用語指示。',
8273
+ 'panel.termForbidden': '禁止',
8274
+ 'panel.termPreferred': '必須',
8275
+ 'brief.title': '文書ブリーフ',
8276
+ 'brief.hint': 'この情報は事前翻訳と agent のコンテキストに渡されます。空欄は制約なし。',
8277
+ 'brief.domain': '分野',
8278
+ 'brief.audience': '読者',
8279
+ 'brief.tone': 'トーン',
8280
+ 'brief.notes': '補足',
8281
+ 'brief.doNotTranslate': '翻訳しない語句',
8282
+ 'brief.styleSample': 'スタイル例',
8283
+ 'brief.save': 'ブリーフを保存',
8284
+ 'brief.saved': 'ブリーフを保存しました',
8285
+ 'brief.empty': 'ブリーフは未入力です',
8286
+ 'brief.placeholder.domain': '例:法律、医療、ゲーム',
8287
+ 'brief.placeholder.audience': '例:エンドユーザー、開発者',
8288
+ 'brief.placeholder.tone': '例:正式、簡潔、会話的',
8289
+ 'brief.placeholder.notes': '用語の好み、書式、背景…',
8290
+ 'brief.placeholder.doNotTranslate': '1 行に 1 語句',
8291
+ 'brief.placeholder.styleSample': '希望するスタイルの訳文を貼り付け…',
8292
+ 'editor.unconfirm': '確認を取り消す',
8293
+ 'editor.unconfirmHint': '確認だけを解除し、TM は変更しません',
8294
+ 'editor.undo': '編集を元に戻す',
8295
+ 'editor.redo': '編集をやり直す',
8296
+ 'editor.lock': 'ロック',
8297
+ 'editor.unlock': 'ロック解除',
8298
+ 'editor.locked': 'ロック中',
8299
+ 'toast.locked': 'セグメントをロックしました',
8300
+ 'toast.unlocked': 'セグメントのロックを解除しました',
8301
+ 'editor.brief': 'ブリーフ',
8302
+ 'editor.risk': 'リスク',
8303
+ 'qa.title': 'チェック',
8304
+ 'qa.refresh': '再チェック',
8305
+ 'qa.empty': '問題はありません。',
8306
+ 'qa.loading': 'チェック中…',
8307
+ 'qa.review': 'AI 校閲',
8308
+ 'qa.reviewing': '校閲中…',
8309
+ 'qa.reviewTitle': 'AI 校閲の提案',
8310
+ 'qa.reviewEmpty': 'モデルが修正すべき文を見つけませんでした。',
8311
+ 'qa.reviewApply': '提案を反映',
8312
+ 'qa.high': '高',
8313
+ 'qa.medium': '中',
8314
+ 'qa.low': '低',
8315
+ 'qa.jump': 'セグメントへ',
8316
+ 'qa.risk': 'リスク',
8317
+ 'qa.code.TERM_FORBIDDEN': '禁止用語',
8318
+ 'qa.code.LENGTH_DRIFT': '長さの不一致',
8319
+ 'qa.fixCopy': '原文をコピー',
8320
+ 'qa.fixClear': '訳文を空にする',
8321
+ 'qa.fixTerm': '用語を挿入',
8322
+ 'toast.qaFixed': '修正しました。チェックを更新しました',
8323
+ 'toast.unconfirmed': '確認を取り消しました。編集を続けられます',
8324
+ 'toast.briefSaved': 'ブリーフを保存しました',
8325
+ 'toast.termForbidden': '禁止用語のため挿入できません',
8326
+ 'editor.resources': 'リソース',
8327
+ 'resources.title': '文書リソース',
8328
+ 'resources.hint': 'この文書が読み書きする TM と用語集を限定します。「グローバルを継承」を外すと、下で選んだライブラリだけを使います。',
8329
+ 'resources.inheritTm': 'グローバル TM マウントを継承',
8330
+ 'resources.inheritGlossary': 'グローバル用語集マウントを継承',
8331
+ 'resources.tm': '翻訳メモリ',
8332
+ 'resources.glossary': '用語集',
8333
+ 'resources.defaultWrite': '既定の書き込み先',
8334
+ 'resources.followDefault': 'グローバル既定に従う',
8335
+ 'resources.firstBound': '最初のバインド先',
8336
+ 'resources.none': '未バインド(読み書きなし)',
8337
+ 'resources.empty': '利用できるライブラリがありません。',
8338
+ 'resources.save': 'リソースを保存',
8339
+ 'toast.resourcesSaved': '文書リソースを保存しました',
8340
+ 'editor.analysis': '分析',
8341
+ 'analysis.title': '文書分析',
8342
+ 'analysis.hint': 'この文書に紐づく TM で集計します。マッチ率は見積もりの参考値です。',
8343
+ 'analysis.words': '語数',
8344
+ 'analysis.characters': '文字数',
8345
+ 'analysis.segments': 'セグメント',
8346
+ 'analysis.duplicates': '重複セグメント',
8347
+ 'analysis.translated': '訳文あり',
8348
+ 'analysis.confirmed': '確認済み',
8349
+ 'analysis.ice': 'ICE / コンテキスト',
8350
+ 'analysis.exact': '完全一致',
8351
+ 'analysis.fuzzy85': '85–99%',
8352
+ 'analysis.fuzzy70': '70–84%',
8353
+ 'analysis.new': '新規',
8354
+ 'analysis.matchRate': 'マッチ率',
8355
+ 'analysis.leverage': '加重レバレッジ',
8356
+ 'analysis.exportCsv': 'CSV を書き出す',
8357
+ 'analysis.exportJson': 'JSON を書き出す',
8358
+ 'analysis.close': '閉じる',
8359
+ 'replace.open': '検索と置換',
8360
+ 'replace.title': '検索と置換',
8361
+ 'replace.hint': '原文または訳文を検索して置換します。適用前に影響範囲をプレビューしてください。',
8362
+ 'replace.query': '検索',
8363
+ 'replace.queryPlaceholder': '検索するテキスト',
8364
+ 'replace.replacement': '置換後',
8365
+ 'replace.replacementPlaceholder': '空欄なら削除',
8366
+ 'replace.scope': '範囲',
8367
+ 'replace.scope.target': '訳文のみ',
8368
+ 'replace.scope.source': '原文のみ',
8369
+ 'replace.scope.both': '原文と訳文',
8370
+ 'replace.matchCase': '大文字と小文字を区別',
8371
+ 'replace.regex': '正規表現',
8372
+ 'replace.preview': 'プレビュー',
8373
+ 'replace.apply': '置換を適用',
8374
+ 'replace.undo': '直前の置換を元に戻す',
8375
+ 'replace.results': '{count} 件、{segments} セグメント',
8376
+ 'replace.truncated': '件数が多いため先頭 200 件のみ表示します。',
8377
+ 'replace.noResults': '一致なし。',
8378
+ 'replace.field.source': '原文',
8379
+ 'replace.field.target': '訳文',
8380
+ 'toast.replaced': '{count} 件置換しました',
8381
+ 'toast.replaceUndone': '置換を元に戻しました',
8382
+ 'palette.title': 'コマンドパレット',
8383
+ 'palette.placeholder': 'コマンドを入力…',
8384
+ 'palette.empty': '一致するコマンドがありません。',
8385
+ 'palette.footer': '↑↓ 選択 · Enter 実行 · Esc 閉じる · Ctrl+K 開く',
8386
+ 'cmd.new': '新規文書',
8387
+ 'cmd.newHint': 'テキストまたはファイルから開始',
8388
+ 'cmd.import': 'ファイルをインポート',
8389
+ 'cmd.export': 'エクスポート',
8390
+ 'cmd.pretranslate': '事前翻訳',
8391
+ 'cmd.replace': '検索と置換',
8392
+ 'cmd.brief': '文書ブリーフ',
8393
+ 'cmd.resources': '文書リソース',
8394
+ 'cmd.analysis': '文書分析',
8395
+ 'cmd.clearPretranslated': '事前翻訳をクリア',
8396
+ 'cmd.notes': 'セグメントメモ',
8397
+ 'cmd.review': 'この文書を AI 校閲',
8398
+ 'editor.notes': 'メモ',
8399
+ 'notes.title': 'セグメントメモ',
8400
+ 'notes.hint': 'メモはこの文書にのみ保存され、TM には書き込まれません。',
8401
+ 'notes.empty': 'メモはまだありません。',
8402
+ 'notes.placeholder': 'レビューコメント、用語の疑問、agent へのヒント…',
8403
+ 'notes.add': 'メモを追加',
8404
+ 'notes.save': '保存',
8405
+ 'notes.cancel': 'キャンセル',
8406
+ 'notes.edit': '編集',
8407
+ 'notes.delete': '削除',
8408
+ 'notes.user': 'あなた',
8409
+ 'toast.noteSaved': 'メモを保存しました',
8410
+ 'toast.noteDeleted': 'メモを削除しました',
8411
+ 'cmd.documents': '文書ワークスペース',
8412
+ 'cmd.libraries': 'ライブラリ管理',
8413
+ 'cmd.align': 'アラインメント',
8414
+ 'cmd.settings': '設定',
8415
+ })
8416
+
5888
8417
  // UI languages offered by the in-app switch. 'auto' follows the DSH locale
5889
8418
  // through the bound DSH translator; the other entries use the dictionaries
5890
8419
  // above. Labels are native names, except 'auto', which is localized.
8420
+ Object.assign(ZH, {
8421
+ 'editor.merge': '合并下一段',
8422
+ 'editor.split': '拆分',
8423
+ 'editor.mergeHint': '把当前句段和同一段落的下一句合并;合并后确认状态会重置。',
8424
+ 'editor.splitHint': '按译文光标位置大致拆分当前句段;拆分后两侧都需要重新确认。',
8425
+ 'toast.merged': '已合并句段',
8426
+ 'toast.split': '已拆分句段',
8427
+ 'modal.confirm.mergeTitle': '合并句段',
8428
+ 'modal.confirm.merge': '合并会新建一个句段,并丢失两条原文各自的确认状态。确定继续?',
8429
+ 'modal.confirm.splitTitle': '拆分句段',
8430
+ 'modal.confirm.split': '拆分后两侧都会变成未确认状态,译文按比例切开。确定继续?',
8431
+ })
8432
+ Object.assign(EN, {
8433
+ 'editor.merge': 'Merge next',
8434
+ 'editor.split': 'Split',
8435
+ 'editor.mergeHint': 'Merge this segment with the next one in the same paragraph; confirmation resets.',
8436
+ 'editor.splitHint': 'Split at the target cursor position; both halves need confirmation again.',
8437
+ 'toast.merged': 'Segment merged',
8438
+ 'toast.split': 'Segment split',
8439
+ 'modal.confirm.mergeTitle': 'Merge segments',
8440
+ 'modal.confirm.merge': 'Merging creates a new segment and discards the confirmation state of both originals. Continue?',
8441
+ 'modal.confirm.splitTitle': 'Split segment',
8442
+ 'modal.confirm.split': 'Both halves become unconfirmed and the target is split proportionally. Continue?',
8443
+ })
8444
+ Object.assign(JA, {
8445
+ 'editor.merge': '次を結合',
8446
+ 'editor.split': '分割',
8447
+ 'editor.mergeHint': '同じ段落の次のセグメントと結合します。確認状態はリセットされます。',
8448
+ 'editor.splitHint': '訳文のカーソル位置を目安に分割します。両側とも再確認が必要です。',
8449
+ 'toast.merged': 'セグメントを結合しました',
8450
+ 'toast.split': 'セグメントを分割しました',
8451
+ 'modal.confirm.mergeTitle': 'セグメントを結合',
8452
+ 'modal.confirm.merge': '結合すると新しいセグメントになり、元の確認状態は失われます。続行しますか?',
8453
+ 'modal.confirm.splitTitle': 'セグメントを分割',
8454
+ 'modal.confirm.split': '分割後は両側とも未確認になり、訳文は比率で分割されます。続行しますか?',
8455
+ })
5891
8456
  const UI_LANGS = [
5892
8457
  ['auto', ''],
5893
8458
  ['zh', '简体中文'],
@@ -5973,6 +8538,8 @@ window.__ModuleLoader__.load({
5973
8538
  pageSizeOptions: PAGE_SIZE_OPTIONS,
5974
8539
  pageSizeDefault: PAGE_SIZE_DEFAULT,
5975
8540
  pageSizeStorageKey: PAGE_SIZE_STORAGE_KEY,
8541
+ defaultFuzzyThreshold: DEFAULT_FUZZY_THRESHOLD,
8542
+ fuzzyPercent,
5976
8543
  cssScaled: CSS_SCALED,
5977
8544
  syncPollMs: SYNC_POLL_MS,
5978
8545
  makeTranslator,