dsh-cait 1.0.5 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +159 -0
- package/README.ja.md +100 -18
- package/README.md +126 -19
- package/README.zh-CN.md +90 -17
- package/client/components.js +2023 -347
- package/client/entry.js +13 -0
- package/client/helpers.js +110 -7
- package/client/locales.js +408 -0
- package/client/styles.js +137 -3
- package/client.js +2690 -356
- package/index.js +3 -0
- package/lib/aligner.js +411 -0
- package/lib/api.js +31 -0
- package/lib/context.js +71 -0
- package/lib/glossary.js +4 -4
- package/lib/import-export.js +72 -4
- package/lib/libraries.js +55 -0
- package/lib/library-export.js +75 -0
- package/lib/library-view.js +215 -0
- package/lib/mt.js +17 -7
- package/lib/persistence.js +72 -7
- package/lib/pre-translate.js +263 -0
- package/lib/propagate.js +133 -0
- package/lib/qa.js +167 -0
- package/lib/store-utils.js +43 -4
- package/lib/store.js +131 -12
- package/lib/term-extract.js +190 -0
- package/lib/terms.js +102 -0
- package/lib/tm.js +123 -11
- package/lib/tmx.js +47 -2
- package/lib/tools.js +472 -0
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -7,6 +7,9 @@ window.__ModuleLoader__.load({
|
|
|
7
7
|
|
|
8
8
|
const API = '/cait/api/rpc'
|
|
9
9
|
const SYNC_POLL_MS = 2000
|
|
10
|
+
// One pre-translation block is 8 segments: roughly 12 s with a warm route,
|
|
11
|
+
// short enough that one cancellation click never waits long.
|
|
12
|
+
const PRETRANSLATE_BLOCK_SIZE = 8
|
|
10
13
|
|
|
11
14
|
// Only the last mounted library must stay checked. An unmounted library is
|
|
12
15
|
// always selectable, even when that is currently the only mounted one.
|
|
@@ -50,14 +53,16 @@ window.__ModuleLoader__.load({
|
|
|
50
53
|
// --cait-u (a unitless multiplier) tracks the size of the panel.
|
|
51
54
|
const CAIT_UNIT_VAR = '--cait-u'
|
|
52
55
|
// The designed density is calibrated for roughly 1100×680 CSS pixels of
|
|
53
|
-
// DSH panel area.
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// to 1.8× before the cap applies.
|
|
56
|
+
// DSH panel area. Growing linearly from there made a maximized window reach
|
|
57
|
+
// 1.8×, which is visibly larger than the surrounding DSH chrome and eats the
|
|
58
|
+
// horizontal budget the three-column review view needs. Growth is therefore
|
|
59
|
+
// damped and capped well below the old ceiling.
|
|
58
60
|
const CAIT_UI_BASE = { width: 1100, height: 680 }
|
|
59
61
|
const CAIT_UI_MIN_UNIT = 1
|
|
60
|
-
const CAIT_UI_MAX_UNIT = 1.
|
|
62
|
+
const CAIT_UI_MAX_UNIT = 1.45
|
|
63
|
+
// 0.6 keeps small and mid panels close to the old density while flattening
|
|
64
|
+
// the large end: 1920×1080 lands at 1.353, 2560 and 4K stop at 1.45.
|
|
65
|
+
const CAIT_UI_DAMPING = 0.6
|
|
61
66
|
|
|
62
67
|
// Grow with the panel but never shrink below the designed size; the height
|
|
63
68
|
// term keeps short/wide windows from becoming oversized and scroll-heavy.
|
|
@@ -66,7 +71,8 @@ window.__ModuleLoader__.load({
|
|
|
66
71
|
const h = Number(height)
|
|
67
72
|
if (!(w > 0) || !(h > 0)) return CAIT_UI_MIN_UNIT
|
|
68
73
|
const fit = Math.min(w / CAIT_UI_BASE.width, h / CAIT_UI_BASE.height)
|
|
69
|
-
const
|
|
74
|
+
const grown = 1 + (fit - 1) * CAIT_UI_DAMPING
|
|
75
|
+
const unit = Math.min(CAIT_UI_MAX_UNIT, Math.max(CAIT_UI_MIN_UNIT, grown))
|
|
70
76
|
return Math.round(unit * 1000) / 1000
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -109,10 +115,109 @@ window.__ModuleLoader__.load({
|
|
|
109
115
|
return output
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
// --- theme --------------------------------------------------------------
|
|
119
|
+
// DSH marks the dark theme on <body>; the tokens handle every authored
|
|
120
|
+
// colour, but native widgets (select popups, checkboxes, scrollbars, caret)
|
|
121
|
+
// follow `color-scheme`, which tokens cannot reach. Mirror the host's theme
|
|
122
|
+
// onto our own root attribute and let CSS set the scheme from it.
|
|
123
|
+
const CAIT_THEME_ATTR = 'data-cait-theme'
|
|
124
|
+
|
|
125
|
+
function detectThemeKind(doc) {
|
|
126
|
+
const root = doc?.documentElement
|
|
127
|
+
const body = doc?.body
|
|
128
|
+
if (body?.hasAttribute?.('data-ds-dark-theme')) return 'dark'
|
|
129
|
+
const explicit = root?.getAttribute?.('data-theme') || body?.getAttribute?.('data-theme')
|
|
130
|
+
if (explicit === 'dark') return 'dark'
|
|
131
|
+
return 'light'
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- paging -------------------------------------------------------------
|
|
135
|
+
// Both the segment rail and the library page page through server-side
|
|
136
|
+
// windows, so the arithmetic lives here: one place to clamp, one place to
|
|
137
|
+
// validate a typed segment number, one place to remember the page size.
|
|
138
|
+
const PAGE_SIZE_OPTIONS = [20, 50, 100, 200]
|
|
139
|
+
const PAGE_SIZE_DEFAULT = 50
|
|
140
|
+
const PAGE_SIZE_STORAGE_KEY = 'cait:pageSize'
|
|
141
|
+
|
|
142
|
+
function normalizePageSize(value) {
|
|
143
|
+
// A missing setting arrives as null (localStorage) or undefined, and
|
|
144
|
+
// Number(null) is 0, which would snap to the smallest page instead of the
|
|
145
|
+
// documented default.
|
|
146
|
+
if (value === null || value === undefined || value === '') return PAGE_SIZE_DEFAULT
|
|
147
|
+
const size = Number(value)
|
|
148
|
+
if (!Number.isFinite(size)) return PAGE_SIZE_DEFAULT
|
|
149
|
+
let best = PAGE_SIZE_OPTIONS[0]
|
|
150
|
+
let bestGap = Infinity
|
|
151
|
+
for (const option of PAGE_SIZE_OPTIONS) {
|
|
152
|
+
const gap = Math.abs(option - size)
|
|
153
|
+
if (gap < bestGap) {
|
|
154
|
+
best = option
|
|
155
|
+
bestGap = gap
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return best
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readPageSize(storage) {
|
|
162
|
+
try {
|
|
163
|
+
return normalizePageSize(storage?.getItem?.(PAGE_SIZE_STORAGE_KEY))
|
|
164
|
+
} catch {
|
|
165
|
+
return PAGE_SIZE_DEFAULT
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function writePageSize(storage, value) {
|
|
170
|
+
const size = normalizePageSize(value)
|
|
171
|
+
try {
|
|
172
|
+
storage?.setItem?.(PAGE_SIZE_STORAGE_KEY, String(size))
|
|
173
|
+
} catch {
|
|
174
|
+
// A blocked storage must not break paging; the in-memory value wins.
|
|
175
|
+
}
|
|
176
|
+
return size
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function pageCount(total, pageSize) {
|
|
180
|
+
const size = normalizePageSize(pageSize)
|
|
181
|
+
const count = Number(total)
|
|
182
|
+
if (!Number.isFinite(count) || count <= 0) return 1
|
|
183
|
+
return Math.max(1, Math.ceil(count / size))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clampPage(page, total, pageSize) {
|
|
187
|
+
const last = pageCount(total, pageSize)
|
|
188
|
+
const wanted = Number(page)
|
|
189
|
+
if (!Number.isFinite(wanted)) return 1
|
|
190
|
+
return Math.min(last, Math.max(1, Math.trunc(wanted)))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Returns 1-based inclusive bounds plus the row count, so callers can print
|
|
194
|
+
// "x–y of n" without repeating the arithmetic.
|
|
195
|
+
function pageBounds(page, total, pageSize) {
|
|
196
|
+
const size = normalizePageSize(pageSize)
|
|
197
|
+
const count = Math.max(0, Number(total) || 0)
|
|
198
|
+
const current = clampPage(page, count, size)
|
|
199
|
+
const start = count === 0 ? 0 : (current - 1) * size + 1
|
|
200
|
+
const end = Math.min(count, current * size)
|
|
201
|
+
return { page: current, pageSize: size, start, end, total: count, pages: pageCount(count, size) }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// A translator types "42" meaning segment 42, so the input is 1-based; the
|
|
205
|
+
// return value is the 0-based index the store uses.
|
|
206
|
+
function segmentNumberToIndex(value, total) {
|
|
207
|
+
const text = String(value ?? '').trim()
|
|
208
|
+
if (!/^\d+$/.test(text)) return null
|
|
209
|
+
const number = Number(text)
|
|
210
|
+
const count = Number(total)
|
|
211
|
+
if (!Number.isFinite(count) || number < 1 || number > count) return null
|
|
212
|
+
return number - 1
|
|
213
|
+
}
|
|
214
|
+
|
|
112
215
|
const CSS = `
|
|
113
|
-
.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}
|
|
216
|
+
.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
|
+
.cait-root[data-cait-theme="dark"]{color-scheme:dark}
|
|
114
218
|
.cait-root *{box-sizing:border-box}
|
|
115
219
|
.cait-root input,.cait-root textarea,.cait-root select{font:inherit;color:inherit}
|
|
220
|
+
.cait-root select option{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}
|
|
116
221
|
.cait-root button{font:inherit;cursor:pointer}
|
|
117
222
|
.cait-root button:disabled{cursor:not-allowed;opacity:.48}
|
|
118
223
|
.cait-root ::selection{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 28%,transparent)}
|
|
@@ -151,10 +256,11 @@ window.__ModuleLoader__.load({
|
|
|
151
256
|
.cait-doc-name{max-width:260px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:12px;font-weight:550}
|
|
152
257
|
.cait-doc-progress{font-size:11px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
153
258
|
.cait-actions{margin-left:auto;display:flex;align-items:center;gap:6px;flex:0 0 auto}
|
|
259
|
+
.cait-pre-status{display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 3px 0 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:11.5px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
154
260
|
.cait-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;height:31px;padding:0 11px;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500;white-space:nowrap;transition:background .12s,border-color .12s,transform .06s}
|
|
155
261
|
.cait-btn:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,var(--dsw-alias-bg-layer-1));border-color:var(--dsw-alias-border-l2)}
|
|
156
262
|
.cait-btn:active:not(:disabled){transform:translateY(.5px)}
|
|
157
|
-
.cait-btn-primary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-
|
|
263
|
+
.cait-btn-primary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-label-primary-inverted)}
|
|
158
264
|
.cait-btn-primary:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-brand-primary) 88%,var(--dsw-alias-label-primary));border-color:var(--dsw-alias-brand-primary)}
|
|
159
265
|
.cait-btn-danger{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))}
|
|
160
266
|
.cait-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 9%,var(--dsw-alias-bg-layer-1))}
|
|
@@ -189,12 +295,33 @@ window.__ModuleLoader__.load({
|
|
|
189
295
|
.cait-seg-index{font-size:9.5px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);letter-spacing:.03em}
|
|
190
296
|
.cait-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-idle-primary);flex:0 0 auto}
|
|
191
297
|
.cait-dot.confirmed{background:var(--dsw-alias-state-success-primary)}
|
|
298
|
+
.cait-dot.translated{background:var(--dsw-alias-button-primary-fill)}
|
|
299
|
+
.cait-seg-warn{width:5px;height:5px;border-radius:50%;background:var(--dsw-alias-state-warn-primary);flex:0 0 auto}
|
|
300
|
+
.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}
|
|
192
301
|
.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)}
|
|
193
302
|
.cait-segitem.active .cait-seg-text{font-weight:550}
|
|
194
303
|
.cait-seg-target{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;color:var(--dsw-alias-label-secondary)}
|
|
304
|
+
.cait-rail-filters{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}
|
|
305
|
+
.cait-filterchip{display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 7px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:10px;cursor:pointer}
|
|
306
|
+
.cait-filterchip:hover{background:var(--dsw-alias-bg-layer-1)}
|
|
307
|
+
.cait-filterchip.active{border-color:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary);background:color-mix(in srgb,var(--dsw-alias-button-primary-fill) 10%,transparent)}
|
|
308
|
+
.cait-filtercount{font-variant-numeric:tabular-nums;opacity:.85}
|
|
309
|
+
.cait-batchbtn{display:inline-flex;align-items:center;gap:4px;height:20px;margin-top:6px;padding:0 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:999px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:10px;cursor:pointer}
|
|
310
|
+
.cait-batchbtn:hover:not(:disabled){border-color:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary)}
|
|
311
|
+
.cait-suggest-card{display:flex;flex-direction:column;gap:5px;margin-top:10px;padding:9px 11px;border:1px dashed color-mix(in srgb,var(--dsw-alias-button-primary-fill) 45%,transparent);border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-button-primary-fill) 6%,transparent)}
|
|
312
|
+
.cait-suggest-text{font-size:11.5px;line-height:1.55;color:var(--dsw-alias-label-primary);white-space:pre-wrap}
|
|
313
|
+
.cait-suggest-empty{padding:10px 0;color:var(--dsw-alias-label-secondary);font-size:11.5px;font-style:italic}
|
|
314
|
+
.cait-rev-hint{margin-top:auto;padding-top:6px;color:var(--dsw-alias-label-secondary);font-size:10px;line-height:1.45}
|
|
315
|
+
.cait-originchip{display:inline-flex;align-items:center;height:17px;padding:0 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:9.5px;font-weight:600;letter-spacing:.02em}
|
|
195
316
|
.cait-main{display:grid;grid-template-columns:minmax(0,1fr) 344px;min-width:0;min-height:0;overflow:hidden}
|
|
196
317
|
.cait-main.cait-main-empty{grid-template-columns:minmax(0,1fr)}
|
|
197
318
|
.cait-editor-col{min-width:0;min-height:0;overflow-y:auto;padding:22px 26px 44px}
|
|
319
|
+
.cait-review{display:grid;gap:10px;grid-template-columns:minmax(0,1fr);grid-template-areas:"src" "sugg" "target";align-items:start}
|
|
320
|
+
.cait-revcol{min-width:0;padding:13px 14px;border:1px solid var(--dsw-alias-border-l1);border-radius:11px;background:var(--dsw-alias-bg-layer-1)}
|
|
321
|
+
.cait-rev-src{grid-area:src}
|
|
322
|
+
.cait-rev-sugg{grid-area:sugg;display:flex;flex-direction:column;gap:6px;border-style:dashed;border-color:color-mix(in srgb,var(--dsw-alias-button-primary-fill) 38%,transparent)}
|
|
323
|
+
.cait-rev-target{grid-area:target;display:flex;flex-direction:column;gap:6px}
|
|
324
|
+
.cait-rev-target .cait-textarea{min-height:150px;padding:9px 10px;border-radius:8px;background:var(--dsw-alias-bg-base)}
|
|
198
325
|
.cait-doc-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:16px}
|
|
199
326
|
.cait-doc-title{max-width:680px;margin:0;font-size:17px;font-weight:650;line-height:1.35;letter-spacing:-.01em}
|
|
200
327
|
.cait-doc-meta{display:flex;align-items:center;gap:6px;margin-top:4px;font-size:10.5px;color:var(--dsw-alias-label-secondary)}
|
|
@@ -208,10 +335,12 @@ window.__ModuleLoader__.load({
|
|
|
208
335
|
.cait-badge{display:inline-flex;align-items:center;height:21px;padding:0 8px;border-radius:999px;font-size:10px;font-weight:600;border:1px solid transparent}
|
|
209
336
|
.cait-badge.pending{color:var(--dsw-alias-state-warn-primary);background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 11%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 25%,transparent)}
|
|
210
337
|
.cait-badge.confirmed{color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 11%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 25%,transparent)}
|
|
338
|
+
.cait-badge.translated{color:var(--dsw-alias-button-primary-fill);background:color-mix(in srgb,var(--dsw-alias-button-primary-fill) 11%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-button-primary-fill) 25%,transparent)}
|
|
211
339
|
.cait-source-card{padding:16px 18px;border:1px solid var(--dsw-alias-border-l1);border-radius:11px;background:var(--dsw-alias-bg-layer-1)}
|
|
212
340
|
.cait-labelrow{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:8px}
|
|
213
341
|
.cait-label{font-size:10px;font-weight:650;letter-spacing:.09em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}
|
|
214
342
|
.cait-source-text{font-size:17px;line-height:1.65;letter-spacing:.005em;overflow-wrap:anywhere}
|
|
343
|
+
.cait-rev-src .cait-source-text{font-size:15px;line-height:1.6}
|
|
215
344
|
.cait-source-text mark{padding:0 2px;border-radius:3px;background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 22%,transparent);color:inherit;box-shadow:inset 0 -1px 0 color-mix(in srgb,var(--dsw-alias-state-warn-primary) 65%,transparent)}
|
|
216
345
|
.cait-hitrow{display:flex;flex-wrap:wrap;align-items:center;gap:5px;margin-top:12px}
|
|
217
346
|
.cait-chip{display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:3px 7px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-warn-primary) 26%,var(--dsw-alias-border-l1));border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 8%,transparent);font-size:10.5px;color:var(--dsw-alias-label-primary)}
|
|
@@ -242,6 +371,8 @@ window.__ModuleLoader__.load({
|
|
|
242
371
|
.cait-libmeta{display:flex;flex-direction:column;gap:1px;min-width:0}
|
|
243
372
|
.cait-libmeta strong{font-size:11.5px;font-weight:650;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
244
373
|
.cait-libmeta small{font-size:9.5px;color:var(--dsw-alias-label-secondary)}
|
|
374
|
+
.cait-libactions{display:flex;align-items:center;gap:4px;margin-left:auto;flex:0 0 auto}
|
|
375
|
+
.cait-librename{height:24px;font-size:11.5px;min-width:0;flex:1}
|
|
245
376
|
.cait-libdefault{display:inline-flex;align-items:center;gap:4px;font-size:9.5px;color:var(--dsw-alias-label-secondary);white-space:nowrap;cursor:pointer}
|
|
246
377
|
.cait-libdefault input{width:11px;height:11px;accent-color:var(--dsw-alias-brand-primary)}
|
|
247
378
|
.cait-libdefault:has(input:checked){color:var(--dsw-alias-brand-primary);font-weight:650}
|
|
@@ -262,8 +393,11 @@ window.__ModuleLoader__.load({
|
|
|
262
393
|
.cait-card{margin-bottom:8px;padding:10px;border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1);transition:border-color .12s,background .12s}
|
|
263
394
|
.cait-card:hover{border-color:var(--dsw-alias-border-l2)}
|
|
264
395
|
.cait-card-head{display:flex;align-items:center;gap:5px;margin-bottom:7px}
|
|
396
|
+
.cait-tmedit{display:flex;flex-direction:column;gap:6px;margin-top:3px}
|
|
397
|
+
.cait-tmedit-input{min-height:64px;font-size:11.5px}
|
|
265
398
|
.cait-score{display:inline-flex;align-items:center;height:18px;padding:0 6px;border-radius:5px;background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 13%,transparent);color:var(--dsw-alias-state-warn-primary);font-size:9.5px;font-weight:750;font-variant-numeric:tabular-nums}
|
|
266
399
|
.cait-score.exact{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 13%,transparent);color:var(--dsw-alias-state-success-primary)}
|
|
400
|
+
.cait-score.context{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 20%,transparent);color:var(--dsw-alias-state-success-primary)}
|
|
267
401
|
.cait-score.near_exact{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 13%,transparent);color:var(--dsw-alias-brand-primary)}
|
|
268
402
|
.cait-score.high_fuzzy{background:color-mix(in srgb,var(--dsw-alias-state-warn-primary) 13%,transparent);color:var(--dsw-alias-state-warn-primary)}
|
|
269
403
|
.cait-score.medium_fuzzy{background:color-mix(in srgb,var(--dsw-alias-state-idle-primary) 13%,transparent);color:var(--dsw-alias-state-idle-primary)}
|
|
@@ -272,6 +406,7 @@ window.__ModuleLoader__.load({
|
|
|
272
406
|
.cait-minibtn{margin-left:auto;height:23px;padding:0 7px;border:1px solid var(--dsw-alias-border-l1);border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:10px;font-weight:600}
|
|
273
407
|
.cait-minibtn:hover{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,var(--dsw-alias-border-l1));color:var(--dsw-alias-brand-primary)}
|
|
274
408
|
.cait-card-source{margin:0 0 5px;font-size:11px;line-height:1.45;color:var(--dsw-alias-label-secondary);overflow-wrap:anywhere}
|
|
409
|
+
.cait-card-context{margin:0 0 5px;font-size:9.5px;line-height:1.5;color:var(--dsw-alias-label-secondary);opacity:.75;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
275
410
|
.cait-card-target{margin:0;font-size:12.5px;line-height:1.5;font-weight:500;overflow-wrap:anywhere}
|
|
276
411
|
.cait-tiny{font-size:10px}
|
|
277
412
|
.cait-muted{color:var(--dsw-alias-label-secondary)}
|
|
@@ -301,6 +436,7 @@ window.__ModuleLoader__.load({
|
|
|
301
436
|
.cait-mt-actions{display:flex;gap:5px;justify-content:flex-end}
|
|
302
437
|
.cait-mt-error{padding:8px;margin-bottom:8px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 30%,var(--dsw-alias-border-l1));border-radius:7px;background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 7%,transparent);color:var(--dsw-alias-state-error-primary);font-size:10.5px;line-height:1.5}
|
|
303
438
|
.cait-mt-hint{margin-top:9px;font-size:10px;line-height:1.55;color:var(--dsw-alias-label-secondary)}
|
|
439
|
+
.cait-paneljump{display:flex;align-items:center;justify-content:flex-end;margin-top:1px}
|
|
304
440
|
.cait-empty{display:grid;place-items:center;min-height:0;overflow-y:auto;padding:34px 28px}
|
|
305
441
|
.cait-empty-inner{width:min(680px,100%);padding:6px 0 20px}
|
|
306
442
|
.cait-kicker{display:inline-flex;align-items:center;gap:6px;height:22px;padding:0 8px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 22%,var(--dsw-alias-border-l1));border-radius:999px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 7%,transparent);color:var(--dsw-alias-brand-primary);font-size:10px;font-weight:700;letter-spacing:.08em}
|
|
@@ -360,7 +496,108 @@ window.__ModuleLoader__.load({
|
|
|
360
496
|
@keyframes cait-toast-in{from{opacity:0;transform:translate(-50%,4px)}to{opacity:1;transform:translate(-50%,0)}}
|
|
361
497
|
.cait-errorcard{width:min(430px,100%);padding:18px;border:1px solid var(--dsw-alias-border-l1);border-radius:10px;background:var(--dsw-alias-bg-layer-1);text-align:center}
|
|
362
498
|
.cait-errorcard p{margin:0 0 11px;font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.6}
|
|
363
|
-
|
|
499
|
+
.cait-viewtabs{display:flex;align-items:center;gap:2px;padding:2px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);flex:0 0 auto}
|
|
500
|
+
.cait-viewtab{height:25px;padding:0 11px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11.5px;font-weight:550;white-space:nowrap}
|
|
501
|
+
.cait-viewtab:hover{color:var(--dsw-alias-label-primary)}
|
|
502
|
+
.cait-viewtab.active{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);box-shadow:0 0 0 1px var(--dsw-alias-border-l1) inset}
|
|
503
|
+
.cait-rail-goto{display:flex;align-items:center;gap:5px;margin-top:8px}
|
|
504
|
+
.cait-goto-input{width:100%;min-width:0;height:26px;padding:0 7px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;outline:0;background:var(--dsw-alias-bg-base);font-size:11px;font-variant-numeric:tabular-nums}
|
|
505
|
+
.cait-goto-input:focus{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 55%,var(--dsw-alias-border-l1))}
|
|
506
|
+
.cait-goto-input.bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
507
|
+
.cait-gotobtn{display:inline-grid;place-items:center;width:26px;height:26px;padding:0;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);flex:0 0 auto}
|
|
508
|
+
.cait-gotobtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
|
|
509
|
+
.cait-rail-page{display:flex;align-items:center;gap:6px;padding:8px 10px;border-top:1px solid var(--dsw-alias-border-l1);flex:0 0 auto}
|
|
510
|
+
.cait-pagebtn{display:inline-grid;place-items:center;width:24px;height:24px;padding:0;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);flex:0 0 auto}
|
|
511
|
+
.cait-pagebtn:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
|
|
512
|
+
.cait-pageinfo{flex:1;min-width:0;text-align:center;font-size:10.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
513
|
+
.cait-pagesize{height:24px;padding:0 3px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;background:var(--dsw-alias-bg-layer-1);font-size:10.5px;flex:0 0 auto}
|
|
514
|
+
.cait-libpage{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}
|
|
515
|
+
.cait-libpage-head{display:flex;align-items:center;gap:12px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:0 0 auto}
|
|
516
|
+
.cait-libtabs{display:flex;align-items:center;gap:2px;padding:2px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1)}
|
|
517
|
+
.cait-libtabs-item{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 12px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary);font-size:11.5px;font-weight:550}
|
|
518
|
+
.cait-libtabs-item:hover{color:var(--dsw-alias-label-primary)}
|
|
519
|
+
.cait-libtabs-item.active{background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);box-shadow:0 0 0 1px var(--dsw-alias-border-l1) inset}
|
|
520
|
+
.cait-libtabs-count{font-size:10px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
521
|
+
.cait-libhint{margin-left:auto;font-size:10.5px;color:var(--dsw-alias-label-secondary)}
|
|
522
|
+
.cait-libpage-body{flex:1;min-height:0;display:grid;grid-template-columns:224px minmax(0,1fr) 300px;overflow:hidden}
|
|
523
|
+
.cait-liblist{display:flex;flex-direction:column;min-height:0;overflow-y:auto;padding:8px;gap:2px;border-right:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 45%,var(--dsw-alias-bg-base))}
|
|
524
|
+
.cait-liblist-head{padding:3px 5px 7px;font-size:10px;font-weight:650;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}
|
|
525
|
+
.cait-librow{display:flex;align-items:center;gap:6px;padding:5px 5px;border-radius:6px}
|
|
526
|
+
.cait-librow:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 5%,transparent)}
|
|
527
|
+
.cait-librow.scoped{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 9%,transparent)}
|
|
528
|
+
.cait-libname{display:flex;align-items:center;gap:6px;min-width:0;flex:1;padding:0;border:0;background:transparent;color:var(--dsw-alias-label-primary);font-size:11.5px;text-align:left}
|
|
529
|
+
.cait-libname-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
530
|
+
.cait-libcount{font-size:9.5px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
531
|
+
.cait-libmounted{font-size:8px;color:var(--dsw-alias-brand-primary);line-height:1}
|
|
532
|
+
.cait-libactions{display:flex;align-items:center;gap:2px;margin-left:auto;flex:0 0 auto;opacity:.55}
|
|
533
|
+
.cait-librow:hover .cait-libactions{opacity:1}
|
|
534
|
+
.cait-libnew{display:flex;align-items:center;gap:4px;margin-top:8px;padding-top:8px;border-top:1px solid var(--dsw-alias-border-l1)}
|
|
535
|
+
.cait-libnew input{min-width:0;flex:1;height:25px;padding:0 7px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;outline:0;background:var(--dsw-alias-bg-base);font-size:11px}
|
|
536
|
+
.cait-libmain{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden}
|
|
537
|
+
.cait-libtoolbar{display:flex;align-items:center;gap:6px;padding:9px 12px 6px;flex-wrap:wrap;flex:0 0 auto}
|
|
538
|
+
.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
|
+
.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
|
+
.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}
|
|
541
|
+
.cait-libscope{display:flex;align-items:center;gap:4px;padding:0 12px 8px;flex-wrap:wrap;flex:0 0 auto}
|
|
542
|
+
.cait-libtable-wrap{flex:1;min-height:0;overflow:auto;padding:0 12px}
|
|
543
|
+
.cait-libtable{width:100%;border-collapse:collapse;table-layout:fixed}
|
|
544
|
+
.cait-libtable th{position:sticky;top:0;z-index:1;padding:6px 8px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);text-align:left;font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}
|
|
545
|
+
.cait-libtable td{padding:6px 8px;border-bottom:1px solid color-mix(in srgb,var(--dsw-alias-border-l1) 55%,transparent);font-size:11.5px;vertical-align:top;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
546
|
+
.cait-libtable tbody tr{cursor:pointer}
|
|
547
|
+
.cait-libtable tbody tr:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 4%,transparent)}
|
|
548
|
+
.cait-libtable tbody tr.active{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 9%,transparent)}
|
|
549
|
+
.cait-libtable td.note,.cait-libtable td.lang,.cait-libtable td.ctx{color:var(--dsw-alias-label-secondary)}
|
|
550
|
+
.cait-libtable td.ctx{font-size:10.5px}
|
|
551
|
+
.cait-libtable th:nth-child(1),.cait-libtable td:nth-child(1){width:30%}
|
|
552
|
+
.cait-libtable th:nth-child(2),.cait-libtable td:nth-child(2){width:28%}
|
|
553
|
+
.cait-libpage-bar{border-top:1px solid var(--dsw-alias-border-l1);padding:7px 12px}
|
|
554
|
+
.cait-libdetail{display:flex;flex-direction:column;min-height:0;overflow-y:auto;padding:10px 12px 14px;border-left:1px solid var(--dsw-alias-border-l1);background:color-mix(in srgb,var(--dsw-alias-bg-layer-1) 45%,var(--dsw-alias-bg-base))}
|
|
555
|
+
.cait-libdetail-title{padding:2px 0 8px;font-size:10px;font-weight:650;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}
|
|
556
|
+
.cait-libform{display:flex;flex-direction:column;gap:4px}
|
|
557
|
+
.cait-libform label{font-size:10.5px;color:var(--dsw-alias-label-secondary);margin-top:5px}
|
|
558
|
+
.cait-libinput{width:100%;min-height:28px;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;outline:0;background:var(--dsw-alias-bg-base);font-size:11.5px;line-height:1.5;resize:vertical}
|
|
559
|
+
.cait-libinput:focus{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 55%,var(--dsw-alias-border-l1))}
|
|
560
|
+
.cait-libcheck{display:flex;align-items:center;gap:6px;margin-top:8px;font-size:11px;color:var(--dsw-alias-label-primary)}
|
|
561
|
+
.cait-libnote{margin:6px 0 0;font-size:10px;line-height:1.5;color:var(--dsw-alias-label-secondary)}
|
|
562
|
+
.cait-libform-actions{display:flex;align-items:center;gap:6px;margin-top:11px}
|
|
563
|
+
.cait-libform-actions .cait-btn{flex:1}
|
|
564
|
+
.cait-batchbtn-plain{border-style:dashed}
|
|
565
|
+
.cait-alignpage{display:flex;flex-direction:column;gap:10px;flex:1;min-height:0;padding:12px 14px 14px;overflow:auto}
|
|
566
|
+
.cait-alignhead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
|
567
|
+
.cait-alignhead h2{margin:0 0 3px;font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}
|
|
568
|
+
.cait-alignactions{display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
|
569
|
+
.cait-alignactions .cait-libinput{width:118px;min-height:26px;height:26px}
|
|
570
|
+
.cait-alignfiles{display:flex;gap:8px;flex-wrap:wrap}
|
|
571
|
+
.cait-alignfile{display:flex;align-items:center;gap:8px;padding:7px 9px;border:1px dashed var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);min-width:0;flex:1 1 300px}
|
|
572
|
+
.cait-alignlabel{font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:var(--dsw-alias-label-secondary);flex:0 0 auto}
|
|
573
|
+
.cait-alignname{font-size:11.5px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
|
574
|
+
.cait-alignname.empty{color:var(--dsw-alias-label-secondary)}
|
|
575
|
+
.cait-alignmeta{font-size:10px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:0 0 auto}
|
|
576
|
+
.cait-alignstats{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums}
|
|
577
|
+
.cait-alignstats .cait-btn{margin-left:auto}
|
|
578
|
+
.cait-alignwarn{color:var(--dsw-alias-state-warning-primary)}
|
|
579
|
+
.cait-aligntable-wrap{flex:1;min-height:0;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-base)}
|
|
580
|
+
.cait-aligntable td{vertical-align:top;font-size:11.5px;line-height:1.55}
|
|
581
|
+
.cait-aligntable tr.included{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 4%,transparent)}
|
|
582
|
+
.cait-aligntable td:first-child{width:28px}
|
|
583
|
+
.cait-aligntable td:nth-child(4){width:74px}
|
|
584
|
+
.cait-aligntable td:nth-child(5){width:86px;white-space:nowrap}
|
|
585
|
+
.cait-confidence{display:inline-block;padding:1px 6px;border-radius:999px;font-size:10px;font-variant-numeric:tabular-nums;border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-secondary)}
|
|
586
|
+
.cait-confidence.high{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 45%,transparent);color:var(--dsw-alias-state-success-primary)}
|
|
587
|
+
.cait-confidence.low{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 45%,transparent);color:var(--dsw-alias-state-warning-primary)}
|
|
588
|
+
.cait-alignmissing{color:var(--dsw-alias-label-secondary)}
|
|
589
|
+
.cait-alignrow-actions{display:flex;gap:2px;justify-content:flex-end}
|
|
590
|
+
.cait-termpanel{border:1px solid var(--dsw-alias-border-l1);border-radius:9px;background:var(--dsw-alias-bg-layer-1);padding:9px 10px;max-height:46vh;display:flex;flex-direction:column;gap:7px}
|
|
591
|
+
.cait-termpanel-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
592
|
+
.cait-termpanel-head strong{font-size:12px;color:var(--dsw-alias-label-primary)}
|
|
593
|
+
.cait-termpanel-actions{margin-left:auto;display:flex;gap:6px}
|
|
594
|
+
.cait-termlist{overflow:auto;display:flex;flex-direction:column;gap:3px}
|
|
595
|
+
.cait-termrow{display:flex;align-items:center;gap:7px;padding:3px 4px;border-radius:6px;font-size:11.5px}
|
|
596
|
+
.cait-termrow.picked{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 6%,transparent)}
|
|
597
|
+
.cait-termname{font-family:var(--cait-mono,ui-monospace,monospace);color:var(--dsw-alias-label-primary);min-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
598
|
+
.cait-termcount{font-size:10px;color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:0 0 auto}
|
|
599
|
+
.cait-termtarget{flex:1;min-width:120px;min-height:24px;height:24px}
|
|
600
|
+
@media(max-width:1180px){.cait-main{grid-template-columns:minmax(0,1fr) 308px}.cait-body{grid-template-columns:218px minmax(0,1fr)}.cait-editor-col{padding:18px 20px 34px}.cait-actions .cait-btn-label{display:none}.cait-actions .cait-btn{padding:0 8px}.cait-libpage-body{grid-template-columns:196px minmax(0,1fr) 264px}}
|
|
364
601
|
@media(max-width:940px){.cait-topbar{flex-wrap:wrap}.cait-body{grid-template-columns:190px minmax(0,1fr)}.cait-main{grid-template-columns:minmax(0,1fr)}.cait-cat{border-left:0;border-top:1px solid var(--dsw-alias-border-l1);max-height:43%;min-height:210px}.cait-features{grid-template-columns:1fr}.cait-docbar{display:flex;order:3;flex:1 1 100%;padding:7px 0 0;border-left:0;border-top:1px solid var(--dsw-alias-border-l1)}}
|
|
365
602
|
@media(max-width:700px){.cait-body{grid-template-columns:minmax(0,1fr)}.cait-rail{display:none}.cait-editor-col{padding:14px}.cait-topbar{padding:9px 10px}.cait-export-grid{grid-template-columns:1fr}}
|
|
366
603
|
`
|
|
@@ -496,7 +733,11 @@ window.__ModuleLoader__.load({
|
|
|
496
733
|
globe: ['M8 14A6 6 0 1 0 8 2a6 6 0 0 0 0 12Z', 'M2.5 8h11', 'M8 2c2 2 2 10 0 12', 'M8 2c-2 2-2 10 0 12'],
|
|
497
734
|
alert: ['M8 2L14 13H2L8 2Z', 'M8 6v3.5', 'M8 11.5v.2'],
|
|
498
735
|
save: ['M3 2h8l2 2v10H3z', 'M5 2v4h6V2', 'M5 14v-4h6v4'],
|
|
736
|
+
pencil: ['M11 2.5l2.5 2.5-8 8L2 14l1-3.5 8-8Z', 'M9.5 4l2.5 2.5'],
|
|
499
737
|
languages: ['M2 4h7', 'M5.5 4v2c0 2-1.5 4-3.5 5', 'M4 8c0 1.5 2 3 4.5 3.5', 'M9 13l3-7 3 7', 'M10 11h4'],
|
|
738
|
+
check: ['M3 8.5l3.2 3.2L13 4.3'],
|
|
739
|
+
align: ['M2 5h8', 'M2 11h5', 'M12 3v10', 'M10 11l2 2 2-2'],
|
|
740
|
+
folder: ['M2 4h4l1.5 2H14v6.5H2z'],
|
|
500
741
|
}
|
|
501
742
|
const shape = paths[name] || paths.file
|
|
502
743
|
return h(
|
|
@@ -759,10 +1000,21 @@ window.__ModuleLoader__.load({
|
|
|
759
1000
|
onCloseDoc,
|
|
760
1001
|
onNewTab,
|
|
761
1002
|
onCleanClosed,
|
|
1003
|
+
pre,
|
|
1004
|
+
onPreTranslate,
|
|
1005
|
+
onCancelPreTranslate,
|
|
1006
|
+
view,
|
|
1007
|
+
onView,
|
|
762
1008
|
}) {
|
|
763
1009
|
const progress = doc?.progress || { percent: 0, confirmed: 0, total: 0 }
|
|
764
1010
|
const openIds = new Set((data.openDocuments || []).map((item) => item.id))
|
|
765
1011
|
const closedDocuments = (data.documents || []).filter((item) => !openIds.has(item.id))
|
|
1012
|
+
const documentsView = view === 'documents'
|
|
1013
|
+
const viewTabs = [
|
|
1014
|
+
{ id: 'documents', label: t('nav.documents') },
|
|
1015
|
+
{ id: 'libraries', label: t('nav.libraries') },
|
|
1016
|
+
{ id: 'align', label: t('nav.align') },
|
|
1017
|
+
]
|
|
766
1018
|
return h(
|
|
767
1019
|
'header',
|
|
768
1020
|
{ className: 'cait-topbar' },
|
|
@@ -774,83 +1026,314 @@ window.__ModuleLoader__.load({
|
|
|
774
1026
|
),
|
|
775
1027
|
h(
|
|
776
1028
|
'div',
|
|
777
|
-
{ className: 'cait-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1029
|
+
{ className: 'cait-viewtabs' },
|
|
1030
|
+
viewTabs.map((tab) =>
|
|
1031
|
+
h(
|
|
1032
|
+
'button',
|
|
1033
|
+
{
|
|
1034
|
+
key: tab.id,
|
|
1035
|
+
type: 'button',
|
|
1036
|
+
className: `cait-viewtab${view === tab.id ? ' active' : ''}`,
|
|
1037
|
+
onClick: () => onView(tab.id),
|
|
1038
|
+
},
|
|
1039
|
+
tab.label,
|
|
1040
|
+
),
|
|
1041
|
+
),
|
|
1042
|
+
),
|
|
1043
|
+
documentsView
|
|
1044
|
+
? h(
|
|
1045
|
+
'div',
|
|
1046
|
+
{ className: 'cait-docbar' },
|
|
1047
|
+
h(DocTabs, {
|
|
1048
|
+
t,
|
|
1049
|
+
documents: data.openDocuments || data.documents || [],
|
|
1050
|
+
activeId: doc?.id,
|
|
1051
|
+
busy,
|
|
1052
|
+
onOpen: onOpenDoc,
|
|
1053
|
+
onClose: onCloseDoc,
|
|
1054
|
+
onNew: onNewTab,
|
|
1055
|
+
}),
|
|
1056
|
+
h(ClosedDocPicker, { t, documents: closedDocuments, busy, onOpen: onOpenDoc }),
|
|
1057
|
+
closedDocuments.length
|
|
1058
|
+
? h(
|
|
1059
|
+
'button',
|
|
1060
|
+
{
|
|
1061
|
+
type: 'button',
|
|
1062
|
+
className: 'cait-doc-clean',
|
|
1063
|
+
title: t('doc.clean'),
|
|
1064
|
+
'aria-label': t('doc.clean'),
|
|
1065
|
+
disabled: Boolean(busy),
|
|
1066
|
+
onClick: onCleanClosed,
|
|
1067
|
+
},
|
|
1068
|
+
h(Icon, { name: 'trash', size: 13 }),
|
|
1069
|
+
)
|
|
1070
|
+
: null,
|
|
1071
|
+
doc
|
|
1072
|
+
? h('span', { className: 'cait-doc-progress' }, `${progress.confirmed}/${progress.total} · ${progress.percent}%`)
|
|
1073
|
+
: null,
|
|
1074
|
+
)
|
|
1075
|
+
: h('div', { className: 'cait-docbar' }),
|
|
1076
|
+
h(
|
|
1077
|
+
'div',
|
|
1078
|
+
{ className: 'cait-actions' },
|
|
1079
|
+
documentsView && pre && pre.running
|
|
1080
|
+
? h(
|
|
1081
|
+
'span',
|
|
1082
|
+
{ className: 'cait-pre-status', title: t('pretranslate.running') },
|
|
1083
|
+
h(Spinner),
|
|
1084
|
+
h('span', null, `${t('pretranslate.running')} ${pre.done}/${pre.total}`),
|
|
1085
|
+
h(
|
|
1086
|
+
'button',
|
|
1087
|
+
{
|
|
1088
|
+
type: 'button',
|
|
1089
|
+
className: 'cait-iconbtn',
|
|
1090
|
+
title: t('pretranslate.cancel'),
|
|
1091
|
+
'aria-label': t('pretranslate.cancel'),
|
|
1092
|
+
onClick: onCancelPreTranslate,
|
|
1093
|
+
},
|
|
1094
|
+
h(Icon, { name: 'close', size: 13 }),
|
|
1095
|
+
),
|
|
1096
|
+
)
|
|
1097
|
+
: null,
|
|
1098
|
+
documentsView && !(pre && pre.running)
|
|
789
1099
|
? h(
|
|
790
1100
|
'button',
|
|
791
1101
|
{
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
disabled: Boolean(busy),
|
|
797
|
-
onClick: onCleanClosed,
|
|
1102
|
+
className: 'cait-btn',
|
|
1103
|
+
onClick: onPreTranslate,
|
|
1104
|
+
disabled: Boolean(busy) || !doc || !(doc.progress?.total > 0),
|
|
1105
|
+
title: t('pretranslate.title'),
|
|
798
1106
|
},
|
|
799
|
-
h(Icon, { name: '
|
|
1107
|
+
h(Icon, { name: 'sparkles', size: 14 }),
|
|
1108
|
+
h('span', { className: 'cait-btn-label' }, t('pretranslate.start')),
|
|
800
1109
|
)
|
|
801
1110
|
: null,
|
|
802
|
-
|
|
803
|
-
? h(
|
|
1111
|
+
documentsView
|
|
1112
|
+
? h(
|
|
1113
|
+
'button',
|
|
1114
|
+
{ className: 'cait-btn', onClick: onNew, disabled: Boolean(busy), title: t('nav.new') },
|
|
1115
|
+
h(Icon, { name: 'plus', size: 14 }),
|
|
1116
|
+
h('span', { className: 'cait-btn-label' }, t('nav.new')),
|
|
1117
|
+
)
|
|
1118
|
+
: null,
|
|
1119
|
+
documentsView
|
|
1120
|
+
? h(
|
|
1121
|
+
'button',
|
|
1122
|
+
{ className: 'cait-btn', onClick: onImport, disabled: Boolean(busy), title: t('nav.import') },
|
|
1123
|
+
h(Icon, { name: 'upload', size: 14 }),
|
|
1124
|
+
h('span', { className: 'cait-btn-label' }, t('nav.import')),
|
|
1125
|
+
)
|
|
1126
|
+
: null,
|
|
1127
|
+
documentsView
|
|
1128
|
+
? h(
|
|
1129
|
+
'button',
|
|
1130
|
+
{ className: 'cait-btn', onClick: onExport, disabled: Boolean(busy) || !doc, title: t('nav.export') },
|
|
1131
|
+
h(Icon, { name: 'download', size: 14 }),
|
|
1132
|
+
h('span', { className: 'cait-btn-label' }, t('nav.export')),
|
|
1133
|
+
)
|
|
804
1134
|
: null,
|
|
805
|
-
),
|
|
806
|
-
h(
|
|
807
|
-
'div',
|
|
808
|
-
{ className: 'cait-actions' },
|
|
809
|
-
h(
|
|
810
|
-
'button',
|
|
811
|
-
{ className: 'cait-btn', onClick: onNew, disabled: Boolean(busy), title: t('nav.new') },
|
|
812
|
-
h(Icon, { name: 'plus', size: 14 }),
|
|
813
|
-
h('span', { className: 'cait-btn-label' }, t('nav.new')),
|
|
814
|
-
),
|
|
815
|
-
h(
|
|
816
|
-
'button',
|
|
817
|
-
{ className: 'cait-btn', onClick: onImport, disabled: Boolean(busy), title: t('nav.import') },
|
|
818
|
-
h(Icon, { name: 'upload', size: 14 }),
|
|
819
|
-
h('span', { className: 'cait-btn-label' }, t('nav.import')),
|
|
820
|
-
),
|
|
821
|
-
h(
|
|
822
|
-
'button',
|
|
823
|
-
{ className: 'cait-btn', onClick: onExport, disabled: Boolean(busy) || !doc, title: t('nav.export') },
|
|
824
|
-
h(Icon, { name: 'download', size: 14 }),
|
|
825
|
-
h('span', { className: 'cait-btn-label' }, t('nav.export')),
|
|
826
|
-
),
|
|
827
1135
|
h('button', { className: 'cait-iconbtn', onClick: onSettings, disabled: Boolean(busy), title: t('nav.settings') }, h(Icon, { name: 'settings', size: 15 })),
|
|
828
1136
|
),
|
|
829
1137
|
)
|
|
830
1138
|
}
|
|
831
1139
|
|
|
832
|
-
|
|
1140
|
+
const SEGMENT_FILTERS = ['all', 'pending', 'translated', 'confirmed']
|
|
1141
|
+
|
|
1142
|
+
function SegmentRail({ t, doc, selected, query, setQuery, filter, setFilter, onSelect, onConfirmAll, onPropagate, busy, qa }) {
|
|
833
1143
|
const activeRef = useRef(null)
|
|
1144
|
+
const storage = typeof localStorage !== 'undefined' ? localStorage : null
|
|
1145
|
+
const [pageSize, setPageSize] = useState(() => readPageSize(storage))
|
|
1146
|
+
const [page, setPage] = useState(1)
|
|
1147
|
+
const [gotoText, setGotoText] = useState('')
|
|
1148
|
+
const [gotoBad, setGotoBad] = useState(false)
|
|
1149
|
+
const lastSelected = useRef(selected)
|
|
834
1150
|
useEffect(() => {
|
|
835
1151
|
if (activeRef.current) activeRef.current.scrollIntoView({ block: 'nearest' })
|
|
836
|
-
}, [selected])
|
|
1152
|
+
}, [selected, filter])
|
|
1153
|
+
|
|
1154
|
+
// Advisory QA marks: the check never blocks editing or exporting, it only
|
|
1155
|
+
// flags the segment in the rail so review can start where it matters.
|
|
1156
|
+
const warns = new Map()
|
|
1157
|
+
for (const issue of qa?.issues || []) {
|
|
1158
|
+
const list = warns.get(issue.index) || []
|
|
1159
|
+
list.push(issue.code)
|
|
1160
|
+
warns.set(issue.index, list)
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
const counts = {
|
|
1164
|
+
all: doc.progress.total,
|
|
1165
|
+
pending: doc.progress.pending,
|
|
1166
|
+
translated: doc.progress.translated,
|
|
1167
|
+
confirmed: doc.progress.confirmed,
|
|
1168
|
+
}
|
|
837
1169
|
|
|
1170
|
+
// How many empty segments could be filled from a sibling with the same
|
|
1171
|
+
// source. Shown on the rail so propagation is a visible, one-click choice
|
|
1172
|
+
// rather than something happening behind the user's back.
|
|
1173
|
+
const duplicateGroups = new Map()
|
|
1174
|
+
for (const segment of doc.segments) {
|
|
1175
|
+
const key = String(segment.source || '').replace(/\s+/g, ' ').trim().toLowerCase()
|
|
1176
|
+
if (!key) continue
|
|
1177
|
+
const list = duplicateGroups.get(key) || []
|
|
1178
|
+
list.push(segment)
|
|
1179
|
+
duplicateGroups.set(key, list)
|
|
1180
|
+
}
|
|
1181
|
+
let duplicated = 0
|
|
1182
|
+
for (const list of duplicateGroups.values()) {
|
|
1183
|
+
if (list.length < 2 || !list.some((segment) => segment.target)) continue
|
|
1184
|
+
duplicated += list.filter((segment) => !segment.target).length
|
|
1185
|
+
}
|
|
838
1186
|
const needle = query.trim().toLowerCase()
|
|
839
|
-
const
|
|
1187
|
+
const filtered = doc.segments
|
|
840
1188
|
.map((segment, index) => ({ segment, index }))
|
|
1189
|
+
.filter(({ segment }) => filter === 'all' || segment.status === filter)
|
|
841
1190
|
.filter(({ segment }) => {
|
|
842
1191
|
if (!needle) return true
|
|
843
|
-
return
|
|
1192
|
+
return (
|
|
1193
|
+
segment.source.toLowerCase().includes(needle) ||
|
|
1194
|
+
(segment.target || '').toLowerCase().includes(needle) ||
|
|
1195
|
+
(segment.suggestion || '').toLowerCase().includes(needle)
|
|
1196
|
+
)
|
|
844
1197
|
})
|
|
845
1198
|
|
|
1199
|
+
// A rail window is a view over the filtered list: the page is clamped to
|
|
1200
|
+
// the result count instead of being reset, so narrowing a filter keeps the
|
|
1201
|
+
// reader near where they were.
|
|
1202
|
+
const bounds = pageBounds(page, filtered.length, pageSize)
|
|
1203
|
+
const firstRow = bounds.start === 0 ? 0 : bounds.start - 1
|
|
1204
|
+
const visible = filtered.slice(firstRow, bounds.end)
|
|
1205
|
+
|
|
1206
|
+
useEffect(() => {
|
|
1207
|
+
if (bounds.page !== page) setPage(bounds.page)
|
|
1208
|
+
}, [bounds.page, page])
|
|
1209
|
+
|
|
1210
|
+
// Selection can move from outside the rail (next-unconfirmed, batch
|
|
1211
|
+
// confirm), so follow it onto the page that holds it.
|
|
1212
|
+
useEffect(() => {
|
|
1213
|
+
if (lastSelected.current === selected) return
|
|
1214
|
+
lastSelected.current = selected
|
|
1215
|
+
const position = filtered.findIndex((item) => item.index === selected)
|
|
1216
|
+
if (position < 0) return
|
|
1217
|
+
const target = Math.floor(position / pageSize) + 1
|
|
1218
|
+
if (target !== bounds.page) setPage(target)
|
|
1219
|
+
}, [selected])
|
|
1220
|
+
|
|
1221
|
+
const changePageSize = (value) => {
|
|
1222
|
+
const size = writePageSize(storage, value)
|
|
1223
|
+
setPageSize(size)
|
|
1224
|
+
setPage(1)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
const submitGoto = () => {
|
|
1228
|
+
const index = segmentNumberToIndex(gotoText, doc.progress.total)
|
|
1229
|
+
if (index === null) {
|
|
1230
|
+
setGotoBad(true)
|
|
1231
|
+
return
|
|
1232
|
+
}
|
|
1233
|
+
setGotoBad(false)
|
|
1234
|
+
setGotoText('')
|
|
1235
|
+
// A number is useless while a filter or a search hides it, so jumping is
|
|
1236
|
+
// an explicit "show me this segment" and clears both.
|
|
1237
|
+
if (needle || filter !== 'all') {
|
|
1238
|
+
setQuery('')
|
|
1239
|
+
setFilter('all')
|
|
1240
|
+
}
|
|
1241
|
+
lastSelected.current = index
|
|
1242
|
+
setPage(Math.floor(index / pageSize) + 1)
|
|
1243
|
+
onSelect(index)
|
|
1244
|
+
}
|
|
1245
|
+
|
|
846
1246
|
return h(
|
|
847
1247
|
'aside',
|
|
848
1248
|
{ className: 'cait-rail' },
|
|
849
1249
|
h(
|
|
850
1250
|
'div',
|
|
851
1251
|
{ className: 'cait-rail-head' },
|
|
852
|
-
h(
|
|
1252
|
+
h(
|
|
1253
|
+
'div',
|
|
1254
|
+
{ className: 'cait-rail-title' },
|
|
1255
|
+
h('span', null, t('rail.title')),
|
|
1256
|
+
h(
|
|
1257
|
+
'span',
|
|
1258
|
+
{ className: 'cait-count', title: `${filtered.length} / ${doc.progress.total}` },
|
|
1259
|
+
needle || filter !== 'all' ? `${filtered.length}/${doc.progress.total}` : doc.progress.total,
|
|
1260
|
+
),
|
|
1261
|
+
),
|
|
853
1262
|
h('div', { className: 'cait-mini-progress' }, h('i', { style: { width: `${doc.progress.percent}%` } })),
|
|
1263
|
+
h(
|
|
1264
|
+
'div',
|
|
1265
|
+
{ className: 'cait-rail-filters' },
|
|
1266
|
+
SEGMENT_FILTERS.map((key) =>
|
|
1267
|
+
h(
|
|
1268
|
+
'button',
|
|
1269
|
+
{
|
|
1270
|
+
key,
|
|
1271
|
+
className: `cait-filterchip${filter === key ? ' active' : ''}`,
|
|
1272
|
+
onClick: () => setFilter(key),
|
|
1273
|
+
title: t(`rail.filter.${key}`),
|
|
1274
|
+
},
|
|
1275
|
+
h('span', null, t(`rail.filter.${key}`)),
|
|
1276
|
+
h('span', { className: `cait-filtercount ${key}` }, counts[key]),
|
|
1277
|
+
),
|
|
1278
|
+
),
|
|
1279
|
+
),
|
|
1280
|
+
doc.progress.translated > 0
|
|
1281
|
+
? h(
|
|
1282
|
+
'button',
|
|
1283
|
+
{
|
|
1284
|
+
className: 'cait-batchbtn',
|
|
1285
|
+
onClick: onConfirmAll,
|
|
1286
|
+
disabled: Boolean(busy),
|
|
1287
|
+
title: t('rail.confirmAllHint'),
|
|
1288
|
+
},
|
|
1289
|
+
h(Icon, { name: 'check', size: 11 }),
|
|
1290
|
+
`${t('rail.confirmAll')} · ${doc.progress.translated}`,
|
|
1291
|
+
)
|
|
1292
|
+
: null,
|
|
1293
|
+
duplicated > 0
|
|
1294
|
+
? h(
|
|
1295
|
+
'button',
|
|
1296
|
+
{
|
|
1297
|
+
className: 'cait-batchbtn cait-batchbtn-plain',
|
|
1298
|
+
onClick: onPropagate,
|
|
1299
|
+
disabled: Boolean(busy),
|
|
1300
|
+
title: t('rail.propagateMessage'),
|
|
1301
|
+
},
|
|
1302
|
+
h(Icon, { name: 'copy', size: 11 }),
|
|
1303
|
+
`${t('rail.propagate')} · ${duplicated}`,
|
|
1304
|
+
)
|
|
1305
|
+
: null,
|
|
1306
|
+
h(
|
|
1307
|
+
'div',
|
|
1308
|
+
{ className: 'cait-rail-goto' },
|
|
1309
|
+
h('input', {
|
|
1310
|
+
className: `cait-goto-input${gotoBad ? ' bad' : ''}`,
|
|
1311
|
+
value: gotoText,
|
|
1312
|
+
onChange: (event) => {
|
|
1313
|
+
setGotoText(event.target.value)
|
|
1314
|
+
setGotoBad(false)
|
|
1315
|
+
},
|
|
1316
|
+
onKeyDown: (event) => {
|
|
1317
|
+
if (event.key === 'Enter') submitGoto()
|
|
1318
|
+
},
|
|
1319
|
+
placeholder: t('rail.goto'),
|
|
1320
|
+
'aria-label': t('rail.goto'),
|
|
1321
|
+
title: gotoBad ? t('rail.gotoInvalid') : t('rail.goto'),
|
|
1322
|
+
inputMode: 'numeric',
|
|
1323
|
+
}),
|
|
1324
|
+
h(
|
|
1325
|
+
'button',
|
|
1326
|
+
{
|
|
1327
|
+
type: 'button',
|
|
1328
|
+
className: 'cait-gotobtn',
|
|
1329
|
+
onClick: submitGoto,
|
|
1330
|
+
disabled: Boolean(busy),
|
|
1331
|
+
title: t('rail.goto'),
|
|
1332
|
+
'aria-label': t('rail.goto'),
|
|
1333
|
+
},
|
|
1334
|
+
h(Icon, { name: 'arrowRight', size: 12 }),
|
|
1335
|
+
),
|
|
1336
|
+
),
|
|
854
1337
|
),
|
|
855
1338
|
h(
|
|
856
1339
|
'div',
|
|
@@ -861,8 +1344,8 @@ window.__ModuleLoader__.load({
|
|
|
861
1344
|
h(
|
|
862
1345
|
'div',
|
|
863
1346
|
{ className: 'cait-rail-list' },
|
|
864
|
-
|
|
865
|
-
?
|
|
1347
|
+
visible.length
|
|
1348
|
+
? visible.map(({ segment, index }) =>
|
|
866
1349
|
h(
|
|
867
1350
|
'button',
|
|
868
1351
|
{
|
|
@@ -880,6 +1363,13 @@ window.__ModuleLoader__.load({
|
|
|
880
1363
|
{ className: 'cait-seg-line' },
|
|
881
1364
|
h('span', { className: 'cait-seg-index' }, String(index + 1).padStart(2, '0')),
|
|
882
1365
|
h('span', { className: `cait-dot ${segment.status}` }),
|
|
1366
|
+
warns.has(index)
|
|
1367
|
+
? h('span', {
|
|
1368
|
+
className: 'cait-seg-warn',
|
|
1369
|
+
title: (warns.get(index) || []).map((code) => t(`qa.code.${code}`)).join(' · '),
|
|
1370
|
+
'aria-label': t('qa.warn'),
|
|
1371
|
+
})
|
|
1372
|
+
: null,
|
|
883
1373
|
),
|
|
884
1374
|
h('span', { className: 'cait-seg-text' }, segment.source),
|
|
885
1375
|
segment.target ? h('span', { className: 'cait-seg-target' }, segment.target) : null,
|
|
@@ -888,16 +1378,68 @@ window.__ModuleLoader__.load({
|
|
|
888
1378
|
)
|
|
889
1379
|
: h('div', { className: 'cait-empty-note' }, t('rail.empty')),
|
|
890
1380
|
),
|
|
1381
|
+
h(
|
|
1382
|
+
'div',
|
|
1383
|
+
{ className: 'cait-rail-page' },
|
|
1384
|
+
h(
|
|
1385
|
+
'button',
|
|
1386
|
+
{
|
|
1387
|
+
type: 'button',
|
|
1388
|
+
className: 'cait-pagebtn',
|
|
1389
|
+
onClick: () => setPage(bounds.page - 1),
|
|
1390
|
+
disabled: bounds.page <= 1,
|
|
1391
|
+
title: t('library.prev'),
|
|
1392
|
+
'aria-label': t('library.prev'),
|
|
1393
|
+
},
|
|
1394
|
+
h(Icon, { name: 'chevronLeft', size: 12 }),
|
|
1395
|
+
),
|
|
1396
|
+
h('span', { className: 'cait-pageinfo' }, t('rail.pageInfo', { start: bounds.start, end: bounds.end, total: bounds.total })),
|
|
1397
|
+
h(
|
|
1398
|
+
'button',
|
|
1399
|
+
{
|
|
1400
|
+
type: 'button',
|
|
1401
|
+
className: 'cait-pagebtn',
|
|
1402
|
+
onClick: () => setPage(bounds.page + 1),
|
|
1403
|
+
disabled: bounds.page >= bounds.pages,
|
|
1404
|
+
title: t('library.next'),
|
|
1405
|
+
'aria-label': t('library.next'),
|
|
1406
|
+
},
|
|
1407
|
+
h(Icon, { name: 'chevronRight', size: 12 }),
|
|
1408
|
+
),
|
|
1409
|
+
h(
|
|
1410
|
+
'select',
|
|
1411
|
+
{
|
|
1412
|
+
className: 'cait-pagesize',
|
|
1413
|
+
value: String(pageSize),
|
|
1414
|
+
onChange: (event) => changePageSize(Number(event.target.value)),
|
|
1415
|
+
title: t('library.pageSize'),
|
|
1416
|
+
'aria-label': t('library.pageSize'),
|
|
1417
|
+
},
|
|
1418
|
+
PAGE_SIZE_OPTIONS.map((size) => h('option', { key: size, value: String(size) }, String(size))),
|
|
1419
|
+
),
|
|
1420
|
+
),
|
|
891
1421
|
)
|
|
892
1422
|
}
|
|
893
1423
|
|
|
894
|
-
function EditorPane({ t, doc, index, segment, detail, draft, setDraft, dirty, busy, onConfirm, onNext, onPrev, onSave, onApplyTerm, textareaRef }) {
|
|
1424
|
+
function EditorPane({ t, doc, index, segment, detail, draft, setDraft, dirty, busy, onConfirm, onNext, onPrev, onSave, onAdopt, onApplyTerm, textareaRef }) {
|
|
895
1425
|
const hits = detail?.index === index ? detail.glossaryHits : []
|
|
896
1426
|
const charCount = Array.from(draft || '').length
|
|
897
1427
|
const progress = doc.progress
|
|
1428
|
+
const remaining = progress.total - progress.confirmed
|
|
1429
|
+
const statusLabel =
|
|
1430
|
+
segment.status === 'confirmed'
|
|
1431
|
+
? t('status.confirmed')
|
|
1432
|
+
: segment.status === 'translated'
|
|
1433
|
+
? t('status.translated')
|
|
1434
|
+
: t('status.pending')
|
|
1435
|
+
|
|
1436
|
+
// The middle column keeps the machine/agent baseline that produced the
|
|
1437
|
+
// current text, so editing in the right column has something to compare
|
|
1438
|
+
// against. It is stored in `suggestion`, which adoption already preserves.
|
|
1439
|
+
const baselineText = segment.suggestion || ''
|
|
1440
|
+
const baselineOrigin = segment.origin || 'agent'
|
|
898
1441
|
|
|
899
|
-
const onKeyDown = (event) => {
|
|
900
|
-
const mod = event.metaKey || event.ctrlKey
|
|
1442
|
+
const onKeyDown = (event) => { const mod = event.metaKey || event.ctrlKey
|
|
901
1443
|
if (mod && event.key === 'Enter') {
|
|
902
1444
|
event.preventDefault()
|
|
903
1445
|
onConfirm()
|
|
@@ -944,61 +1486,94 @@ window.__ModuleLoader__.load({
|
|
|
944
1486
|
'div',
|
|
945
1487
|
{ className: 'cait-seg-head' },
|
|
946
1488
|
h('span', { className: 'cait-seg-label' }, `${t('editor.segment')} ${index + 1} / ${doc.segments.length}`),
|
|
947
|
-
h('span', { className: `cait-badge ${segment.status}` },
|
|
1489
|
+
h('span', { className: `cait-badge ${segment.status}` }, statusLabel),
|
|
948
1490
|
),
|
|
949
1491
|
h(
|
|
950
1492
|
'div',
|
|
951
|
-
{ className: 'cait-
|
|
1493
|
+
{ className: 'cait-review' },
|
|
952
1494
|
h(
|
|
953
|
-
'
|
|
954
|
-
{ className: 'cait-
|
|
955
|
-
h(
|
|
956
|
-
|
|
1495
|
+
'section',
|
|
1496
|
+
{ className: 'cait-revcol cait-rev-src' },
|
|
1497
|
+
h(
|
|
1498
|
+
'div',
|
|
1499
|
+
{ className: 'cait-labelrow' },
|
|
1500
|
+
h('span', { className: 'cait-label' }, t('editor.source')),
|
|
1501
|
+
h('span', { className: 'cait-langchip' }, doc.sourceLang),
|
|
1502
|
+
),
|
|
1503
|
+
h('div', { className: 'cait-source-text' }, highlightSource(segment.source, hits)),
|
|
1504
|
+
hits.length
|
|
1505
|
+
? h(
|
|
1506
|
+
'div',
|
|
1507
|
+
{ className: 'cait-hitrow' },
|
|
1508
|
+
hits.map((hit) =>
|
|
1509
|
+
h(
|
|
1510
|
+
'button',
|
|
1511
|
+
{
|
|
1512
|
+
key: hit.id,
|
|
1513
|
+
className: 'cait-chip cait-chip-action',
|
|
1514
|
+
onClick: () => onApplyTerm(hit),
|
|
1515
|
+
title: hit.note || `${hit.term} → ${hit.target}`,
|
|
1516
|
+
},
|
|
1517
|
+
h(Icon, { name: 'book', size: 11 }),
|
|
1518
|
+
`${hit.term} → ${hit.target}`,
|
|
1519
|
+
),
|
|
1520
|
+
),
|
|
1521
|
+
)
|
|
1522
|
+
: null,
|
|
957
1523
|
),
|
|
958
|
-
h(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1524
|
+
h(
|
|
1525
|
+
'section',
|
|
1526
|
+
{ className: 'cait-revcol cait-rev-sugg' },
|
|
1527
|
+
h(
|
|
1528
|
+
'div',
|
|
1529
|
+
{ className: 'cait-labelrow' },
|
|
1530
|
+
h('span', { className: 'cait-label' }, t('editor.suggestion')),
|
|
1531
|
+
baselineText
|
|
1532
|
+
? h('span', { className: `cait-originchip ${baselineOrigin}` }, t(`origin.${baselineOrigin}`))
|
|
1533
|
+
: null,
|
|
1534
|
+
segment.suggestion
|
|
1535
|
+
? h(
|
|
965
1536
|
'button',
|
|
966
1537
|
{
|
|
967
|
-
key: hit.id,
|
|
968
1538
|
className: 'cait-chip cait-chip-action',
|
|
969
|
-
onClick:
|
|
970
|
-
|
|
1539
|
+
onClick: onAdopt,
|
|
1540
|
+
disabled: Boolean(busy) || draft.trim() === segment.suggestion,
|
|
1541
|
+
title: t('editor.adoptHint'),
|
|
971
1542
|
},
|
|
972
|
-
h(Icon, { name: '
|
|
973
|
-
|
|
974
|
-
)
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1543
|
+
h(Icon, { name: 'download', size: 11 }),
|
|
1544
|
+
t('editor.adopt'),
|
|
1545
|
+
)
|
|
1546
|
+
: null,
|
|
1547
|
+
),
|
|
1548
|
+
baselineText
|
|
1549
|
+
? h('div', { className: 'cait-suggest-text' }, baselineText)
|
|
1550
|
+
: h('div', { className: 'cait-suggest-empty' }, t('editor.noSuggestion')),
|
|
1551
|
+
h('div', { className: 'cait-rev-hint' }, segment.suggestion ? t('editor.adoptHint') : t('editor.baselineHint')),
|
|
1552
|
+
),
|
|
982
1553
|
h(
|
|
983
|
-
'
|
|
984
|
-
{ className: 'cait-
|
|
985
|
-
h('span', { className: 'cait-label' }, t('editor.target')),
|
|
1554
|
+
'section',
|
|
1555
|
+
{ className: 'cait-revcol cait-rev-target' },
|
|
986
1556
|
h(
|
|
987
|
-
'
|
|
988
|
-
{ className: 'cait-
|
|
989
|
-
|
|
1557
|
+
'div',
|
|
1558
|
+
{ className: 'cait-labelrow' },
|
|
1559
|
+
h('span', { className: 'cait-label' }, t('editor.target')),
|
|
1560
|
+
h(
|
|
1561
|
+
'span',
|
|
1562
|
+
{ className: 'cait-meta' },
|
|
1563
|
+
`${charCount} ${t('editor.chars')}${dirty ? ` · ${t('editor.unsaved')}` : ''}`,
|
|
1564
|
+
),
|
|
990
1565
|
),
|
|
1566
|
+
h('textarea', {
|
|
1567
|
+
ref: textareaRef,
|
|
1568
|
+
className: 'cait-textarea',
|
|
1569
|
+
value: draft,
|
|
1570
|
+
onChange: (event) => setDraft(event.target.value),
|
|
1571
|
+
onKeyDown,
|
|
1572
|
+
placeholder: t('editor.targetPlaceholder'),
|
|
1573
|
+
spellCheck: false,
|
|
1574
|
+
rows: 8,
|
|
1575
|
+
}),
|
|
991
1576
|
),
|
|
992
|
-
h('textarea', {
|
|
993
|
-
ref: textareaRef,
|
|
994
|
-
className: 'cait-textarea',
|
|
995
|
-
value: draft,
|
|
996
|
-
onChange: (event) => setDraft(event.target.value),
|
|
997
|
-
onKeyDown,
|
|
998
|
-
placeholder: t('editor.targetPlaceholder'),
|
|
999
|
-
spellCheck: false,
|
|
1000
|
-
rows: 8,
|
|
1001
|
-
}),
|
|
1002
1577
|
),
|
|
1003
1578
|
h(
|
|
1004
1579
|
'div',
|
|
@@ -1031,16 +1606,30 @@ window.__ModuleLoader__.load({
|
|
|
1031
1606
|
h(
|
|
1032
1607
|
'div',
|
|
1033
1608
|
{ className: 'cait-editor-foot' },
|
|
1034
|
-
|
|
1609
|
+
remaining === 0
|
|
1035
1610
|
? h('span', { className: 'cait-donechip' }, h(Icon, { name: 'check', size: 11 }), t('editor.done'))
|
|
1036
|
-
: h('span', null, `${
|
|
1611
|
+
: h('span', null, `${remaining} ${t('editor.remaining')}`),
|
|
1037
1612
|
segment.confirmedAt ? h('span', null, `${t('editor.confirmedAt')} ${new Date(segment.confirmedAt).toLocaleString()}`) : null,
|
|
1038
1613
|
),
|
|
1039
1614
|
)
|
|
1040
1615
|
}
|
|
1041
1616
|
|
|
1042
|
-
function TmMatchCard({ t, match, onApply }) {
|
|
1617
|
+
function TmMatchCard({ t, match, onApply, onEdit, onDelete, busy }) {
|
|
1043
1618
|
const matchType = match.matchType || (match.exact ? 'exact' : 'low_fuzzy')
|
|
1619
|
+
const [editing, setEditing] = useState(false)
|
|
1620
|
+
const [value, setValue] = useState(match.target)
|
|
1621
|
+
|
|
1622
|
+
useEffect(() => {
|
|
1623
|
+
setEditing(false)
|
|
1624
|
+
setValue(match.target)
|
|
1625
|
+
}, [match.id, match.target])
|
|
1626
|
+
|
|
1627
|
+
const save = async () => {
|
|
1628
|
+
if (!value.trim()) return
|
|
1629
|
+
const ok = await onEdit({ id: match.id, target: value })
|
|
1630
|
+
if (ok) setEditing(false)
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1044
1633
|
return h(
|
|
1045
1634
|
'article',
|
|
1046
1635
|
{ className: 'cait-card' },
|
|
@@ -1049,10 +1638,56 @@ window.__ModuleLoader__.load({
|
|
|
1049
1638
|
{ className: 'cait-card-head' },
|
|
1050
1639
|
h('span', { className: `cait-score ${matchType}` }, `${match.percent}%`),
|
|
1051
1640
|
h('span', { className: 'cait-score-label' }, t(`match.${matchType}`)),
|
|
1052
|
-
h(
|
|
1641
|
+
h(
|
|
1642
|
+
'div',
|
|
1643
|
+
{ className: 'cait-term-actions' },
|
|
1644
|
+
h('button', { className: 'cait-minibtn', onClick: () => onApply(match) }, t('panel.apply')),
|
|
1645
|
+
h(
|
|
1646
|
+
'button',
|
|
1647
|
+
{ className: 'cait-minibtn', onClick: () => setEditing((current) => !current), disabled: Boolean(busy), title: t('panel.edit') },
|
|
1648
|
+
h(Icon, { name: 'pencil', size: 11 }),
|
|
1649
|
+
),
|
|
1650
|
+
h(
|
|
1651
|
+
'button',
|
|
1652
|
+
{ className: 'cait-deletebtn', onClick: () => onDelete(match), disabled: Boolean(busy), title: t('panel.delete') },
|
|
1653
|
+
h(Icon, { name: 'trash', size: 11 }),
|
|
1654
|
+
),
|
|
1655
|
+
),
|
|
1053
1656
|
),
|
|
1054
1657
|
h('p', { className: 'cait-card-source' }, match.source),
|
|
1055
|
-
|
|
1658
|
+
match.context
|
|
1659
|
+
? h(
|
|
1660
|
+
'p',
|
|
1661
|
+
{ className: 'cait-card-context' },
|
|
1662
|
+
`${t('panel.contextPrev')} ${match.context.prev || '—'} · ${t('panel.contextNext')} ${match.context.next || '—'}`,
|
|
1663
|
+
)
|
|
1664
|
+
: null,
|
|
1665
|
+
editing
|
|
1666
|
+
? h(
|
|
1667
|
+
'div',
|
|
1668
|
+
{ className: 'cait-tmedit' },
|
|
1669
|
+
h('textarea', {
|
|
1670
|
+
className: 'cait-textarea cait-tmedit-input',
|
|
1671
|
+
value,
|
|
1672
|
+
rows: 3,
|
|
1673
|
+
autoFocus: true,
|
|
1674
|
+
onChange: (event) => setValue(event.target.value),
|
|
1675
|
+
onKeyDown: (event) => {
|
|
1676
|
+
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
|
|
1677
|
+
event.preventDefault()
|
|
1678
|
+
save()
|
|
1679
|
+
}
|
|
1680
|
+
if (event.key === 'Escape') setEditing(false)
|
|
1681
|
+
},
|
|
1682
|
+
}),
|
|
1683
|
+
h(
|
|
1684
|
+
'div',
|
|
1685
|
+
{ className: 'cait-formrow' },
|
|
1686
|
+
h('button', { className: 'cait-btn cait-btn-primary', onClick: save, disabled: Boolean(busy) || !value.trim() }, t('panel.saveEntry')),
|
|
1687
|
+
h('button', { className: 'cait-btn cait-btn-ghost', onClick: () => setEditing(false) }, t('panel.cancel')),
|
|
1688
|
+
),
|
|
1689
|
+
)
|
|
1690
|
+
: h('p', { className: 'cait-card-target' }, match.target),
|
|
1056
1691
|
)
|
|
1057
1692
|
}
|
|
1058
1693
|
|
|
@@ -1069,6 +1704,8 @@ window.__ModuleLoader__.load({
|
|
|
1069
1704
|
tmResults,
|
|
1070
1705
|
tmError,
|
|
1071
1706
|
onApplyTm,
|
|
1707
|
+
onEditTm,
|
|
1708
|
+
onDeleteTm,
|
|
1072
1709
|
onImportTmx,
|
|
1073
1710
|
onClearTm,
|
|
1074
1711
|
onAddTerm,
|
|
@@ -1083,7 +1720,7 @@ window.__ModuleLoader__.load({
|
|
|
1083
1720
|
onApplyMt,
|
|
1084
1721
|
onCopy,
|
|
1085
1722
|
onOpenSettings,
|
|
1086
|
-
|
|
1723
|
+
onSearchLibrary,
|
|
1087
1724
|
busy,
|
|
1088
1725
|
}) {
|
|
1089
1726
|
const [termQuery, setTermQuery] = useState('')
|
|
@@ -1130,7 +1767,7 @@ window.__ModuleLoader__.load({
|
|
|
1130
1767
|
'button',
|
|
1131
1768
|
{
|
|
1132
1769
|
className: 'cait-btn cait-libopen',
|
|
1133
|
-
onClick:
|
|
1770
|
+
onClick: () => onSearchLibrary(tab === 'glossary' ? 'glossary' : 'tm', tab === 'tm' ? tmQuery : ''),
|
|
1134
1771
|
disabled: Boolean(busy),
|
|
1135
1772
|
title: t('libraries.open'),
|
|
1136
1773
|
},
|
|
@@ -1194,6 +1831,17 @@ window.__ModuleLoader__.load({
|
|
|
1194
1831
|
}),
|
|
1195
1832
|
h('button', { className: 'cait-iconbtn', type: 'submit', title: t('panel.search') }, h(Icon, { name: 'arrowRight', size: 13 })),
|
|
1196
1833
|
),
|
|
1834
|
+
// The panel searches what is mounted; the library workspace
|
|
1835
|
+
// searches everything. This carries the typed query across.
|
|
1836
|
+
h(
|
|
1837
|
+
'div',
|
|
1838
|
+
{ className: 'cait-paneljump' },
|
|
1839
|
+
h(
|
|
1840
|
+
'button',
|
|
1841
|
+
{ className: 'cait-linkbtn', type: 'button', onClick: () => onSearchLibrary('tm', tmQuery) },
|
|
1842
|
+
t('panel.searchInLibrary'),
|
|
1843
|
+
),
|
|
1844
|
+
),
|
|
1197
1845
|
h(
|
|
1198
1846
|
'div',
|
|
1199
1847
|
{ className: 'cait-listhead' },
|
|
@@ -1202,7 +1850,17 @@ window.__ModuleLoader__.load({
|
|
|
1202
1850
|
),
|
|
1203
1851
|
tmError ? h('div', { className: 'cait-mt-error' }, tmError) : null,
|
|
1204
1852
|
tmMatches.length
|
|
1205
|
-
? tmMatches.map((match) =>
|
|
1853
|
+
? tmMatches.map((match) =>
|
|
1854
|
+
h(TmMatchCard, {
|
|
1855
|
+
key: match.id,
|
|
1856
|
+
t,
|
|
1857
|
+
match,
|
|
1858
|
+
busy,
|
|
1859
|
+
onApply: onApplyTm,
|
|
1860
|
+
onEdit: onEditTm,
|
|
1861
|
+
onDelete: onDeleteTm,
|
|
1862
|
+
}),
|
|
1863
|
+
)
|
|
1206
1864
|
: h('p', { className: 'cait-empty-note' }, tmResults ? t('panel.noMatches') : t('panel.noCurrentMatches')),
|
|
1207
1865
|
h('p', { className: 'cait-mt-hint' }, t('panel.tmHint')),
|
|
1208
1866
|
h(
|
|
@@ -1242,75 +1900,20 @@ window.__ModuleLoader__.load({
|
|
|
1242
1900
|
),
|
|
1243
1901
|
)
|
|
1244
1902
|
: h('p', { className: 'cait-empty-note' }, t('panel.noHits')),
|
|
1903
|
+
// The panel is a per-segment view: whole-library browsing,
|
|
1904
|
+
// adding and editing live on the library page, so the panel
|
|
1905
|
+
// stays short instead of dumping the mounted glossary.
|
|
1906
|
+
h('p', { className: 'cait-mt-hint' }, t('panel.glossaryMatchesOnly')),
|
|
1245
1907
|
h(
|
|
1246
1908
|
'div',
|
|
1247
|
-
{ className: 'cait-
|
|
1248
|
-
h(
|
|
1249
|
-
|
|
1909
|
+
{ className: 'cait-panel-foot' },
|
|
1910
|
+
h(
|
|
1911
|
+
'button',
|
|
1912
|
+
{ className: 'cait-btn', onClick: () => onSearchLibrary('glossary', ''), disabled: Boolean(busy) },
|
|
1913
|
+
h(Icon, { name: 'book', size: 12 }),
|
|
1914
|
+
t('panel.viewAll'),
|
|
1915
|
+
),
|
|
1250
1916
|
),
|
|
1251
|
-
showTermForm
|
|
1252
|
-
? h(
|
|
1253
|
-
'form',
|
|
1254
|
-
{ className: 'cait-termform', onSubmit: submitTerm },
|
|
1255
|
-
h('input', {
|
|
1256
|
-
className: 'cait-input',
|
|
1257
|
-
value: termForm.source,
|
|
1258
|
-
onChange: (event) => setTermForm({ ...termForm, source: event.target.value }),
|
|
1259
|
-
placeholder: t('panel.termSource'),
|
|
1260
|
-
autoFocus: true,
|
|
1261
|
-
}),
|
|
1262
|
-
h(
|
|
1263
|
-
'div',
|
|
1264
|
-
{ className: 'cait-formrow' },
|
|
1265
|
-
h('input', {
|
|
1266
|
-
className: 'cait-input',
|
|
1267
|
-
value: termForm.target,
|
|
1268
|
-
onChange: (event) => setTermForm({ ...termForm, target: event.target.value }),
|
|
1269
|
-
placeholder: t('panel.termTarget'),
|
|
1270
|
-
}),
|
|
1271
|
-
h('input', {
|
|
1272
|
-
className: 'cait-input',
|
|
1273
|
-
value: termForm.note,
|
|
1274
|
-
onChange: (event) => setTermForm({ ...termForm, note: event.target.value }),
|
|
1275
|
-
placeholder: t('panel.termNote'),
|
|
1276
|
-
}),
|
|
1277
|
-
),
|
|
1278
|
-
h(
|
|
1279
|
-
'div',
|
|
1280
|
-
{ className: 'cait-formrow' },
|
|
1281
|
-
h('button', { className: 'cait-btn cait-btn-primary', type: 'submit', disabled: Boolean(busy) || !termForm.source.trim() || !termForm.target.trim() }, t('panel.saveTerm')),
|
|
1282
|
-
h('button', { className: 'cait-btn cait-btn-ghost', type: 'button', onClick: () => setShowTermForm(false) }, t('panel.cancel')),
|
|
1283
|
-
),
|
|
1284
|
-
)
|
|
1285
|
-
: null,
|
|
1286
|
-
h(
|
|
1287
|
-
'div',
|
|
1288
|
-
{ className: 'cait-search' },
|
|
1289
|
-
h(Icon, { name: 'search', size: 13 }),
|
|
1290
|
-
h('input', { value: termQuery, onChange: (event) => setTermQuery(event.target.value), placeholder: t('panel.termSearch'), 'aria-label': t('panel.termSearch') }),
|
|
1291
|
-
),
|
|
1292
|
-
terms.length
|
|
1293
|
-
? terms.map((term) =>
|
|
1294
|
-
h(
|
|
1295
|
-
'div',
|
|
1296
|
-
{ key: term.id, className: 'cait-term' },
|
|
1297
|
-
h(
|
|
1298
|
-
'div',
|
|
1299
|
-
{ className: 'cait-term-main' },
|
|
1300
|
-
h('strong', null, term.source),
|
|
1301
|
-
h(Icon, { name: 'arrowRight', size: 11, className: 'cait-term-arrow' }),
|
|
1302
|
-
h('span', null, term.target),
|
|
1303
|
-
),
|
|
1304
|
-
term.note ? h('div', { className: 'cait-muted cait-tiny', style: { marginTop: 2 } }, term.note) : null,
|
|
1305
|
-
h(
|
|
1306
|
-
'div',
|
|
1307
|
-
{ className: 'cait-term-actions' },
|
|
1308
|
-
h('button', { className: 'cait-minibtn', onClick: () => onApplyTerm(term) }, t('panel.apply')),
|
|
1309
|
-
h('button', { className: 'cait-deletebtn', onClick: () => onDeleteTerm(term), title: t('panel.delete') }, h(Icon, { name: 'trash', size: 11 })),
|
|
1310
|
-
),
|
|
1311
|
-
),
|
|
1312
|
-
)
|
|
1313
|
-
: h('p', { className: 'cait-empty-note' }, t('panel.noTerms')),
|
|
1314
1917
|
h(
|
|
1315
1918
|
'div',
|
|
1316
1919
|
{ className: 'cait-panel-foot' },
|
|
@@ -1536,35 +2139,1199 @@ window.__ModuleLoader__.load({
|
|
|
1536
2139
|
h('strong', null, file ? file.name : t('empty.drop')),
|
|
1537
2140
|
h('span', null, file ? formatBytes(file.size) : t('modal.new.dropHint')),
|
|
1538
2141
|
),
|
|
1539
|
-
h(
|
|
1540
|
-
'div',
|
|
1541
|
-
{ className: 'cait-formfoot' },
|
|
1542
|
-
h('span', { className: 'cait-muted' }, t('empty.formats')),
|
|
2142
|
+
h(
|
|
2143
|
+
'div',
|
|
2144
|
+
{ className: 'cait-formfoot' },
|
|
2145
|
+
h('span', { className: 'cait-muted' }, t('empty.formats')),
|
|
2146
|
+
h(
|
|
2147
|
+
'button',
|
|
2148
|
+
{ className: 'cait-btn cait-btn-primary', type: 'submit', disabled: Boolean(busy) || !canSubmit },
|
|
2149
|
+
busy === 'create' ? h(Spinner) : h(Icon, { name: 'plus', size: 13 }),
|
|
2150
|
+
busy === 'create' ? t('modal.new.importing') : submitLabel,
|
|
2151
|
+
),
|
|
2152
|
+
),
|
|
2153
|
+
)
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function NewDocModal({ t, settings, busy, defaultMode, onClose, onCreate }) {
|
|
2157
|
+
return h(
|
|
2158
|
+
Modal,
|
|
2159
|
+
{ title: t('modal.new.title'), onClose },
|
|
2160
|
+
h(DocumentSourceForm, { t, settings, busy, submitLabel: t('modal.new.create'), onSubmit: onCreate, defaultMode }),
|
|
2161
|
+
h(
|
|
2162
|
+
'div',
|
|
2163
|
+
{ className: 'cait-demo-row' },
|
|
2164
|
+
h('span', null, t('modal.new.sample')),
|
|
2165
|
+
h('button', { className: 'cait-linkbtn', onClick: () => onCreate({ demo: true }), disabled: Boolean(busy) }, t('modal.new.demo')),
|
|
2166
|
+
),
|
|
2167
|
+
)
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
/**
|
|
2171
|
+
* The library workspace: browse, search, page through, edit and export the
|
|
2172
|
+
* translation memories and glossaries themselves. It deliberately does not
|
|
2173
|
+
* need an open document — the rail and the right panel stay document-scoped
|
|
2174
|
+
* and this page is the only place that manages whole libraries.
|
|
2175
|
+
*/
|
|
2176
|
+
function AlignPage({ t, data, busy, notify, report, onApplyState, onConfirm }) {
|
|
2177
|
+
const [sourceFile, setSourceFile] = useState(null)
|
|
2178
|
+
const [targetFile, setTargetFile] = useState(null)
|
|
2179
|
+
const [sourceLang, setSourceLang] = useState('')
|
|
2180
|
+
const [targetLang, setTargetLang] = useState('')
|
|
2181
|
+
const [result, setResult] = useState(null)
|
|
2182
|
+
const [rows, setRows] = useState([])
|
|
2183
|
+
const [working, setWorking] = useState(false)
|
|
2184
|
+
const [tmId, setTmId] = useState('')
|
|
2185
|
+
const sourceRef = useRef(null)
|
|
2186
|
+
const targetRef = useRef(null)
|
|
2187
|
+
const libraries = data?.tmLibraries || []
|
|
2188
|
+
|
|
2189
|
+
useEffect(() => {
|
|
2190
|
+
if (!tmId && libraries.length) setTmId(libraries[0].id)
|
|
2191
|
+
}, [libraries.length])
|
|
2192
|
+
|
|
2193
|
+
const readBase64 = (file) =>
|
|
2194
|
+
new Promise((resolve, reject) => {
|
|
2195
|
+
const reader = new FileReader()
|
|
2196
|
+
reader.onerror = () => reject(new Error(t('align.readError')))
|
|
2197
|
+
reader.onload = () => {
|
|
2198
|
+
const value = String(reader.result || '')
|
|
2199
|
+
const comma = value.indexOf(',')
|
|
2200
|
+
resolve(comma >= 0 ? value.slice(comma + 1) : '')
|
|
2201
|
+
}
|
|
2202
|
+
reader.readAsDataURL(file)
|
|
2203
|
+
})
|
|
2204
|
+
|
|
2205
|
+
const pick = async (which, file) => {
|
|
2206
|
+
if (!file) return
|
|
2207
|
+
setWorking(true)
|
|
2208
|
+
try {
|
|
2209
|
+
const uploaded = await rpc('readUploadText', {
|
|
2210
|
+
name: file.name,
|
|
2211
|
+
base64: await readBase64(file),
|
|
2212
|
+
})
|
|
2213
|
+
const picked = { name: uploaded.name, text: uploaded.text, chars: uploaded.chars }
|
|
2214
|
+
if (which === 'source') setSourceFile(picked)
|
|
2215
|
+
else setTargetFile(picked)
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
report(error)
|
|
2218
|
+
} finally {
|
|
2219
|
+
setWorking(false)
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const run = async () => {
|
|
2224
|
+
if (!sourceFile || !targetFile) {
|
|
2225
|
+
notify(t('align.needFiles'), true)
|
|
2226
|
+
return
|
|
2227
|
+
}
|
|
2228
|
+
setWorking(true)
|
|
2229
|
+
try {
|
|
2230
|
+
const aligned = await rpc('alignUploads', {
|
|
2231
|
+
sourceFile: { name: sourceFile.name, text: sourceFile.text },
|
|
2232
|
+
targetFile: { name: targetFile.name, text: targetFile.text },
|
|
2233
|
+
sourceLang,
|
|
2234
|
+
targetLang,
|
|
2235
|
+
})
|
|
2236
|
+
setResult(aligned)
|
|
2237
|
+
setRows(aligned.rows.map((row) => ({ ...row, include: row.confidence === 'high' })))
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
report(error)
|
|
2240
|
+
} finally {
|
|
2241
|
+
setWorking(false)
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
const updateRow = (at, patch) =>
|
|
2246
|
+
setRows((current) => current.map((row, position) => (position === at ? { ...row, ...patch } : row)))
|
|
2247
|
+
|
|
2248
|
+
const removeRow = (at) => setRows((current) => current.filter((_row, position) => position !== at))
|
|
2249
|
+
|
|
2250
|
+
const mergeRow = (at) =>
|
|
2251
|
+
setRows((current) => {
|
|
2252
|
+
const head = current[at]
|
|
2253
|
+
const next = current[at + 1]
|
|
2254
|
+
if (!head || !next) return current
|
|
2255
|
+
const merged = {
|
|
2256
|
+
...head,
|
|
2257
|
+
target: [head.target, next.target].filter(Boolean).join(' '),
|
|
2258
|
+
include: head.include,
|
|
2259
|
+
merged: true,
|
|
2260
|
+
}
|
|
2261
|
+
return [...current.slice(0, at), merged, ...current.slice(at + 2)]
|
|
2262
|
+
})
|
|
2263
|
+
|
|
2264
|
+
const splitRow = (at) =>
|
|
2265
|
+
setRows((current) => {
|
|
2266
|
+
const row = current[at]
|
|
2267
|
+
if (!row || !row.target) return current
|
|
2268
|
+
const parts = String(row.target).split(/\n+/).filter(Boolean)
|
|
2269
|
+
if (parts.length < 2) return current
|
|
2270
|
+
const replacement = parts.map((part) => ({ ...row, target: part }))
|
|
2271
|
+
return [...current.slice(0, at), ...replacement, ...current.slice(at + 1)]
|
|
2272
|
+
})
|
|
2273
|
+
|
|
2274
|
+
const write = async () => {
|
|
2275
|
+
const pairs = rows
|
|
2276
|
+
.filter((row) => row.include && row.source && row.target)
|
|
2277
|
+
.map((row) => ({ source: row.source, target: row.target, sourceLang, targetLang }))
|
|
2278
|
+
if (!pairs.length) {
|
|
2279
|
+
notify(t('align.nothing'), true)
|
|
2280
|
+
return
|
|
2281
|
+
}
|
|
2282
|
+
onConfirm({
|
|
2283
|
+
title: t('align.write'),
|
|
2284
|
+
message: t('align.writeMessage', { count: pairs.length }),
|
|
2285
|
+
action: async () => {
|
|
2286
|
+
setWorking(true)
|
|
2287
|
+
try {
|
|
2288
|
+
const written = await rpc('writeAlignment', { pairs, tmId: tmId || undefined })
|
|
2289
|
+
notify(t('align.written', { written: written.written, skipped: written.skipped }))
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
report(error)
|
|
2292
|
+
} finally {
|
|
2293
|
+
setWorking(false)
|
|
2294
|
+
}
|
|
2295
|
+
},
|
|
2296
|
+
})
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
const included = rows.filter((row) => row.include && row.source && row.target).length
|
|
2300
|
+
const fileCard = (which, file, ref) =>
|
|
2301
|
+
h(
|
|
2302
|
+
'div',
|
|
2303
|
+
{ className: 'cait-alignfile' },
|
|
2304
|
+
h('span', { className: 'cait-alignlabel' }, t(which === 'source' ? 'align.sourceFile' : 'align.targetFile')),
|
|
2305
|
+
h('span', { className: `cait-alignname${file ? '' : ' empty'}` }, file ? file.name : t('align.pick')),
|
|
2306
|
+
h('span', { className: 'cait-alignmeta' }, file ? t('align.chars', { count: file.chars }) : ''),
|
|
2307
|
+
h('input', {
|
|
2308
|
+
ref,
|
|
2309
|
+
type: 'file',
|
|
2310
|
+
accept: '.txt,.md,.docx,.epub,.html,.htm,.xlsx,.pptx,.csv,.srt,.vtt,.po,.json,.rtf,.xliff',
|
|
2311
|
+
style: { display: 'none' },
|
|
2312
|
+
onChange: (event) => {
|
|
2313
|
+
const chosen = event.target.files && event.target.files[0]
|
|
2314
|
+
pick(which, chosen)
|
|
2315
|
+
event.target.value = ''
|
|
2316
|
+
},
|
|
2317
|
+
}),
|
|
2318
|
+
h(
|
|
2319
|
+
'button',
|
|
2320
|
+
{ type: 'button', className: 'cait-btn', onClick: () => ref.current && ref.current.click(), disabled: working },
|
|
2321
|
+
h(Icon, { name: 'folder', size: 13 }),
|
|
2322
|
+
h('span', { className: 'cait-btn-label' }, t('align.choose')),
|
|
2323
|
+
),
|
|
2324
|
+
)
|
|
2325
|
+
|
|
2326
|
+
return h(
|
|
2327
|
+
'div',
|
|
2328
|
+
{ className: 'cait-alignpage' },
|
|
2329
|
+
h(
|
|
2330
|
+
'div',
|
|
2331
|
+
{ className: 'cait-alignhead' },
|
|
2332
|
+
h('div', null, h('h2', null, t('align.title')), h('p', { className: 'cait-mt-hint' }, t('align.hint'))),
|
|
2333
|
+
h(
|
|
2334
|
+
'div',
|
|
2335
|
+
{ className: 'cait-alignactions' },
|
|
2336
|
+
h('input', {
|
|
2337
|
+
className: 'cait-libinput',
|
|
2338
|
+
placeholder: t('align.sourceLang'),
|
|
2339
|
+
value: sourceLang,
|
|
2340
|
+
onChange: (event) => setSourceLang(event.target.value),
|
|
2341
|
+
}),
|
|
2342
|
+
h('input', {
|
|
2343
|
+
className: 'cait-libinput',
|
|
2344
|
+
placeholder: t('align.targetLang'),
|
|
2345
|
+
value: targetLang,
|
|
2346
|
+
onChange: (event) => setTargetLang(event.target.value),
|
|
2347
|
+
}),
|
|
2348
|
+
h(
|
|
2349
|
+
'select',
|
|
2350
|
+
{
|
|
2351
|
+
className: 'cait-select cait-liblang',
|
|
2352
|
+
value: tmId,
|
|
2353
|
+
onChange: (event) => setTmId(event.target.value),
|
|
2354
|
+
},
|
|
2355
|
+
libraries.map((library) => h('option', { key: library.id, value: library.id }, library.name)),
|
|
2356
|
+
),
|
|
2357
|
+
h(
|
|
2358
|
+
'button',
|
|
2359
|
+
{ type: 'button', className: 'cait-btn cait-btn-primary', onClick: run, disabled: working || !sourceFile || !targetFile },
|
|
2360
|
+
h(Icon, { name: 'align', size: 13 }),
|
|
2361
|
+
h('span', { className: 'cait-btn-label' }, working ? t('align.running') : t('align.run')),
|
|
2362
|
+
),
|
|
2363
|
+
),
|
|
2364
|
+
),
|
|
2365
|
+
h('div', { className: 'cait-alignfiles' }, fileCard('source', sourceFile, sourceRef), fileCard('target', targetFile, targetRef)),
|
|
2366
|
+
result
|
|
2367
|
+
? h(
|
|
2368
|
+
'div',
|
|
2369
|
+
{ className: 'cait-alignstats' },
|
|
2370
|
+
h('span', null, t('align.stats', {
|
|
2371
|
+
high: result.high,
|
|
2372
|
+
medium: result.medium,
|
|
2373
|
+
low: result.low,
|
|
2374
|
+
})),
|
|
2375
|
+
h('span', null, `${t('align.unpairedSource')}: ${result.unpairedSource}`),
|
|
2376
|
+
h('span', null, `${t('align.unpairedTarget')}: ${result.unpairedTarget}`),
|
|
2377
|
+
result.blocked ? h('span', { className: 'cait-alignwarn' }, t('align.blocked')) : null,
|
|
2378
|
+
h(
|
|
2379
|
+
'button',
|
|
2380
|
+
{ type: 'button', className: 'cait-btn', onClick: write, disabled: working || !included },
|
|
2381
|
+
h(Icon, { name: 'check', size: 13 }),
|
|
2382
|
+
h('span', { className: 'cait-btn-label' }, t('align.writeCount', { count: included })),
|
|
2383
|
+
),
|
|
2384
|
+
)
|
|
2385
|
+
: null,
|
|
2386
|
+
rows.length
|
|
2387
|
+
? h(
|
|
2388
|
+
'div',
|
|
2389
|
+
{ className: 'cait-libtable-wrap cait-aligntable-wrap' },
|
|
2390
|
+
h(
|
|
2391
|
+
'table',
|
|
2392
|
+
{ className: 'cait-libtable cait-aligntable' },
|
|
2393
|
+
h(
|
|
2394
|
+
'thead',
|
|
2395
|
+
null,
|
|
2396
|
+
h(
|
|
2397
|
+
'tr',
|
|
2398
|
+
null,
|
|
2399
|
+
h('th', null, ''),
|
|
2400
|
+
h('th', null, t('align.sourceFile')),
|
|
2401
|
+
h('th', null, t('align.targetFile')),
|
|
2402
|
+
h('th', null, t('align.confidence')),
|
|
2403
|
+
h('th', null, ''),
|
|
2404
|
+
),
|
|
2405
|
+
),
|
|
2406
|
+
h(
|
|
2407
|
+
'tbody',
|
|
2408
|
+
null,
|
|
2409
|
+
rows.map((row, at) =>
|
|
2410
|
+
h(
|
|
2411
|
+
'tr',
|
|
2412
|
+
{ key: `${row.sourceIndex}-${row.targetIndex}-${at}`, className: row.include ? 'included' : '' },
|
|
2413
|
+
h(
|
|
2414
|
+
'td',
|
|
2415
|
+
null,
|
|
2416
|
+
h('input', {
|
|
2417
|
+
type: 'checkbox',
|
|
2418
|
+
checked: Boolean(row.include),
|
|
2419
|
+
onChange: (event) => updateRow(at, { include: event.target.checked }),
|
|
2420
|
+
}),
|
|
2421
|
+
),
|
|
2422
|
+
h('td', null, row.source),
|
|
2423
|
+
h('td', null, row.target || h('span', { className: 'cait-alignmissing' }, '—')),
|
|
2424
|
+
h(
|
|
2425
|
+
'td',
|
|
2426
|
+
null,
|
|
2427
|
+
h('span', { className: `cait-confidence ${row.confidence}` }, `${Math.round(row.score * 100)}%`),
|
|
2428
|
+
),
|
|
2429
|
+
h(
|
|
2430
|
+
'td',
|
|
2431
|
+
{ className: 'cait-alignrow-actions' },
|
|
2432
|
+
h(
|
|
2433
|
+
'button',
|
|
2434
|
+
{ type: 'button', className: 'cait-minibtn', title: t('align.merge'), onClick: () => mergeRow(at) },
|
|
2435
|
+
h(Icon, { name: 'chevronDown', size: 12 }),
|
|
2436
|
+
),
|
|
2437
|
+
h(
|
|
2438
|
+
'button',
|
|
2439
|
+
{ type: 'button', className: 'cait-minibtn', title: t('align.split'), onClick: () => splitRow(at) },
|
|
2440
|
+
h(Icon, { name: 'chevronRight', size: 12 }),
|
|
2441
|
+
),
|
|
2442
|
+
h(
|
|
2443
|
+
'button',
|
|
2444
|
+
{ type: 'button', className: 'cait-minibtn cait-deletebtn', title: t('align.remove'), onClick: () => removeRow(at) },
|
|
2445
|
+
h(Icon, { name: 'trash', size: 12 }),
|
|
2446
|
+
),
|
|
2447
|
+
),
|
|
2448
|
+
),
|
|
2449
|
+
),
|
|
2450
|
+
),
|
|
2451
|
+
),
|
|
2452
|
+
)
|
|
2453
|
+
: null,
|
|
2454
|
+
)
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
function LibraryPage({
|
|
2458
|
+
t,
|
|
2459
|
+
data,
|
|
2460
|
+
settings,
|
|
2461
|
+
busy,
|
|
2462
|
+
notify,
|
|
2463
|
+
report,
|
|
2464
|
+
seed,
|
|
2465
|
+
onApplyState,
|
|
2466
|
+
onConfirm,
|
|
2467
|
+
onCreateLibrary,
|
|
2468
|
+
onRenameLibrary,
|
|
2469
|
+
onDeleteLibrary,
|
|
2470
|
+
}) {
|
|
2471
|
+
const storage = typeof localStorage !== 'undefined' ? localStorage : null
|
|
2472
|
+
const [kind, setKind] = useState(seed?.kind === 'glossary' ? 'glossary' : 'tm')
|
|
2473
|
+
const [scopeIds, setScopeIds] = useState([])
|
|
2474
|
+
const [query, setQuery] = useState(seed?.query || '')
|
|
2475
|
+
const [debouncedQuery, setDebouncedQuery] = useState('')
|
|
2476
|
+
const [lang, setLang] = useState('')
|
|
2477
|
+
const [page, setPage] = useState(1)
|
|
2478
|
+
const [pageSize, setPageSize] = useState(() => readPageSize(storage))
|
|
2479
|
+
const [rows, setRows] = useState(null)
|
|
2480
|
+
const [loading, setLoading] = useState(false)
|
|
2481
|
+
const [working, setWorking] = useState('')
|
|
2482
|
+
const [selectedId, setSelectedId] = useState('')
|
|
2483
|
+
const [draft, setDraft] = useState(null)
|
|
2484
|
+
const [renamingId, setRenamingId] = useState('')
|
|
2485
|
+
const [renameText, setRenameText] = useState('')
|
|
2486
|
+
const [newLibraryName, setNewLibraryName] = useState('')
|
|
2487
|
+
const [adding, setAdding] = useState(false)
|
|
2488
|
+
const [exportFormat, setExportFormat] = useState('tbx')
|
|
2489
|
+
const [terms, setTerms] = useState(null)
|
|
2490
|
+
const fileRef = useRef(null)
|
|
2491
|
+
const importMode = useRef('')
|
|
2492
|
+
|
|
2493
|
+
const libraries = (kind === 'tm' ? data?.libraries?.tm : data?.libraries?.glossary) || []
|
|
2494
|
+
const idKey = kind === 'tm' ? 'tmId' : 'glossaryId'
|
|
2495
|
+
const labelKey = 'name'
|
|
2496
|
+
const scopeKey = scopeIds.join(',')
|
|
2497
|
+
const targetLibraryId = scopeIds[0] || (kind === 'tm' ? data?.mounts?.defaultTmId : data?.mounts?.defaultGlossaryId) || libraries[0]?.[idKey] || ''
|
|
2498
|
+
|
|
2499
|
+
useEffect(() => {
|
|
2500
|
+
const timer = setTimeout(() => setDebouncedQuery(query), 250)
|
|
2501
|
+
return () => clearTimeout(timer)
|
|
2502
|
+
}, [query])
|
|
2503
|
+
|
|
2504
|
+
// The panel can jump into the library with its current search text; a
|
|
2505
|
+
// fresh nonce means "apply this seed now" even when the values repeat.
|
|
2506
|
+
useEffect(() => {
|
|
2507
|
+
if (!seed) return
|
|
2508
|
+
setKind(seed.kind === 'glossary' ? 'glossary' : 'tm')
|
|
2509
|
+
setQuery(seed.query || '')
|
|
2510
|
+
setPage(1)
|
|
2511
|
+
}, [seed?.nonce])
|
|
2512
|
+
|
|
2513
|
+
// Switching kind resets the scope: library ids are not interchangeable.
|
|
2514
|
+
useEffect(() => {
|
|
2515
|
+
setScopeIds([])
|
|
2516
|
+
setPage(1)
|
|
2517
|
+
setSelectedId('')
|
|
2518
|
+
setLang('')
|
|
2519
|
+
}, [kind])
|
|
2520
|
+
|
|
2521
|
+
const loadRows = async () => {
|
|
2522
|
+
setLoading(true)
|
|
2523
|
+
try {
|
|
2524
|
+
const params =
|
|
2525
|
+
kind === 'tm'
|
|
2526
|
+
? { tmIds: scopeIds, query: debouncedQuery, page, pageSize }
|
|
2527
|
+
: { glossaryIds: scopeIds, query: debouncedQuery, page, pageSize }
|
|
2528
|
+
if (lang) params.sourceLang = lang
|
|
2529
|
+
const result = await rpc(kind === 'tm' ? 'listTmEntries' : 'listGlossaryEntries', params)
|
|
2530
|
+
setRows(result)
|
|
2531
|
+
if (result.page !== page) setPage(result.page)
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
report(error)
|
|
2534
|
+
setRows(null)
|
|
2535
|
+
} finally {
|
|
2536
|
+
setLoading(false)
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
useEffect(() => {
|
|
2541
|
+
let alive = true
|
|
2542
|
+
const run = async () => {
|
|
2543
|
+
if (!alive) return
|
|
2544
|
+
await loadRows()
|
|
2545
|
+
}
|
|
2546
|
+
run()
|
|
2547
|
+
return () => {
|
|
2548
|
+
alive = false
|
|
2549
|
+
}
|
|
2550
|
+
}, [kind, scopeKey, debouncedQuery, lang, page, pageSize, data?.revision])
|
|
2551
|
+
|
|
2552
|
+
useEffect(() => {
|
|
2553
|
+
const entry = (rows?.entries || []).find((item) => item.id === selectedId)
|
|
2554
|
+
setDraft(entry ? { ...entry } : null)
|
|
2555
|
+
}, [selectedId, rows])
|
|
2556
|
+
|
|
2557
|
+
const dirty =
|
|
2558
|
+
draft &&
|
|
2559
|
+
rows?.entries?.some(
|
|
2560
|
+
(item) =>
|
|
2561
|
+
item.id === draft.id &&
|
|
2562
|
+
(item.source !== draft.source ||
|
|
2563
|
+
item.target !== draft.target ||
|
|
2564
|
+
(item.note || '') !== (draft.note || '') ||
|
|
2565
|
+
Boolean(item.caseSensitive) !== Boolean(draft.caseSensitive)),
|
|
2566
|
+
)
|
|
2567
|
+
|
|
2568
|
+
const selectKind = (next) => setKind(next)
|
|
2569
|
+
|
|
2570
|
+
const mineTerms = async () => {
|
|
2571
|
+
const active = data?.activeDocument
|
|
2572
|
+
if (!active) {
|
|
2573
|
+
notify(t('term.noDoc'), true)
|
|
2574
|
+
return
|
|
2575
|
+
}
|
|
2576
|
+
setWorking('terms')
|
|
2577
|
+
try {
|
|
2578
|
+
const mined = await rpc('termCandidates', { documentId: active.id, limit: 60 })
|
|
2579
|
+
setTerms(mined.candidates.map((candidate) => ({
|
|
2580
|
+
...candidate,
|
|
2581
|
+
picked: false,
|
|
2582
|
+
target: candidate.evidence[0] || '',
|
|
2583
|
+
})))
|
|
2584
|
+
} catch (error) {
|
|
2585
|
+
report(error)
|
|
2586
|
+
} finally {
|
|
2587
|
+
setWorking('')
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
|
|
2591
|
+
const toggleTerm = (index) =>
|
|
2592
|
+
setTerms((current) =>
|
|
2593
|
+
current.map((item, position) => (position === index ? { ...item, picked: !item.picked } : item)),
|
|
2594
|
+
)
|
|
2595
|
+
|
|
2596
|
+
const setTermTarget = (index, value) =>
|
|
2597
|
+
setTerms((current) =>
|
|
2598
|
+
current.map((item, position) => (position === index ? { ...item, target: value, picked: true } : item)),
|
|
2599
|
+
)
|
|
2600
|
+
|
|
2601
|
+
const addMinedTerms = async () => {
|
|
2602
|
+
const picked = (terms || []).filter((item) => item.picked && item.target.trim())
|
|
2603
|
+
if (!picked.length) return
|
|
2604
|
+
setWorking('terms')
|
|
2605
|
+
try {
|
|
2606
|
+
const result = await rpc('addGlossaryEntries', {
|
|
2607
|
+
glossaryId: targetLibraryId || undefined,
|
|
2608
|
+
entries: picked.map((item) => ({
|
|
2609
|
+
source: item.term,
|
|
2610
|
+
target: item.target.trim(),
|
|
2611
|
+
sourceLang: settings.sourceLang,
|
|
2612
|
+
})),
|
|
2613
|
+
})
|
|
2614
|
+
if (result?.state) onApplyState(result.state)
|
|
2615
|
+
notify(t('term.added').replace('{added}', String(result.added)))
|
|
2616
|
+
setTerms(null)
|
|
2617
|
+
await loadRows()
|
|
2618
|
+
} catch (error) {
|
|
2619
|
+
report(error)
|
|
2620
|
+
} finally {
|
|
2621
|
+
setWorking('')
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
const toggleScope = (id) => {
|
|
2626
|
+
setPage(1)
|
|
2627
|
+
setScopeIds((current) => (current.includes(id) ? current.filter((item) => item !== id) : [...current, id]))
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
const allLanguages = rows?.languages
|
|
2631
|
+
? [...new Set([...(rows.languages.source || []), ...(rows.languages.target || [])])].filter(Boolean)
|
|
2632
|
+
: []
|
|
2633
|
+
|
|
2634
|
+
const changePageSize = (value) => {
|
|
2635
|
+
const size = writePageSize(storage, value)
|
|
2636
|
+
setPageSize(size)
|
|
2637
|
+
setPage(1)
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
const saveEntry = async () => {
|
|
2641
|
+
if (!draft) return
|
|
2642
|
+
setWorking('save')
|
|
2643
|
+
try {
|
|
2644
|
+
const result =
|
|
2645
|
+
kind === 'tm'
|
|
2646
|
+
? await rpc('updateTM', {
|
|
2647
|
+
id: draft.id,
|
|
2648
|
+
source: draft.source,
|
|
2649
|
+
target: draft.target,
|
|
2650
|
+
note: draft.note,
|
|
2651
|
+
})
|
|
2652
|
+
: await rpc('updateGlossary', {
|
|
2653
|
+
id: draft.id,
|
|
2654
|
+
source: draft.source,
|
|
2655
|
+
target: draft.target,
|
|
2656
|
+
note: draft.note,
|
|
2657
|
+
sourceLang: draft.sourceLang,
|
|
2658
|
+
caseSensitive: Boolean(draft.caseSensitive),
|
|
2659
|
+
})
|
|
2660
|
+
if (result?.state) onApplyState(result.state)
|
|
2661
|
+
notify(t('library.entrySaved'))
|
|
2662
|
+
setAdding(false)
|
|
2663
|
+
await loadRows()
|
|
2664
|
+
} catch (error) {
|
|
2665
|
+
report(error)
|
|
2666
|
+
} finally {
|
|
2667
|
+
setWorking('')
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
const addEntry = async () => {
|
|
2672
|
+
if (!draft) return
|
|
2673
|
+
setWorking('add')
|
|
2674
|
+
try {
|
|
2675
|
+
const result =
|
|
2676
|
+
kind === 'tm'
|
|
2677
|
+
? await rpc('addTM', {
|
|
2678
|
+
source: draft.source,
|
|
2679
|
+
target: draft.target,
|
|
2680
|
+
tmId: targetLibraryId,
|
|
2681
|
+
sourceLang: draft.sourceLang || settings.sourceLang,
|
|
2682
|
+
targetLang: draft.targetLang || settings.targetLang,
|
|
2683
|
+
})
|
|
2684
|
+
: await rpc('addGlossary', {
|
|
2685
|
+
source: draft.source,
|
|
2686
|
+
target: draft.target,
|
|
2687
|
+
note: draft.note,
|
|
2688
|
+
glossaryId: targetLibraryId,
|
|
2689
|
+
sourceLang: draft.sourceLang || '',
|
|
2690
|
+
caseSensitive: Boolean(draft.caseSensitive),
|
|
2691
|
+
})
|
|
2692
|
+
if (result?.state) onApplyState(result.state)
|
|
2693
|
+
notify(t('library.entryCreated'))
|
|
2694
|
+
setAdding(false)
|
|
2695
|
+
setSelectedId('')
|
|
2696
|
+
await loadRows()
|
|
2697
|
+
} catch (error) {
|
|
2698
|
+
report(error)
|
|
2699
|
+
} finally {
|
|
2700
|
+
setWorking('')
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
const deleteEntry = () => {
|
|
2705
|
+
if (!draft || adding) return
|
|
2706
|
+
onConfirm({
|
|
2707
|
+
title: t('library.deleteEntry'),
|
|
2708
|
+
message: kind === 'tm' ? t('modal.confirm.deleteTm') : t('library.deleteEntry'),
|
|
2709
|
+
confirmLabel: t('library.deleteEntry'),
|
|
2710
|
+
action: async () => {
|
|
2711
|
+
const result = await rpc(kind === 'tm' ? 'deleteTM' : 'deleteGlossary', { id: draft.id })
|
|
2712
|
+
if (result?.state) onApplyState(result.state)
|
|
2713
|
+
setSelectedId('')
|
|
2714
|
+
notify(t('library.entryDeleted'))
|
|
2715
|
+
await loadRows()
|
|
2716
|
+
},
|
|
2717
|
+
})
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
const clearLibrary = () => {
|
|
2721
|
+
if (!targetLibraryId) return
|
|
2722
|
+
onConfirm({
|
|
2723
|
+
title: t('library.clear'),
|
|
2724
|
+
message: t('modal.confirm.clearTm'),
|
|
2725
|
+
confirmLabel: t('library.clear'),
|
|
2726
|
+
action: async () => {
|
|
2727
|
+
const result =
|
|
2728
|
+
kind === 'tm'
|
|
2729
|
+
? await rpc('clearTM', { tmIds: [targetLibraryId], confirm: true })
|
|
2730
|
+
: await rpc('clearGlossary', { glossaryIds: [targetLibraryId], confirm: true })
|
|
2731
|
+
if (result?.state) onApplyState(result.state)
|
|
2732
|
+
setSelectedId('')
|
|
2733
|
+
await loadRows()
|
|
2734
|
+
},
|
|
2735
|
+
})
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
const exportLibrary = async () => {
|
|
2739
|
+
if (!targetLibraryId) return
|
|
2740
|
+
setWorking('export')
|
|
2741
|
+
try {
|
|
2742
|
+
const result =
|
|
2743
|
+
kind === 'tm'
|
|
2744
|
+
? await rpc('exportTM', { tmId: targetLibraryId, format: 'tmx' })
|
|
2745
|
+
: await rpc('exportGlossary', { glossaryId: targetLibraryId, format: exportFormat })
|
|
2746
|
+
downloadFile(result.filename, result.content, result.mime)
|
|
2747
|
+
notify(t('library.exported'))
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
report(error)
|
|
2750
|
+
} finally {
|
|
2751
|
+
setWorking('')
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
const pickImport = (mode) => {
|
|
2756
|
+
importMode.current = mode
|
|
2757
|
+
if (fileRef.current) {
|
|
2758
|
+
fileRef.current.value = ''
|
|
2759
|
+
fileRef.current.click()
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
|
|
2763
|
+
const runImport = async (file) => {
|
|
2764
|
+
if (!file || !targetLibraryId) return
|
|
2765
|
+
setWorking('import')
|
|
2766
|
+
try {
|
|
2767
|
+
const text = await file.text()
|
|
2768
|
+
if (importMode.current === 'glossary') {
|
|
2769
|
+
const result = await rpc('importGlossary', { text, glossaryId: targetLibraryId })
|
|
2770
|
+
if (result?.state) onApplyState(result.state)
|
|
2771
|
+
notify(`${t('toast.glossaryImported')} · +${result.added} / ~${result.updated}`)
|
|
2772
|
+
} else {
|
|
2773
|
+
const result = await rpc('importTmx', {
|
|
2774
|
+
text,
|
|
2775
|
+
importAs: 'tm',
|
|
2776
|
+
tmId: targetLibraryId,
|
|
2777
|
+
sourceLang: kind === 'tm' ? undefined : settings.sourceLang,
|
|
2778
|
+
targetLang: settings.targetLang,
|
|
2779
|
+
})
|
|
2780
|
+
if (result?.state) onApplyState(result.state)
|
|
2781
|
+
notify(`${t('toast.tmImported')} · +${result.added} / ~${result.updated}`)
|
|
2782
|
+
}
|
|
2783
|
+
await loadRows()
|
|
2784
|
+
} catch (error) {
|
|
2785
|
+
report(error)
|
|
2786
|
+
} finally {
|
|
2787
|
+
setWorking('')
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
const submitRename = async (library) => {
|
|
2792
|
+
const name = renameText.trim()
|
|
2793
|
+
setRenamingId('')
|
|
2794
|
+
if (!name || name === library[labelKey]) return
|
|
2795
|
+
await onRenameLibrary({ kind, id: library[idKey], name })
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
const submitNewLibrary = async () => {
|
|
2799
|
+
const name = newLibraryName.trim()
|
|
2800
|
+
if (!name) return
|
|
2801
|
+
const ok = await onCreateLibrary({ kind, name })
|
|
2802
|
+
if (ok) setNewLibraryName('')
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
const confirmDeleteLibrary = (library) => {
|
|
2806
|
+
if (libraries.length <= 1) return
|
|
2807
|
+
onConfirm({
|
|
2808
|
+
title: t('library.deleteLibrary'),
|
|
2809
|
+
message: t('library.deleteLibraryMessage', { name: library[labelKey] }),
|
|
2810
|
+
confirmLabel: t('library.deleteLibrary'),
|
|
2811
|
+
action: async () => {
|
|
2812
|
+
await onDeleteLibrary({ kind, id: library[idKey], name: library[labelKey] })
|
|
2813
|
+
if (scopeIds.includes(library[idKey])) {
|
|
2814
|
+
setScopeIds((current) => current.filter((item) => item !== library[idKey]))
|
|
2815
|
+
}
|
|
2816
|
+
},
|
|
2817
|
+
})
|
|
2818
|
+
}
|
|
2819
|
+
|
|
2820
|
+
const startAdding = () => {
|
|
2821
|
+
setAdding(true)
|
|
2822
|
+
setSelectedId('')
|
|
2823
|
+
setDraft(
|
|
2824
|
+
kind === 'tm'
|
|
2825
|
+
? {
|
|
2826
|
+
id: '',
|
|
2827
|
+
source: '',
|
|
2828
|
+
target: '',
|
|
2829
|
+
note: '',
|
|
2830
|
+
sourceLang: settings.sourceLang,
|
|
2831
|
+
targetLang: settings.targetLang,
|
|
2832
|
+
}
|
|
2833
|
+
: { id: '', source: '', target: '', note: '', sourceLang: settings.sourceLang, caseSensitive: false },
|
|
2834
|
+
)
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
const bounds = pageBounds(rows?.page || 1, rows?.total || 0, rows?.pageSize || pageSize)
|
|
2838
|
+
const contextLabel = (entry) =>
|
|
2839
|
+
entry.context ? `${entry.context.prev || '—'} · ${entry.context.next || '—'}` : '—'
|
|
2840
|
+
const columns = kind === 'tm'
|
|
2841
|
+
? [t('library.colSource'), t('library.colTarget'), t('library.colNote'), t('library.colContext'), t('library.colLang'), t('library.colUpdated')]
|
|
2842
|
+
: [t('library.colSource'), t('library.colTarget'), t('library.colNote'), t('library.colLang'), t('library.colCase'), t('library.colUpdated')]
|
|
2843
|
+
|
|
2844
|
+
return h(
|
|
2845
|
+
'div',
|
|
2846
|
+
{ className: 'cait-libpage' },
|
|
2847
|
+
h(
|
|
2848
|
+
'div',
|
|
2849
|
+
{ className: 'cait-libpage-head' },
|
|
2850
|
+
h(
|
|
2851
|
+
'div',
|
|
2852
|
+
{ className: 'cait-libtabs' },
|
|
2853
|
+
h(
|
|
2854
|
+
'button',
|
|
2855
|
+
{ className: `cait-libtabs-item${kind === 'tm' ? ' active' : ''}`, onClick: () => selectKind('tm') },
|
|
2856
|
+
t('library.tmTab'),
|
|
2857
|
+
h('span', { className: 'cait-libtabs-count' }, data?.libraries?.tm?.length || 0),
|
|
2858
|
+
),
|
|
2859
|
+
h(
|
|
2860
|
+
'button',
|
|
2861
|
+
{ className: `cait-libtabs-item${kind === 'glossary' ? ' active' : ''}`, onClick: () => selectKind('glossary') },
|
|
2862
|
+
t('library.glossaryTab'),
|
|
2863
|
+
h('span', { className: 'cait-libtabs-count' }, data?.libraries?.glossary?.length || 0),
|
|
2864
|
+
),
|
|
2865
|
+
),
|
|
2866
|
+
h('span', { className: 'cait-libhint' }, t('library.noDocHint')),
|
|
2867
|
+
),
|
|
2868
|
+
h(
|
|
2869
|
+
'div',
|
|
2870
|
+
{ className: 'cait-libpage-body' },
|
|
2871
|
+
h(
|
|
2872
|
+
'div',
|
|
2873
|
+
{ className: 'cait-liblist' },
|
|
2874
|
+
h(
|
|
2875
|
+
'div',
|
|
2876
|
+
{ className: 'cait-liblist-head' },
|
|
2877
|
+
t('library.title'),
|
|
2878
|
+
),
|
|
2879
|
+
libraries.map((library) =>
|
|
2880
|
+
h(
|
|
2881
|
+
'div',
|
|
2882
|
+
{ key: library[idKey], className: `cait-librow${scopeIds.includes(library[idKey]) ? ' scoped' : ''}` },
|
|
2883
|
+
h('input', {
|
|
2884
|
+
type: 'checkbox',
|
|
2885
|
+
checked: scopeIds.includes(library[idKey]),
|
|
2886
|
+
onChange: () => toggleScope(library[idKey]),
|
|
2887
|
+
title: t('library.scope'),
|
|
2888
|
+
'aria-label': t('library.scope'),
|
|
2889
|
+
}),
|
|
2890
|
+
renamingId === library[idKey]
|
|
2891
|
+
? h('input', {
|
|
2892
|
+
className: 'cait-librename',
|
|
2893
|
+
value: renameText,
|
|
2894
|
+
autoFocus: true,
|
|
2895
|
+
onChange: (event) => setRenameText(event.target.value),
|
|
2896
|
+
onKeyDown: (event) => {
|
|
2897
|
+
if (event.key === 'Enter') submitRename(library)
|
|
2898
|
+
if (event.key === 'Escape') setRenamingId('')
|
|
2899
|
+
},
|
|
2900
|
+
onBlur: () => submitRename(library),
|
|
2901
|
+
})
|
|
2902
|
+
: h(
|
|
2903
|
+
'button',
|
|
2904
|
+
{
|
|
2905
|
+
type: 'button',
|
|
2906
|
+
className: 'cait-libname',
|
|
2907
|
+
onClick: () => toggleScope(library[idKey]),
|
|
2908
|
+
title: library[labelKey],
|
|
2909
|
+
},
|
|
2910
|
+
h('span', { className: 'cait-libname-text' }, library[labelKey]),
|
|
2911
|
+
h('span', { className: 'cait-libcount' }, library.entryCount),
|
|
2912
|
+
library.mounted ? h('span', { className: 'cait-libmounted' }, '●') : null,
|
|
2913
|
+
),
|
|
2914
|
+
h(
|
|
2915
|
+
'span',
|
|
2916
|
+
{ className: 'cait-libactions' },
|
|
2917
|
+
h(
|
|
2918
|
+
'button',
|
|
2919
|
+
{
|
|
2920
|
+
type: 'button',
|
|
2921
|
+
className: 'cait-minibtn',
|
|
2922
|
+
title: t('library.rename'),
|
|
2923
|
+
onClick: () => {
|
|
2924
|
+
setRenamingId(library[idKey])
|
|
2925
|
+
setRenameText(library[labelKey])
|
|
2926
|
+
},
|
|
2927
|
+
},
|
|
2928
|
+
h(Icon, { name: 'pencil', size: 12 }),
|
|
2929
|
+
),
|
|
2930
|
+
h(
|
|
2931
|
+
'button',
|
|
2932
|
+
{
|
|
2933
|
+
type: 'button',
|
|
2934
|
+
className: 'cait-deletebtn',
|
|
2935
|
+
title: t('library.deleteLibrary'),
|
|
2936
|
+
disabled: libraries.length <= 1,
|
|
2937
|
+
onClick: () => confirmDeleteLibrary(library),
|
|
2938
|
+
},
|
|
2939
|
+
h(Icon, { name: 'trash', size: 12 }),
|
|
2940
|
+
),
|
|
2941
|
+
),
|
|
2942
|
+
),
|
|
2943
|
+
),
|
|
2944
|
+
h(
|
|
2945
|
+
'div',
|
|
2946
|
+
{ className: 'cait-libnew' },
|
|
2947
|
+
h('input', {
|
|
2948
|
+
value: newLibraryName,
|
|
2949
|
+
placeholder: t('library.newLibrary'),
|
|
2950
|
+
onChange: (event) => setNewLibraryName(event.target.value),
|
|
2951
|
+
onKeyDown: (event) => {
|
|
2952
|
+
if (event.key === 'Enter') submitNewLibrary()
|
|
2953
|
+
},
|
|
2954
|
+
}),
|
|
2955
|
+
h(
|
|
2956
|
+
'button',
|
|
2957
|
+
{ type: 'button', className: 'cait-minibtn', onClick: submitNewLibrary, disabled: !newLibraryName.trim() },
|
|
2958
|
+
h(Icon, { name: 'plus', size: 12 }),
|
|
2959
|
+
),
|
|
2960
|
+
),
|
|
2961
|
+
),
|
|
2962
|
+
h(
|
|
2963
|
+
'div',
|
|
2964
|
+
{ className: 'cait-libmain' },
|
|
2965
|
+
h(
|
|
2966
|
+
'div',
|
|
2967
|
+
{ className: 'cait-libtoolbar' },
|
|
2968
|
+
h(
|
|
2969
|
+
'div',
|
|
2970
|
+
{ className: 'cait-libsearch' },
|
|
2971
|
+
h(Icon, { name: 'search', size: 13 }),
|
|
2972
|
+
h('input', {
|
|
2973
|
+
value: query,
|
|
2974
|
+
onChange: (event) => {
|
|
2975
|
+
setQuery(event.target.value)
|
|
2976
|
+
setPage(1)
|
|
2977
|
+
},
|
|
2978
|
+
placeholder: t('library.search'),
|
|
2979
|
+
'aria-label': t('library.search'),
|
|
2980
|
+
}),
|
|
2981
|
+
),
|
|
2982
|
+
h(
|
|
2983
|
+
'select',
|
|
2984
|
+
{
|
|
2985
|
+
className: 'cait-liblang',
|
|
2986
|
+
value: lang,
|
|
2987
|
+
onChange: (event) => {
|
|
2988
|
+
setLang(event.target.value)
|
|
2989
|
+
setPage(1)
|
|
2990
|
+
},
|
|
2991
|
+
title: t('library.language'),
|
|
2992
|
+
'aria-label': t('library.language'),
|
|
2993
|
+
},
|
|
2994
|
+
h('option', { value: '' }, t('library.language')),
|
|
2995
|
+
allLanguages.map((code) => h('option', { key: code, value: code }, code)),
|
|
2996
|
+
),
|
|
2997
|
+
kind === 'glossary'
|
|
2998
|
+
? h(
|
|
2999
|
+
'select',
|
|
3000
|
+
{
|
|
3001
|
+
className: 'cait-liblang',
|
|
3002
|
+
value: exportFormat,
|
|
3003
|
+
onChange: (event) => setExportFormat(event.target.value),
|
|
3004
|
+
title: t('library.exportFormat'),
|
|
3005
|
+
'aria-label': t('library.exportFormat'),
|
|
3006
|
+
},
|
|
3007
|
+
['tbx', 'csv', 'json'].map((format) => h('option', { key: format, value: format }, format.toUpperCase())),
|
|
3008
|
+
)
|
|
3009
|
+
: null,
|
|
3010
|
+
kind === 'glossary'
|
|
3011
|
+
? h(
|
|
3012
|
+
'button',
|
|
3013
|
+
{
|
|
3014
|
+
type: 'button',
|
|
3015
|
+
className: 'cait-btn',
|
|
3016
|
+
onClick: mineTerms,
|
|
3017
|
+
disabled: Boolean(busy) || working === 'terms' || !data?.activeDocument,
|
|
3018
|
+
title: t('term.extract'),
|
|
3019
|
+
},
|
|
3020
|
+
h(Icon, { name: 'sparkles', size: 13 }),
|
|
3021
|
+
h('span', { className: 'cait-btn-label' }, t('term.extract')),
|
|
3022
|
+
)
|
|
3023
|
+
: null,
|
|
3024
|
+
h(
|
|
3025
|
+
'button',
|
|
3026
|
+
{ type: 'button', className: 'cait-btn', onClick: startAdding, disabled: Boolean(busy) },
|
|
3027
|
+
h(Icon, { name: 'plus', size: 13 }),
|
|
3028
|
+
h('span', { className: 'cait-btn-label' }, t('library.add')),
|
|
3029
|
+
),
|
|
3030
|
+
h(
|
|
3031
|
+
'button',
|
|
3032
|
+
{
|
|
3033
|
+
type: 'button',
|
|
3034
|
+
className: 'cait-btn',
|
|
3035
|
+
onClick: () => pickImport(kind),
|
|
3036
|
+
disabled: Boolean(busy) || !targetLibraryId,
|
|
3037
|
+
title: t('library.import'),
|
|
3038
|
+
},
|
|
3039
|
+
h(Icon, { name: 'upload', size: 13 }),
|
|
3040
|
+
h('span', { className: 'cait-btn-label' }, t('library.import')),
|
|
3041
|
+
),
|
|
3042
|
+
h(
|
|
3043
|
+
'button',
|
|
3044
|
+
{
|
|
3045
|
+
type: 'button',
|
|
3046
|
+
className: 'cait-btn',
|
|
3047
|
+
onClick: exportLibrary,
|
|
3048
|
+
disabled: Boolean(busy) || working === 'export' || !targetLibraryId,
|
|
3049
|
+
title: t('library.export'),
|
|
3050
|
+
},
|
|
3051
|
+
h(Icon, { name: 'download', size: 13 }),
|
|
3052
|
+
h('span', { className: 'cait-btn-label' }, kind === 'tm' ? 'TMX' : exportFormat.toUpperCase()),
|
|
3053
|
+
),
|
|
3054
|
+
h(
|
|
3055
|
+
'button',
|
|
3056
|
+
{
|
|
3057
|
+
type: 'button',
|
|
3058
|
+
className: 'cait-btn',
|
|
3059
|
+
onClick: clearLibrary,
|
|
3060
|
+
disabled: Boolean(busy) || !targetLibraryId,
|
|
3061
|
+
title: t('library.clear'),
|
|
3062
|
+
},
|
|
3063
|
+
h(Icon, { name: 'trash', size: 13 }),
|
|
3064
|
+
),
|
|
3065
|
+
h('input', {
|
|
3066
|
+
ref: fileRef,
|
|
3067
|
+
type: 'file',
|
|
3068
|
+
accept: kind === 'tm' ? '.tmx' : '.tbx,.csv,.json',
|
|
3069
|
+
style: { display: 'none' },
|
|
3070
|
+
onChange: (event) => {
|
|
3071
|
+
const file = event.target.files && event.target.files[0]
|
|
3072
|
+
if (file) runImport(file)
|
|
3073
|
+
},
|
|
3074
|
+
}),
|
|
3075
|
+
),
|
|
3076
|
+
h(
|
|
3077
|
+
'div',
|
|
3078
|
+
{ className: 'cait-libscope' },
|
|
3079
|
+
h(
|
|
3080
|
+
'button',
|
|
3081
|
+
{
|
|
3082
|
+
type: 'button',
|
|
3083
|
+
className: `cait-filterchip${scopeIds.length === 0 ? ' active' : ''}`,
|
|
3084
|
+
onClick: () => {
|
|
3085
|
+
setScopeIds([])
|
|
3086
|
+
setPage(1)
|
|
3087
|
+
},
|
|
3088
|
+
},
|
|
3089
|
+
t('library.scopeAll'),
|
|
3090
|
+
),
|
|
3091
|
+
libraries.map((library) =>
|
|
3092
|
+
h(
|
|
3093
|
+
'button',
|
|
3094
|
+
{
|
|
3095
|
+
key: library[idKey],
|
|
3096
|
+
type: 'button',
|
|
3097
|
+
className: `cait-filterchip${scopeIds.includes(library[idKey]) ? ' active' : ''}`,
|
|
3098
|
+
onClick: () => toggleScope(library[idKey]),
|
|
3099
|
+
},
|
|
3100
|
+
library[labelKey],
|
|
3101
|
+
),
|
|
3102
|
+
),
|
|
3103
|
+
),
|
|
3104
|
+
h(
|
|
3105
|
+
'div',
|
|
3106
|
+
{ className: 'cait-libtable-wrap' },
|
|
3107
|
+
loading && !rows?.entries?.length
|
|
3108
|
+
? h('div', { className: 'cait-center' }, h(Spinner), t('common.loading'))
|
|
3109
|
+
: rows?.entries?.length
|
|
3110
|
+
? h(
|
|
3111
|
+
'table',
|
|
3112
|
+
{ className: 'cait-libtable' },
|
|
3113
|
+
h('thead', null, h('tr', null, columns.map((column) => h('th', { key: column }, column)))),
|
|
3114
|
+
h(
|
|
3115
|
+
'tbody',
|
|
3116
|
+
null,
|
|
3117
|
+
rows.entries.map((entry) =>
|
|
3118
|
+
h(
|
|
3119
|
+
'tr',
|
|
3120
|
+
{
|
|
3121
|
+
key: entry.id,
|
|
3122
|
+
className: entry.id === selectedId ? 'active' : '',
|
|
3123
|
+
onClick: () => {
|
|
3124
|
+
setAdding(false)
|
|
3125
|
+
setSelectedId(entry.id)
|
|
3126
|
+
},
|
|
3127
|
+
},
|
|
3128
|
+
h('td', { className: 'src', title: entry.source }, entry.source),
|
|
3129
|
+
h('td', { className: 'tgt', title: entry.target }, entry.target),
|
|
3130
|
+
h('td', { className: 'note' }, entry.note || ''),
|
|
3131
|
+
kind === 'tm'
|
|
3132
|
+
? h(
|
|
3133
|
+
'td',
|
|
3134
|
+
{ className: 'ctx', title: contextLabel(entry) },
|
|
3135
|
+
contextLabel(entry),
|
|
3136
|
+
)
|
|
3137
|
+
: null,
|
|
3138
|
+
h(
|
|
3139
|
+
'td',
|
|
3140
|
+
{ className: 'lang' },
|
|
3141
|
+
kind === 'tm' ? `${entry.sourceLang} → ${entry.targetLang}` : entry.sourceLang || '—',
|
|
3142
|
+
),
|
|
3143
|
+
kind === 'glossary'
|
|
3144
|
+
? h('td', { className: 'case' }, entry.caseSensitive ? '✓' : '')
|
|
3145
|
+
: null,
|
|
3146
|
+
h('td', { className: 'upd' }, String(entry.updatedAt || '').slice(0, 10)),
|
|
3147
|
+
),
|
|
3148
|
+
),
|
|
3149
|
+
),
|
|
3150
|
+
)
|
|
3151
|
+
: h('div', { className: 'cait-empty-note' }, query || lang ? t('library.noMatch') : t('library.empty')),
|
|
3152
|
+
),
|
|
3153
|
+
h(
|
|
3154
|
+
'div',
|
|
3155
|
+
{ className: 'cait-rail-page cait-libpage-bar' },
|
|
3156
|
+
h(
|
|
3157
|
+
'button',
|
|
3158
|
+
{
|
|
3159
|
+
type: 'button',
|
|
3160
|
+
className: 'cait-pagebtn',
|
|
3161
|
+
onClick: () => setPage(Math.max(1, (rows?.page || 1) - 1)),
|
|
3162
|
+
disabled: (rows?.page || 1) <= 1,
|
|
3163
|
+
title: t('library.prev'),
|
|
3164
|
+
'aria-label': t('library.prev'),
|
|
3165
|
+
},
|
|
3166
|
+
h(Icon, { name: 'chevronLeft', size: 12 }),
|
|
3167
|
+
),
|
|
3168
|
+
h('span', { className: 'cait-pageinfo' }, t('library.pageInfo', { start: bounds.start, end: bounds.end, total: bounds.total })),
|
|
3169
|
+
h(
|
|
3170
|
+
'button',
|
|
3171
|
+
{
|
|
3172
|
+
type: 'button',
|
|
3173
|
+
className: 'cait-pagebtn',
|
|
3174
|
+
onClick: () => setPage((rows?.page || 1) + 1),
|
|
3175
|
+
disabled: (rows?.page || 1) >= (rows?.totalPages || 1),
|
|
3176
|
+
title: t('library.next'),
|
|
3177
|
+
'aria-label': t('library.next'),
|
|
3178
|
+
},
|
|
3179
|
+
h(Icon, { name: 'chevronRight', size: 12 }),
|
|
3180
|
+
),
|
|
3181
|
+
h(
|
|
3182
|
+
'select',
|
|
3183
|
+
{
|
|
3184
|
+
className: 'cait-pagesize',
|
|
3185
|
+
value: String(pageSize),
|
|
3186
|
+
onChange: (event) => changePageSize(Number(event.target.value)),
|
|
3187
|
+
title: t('library.pageSize'),
|
|
3188
|
+
'aria-label': t('library.pageSize'),
|
|
3189
|
+
},
|
|
3190
|
+
PAGE_SIZE_OPTIONS.map((size) => h('option', { key: size, value: String(size) }, String(size))),
|
|
3191
|
+
),
|
|
3192
|
+
),
|
|
3193
|
+
),
|
|
1543
3194
|
h(
|
|
1544
|
-
'
|
|
1545
|
-
{ className: 'cait-
|
|
1546
|
-
|
|
1547
|
-
|
|
3195
|
+
'aside',
|
|
3196
|
+
{ className: 'cait-libdetail' },
|
|
3197
|
+
h('div', { className: 'cait-libdetail-title' }, t('library.detail')),
|
|
3198
|
+
draft
|
|
3199
|
+
? h(
|
|
3200
|
+
'div',
|
|
3201
|
+
{ className: 'cait-libform' },
|
|
3202
|
+
h('label', null, t('library.colSource')),
|
|
3203
|
+
h('textarea', {
|
|
3204
|
+
className: 'cait-libinput',
|
|
3205
|
+
value: draft.source,
|
|
3206
|
+
onChange: (event) => setDraft({ ...draft, source: event.target.value }),
|
|
3207
|
+
}),
|
|
3208
|
+
h('label', null, t('library.colTarget')),
|
|
3209
|
+
h('textarea', {
|
|
3210
|
+
className: 'cait-libinput',
|
|
3211
|
+
value: draft.target,
|
|
3212
|
+
onChange: (event) => setDraft({ ...draft, target: event.target.value }),
|
|
3213
|
+
}),
|
|
3214
|
+
h('label', null, t('library.colNote')),
|
|
3215
|
+
h('input', {
|
|
3216
|
+
className: 'cait-libinput',
|
|
3217
|
+
value: draft.note || '',
|
|
3218
|
+
onChange: (event) => setDraft({ ...draft, note: event.target.value }),
|
|
3219
|
+
}),
|
|
3220
|
+
h('label', null, t('library.colLang')),
|
|
3221
|
+
h('input', {
|
|
3222
|
+
className: 'cait-libinput',
|
|
3223
|
+
value: draft.sourceLang || '',
|
|
3224
|
+
disabled: adding,
|
|
3225
|
+
onChange: (event) => setDraft({ ...draft, sourceLang: event.target.value }),
|
|
3226
|
+
}),
|
|
3227
|
+
kind === 'glossary'
|
|
3228
|
+
? h(
|
|
3229
|
+
'label',
|
|
3230
|
+
{ className: 'cait-libcheck' },
|
|
3231
|
+
h('input', {
|
|
3232
|
+
type: 'checkbox',
|
|
3233
|
+
checked: Boolean(draft.caseSensitive),
|
|
3234
|
+
onChange: (event) => setDraft({ ...draft, caseSensitive: event.target.checked }),
|
|
3235
|
+
}),
|
|
3236
|
+
h('span', null, t('library.colCase')),
|
|
3237
|
+
)
|
|
3238
|
+
: null,
|
|
3239
|
+
kind === 'glossary' ? h('p', { className: 'cait-libnote' }, t('library.caseSensitiveHint')) : null,
|
|
3240
|
+
h(
|
|
3241
|
+
'div',
|
|
3242
|
+
{ className: 'cait-libform-actions' },
|
|
3243
|
+
h(
|
|
3244
|
+
'button',
|
|
3245
|
+
{
|
|
3246
|
+
type: 'button',
|
|
3247
|
+
className: 'cait-btn cait-btn-primary',
|
|
3248
|
+
onClick: adding ? addEntry : saveEntry,
|
|
3249
|
+
disabled: Boolean(busy) || working !== '' || !draft.source.trim() || !draft.target.trim(),
|
|
3250
|
+
},
|
|
3251
|
+
adding ? t('library.add') : t('library.save'),
|
|
3252
|
+
),
|
|
3253
|
+
adding
|
|
3254
|
+
? h(
|
|
3255
|
+
'button',
|
|
3256
|
+
{
|
|
3257
|
+
type: 'button',
|
|
3258
|
+
className: 'cait-btn',
|
|
3259
|
+
onClick: () => {
|
|
3260
|
+
setAdding(false)
|
|
3261
|
+
setDraft(null)
|
|
3262
|
+
},
|
|
3263
|
+
},
|
|
3264
|
+
t('panel.cancel'),
|
|
3265
|
+
)
|
|
3266
|
+
: h(
|
|
3267
|
+
'button',
|
|
3268
|
+
{
|
|
3269
|
+
type: 'button',
|
|
3270
|
+
className: 'cait-btn',
|
|
3271
|
+
onClick: deleteEntry,
|
|
3272
|
+
disabled: Boolean(busy) || working !== '',
|
|
3273
|
+
},
|
|
3274
|
+
h(Icon, { name: 'trash', size: 13 }),
|
|
3275
|
+
t('library.deleteEntry'),
|
|
3276
|
+
),
|
|
3277
|
+
),
|
|
3278
|
+
dirty ? h('p', { className: 'cait-libnote' }, t('editor.unsaved')) : null,
|
|
3279
|
+
)
|
|
3280
|
+
: h('div', { className: 'cait-empty-note' }, t('library.detailEmpty')),
|
|
1548
3281
|
),
|
|
1549
3282
|
),
|
|
3283
|
+
terms
|
|
3284
|
+
? h(
|
|
3285
|
+
'div',
|
|
3286
|
+
{ className: 'cait-termpanel' },
|
|
3287
|
+
h(
|
|
3288
|
+
'div',
|
|
3289
|
+
{ className: 'cait-termpanel-head' },
|
|
3290
|
+
h('strong', null, t('term.title')),
|
|
3291
|
+
h('span', { className: 'cait-mt-hint' }, t('term.hint')),
|
|
3292
|
+
h('span', { className: 'cait-termpanel-actions' },
|
|
3293
|
+
h(
|
|
3294
|
+
'button',
|
|
3295
|
+
{ type: 'button', className: 'cait-btn cait-btn-primary', onClick: addMinedTerms, disabled: working === 'terms' },
|
|
3296
|
+
h(Icon, { name: 'check', size: 13 }),
|
|
3297
|
+
t('term.add'),
|
|
3298
|
+
),
|
|
3299
|
+
h('button', { type: 'button', className: 'cait-btn', onClick: () => setTerms(null) }, t('panel.cancel')),
|
|
3300
|
+
),
|
|
3301
|
+
),
|
|
3302
|
+
terms.length
|
|
3303
|
+
? h(
|
|
3304
|
+
'div',
|
|
3305
|
+
{ className: 'cait-termlist' },
|
|
3306
|
+
terms.map((candidate, at) =>
|
|
3307
|
+
h(
|
|
3308
|
+
'div',
|
|
3309
|
+
{ key: `${candidate.term}-${at}`, className: `cait-termrow${candidate.picked ? ' picked' : ''}` },
|
|
3310
|
+
h('input', {
|
|
3311
|
+
type: 'checkbox',
|
|
3312
|
+
checked: Boolean(candidate.picked),
|
|
3313
|
+
onChange: () => toggleTerm(at),
|
|
3314
|
+
}),
|
|
3315
|
+
h('span', { className: 'cait-termname' }, candidate.term),
|
|
3316
|
+
h('span', { className: 'cait-termcount' }, `×${candidate.count}`),
|
|
3317
|
+
candidate.inLibrary ? h('span', { className: 'cait-originchip' }, t('term.inLibrary')) : null,
|
|
3318
|
+
h('input', {
|
|
3319
|
+
className: 'cait-libinput cait-termtarget',
|
|
3320
|
+
placeholder: t('library.colTarget'),
|
|
3321
|
+
value: candidate.target,
|
|
3322
|
+
onChange: (event) => setTermTarget(at, event.target.value),
|
|
3323
|
+
}),
|
|
3324
|
+
),
|
|
3325
|
+
),
|
|
3326
|
+
)
|
|
3327
|
+
: h('div', { className: 'cait-empty-note' }, t('term.empty')),
|
|
3328
|
+
)
|
|
3329
|
+
: null,
|
|
1550
3330
|
)
|
|
1551
3331
|
}
|
|
1552
3332
|
|
|
1553
|
-
function
|
|
1554
|
-
|
|
1555
|
-
Modal,
|
|
1556
|
-
{ title: t('modal.new.title'), onClose },
|
|
1557
|
-
h(DocumentSourceForm, { t, settings, busy, submitLabel: t('modal.new.create'), onSubmit: onCreate, defaultMode }),
|
|
1558
|
-
h(
|
|
1559
|
-
'div',
|
|
1560
|
-
{ className: 'cait-demo-row' },
|
|
1561
|
-
h('span', null, t('modal.new.sample')),
|
|
1562
|
-
h('button', { className: 'cait-linkbtn', onClick: () => onCreate({ demo: true }), disabled: Boolean(busy) }, t('modal.new.demo')),
|
|
1563
|
-
),
|
|
1564
|
-
)
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
function ExportModal({ t, busy, onClose, onExport }) {
|
|
3333
|
+
function ExportModal({ t, busy, onClose, onExport, qa }) {
|
|
3334
|
+
const issues = qa?.issueCount || 0
|
|
1568
3335
|
const items = [
|
|
1569
3336
|
['bilingual-md', t('modal.export.bilingualMd'), t('modal.export.bilingual'), 'file'],
|
|
1570
3337
|
['bilingual-csv', t('modal.export.bilingualCsv'), t('modal.export.bilingual'), 'file'],
|
|
@@ -1590,6 +3357,23 @@ window.__ModuleLoader__.load({
|
|
|
1590
3357
|
Modal,
|
|
1591
3358
|
{ title: t('modal.export.title'), onClose, wide: true },
|
|
1592
3359
|
h('p', { className: 'cait-confirm-text' }, t('modal.export.desc')),
|
|
3360
|
+
issues
|
|
3361
|
+
? h(
|
|
3362
|
+
'div',
|
|
3363
|
+
{ className: 'cait-qa-banner' },
|
|
3364
|
+
h(Icon, { name: 'alert', size: 12 }),
|
|
3365
|
+
h(
|
|
3366
|
+
'span',
|
|
3367
|
+
null,
|
|
3368
|
+
`${t('qa.exportBanner').replace('{count}', String(issues))} `,
|
|
3369
|
+
t('qa.counts')
|
|
3370
|
+
.replace('{placeholder}', String(qa.counts.PLACEHOLDER_MISSING))
|
|
3371
|
+
.replace('{number}', String(qa.counts.NUMBER_MISMATCH))
|
|
3372
|
+
.replace('{term}', String(qa.counts.TERM_NOT_USED))
|
|
3373
|
+
.replace('{empty}', String(qa.counts.EMPTY_TARGET)),
|
|
3374
|
+
),
|
|
3375
|
+
)
|
|
3376
|
+
: null,
|
|
1593
3377
|
h(
|
|
1594
3378
|
'div',
|
|
1595
3379
|
{ className: 'cait-export-grid' },
|
|
@@ -1792,7 +3576,13 @@ window.__ModuleLoader__.load({
|
|
|
1792
3576
|
},
|
|
1793
3577
|
h('option', { value: 'mock' }, t('mt.mock')),
|
|
1794
3578
|
routes.map((item) =>
|
|
1795
|
-
h(
|
|
3579
|
+
h(
|
|
3580
|
+
'option',
|
|
3581
|
+
{ key: item.id, value: item.id, disabled: item.hasModels === false },
|
|
3582
|
+
item.hasModels === false
|
|
3583
|
+
? `${item.name} (${item.id}) · ${t('modal.settings.noModels')}`
|
|
3584
|
+
: `${item.name} (${item.id})`,
|
|
3585
|
+
),
|
|
1796
3586
|
),
|
|
1797
3587
|
routeMissing
|
|
1798
3588
|
? h('option', { value: form.mt.provider }, t('modal.settings.routeMissing'))
|
|
@@ -1857,139 +3647,6 @@ window.__ModuleLoader__.load({
|
|
|
1857
3647
|
)
|
|
1858
3648
|
}
|
|
1859
3649
|
|
|
1860
|
-
function LibrariesModal({ t, data, busy, onClose, onSetMounts, onCreate }) {
|
|
1861
|
-
const libraries = data.libraries || { tm: [], glossary: [] }
|
|
1862
|
-
const mounts = data.mounts || {
|
|
1863
|
-
tmIds: [],
|
|
1864
|
-
glossaryIds: [],
|
|
1865
|
-
defaultTmId: '',
|
|
1866
|
-
defaultGlossaryId: '',
|
|
1867
|
-
}
|
|
1868
|
-
const [tmName, setTmName] = useState('')
|
|
1869
|
-
const [glossaryName, setGlossaryName] = useState('')
|
|
1870
|
-
|
|
1871
|
-
const toggle = (kind, id, checked) => {
|
|
1872
|
-
const current = kind === 'tm' ? mounts.tmIds : mounts.glossaryIds
|
|
1873
|
-
const next = computeNextMountIds(current, id, checked)
|
|
1874
|
-
if (!next) return
|
|
1875
|
-
onSetMounts(kind === 'tm' ? { tmIds: next } : { glossaryIds: next })
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
const create = async (kind, name, reset) => {
|
|
1879
|
-
if (!name.trim()) return
|
|
1880
|
-
const ok = await onCreate({ kind, name: name.trim() })
|
|
1881
|
-
if (ok) reset('')
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
const section = (kind, title, list, defaultId, ids) =>
|
|
1885
|
-
h(
|
|
1886
|
-
'div',
|
|
1887
|
-
{ className: 'cait-libsection' },
|
|
1888
|
-
h(
|
|
1889
|
-
'div',
|
|
1890
|
-
{ className: 'cait-libhead' },
|
|
1891
|
-
h('h4', { className: 'cait-settings-title' }, title),
|
|
1892
|
-
h('span', { className: 'cait-count' }, `${ids.length}/${list.length}`),
|
|
1893
|
-
),
|
|
1894
|
-
list.length
|
|
1895
|
-
? list.map((library) => {
|
|
1896
|
-
const id = kind === 'tm' ? library.tmId : library.glossaryId
|
|
1897
|
-
const mounted = ids.includes(id)
|
|
1898
|
-
return h(
|
|
1899
|
-
'div',
|
|
1900
|
-
{ className: 'cait-librow', key: id },
|
|
1901
|
-
h(
|
|
1902
|
-
'label',
|
|
1903
|
-
{ className: 'cait-libcheck' },
|
|
1904
|
-
h('input', {
|
|
1905
|
-
type: 'checkbox',
|
|
1906
|
-
checked: mounted,
|
|
1907
|
-
disabled: isMountToggleDisabled({
|
|
1908
|
-
mounted,
|
|
1909
|
-
mountedCount: ids.length,
|
|
1910
|
-
busy: Boolean(busy),
|
|
1911
|
-
}),
|
|
1912
|
-
onChange: (event) => toggle(kind, id, event.target.checked),
|
|
1913
|
-
}),
|
|
1914
|
-
h(
|
|
1915
|
-
'span',
|
|
1916
|
-
{ className: 'cait-libmeta' },
|
|
1917
|
-
h('strong', null, library.name),
|
|
1918
|
-
h(
|
|
1919
|
-
'small',
|
|
1920
|
-
null,
|
|
1921
|
-
`${library.entryCount || 0} ${t('libraries.entries')}${
|
|
1922
|
-
library.mounted ? '' : ` · ${t('libraries.notMounted')}`
|
|
1923
|
-
}`,
|
|
1924
|
-
),
|
|
1925
|
-
),
|
|
1926
|
-
),
|
|
1927
|
-
h(
|
|
1928
|
-
'label',
|
|
1929
|
-
{ className: 'cait-libdefault' },
|
|
1930
|
-
h('input', {
|
|
1931
|
-
type: 'radio',
|
|
1932
|
-
name: `cait-default-${kind}`,
|
|
1933
|
-
checked: defaultId === id,
|
|
1934
|
-
disabled: Boolean(busy) || !mounted,
|
|
1935
|
-
onChange: () =>
|
|
1936
|
-
onSetMounts(
|
|
1937
|
-
kind === 'tm'
|
|
1938
|
-
? { defaultTmId: id }
|
|
1939
|
-
: { defaultGlossaryId: id },
|
|
1940
|
-
),
|
|
1941
|
-
}),
|
|
1942
|
-
t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
|
|
1943
|
-
),
|
|
1944
|
-
)
|
|
1945
|
-
})
|
|
1946
|
-
: h('p', { className: 'cait-empty-note' }, t('libraries.empty')),
|
|
1947
|
-
h(
|
|
1948
|
-
'form',
|
|
1949
|
-
{
|
|
1950
|
-
className: 'cait-libcreate',
|
|
1951
|
-
onSubmit: (event) => {
|
|
1952
|
-
event.preventDefault()
|
|
1953
|
-
create(kind, kind === 'tm' ? tmName : glossaryName, kind === 'tm' ? setTmName : setGlossaryName)
|
|
1954
|
-
},
|
|
1955
|
-
},
|
|
1956
|
-
h('input', {
|
|
1957
|
-
className: 'cait-input',
|
|
1958
|
-
value: kind === 'tm' ? tmName : glossaryName,
|
|
1959
|
-
onChange: (event) =>
|
|
1960
|
-
kind === 'tm' ? setTmName(event.target.value) : setGlossaryName(event.target.value),
|
|
1961
|
-
placeholder: t('libraries.namePlaceholder'),
|
|
1962
|
-
disabled: Boolean(busy),
|
|
1963
|
-
}),
|
|
1964
|
-
h(
|
|
1965
|
-
'button',
|
|
1966
|
-
{ className: 'cait-btn', type: 'submit', disabled: Boolean(busy) },
|
|
1967
|
-
h(Icon, { name: 'plus', size: 12 }),
|
|
1968
|
-
t('libraries.create'),
|
|
1969
|
-
),
|
|
1970
|
-
),
|
|
1971
|
-
)
|
|
1972
|
-
|
|
1973
|
-
return h(
|
|
1974
|
-
Modal,
|
|
1975
|
-
{ title: t('libraries.title'), onClose, wide: true },
|
|
1976
|
-
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('libraries.desc')),
|
|
1977
|
-
section('tm', t('libraries.tm'), libraries.tm, mounts.defaultTmId, mounts.tmIds),
|
|
1978
|
-
section(
|
|
1979
|
-
'glossary',
|
|
1980
|
-
t('libraries.glossary'),
|
|
1981
|
-
libraries.glossary,
|
|
1982
|
-
mounts.defaultGlossaryId,
|
|
1983
|
-
mounts.glossaryIds,
|
|
1984
|
-
),
|
|
1985
|
-
h(
|
|
1986
|
-
'div',
|
|
1987
|
-
{ className: 'cait-modal-actions' },
|
|
1988
|
-
h('button', { className: 'cait-btn cait-btn-primary', onClick: onClose }, t('libraries.done')),
|
|
1989
|
-
),
|
|
1990
|
-
)
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
3650
|
function ConfirmModal({ t, state, busy, onClose, onConfirm }) {
|
|
1994
3651
|
return h(
|
|
1995
3652
|
Modal,
|
|
@@ -2042,8 +3699,22 @@ window.__ModuleLoader__.load({
|
|
|
2042
3699
|
const [dirty, setDirty] = useState(false)
|
|
2043
3700
|
const [tab, setTab] = useState('tm')
|
|
2044
3701
|
const [railQuery, setRailQuery] = useState('')
|
|
3702
|
+
const [railFilter, setRailFilter] = useState('all')
|
|
3703
|
+
// Two top-level workspaces: the translator's document and the library
|
|
3704
|
+
// manager. The latter needs no document, so it must not be a modal.
|
|
3705
|
+
const [view, setView] = useState('documents')
|
|
3706
|
+
// A seed carries the panel's search text into the library workspace.
|
|
3707
|
+
const [libSeed, setLibSeed] = useState(null)
|
|
3708
|
+
|
|
3709
|
+
const openLibrary = (kind, query) => {
|
|
3710
|
+
setLibSeed({ kind: kind === 'glossary' ? 'glossary' : 'tm', query: query || '', nonce: Date.now() })
|
|
3711
|
+
setView('libraries')
|
|
3712
|
+
}
|
|
3713
|
+
const [pre, setPre] = useState({ running: false, done: 0, total: 0, filled: 0, failed: 0 })
|
|
3714
|
+
const [qa, setQa] = useState(null)
|
|
2045
3715
|
const [tmQuery, setTmQuery] = useState('')
|
|
2046
3716
|
const [tmResults, setTmResults] = useState(null)
|
|
3717
|
+
const [tmRefresh, setTmRefresh] = useState(0)
|
|
2047
3718
|
const [tmError, setTmError] = useState('')
|
|
2048
3719
|
const [mt, setMt] = useState(null)
|
|
2049
3720
|
const [mtError, setMtError] = useState('')
|
|
@@ -2158,6 +3829,61 @@ window.__ModuleLoader__.load({
|
|
|
2158
3829
|
}
|
|
2159
3830
|
}
|
|
2160
3831
|
|
|
3832
|
+
const preCancelRef = useRef(false)
|
|
3833
|
+
|
|
3834
|
+
// The client drives pre-translation block by block instead of asking the
|
|
3835
|
+
// host for a background job: a block is one RPC, cancellation is simply
|
|
3836
|
+
// "stop looping", and every finished block is already persisted.
|
|
3837
|
+
const runPreTranslate = async () => {
|
|
3838
|
+
const active = data?.activeDocument
|
|
3839
|
+
if (!active || pre.running) return
|
|
3840
|
+
preCancelRef.current = false
|
|
3841
|
+
const initial = active.progress || { confirmed: 0, translated: 0, total: 0 }
|
|
3842
|
+
setPre({
|
|
3843
|
+
running: true,
|
|
3844
|
+
done: initial.confirmed + initial.translated,
|
|
3845
|
+
total: initial.total,
|
|
3846
|
+
filled: 0,
|
|
3847
|
+
failed: 0,
|
|
3848
|
+
})
|
|
3849
|
+
let from = 0
|
|
3850
|
+
let filled = 0
|
|
3851
|
+
let failed = 0
|
|
3852
|
+
try {
|
|
3853
|
+
while (from !== null) {
|
|
3854
|
+
if (preCancelRef.current) break
|
|
3855
|
+
const result = await rpc('preTranslate', {
|
|
3856
|
+
documentId: active.id,
|
|
3857
|
+
from,
|
|
3858
|
+
count: PRETRANSLATE_BLOCK_SIZE,
|
|
3859
|
+
})
|
|
3860
|
+
filled += result.filled
|
|
3861
|
+
failed += result.failed.length
|
|
3862
|
+
from = result.next
|
|
3863
|
+
const progress = result.progress
|
|
3864
|
+
setPre((current) => ({
|
|
3865
|
+
...current,
|
|
3866
|
+
done: progress.confirmed + progress.translated,
|
|
3867
|
+
total: progress.total,
|
|
3868
|
+
filled,
|
|
3869
|
+
failed,
|
|
3870
|
+
}))
|
|
3871
|
+
await refreshState()
|
|
3872
|
+
}
|
|
3873
|
+
if (preCancelRef.current) notify(t('toast.preCancelled'))
|
|
3874
|
+
else if (failed) notify(t('toast.preFailed').replace('{count}', String(failed)))
|
|
3875
|
+
else notify(t('toast.preDone').replace('{count}', String(filled)))
|
|
3876
|
+
} catch (error) {
|
|
3877
|
+
report(error)
|
|
3878
|
+
} finally {
|
|
3879
|
+
setPre((current) => ({ ...current, running: false }))
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
const cancelPreTranslate = () => {
|
|
3884
|
+
preCancelRef.current = true
|
|
3885
|
+
}
|
|
3886
|
+
|
|
2161
3887
|
useEffect(() => {
|
|
2162
3888
|
let alive = true
|
|
2163
3889
|
setLoading(true)
|
|
@@ -2214,6 +3940,26 @@ window.__ModuleLoader__.load({
|
|
|
2214
3940
|
}
|
|
2215
3941
|
}, [data?.revision])
|
|
2216
3942
|
|
|
3943
|
+
useEffect(() => {
|
|
3944
|
+
const id = data?.activeDocument?.id
|
|
3945
|
+
if (!id) {
|
|
3946
|
+
setQa(null)
|
|
3947
|
+
return
|
|
3948
|
+
}
|
|
3949
|
+
let alive = true
|
|
3950
|
+
rpc('getQaReport', { documentId: id })
|
|
3951
|
+
.then((report) => {
|
|
3952
|
+
if (alive) setQa(report)
|
|
3953
|
+
})
|
|
3954
|
+
.catch(() => {
|
|
3955
|
+
// QA is advisory: a failure must never disturb the workbench.
|
|
3956
|
+
if (alive) setQa(null)
|
|
3957
|
+
})
|
|
3958
|
+
return () => {
|
|
3959
|
+
alive = false
|
|
3960
|
+
}
|
|
3961
|
+
}, [data?.activeDocument?.id, data?.revision])
|
|
3962
|
+
|
|
2217
3963
|
useEffect(() => {
|
|
2218
3964
|
let alive = true
|
|
2219
3965
|
rpc('listMtRoutes')
|
|
@@ -2271,7 +4017,7 @@ window.__ModuleLoader__.load({
|
|
|
2271
4017
|
cancelled = true
|
|
2272
4018
|
clearTimeout(timer)
|
|
2273
4019
|
}
|
|
2274
|
-
}, [tmQuery, doc?.id, doc?.sourceLang, doc?.targetLang, settings.fuzzyThreshold, settings.tmLimit])
|
|
4020
|
+
}, [tmQuery, tmRefresh, doc?.id, doc?.sourceLang, doc?.targetLang, settings.fuzzyThreshold, settings.tmLimit])
|
|
2275
4021
|
|
|
2276
4022
|
const saveDraft = async (silent = true) => {
|
|
2277
4023
|
if (!doc || !dirty) return true
|
|
@@ -2328,6 +4074,24 @@ window.__ModuleLoader__.load({
|
|
|
2328
4074
|
}
|
|
2329
4075
|
}
|
|
2330
4076
|
|
|
4077
|
+
const adoptSuggestion = async () => {
|
|
4078
|
+
if (!doc || busy || !segment?.suggestion) return
|
|
4079
|
+
if (dirty) {
|
|
4080
|
+
const saved = await saveDraft(true)
|
|
4081
|
+
if (!saved) return
|
|
4082
|
+
}
|
|
4083
|
+
setBusy('adopt')
|
|
4084
|
+
try {
|
|
4085
|
+
const result = await rpc('adoptSuggestion', { documentId: doc.id, index: selected })
|
|
4086
|
+
adoptState(result.state)
|
|
4087
|
+
notify(t('toast.suggestionAdopted'))
|
|
4088
|
+
} catch (error) {
|
|
4089
|
+
report(error)
|
|
4090
|
+
} finally {
|
|
4091
|
+
setBusy('')
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
|
|
2331
4095
|
const moveSegment = async (delta) => {
|
|
2332
4096
|
if (!doc || busy) return
|
|
2333
4097
|
setBusy('move')
|
|
@@ -2448,8 +4212,72 @@ window.__ModuleLoader__.load({
|
|
|
2448
4212
|
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
2449
4213
|
}
|
|
2450
4214
|
|
|
2451
|
-
const
|
|
2452
|
-
setBusy('
|
|
4215
|
+
const editTm = async ({ id, target }) => {
|
|
4216
|
+
setBusy('tm-edit')
|
|
4217
|
+
try {
|
|
4218
|
+
const result = await rpc('updateTM', { id, target })
|
|
4219
|
+
setData(result.state)
|
|
4220
|
+
setTmRefresh((value) => value + 1)
|
|
4221
|
+
notify(t('toast.tmUpdated'))
|
|
4222
|
+
return true
|
|
4223
|
+
} catch (error) {
|
|
4224
|
+
report(error)
|
|
4225
|
+
return false
|
|
4226
|
+
} finally {
|
|
4227
|
+
setBusy('')
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
|
|
4231
|
+
const deleteTm = (match) => {
|
|
4232
|
+
setModal({
|
|
4233
|
+
type: 'confirm',
|
|
4234
|
+
title: t('modal.confirm.title'),
|
|
4235
|
+
message: `${t('modal.confirm.deleteTm')} “${match.source} → ${match.target}”?`,
|
|
4236
|
+
confirmLabel: t('panel.delete'),
|
|
4237
|
+
action: async () => {
|
|
4238
|
+
const result = await rpc('deleteTM', { id: match.id })
|
|
4239
|
+
setData(result.state)
|
|
4240
|
+
setTmRefresh((value) => value + 1)
|
|
4241
|
+
notify(t('toast.tmDeleted'))
|
|
4242
|
+
},
|
|
4243
|
+
})
|
|
4244
|
+
}
|
|
4245
|
+
|
|
4246
|
+
const confirmAllTranslated = () => {
|
|
4247
|
+
const active = data?.activeDocument
|
|
4248
|
+
const awaiting = active?.progress?.translated || 0
|
|
4249
|
+
if (!active || !awaiting) return
|
|
4250
|
+
setModal({
|
|
4251
|
+
type: 'confirm',
|
|
4252
|
+
title: t('rail.confirmAll'),
|
|
4253
|
+
message: t('rail.confirmAllMessage').replace('{count}', String(awaiting)),
|
|
4254
|
+
confirmLabel: t('rail.confirmAll'),
|
|
4255
|
+
action: async () => {
|
|
4256
|
+
const result = await rpc('confirmSegments', { documentId: active.id, status: 'translated' })
|
|
4257
|
+
await refreshState()
|
|
4258
|
+
setTmRefresh((value) => value + 1)
|
|
4259
|
+
notify(t('toast.confirmedMany').replace('{count}', String(result.confirmed.length)))
|
|
4260
|
+
},
|
|
4261
|
+
})
|
|
4262
|
+
}
|
|
4263
|
+
|
|
4264
|
+
const propagateRepeats = () => {
|
|
4265
|
+
const active = data?.activeDocument
|
|
4266
|
+
if (!active) return
|
|
4267
|
+
setModal({
|
|
4268
|
+
type: 'confirm',
|
|
4269
|
+
title: t('rail.propagate'),
|
|
4270
|
+
message: t('rail.propagateMessage'),
|
|
4271
|
+
confirmLabel: t('rail.propagate'),
|
|
4272
|
+
action: async () => {
|
|
4273
|
+
const result = await rpc('propagateDocument', { documentId: active.id })
|
|
4274
|
+
await refreshState()
|
|
4275
|
+
notify(t('toast.propagated').replace('{count}', String(result.filled)))
|
|
4276
|
+
},
|
|
4277
|
+
})
|
|
4278
|
+
}
|
|
4279
|
+
|
|
4280
|
+
const addTerm = async (form) => { setBusy('term')
|
|
2453
4281
|
try {
|
|
2454
4282
|
const result = await rpc('addGlossary', {
|
|
2455
4283
|
...form,
|
|
@@ -2589,6 +4417,37 @@ window.__ModuleLoader__.load({
|
|
|
2589
4417
|
}
|
|
2590
4418
|
}
|
|
2591
4419
|
|
|
4420
|
+
const renameLibrary = async ({ kind, id, name }) => {
|
|
4421
|
+
setBusy('library')
|
|
4422
|
+
try {
|
|
4423
|
+
const result = await rpc('renameLibrary', { kind, id, name })
|
|
4424
|
+
setData(result.state)
|
|
4425
|
+
notify(t('libraries.renamed'))
|
|
4426
|
+
return true
|
|
4427
|
+
} catch (error) {
|
|
4428
|
+
report(error)
|
|
4429
|
+
return false
|
|
4430
|
+
} finally {
|
|
4431
|
+
setBusy('')
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
const deleteLibrary = ({ kind, id, name }) => {
|
|
4436
|
+
const entries = kind === 'tm' ? t('libraries.deleteTmWarn') : t('libraries.deleteGlossaryWarn')
|
|
4437
|
+
setModal({
|
|
4438
|
+
type: 'confirm',
|
|
4439
|
+
title: t('modal.confirm.title'),
|
|
4440
|
+
message: `${t('libraries.delete')} “${name}”?${entries}`,
|
|
4441
|
+
confirmLabel: t('libraries.delete'),
|
|
4442
|
+
action: async () => {
|
|
4443
|
+
const result = await rpc('deleteLibrary', { kind, id })
|
|
4444
|
+
setData(result.state)
|
|
4445
|
+
setTmResults(null)
|
|
4446
|
+
notify(t('libraries.deleted'))
|
|
4447
|
+
},
|
|
4448
|
+
})
|
|
4449
|
+
}
|
|
4450
|
+
|
|
2592
4451
|
const exportAs = async (format) => {
|
|
2593
4452
|
if (!doc) return
|
|
2594
4453
|
setBusy('export')
|
|
@@ -2704,13 +4563,40 @@ window.__ModuleLoader__.load({
|
|
|
2704
4563
|
element.style.setProperty(CAIT_UNIT_VAR, String(unit))
|
|
2705
4564
|
}
|
|
2706
4565
|
measure()
|
|
4566
|
+
|
|
4567
|
+
// Mirror the host theme so native widgets follow `color-scheme`. DSH
|
|
4568
|
+
// toggles the marker on <body>, so observe that instead of guessing
|
|
4569
|
+
// from colours.
|
|
4570
|
+
const syncTheme = () => {
|
|
4571
|
+
const kind = detectThemeKind(document)
|
|
4572
|
+
if (element.getAttribute(CAIT_THEME_ATTR) !== kind) element.setAttribute(CAIT_THEME_ATTR, kind)
|
|
4573
|
+
}
|
|
4574
|
+
syncTheme()
|
|
4575
|
+
let themeObserver = null
|
|
4576
|
+
if (typeof MutationObserver !== 'undefined' && document.body) {
|
|
4577
|
+
themeObserver = new MutationObserver(syncTheme)
|
|
4578
|
+
themeObserver.observe(document.body, {
|
|
4579
|
+
attributes: true,
|
|
4580
|
+
attributeFilter: ['data-ds-dark-theme', 'data-theme', 'class'],
|
|
4581
|
+
})
|
|
4582
|
+
}
|
|
4583
|
+
const stopTheme = () => {
|
|
4584
|
+
if (themeObserver) themeObserver.disconnect()
|
|
4585
|
+
}
|
|
4586
|
+
|
|
2707
4587
|
if (typeof ResizeObserver === 'undefined') {
|
|
2708
4588
|
window.addEventListener('resize', measure)
|
|
2709
|
-
return () =>
|
|
4589
|
+
return () => {
|
|
4590
|
+
window.removeEventListener('resize', measure)
|
|
4591
|
+
stopTheme()
|
|
4592
|
+
}
|
|
2710
4593
|
}
|
|
2711
4594
|
const observer = new ResizeObserver(measure)
|
|
2712
4595
|
observer.observe(element)
|
|
2713
|
-
return () =>
|
|
4596
|
+
return () => {
|
|
4597
|
+
observer.disconnect()
|
|
4598
|
+
stopTheme()
|
|
4599
|
+
}
|
|
2714
4600
|
}, [])
|
|
2715
4601
|
|
|
2716
4602
|
if (loading) {
|
|
@@ -2757,12 +4643,45 @@ window.__ModuleLoader__.load({
|
|
|
2757
4643
|
onCloseDoc: closeDocumentTab,
|
|
2758
4644
|
onNewTab: () => setModal({ type: 'new', defaultMode: 'file' }),
|
|
2759
4645
|
onCleanClosed: () => setModal({ type: 'recent' }),
|
|
4646
|
+
pre,
|
|
4647
|
+
onPreTranslate: runPreTranslate,
|
|
4648
|
+
onCancelPreTranslate: cancelPreTranslate,
|
|
4649
|
+
view,
|
|
4650
|
+
onView: setView,
|
|
2760
4651
|
}),
|
|
2761
4652
|
h(SaveBanner, { t, storage: data?.storage, stale, onRefresh: refreshState }),
|
|
2762
|
-
|
|
4653
|
+
view === 'libraries'
|
|
4654
|
+
? h(LibraryPage, {
|
|
4655
|
+
t,
|
|
4656
|
+
data,
|
|
4657
|
+
settings,
|
|
4658
|
+
busy,
|
|
4659
|
+
notify,
|
|
4660
|
+
report,
|
|
4661
|
+
seed: libSeed,
|
|
4662
|
+
onApplyState: (state) => setData(state),
|
|
4663
|
+
onConfirm: openConfirm,
|
|
4664
|
+
onCreateLibrary: createLibrary,
|
|
4665
|
+
onRenameLibrary: renameLibrary,
|
|
4666
|
+
onDeleteLibrary: deleteLibrary,
|
|
4667
|
+
})
|
|
4668
|
+
: null,
|
|
4669
|
+
view === 'align'
|
|
4670
|
+
? h(AlignPage, {
|
|
4671
|
+
t,
|
|
4672
|
+
data,
|
|
4673
|
+
busy,
|
|
4674
|
+
notify,
|
|
4675
|
+
report,
|
|
4676
|
+
onApplyState: (state) => setData(state),
|
|
4677
|
+
onConfirm: openConfirm,
|
|
4678
|
+
})
|
|
4679
|
+
: null,
|
|
4680
|
+
view === 'documents'
|
|
4681
|
+
? h(
|
|
2763
4682
|
'div',
|
|
2764
4683
|
{ className: `cait-body${doc ? '' : ' cait-no-doc'}` },
|
|
2765
|
-
doc ? h(SegmentRail, { t, doc, selected: index, query: railQuery, setQuery: setRailQuery, onSelect: chooseSegment }) : null,
|
|
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,
|
|
2766
4685
|
h(
|
|
2767
4686
|
'div',
|
|
2768
4687
|
{ className: `cait-main${doc ? '' : ' cait-main-empty'}` },
|
|
@@ -2784,6 +4703,7 @@ window.__ModuleLoader__.load({
|
|
|
2784
4703
|
onNext: () => moveSegment(1),
|
|
2785
4704
|
onPrev: () => moveSegment(-1),
|
|
2786
4705
|
onSave: () => saveDraft(false),
|
|
4706
|
+
onAdopt: adoptSuggestion,
|
|
2787
4707
|
onApplyTerm: applyTerm,
|
|
2788
4708
|
textareaRef,
|
|
2789
4709
|
})
|
|
@@ -2802,6 +4722,8 @@ window.__ModuleLoader__.load({
|
|
|
2802
4722
|
tmResults,
|
|
2803
4723
|
tmError,
|
|
2804
4724
|
onApplyTm: applyTm,
|
|
4725
|
+
onEditTm: editTm,
|
|
4726
|
+
onDeleteTm: deleteTm,
|
|
2805
4727
|
onImportTmx: importTmxFile,
|
|
2806
4728
|
onClearTm: clearTm,
|
|
2807
4729
|
onAddTerm: addTerm,
|
|
@@ -2822,12 +4744,13 @@ window.__ModuleLoader__.load({
|
|
|
2822
4744
|
},
|
|
2823
4745
|
onCopy: copy,
|
|
2824
4746
|
onOpenSettings: () => setModal({ type: 'settings' }),
|
|
2825
|
-
|
|
4747
|
+
onSearchLibrary: openLibrary,
|
|
2826
4748
|
busy,
|
|
2827
4749
|
})
|
|
2828
4750
|
: null,
|
|
2829
4751
|
),
|
|
2830
|
-
)
|
|
4752
|
+
)
|
|
4753
|
+
: null,
|
|
2831
4754
|
modal?.type === 'new'
|
|
2832
4755
|
? h(NewDocModal, {
|
|
2833
4756
|
t,
|
|
@@ -2839,21 +4762,11 @@ window.__ModuleLoader__.load({
|
|
|
2839
4762
|
})
|
|
2840
4763
|
: null,
|
|
2841
4764
|
modal?.type === 'export'
|
|
2842
|
-
? h(ExportModal, { t, busy, onClose: () => setModal(null), onExport: exportAs })
|
|
4765
|
+
? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs })
|
|
2843
4766
|
: null,
|
|
2844
4767
|
modal?.type === 'settings'
|
|
2845
4768
|
? h(SettingsModal, { t, settings, mtRoutes, busy, onClose: () => setModal(null), onSave: saveSettings })
|
|
2846
4769
|
: null,
|
|
2847
|
-
modal?.type === 'libraries'
|
|
2848
|
-
? h(LibrariesModal, {
|
|
2849
|
-
t,
|
|
2850
|
-
data,
|
|
2851
|
-
busy,
|
|
2852
|
-
onClose: () => setModal(null),
|
|
2853
|
-
onSetMounts: setMounts,
|
|
2854
|
-
onCreate: createLibrary,
|
|
2855
|
-
})
|
|
2856
|
-
: null,
|
|
2857
4770
|
modal?.type === 'recent'
|
|
2858
4771
|
? h(RecentDocsModal, {
|
|
2859
4772
|
t,
|
|
@@ -2920,6 +4833,40 @@ window.__ModuleLoader__.load({
|
|
|
2920
4833
|
'editor.emptyTarget': '请先输入译文',
|
|
2921
4834
|
'editor.done': '全部句段已确认',
|
|
2922
4835
|
'editor.confirmedAt': '确认于',
|
|
4836
|
+
'editor.suggestion': '机器建议',
|
|
4837
|
+
'editor.adopt': '采纳',
|
|
4838
|
+
'editor.adoptHint': '把建议填入译文,保留它的来源',
|
|
4839
|
+
'editor.remaining': '句待确认',
|
|
4840
|
+
'status.translated': '已翻译',
|
|
4841
|
+
'origin.tm': 'TM',
|
|
4842
|
+
'origin.mt': 'MT',
|
|
4843
|
+
'origin.agent': 'Agent',
|
|
4844
|
+
'origin.manual': '手动',
|
|
4845
|
+
'rail.filter.all': '全部',
|
|
4846
|
+
'rail.filter.pending': '待译',
|
|
4847
|
+
'rail.filter.translated': '待审',
|
|
4848
|
+
'rail.filter.confirmed': '已确认',
|
|
4849
|
+
'rail.confirmAll': '全部确认',
|
|
4850
|
+
'rail.confirmAllHint': '把当前所有「待审」句段一次性确认并写入翻译记忆',
|
|
4851
|
+
'rail.confirmAllMessage': '将确认 {count} 个待审句段并写入翻译记忆,句段译文不会经过逐句复核。确定继续?',
|
|
4852
|
+
'toast.confirmedMany': '已确认 {count} 段',
|
|
4853
|
+
'qa.warn': '检查提醒',
|
|
4854
|
+
'qa.exportBanner': '导出前检查发现 {count} 条提醒(不影响导出):',
|
|
4855
|
+
'qa.counts': '占位符 {placeholder} · 数字 {number} · 术语 {term} · 空译文 {empty}',
|
|
4856
|
+
'qa.code.PLACEHOLDER_MISSING': '占位符缺失',
|
|
4857
|
+
'qa.code.NUMBER_MISMATCH': '数字不一致',
|
|
4858
|
+
'qa.code.TERM_NOT_USED': '术语未使用',
|
|
4859
|
+
'qa.code.EMPTY_TARGET': '空译文',
|
|
4860
|
+
'toast.suggestionAdopted': '已采纳建议',
|
|
4861
|
+
'pretranslate.start': '预翻译',
|
|
4862
|
+
'editor.noSuggestion': '暂无建议,可直接翻译或让 agent 补刀',
|
|
4863
|
+
'editor.baselineHint': '中间栏是机器或 agent 的初次产出,右侧可以对照着改',
|
|
4864
|
+
'pretranslate.title': '用翻译记忆与机器翻译给整篇铺底,之后逐句审校',
|
|
4865
|
+
'pretranslate.running': '预翻译中',
|
|
4866
|
+
'pretranslate.cancel': '停止预翻译',
|
|
4867
|
+
'toast.preDone': '预翻译完成,填充 {count} 段',
|
|
4868
|
+
'toast.preCancelled': '已停止预翻译',
|
|
4869
|
+
'toast.preFailed': '预翻译结束,{count} 段失败',
|
|
2923
4870
|
'empty.title': '开始一次 CAT 翻译',
|
|
2924
4871
|
'empty.desc': '粘贴文本、拖入文件,或加载演示文档。CAIT 会把源文切成句段,并同时查询翻译记忆与术语库;确认译文后自动入库。',
|
|
2925
4872
|
'empty.paste': '粘贴文本',
|
|
@@ -2968,6 +4915,13 @@ window.__ModuleLoader__.load({
|
|
|
2968
4915
|
'libraries.create': '新建',
|
|
2969
4916
|
'libraries.done': '完成',
|
|
2970
4917
|
'libraries.created': '库已创建并自动挂载',
|
|
4918
|
+
'libraries.rename': '重命名',
|
|
4919
|
+
'libraries.save': '保存',
|
|
4920
|
+
'libraries.renamed': '库已重命名',
|
|
4921
|
+
'libraries.delete': '删除库',
|
|
4922
|
+
'libraries.deleted': '库已删除',
|
|
4923
|
+
'libraries.deleteTmWarn': '库里的记忆条目也会一起删除,无法撤销。',
|
|
4924
|
+
'libraries.deleteGlossaryWarn': '库里的术语也会一起删除,无法撤销。',
|
|
2971
4925
|
'libraries.mountSaved': '挂载设置已保存',
|
|
2972
4926
|
'panel.tm': '记忆库',
|
|
2973
4927
|
'panel.glossary': '术语库',
|
|
@@ -2985,6 +4939,9 @@ window.__ModuleLoader__.load({
|
|
|
2985
4939
|
'match.high_fuzzy': '高模糊',
|
|
2986
4940
|
'match.medium_fuzzy': '中模糊',
|
|
2987
4941
|
'match.low_fuzzy': '低模糊',
|
|
4942
|
+
'match.context': '上下文匹配',
|
|
4943
|
+
'panel.contextPrev': '上文',
|
|
4944
|
+
'panel.contextNext': '下文',
|
|
2988
4945
|
'panel.apply': '应用',
|
|
2989
4946
|
'panel.importTmx': '导入 TMX',
|
|
2990
4947
|
'panel.clearTm': '清空记忆库',
|
|
@@ -3000,6 +4957,8 @@ window.__ModuleLoader__.load({
|
|
|
3000
4957
|
'panel.saveTerm': '保存术语',
|
|
3001
4958
|
'panel.cancel': '取消',
|
|
3002
4959
|
'panel.delete': '删除',
|
|
4960
|
+
'panel.edit': '编辑',
|
|
4961
|
+
'panel.saveEntry': '保存修改',
|
|
3003
4962
|
'panel.noTerms': '术语库为空。添加术语后,命中会自动高亮。',
|
|
3004
4963
|
'panel.importCsv': '导入 CSV/TSV',
|
|
3005
4964
|
'panel.clearGlossary': '清空术语库',
|
|
@@ -3078,6 +5037,7 @@ window.__ModuleLoader__.load({
|
|
|
3078
5037
|
'modal.confirm.clearGlossary':
|
|
3079
5038
|
'清空当前挂载的术语?未挂载库不会受影响;如要清空全部库,请在 API 调用中显式传 all: true。此操作无法撤销。',
|
|
3080
5039
|
'modal.confirm.deleteTerm': '删除术语',
|
|
5040
|
+
'modal.confirm.deleteTm': '删除记忆库条目',
|
|
3081
5041
|
'modal.confirm.ok': '确认',
|
|
3082
5042
|
'modal.confirm.cancel': '取消',
|
|
3083
5043
|
'toast.confirmed': '句段已确认并写入记忆库',
|
|
@@ -3090,6 +5050,8 @@ window.__ModuleLoader__.load({
|
|
|
3090
5050
|
'toast.draftSaved': '草稿已保存',
|
|
3091
5051
|
'toast.termSaved': '术语已保存',
|
|
3092
5052
|
'toast.termDeleted': '术语已删除',
|
|
5053
|
+
'toast.tmUpdated': '记忆库条目已更新',
|
|
5054
|
+
'toast.tmDeleted': '记忆库条目已删除',
|
|
3093
5055
|
'toast.tmImported': 'TMX 已导入',
|
|
3094
5056
|
'toast.tmxMarkupLost': 'TMX 已导入;{count} 个内联占位符/格式标签已按纯文本处理,格式信息丢失。',
|
|
3095
5057
|
'toast.glossaryImported': '术语已导入',
|
|
@@ -3100,6 +5062,93 @@ window.__ModuleLoader__.load({
|
|
|
3100
5062
|
'toast.copied': '已复制到剪贴板',
|
|
3101
5063
|
'toast.mtReady': '译文建议已生成',
|
|
3102
5064
|
'toast.error': '操作失败',
|
|
5065
|
+
'nav.documents': '文档',
|
|
5066
|
+
'nav.libraries': '库',
|
|
5067
|
+
'library.title': '翻译记忆与术语库',
|
|
5068
|
+
'library.tmTab': '翻译记忆',
|
|
5069
|
+
'library.glossaryTab': '术语库',
|
|
5070
|
+
'library.scope': '搜索范围',
|
|
5071
|
+
'library.scopeAll': '全部库',
|
|
5072
|
+
'library.search': '搜索原文或译文…',
|
|
5073
|
+
'library.language': '全部语言',
|
|
5074
|
+
'library.pageSize': '每页',
|
|
5075
|
+
'library.pageInfo': '第 {start}–{end} 条 / 共 {total} 条',
|
|
5076
|
+
'library.prev': '上一页',
|
|
5077
|
+
'library.next': '下一页',
|
|
5078
|
+
'library.empty': '这个库里还没有条目。',
|
|
5079
|
+
'library.noMatch': '没有匹配的条目。',
|
|
5080
|
+
'library.add': '新增',
|
|
5081
|
+
'library.import': '导入',
|
|
5082
|
+
'library.export': '导出',
|
|
5083
|
+
'library.clear': '清空',
|
|
5084
|
+
'library.newLibrary': '新建库',
|
|
5085
|
+
'library.rename': '重命名',
|
|
5086
|
+
'library.deleteLibrary': '删除库',
|
|
5087
|
+
'library.deleteLibraryMessage': '删除「{name}」?库里的条目也会一起删除。',
|
|
5088
|
+
'library.colSource': '原文',
|
|
5089
|
+
'library.colTarget': '译文',
|
|
5090
|
+
'library.colNote': '备注',
|
|
5091
|
+
'library.colContext': '上下文',
|
|
5092
|
+
'library.colLang': '语言',
|
|
5093
|
+
'library.colCase': '区分大小写',
|
|
5094
|
+
'library.colUpdated': '更新时间',
|
|
5095
|
+
'library.detail': '条目详情',
|
|
5096
|
+
'library.detailEmpty': '点一行查看和编辑。',
|
|
5097
|
+
'library.save': '保存',
|
|
5098
|
+
'library.deleteEntry': '删除条目',
|
|
5099
|
+
'library.entryCreated': '已新增条目',
|
|
5100
|
+
'library.entrySaved': '已保存',
|
|
5101
|
+
'library.entryDeleted': '已删除',
|
|
5102
|
+
'library.exported': '已导出',
|
|
5103
|
+
'library.exportFormat': '导出格式',
|
|
5104
|
+
'library.caseSensitiveHint': '区分大小写时只匹配完全一致的写法。',
|
|
5105
|
+
'library.noDocHint': '不用打开文档也能管理库。',
|
|
5106
|
+
'rail.pageInfo': '第 {start}–{end} 条 / 共 {total} 条',
|
|
5107
|
+
'rail.goto': '跳到编号',
|
|
5108
|
+
'rail.gotoInvalid': '编号超出范围。',
|
|
5109
|
+
'rail.nextUnconfirmed': '下一个未确认',
|
|
5110
|
+
'panel.searchInLibrary': '在库中搜索',
|
|
5111
|
+
'panel.viewAll': '在库页面查看全部',
|
|
5112
|
+
'panel.glossaryMatchesOnly': '这里只显示当前句段命中的术语。',
|
|
5113
|
+
'nav.align': '对齐',
|
|
5114
|
+
'align.title': '对齐两个文件',
|
|
5115
|
+
'align.hint': '分别放入源文件和译文文件,CAIT 会按段落逐句配对。两个文件可以是完全不同的语言。',
|
|
5116
|
+
'align.sourceFile': '源文件',
|
|
5117
|
+
'align.targetFile': '译文文件',
|
|
5118
|
+
'align.pick': '未选择文件',
|
|
5119
|
+
'align.choose': '选择',
|
|
5120
|
+
'align.chars': '{count} 字符',
|
|
5121
|
+
'align.run': '开始对齐',
|
|
5122
|
+
'align.running': '对齐中…',
|
|
5123
|
+
'align.needFiles': '请先选择源文件和译文文件。',
|
|
5124
|
+
'align.readError': '读取文件失败。',
|
|
5125
|
+
'align.sourceLang': '源语言(可选)',
|
|
5126
|
+
'align.targetLang': '目标语言(可选)',
|
|
5127
|
+
'align.stats': '高置信 {high} · 待确认 {medium} · 可疑 {low}',
|
|
5128
|
+
'align.unpairedSource': '未配对原文',
|
|
5129
|
+
'align.unpairedTarget': '未配对译文',
|
|
5130
|
+
'align.blocked': '文件较大,已按段落分块对齐,结果可能不如整体对齐稳定。',
|
|
5131
|
+
'align.confidence': '置信度',
|
|
5132
|
+
'align.merge': '与下一行合并',
|
|
5133
|
+
'align.split': '按换行拆开',
|
|
5134
|
+
'align.remove': '删除这一行',
|
|
5135
|
+
'align.write': '写入记忆库',
|
|
5136
|
+
'align.writeCount': '写入选中的 {count} 条',
|
|
5137
|
+
'align.writeMessage': '将写入 {count} 条,完全相同的条目会被跳过。',
|
|
5138
|
+
'align.written': '已写入 {written} 条,跳过 {skipped} 条。',
|
|
5139
|
+
'align.nothing': '没有可写入的行。',
|
|
5140
|
+
'origin.auto': '自动传播',
|
|
5141
|
+
'rail.propagate': '铺满重复句段',
|
|
5142
|
+
'rail.propagateMessage': '把已有译文填到本文档中其它相同原文的句段(已确认的不会动)。',
|
|
5143
|
+
'toast.propagated': '已铺满 {count} 个句段。',
|
|
5144
|
+
'term.title': '术语候选',
|
|
5145
|
+
'term.hint': '按出现次数与长度排序,点选后填入译文即可入库。',
|
|
5146
|
+
'term.extract': '从文档提取术语',
|
|
5147
|
+
'term.empty': '没有找到重复出现的候选术语。',
|
|
5148
|
+
'term.add': '加入术语库',
|
|
5149
|
+
'term.added': '已加入 {added} 条术语。',
|
|
5150
|
+
'term.noDoc': '先打开一个文档再提取术语。',
|
|
5151
|
+
'term.inLibrary': '已有',
|
|
3103
5152
|
}
|
|
3104
5153
|
|
|
3105
5154
|
const EN = {
|
|
@@ -3151,6 +5200,40 @@ window.__ModuleLoader__.load({
|
|
|
3151
5200
|
'editor.emptyTarget': 'Enter a translation first',
|
|
3152
5201
|
'editor.done': 'All segments confirmed',
|
|
3153
5202
|
'editor.confirmedAt': 'Confirmed',
|
|
5203
|
+
'editor.suggestion': 'Machine suggestion',
|
|
5204
|
+
'editor.adopt': 'Adopt',
|
|
5205
|
+
'editor.adoptHint': 'Fill the target with this suggestion and keep its origin',
|
|
5206
|
+
'editor.remaining': 'to review',
|
|
5207
|
+
'status.translated': 'Translated',
|
|
5208
|
+
'origin.tm': 'TM',
|
|
5209
|
+
'origin.mt': 'MT',
|
|
5210
|
+
'origin.agent': 'Agent',
|
|
5211
|
+
'origin.manual': 'Manual',
|
|
5212
|
+
'rail.filter.all': 'All',
|
|
5213
|
+
'rail.filter.pending': 'To translate',
|
|
5214
|
+
'rail.filter.translated': 'To review',
|
|
5215
|
+
'rail.filter.confirmed': 'Confirmed',
|
|
5216
|
+
'rail.confirmAll': 'Confirm all',
|
|
5217
|
+
'rail.confirmAllHint': 'Confirm every "To review" segment at once and write them to translation memory',
|
|
5218
|
+
'rail.confirmAllMessage': 'This confirms {count} segment(s) awaiting review and writes them to translation memory, without reviewing them one by one. Continue?',
|
|
5219
|
+
'toast.confirmedMany': 'Confirmed {count} segment(s)',
|
|
5220
|
+
'qa.warn': 'Review note',
|
|
5221
|
+
'qa.exportBanner': 'Pre-export check found {count} note(s) — export is not blocked:',
|
|
5222
|
+
'qa.counts': 'placeholders {placeholder} · numbers {number} · terms {term} · empty {empty}',
|
|
5223
|
+
'qa.code.PLACEHOLDER_MISSING': 'placeholder missing',
|
|
5224
|
+
'qa.code.NUMBER_MISMATCH': 'number mismatch',
|
|
5225
|
+
'qa.code.TERM_NOT_USED': 'term unused',
|
|
5226
|
+
'qa.code.EMPTY_TARGET': 'empty target',
|
|
5227
|
+
'toast.suggestionAdopted': 'Suggestion adopted',
|
|
5228
|
+
'pretranslate.start': 'Pre-translate',
|
|
5229
|
+
'editor.noSuggestion': 'No suggestion yet — translate directly or let an agent draft it',
|
|
5230
|
+
'editor.baselineHint': 'The middle column is what the machine or agent produced; edit against it on the right',
|
|
5231
|
+
'pretranslate.title': 'Fill the whole document from TM and machine translation, then review segment by segment',
|
|
5232
|
+
'pretranslate.running': 'Pre-translating',
|
|
5233
|
+
'pretranslate.cancel': 'Stop pre-translation',
|
|
5234
|
+
'toast.preDone': 'Pre-translation finished, {count} segment(s) filled',
|
|
5235
|
+
'toast.preCancelled': 'Pre-translation stopped',
|
|
5236
|
+
'toast.preFailed': 'Pre-translation finished, {count} segment(s) failed',
|
|
3154
5237
|
'empty.title': 'Start a CAT translation',
|
|
3155
5238
|
'empty.desc': 'Paste text, drop a file, or load the demo. CAIT splits the source into segments, queries translation memory and glossary hits, and stores each confirmed pair in TM.',
|
|
3156
5239
|
'empty.paste': 'Paste text',
|
|
@@ -3199,6 +5282,13 @@ window.__ModuleLoader__.load({
|
|
|
3199
5282
|
'libraries.create': 'Create',
|
|
3200
5283
|
'libraries.done': 'Done',
|
|
3201
5284
|
'libraries.created': 'Library created and mounted',
|
|
5285
|
+
'libraries.rename': 'Rename',
|
|
5286
|
+
'libraries.save': 'Save',
|
|
5287
|
+
'libraries.renamed': 'Library renamed',
|
|
5288
|
+
'libraries.delete': 'Delete library',
|
|
5289
|
+
'libraries.deleted': 'Library deleted',
|
|
5290
|
+
'libraries.deleteTmWarn': 'Its TM entries are deleted with it. This cannot be undone.',
|
|
5291
|
+
'libraries.deleteGlossaryWarn': 'Its terms are deleted with it. This cannot be undone.',
|
|
3202
5292
|
'libraries.mountSaved': 'Mount settings saved',
|
|
3203
5293
|
'panel.tm': 'TM',
|
|
3204
5294
|
'panel.glossary': 'Glossary',
|
|
@@ -3216,6 +5306,9 @@ window.__ModuleLoader__.load({
|
|
|
3216
5306
|
'match.high_fuzzy': 'High fuzzy',
|
|
3217
5307
|
'match.medium_fuzzy': 'Medium fuzzy',
|
|
3218
5308
|
'match.low_fuzzy': 'Low fuzzy',
|
|
5309
|
+
'match.context': 'Context match',
|
|
5310
|
+
'panel.contextPrev': 'Prev',
|
|
5311
|
+
'panel.contextNext': 'Next',
|
|
3219
5312
|
'panel.apply': 'Apply',
|
|
3220
5313
|
'panel.importTmx': 'Import TMX',
|
|
3221
5314
|
'panel.clearTm': 'Clear TM',
|
|
@@ -3231,6 +5324,8 @@ window.__ModuleLoader__.load({
|
|
|
3231
5324
|
'panel.saveTerm': 'Save term',
|
|
3232
5325
|
'panel.cancel': 'Cancel',
|
|
3233
5326
|
'panel.delete': 'Delete',
|
|
5327
|
+
'panel.edit': 'Edit',
|
|
5328
|
+
'panel.saveEntry': 'Save changes',
|
|
3234
5329
|
'panel.noTerms': 'The glossary is empty. Hits will be highlighted automatically.',
|
|
3235
5330
|
'panel.importCsv': 'Import CSV/TSV',
|
|
3236
5331
|
'panel.clearGlossary': 'Clear glossary',
|
|
@@ -3309,6 +5404,7 @@ window.__ModuleLoader__.load({
|
|
|
3309
5404
|
'modal.confirm.clearGlossary':
|
|
3310
5405
|
'Clear the currently mounted glossary terms? Unmounted libraries are not affected; pass all: true to the API to clear every library. This cannot be undone.',
|
|
3311
5406
|
'modal.confirm.deleteTerm': 'Delete term',
|
|
5407
|
+
'modal.confirm.deleteTm': 'Delete TM entry',
|
|
3312
5408
|
'modal.confirm.ok': 'Confirm',
|
|
3313
5409
|
'modal.confirm.cancel': 'Cancel',
|
|
3314
5410
|
'toast.confirmed': 'Segment confirmed and written to TM',
|
|
@@ -3321,6 +5417,8 @@ window.__ModuleLoader__.load({
|
|
|
3321
5417
|
'toast.draftSaved': 'Draft saved',
|
|
3322
5418
|
'toast.termSaved': 'Term saved',
|
|
3323
5419
|
'toast.termDeleted': 'Term deleted',
|
|
5420
|
+
'toast.tmUpdated': 'TM entry updated',
|
|
5421
|
+
'toast.tmDeleted': 'TM entry deleted',
|
|
3324
5422
|
'toast.tmImported': 'TMX imported',
|
|
3325
5423
|
'toast.tmxMarkupLost': 'TMX imported; {count} inline placeholder/format tags were flattened to plain text.',
|
|
3326
5424
|
'toast.glossaryImported': 'Glossary imported',
|
|
@@ -3331,6 +5429,93 @@ window.__ModuleLoader__.load({
|
|
|
3331
5429
|
'toast.copied': 'Copied to clipboard',
|
|
3332
5430
|
'toast.mtReady': 'Suggestion ready',
|
|
3333
5431
|
'toast.error': 'Operation failed',
|
|
5432
|
+
'nav.documents': 'Documents',
|
|
5433
|
+
'nav.libraries': 'Libraries',
|
|
5434
|
+
'library.title': 'Translation memory and glossary',
|
|
5435
|
+
'library.tmTab': 'Translation memory',
|
|
5436
|
+
'library.glossaryTab': 'Glossary',
|
|
5437
|
+
'library.scope': 'Search in',
|
|
5438
|
+
'library.scopeAll': 'All libraries',
|
|
5439
|
+
'library.search': 'Search source or target…',
|
|
5440
|
+
'library.language': 'All languages',
|
|
5441
|
+
'library.pageSize': 'Per page',
|
|
5442
|
+
'library.pageInfo': '{start}–{end} of {total}',
|
|
5443
|
+
'library.prev': 'Previous',
|
|
5444
|
+
'library.next': 'Next',
|
|
5445
|
+
'library.empty': 'This library has no entries yet.',
|
|
5446
|
+
'library.noMatch': 'No entries match.',
|
|
5447
|
+
'library.add': 'Add',
|
|
5448
|
+
'library.import': 'Import',
|
|
5449
|
+
'library.export': 'Export',
|
|
5450
|
+
'library.clear': 'Clear',
|
|
5451
|
+
'library.newLibrary': 'New library',
|
|
5452
|
+
'library.rename': 'Rename',
|
|
5453
|
+
'library.deleteLibrary': 'Delete library',
|
|
5454
|
+
'library.deleteLibraryMessage': 'Delete “{name}”? Its entries will be deleted too.',
|
|
5455
|
+
'library.colSource': 'Source',
|
|
5456
|
+
'library.colTarget': 'Target',
|
|
5457
|
+
'library.colNote': 'Note',
|
|
5458
|
+
'library.colContext': 'Context',
|
|
5459
|
+
'library.colLang': 'Language',
|
|
5460
|
+
'library.colCase': 'Case-sensitive',
|
|
5461
|
+
'library.colUpdated': 'Updated',
|
|
5462
|
+
'library.detail': 'Entry',
|
|
5463
|
+
'library.detailEmpty': 'Select a row to view and edit it.',
|
|
5464
|
+
'library.save': 'Save',
|
|
5465
|
+
'library.deleteEntry': 'Delete entry',
|
|
5466
|
+
'library.entryCreated': 'Entry added',
|
|
5467
|
+
'library.entrySaved': 'Saved',
|
|
5468
|
+
'library.entryDeleted': 'Deleted',
|
|
5469
|
+
'library.exported': 'Exported',
|
|
5470
|
+
'library.exportFormat': 'Format',
|
|
5471
|
+
'library.caseSensitiveHint': 'Case-sensitive terms match only the exact spelling.',
|
|
5472
|
+
'library.noDocHint': 'Manage libraries without opening a document.',
|
|
5473
|
+
'rail.pageInfo': '{start}–{end} of {total}',
|
|
5474
|
+
'rail.goto': 'Go to segment',
|
|
5475
|
+
'rail.gotoInvalid': 'Segment number is out of range.',
|
|
5476
|
+
'rail.nextUnconfirmed': 'Next unconfirmed',
|
|
5477
|
+
'panel.searchInLibrary': 'Search the library',
|
|
5478
|
+
'panel.viewAll': 'View all in libraries',
|
|
5479
|
+
'panel.glossaryMatchesOnly': 'Only terms that match this segment are listed here.',
|
|
5480
|
+
'nav.align': 'Align',
|
|
5481
|
+
'align.title': 'Align two files',
|
|
5482
|
+
'align.hint': 'Drop the source file and the translated file. CAIT pairs them paragraph by paragraph — they can be in completely different languages.',
|
|
5483
|
+
'align.sourceFile': 'Source file',
|
|
5484
|
+
'align.targetFile': 'Translated file',
|
|
5485
|
+
'align.pick': 'No file chosen',
|
|
5486
|
+
'align.choose': 'Choose',
|
|
5487
|
+
'align.chars': '{count} chars',
|
|
5488
|
+
'align.run': 'Align',
|
|
5489
|
+
'align.running': 'Aligning…',
|
|
5490
|
+
'align.needFiles': 'Choose both files first.',
|
|
5491
|
+
'align.readError': 'Could not read that file.',
|
|
5492
|
+
'align.sourceLang': 'Source language (optional)',
|
|
5493
|
+
'align.targetLang': 'Target language (optional)',
|
|
5494
|
+
'align.stats': 'high {high} · to review {medium} · doubtful {low}',
|
|
5495
|
+
'align.unpairedSource': 'Unpaired source',
|
|
5496
|
+
'align.unpairedTarget': 'Unpaired target',
|
|
5497
|
+
'align.blocked': 'Large files are aligned in paragraph blocks; results may be less stable than a whole-file pass.',
|
|
5498
|
+
'align.confidence': 'Confidence',
|
|
5499
|
+
'align.merge': 'Merge with the next row',
|
|
5500
|
+
'align.split': 'Split on line breaks',
|
|
5501
|
+
'align.remove': 'Remove this row',
|
|
5502
|
+
'align.write': 'Write to memory',
|
|
5503
|
+
'align.writeCount': 'Write {count} selected',
|
|
5504
|
+
'align.writeMessage': 'This writes {count} pair(s); identical entries are skipped.',
|
|
5505
|
+
'align.written': 'Wrote {written}, skipped {skipped}.',
|
|
5506
|
+
'align.nothing': 'Nothing selected to write.',
|
|
5507
|
+
'origin.auto': 'Propagated',
|
|
5508
|
+
'rail.propagate': 'Fill repeated segments',
|
|
5509
|
+
'rail.propagateMessage': 'Copy the existing translation to other segments with the same source. Confirmed segments are left alone.',
|
|
5510
|
+
'toast.propagated': 'Filled {count} segment(s).',
|
|
5511
|
+
'term.title': 'Terminology candidates',
|
|
5512
|
+
'term.hint': 'Ranked by occurrences and length. Pick the real terms, type a translation, and add them.',
|
|
5513
|
+
'term.extract': 'Mine terms from the document',
|
|
5514
|
+
'term.empty': 'No repeated candidate found.',
|
|
5515
|
+
'term.add': 'Add to glossary',
|
|
5516
|
+
'term.added': 'Added {added} term(s).',
|
|
5517
|
+
'term.noDoc': 'Open a document before mining terms.',
|
|
5518
|
+
'term.inLibrary': 'in library',
|
|
3334
5519
|
}
|
|
3335
5520
|
|
|
3336
5521
|
const JA = {
|
|
@@ -3382,6 +5567,40 @@ window.__ModuleLoader__.load({
|
|
|
3382
5567
|
'editor.emptyTarget': '先に訳文を入力してください',
|
|
3383
5568
|
'editor.done': 'すべてのセグメントを確認済み',
|
|
3384
5569
|
'editor.confirmedAt': '確認日時',
|
|
5570
|
+
'editor.suggestion': '機械翻訳の提案',
|
|
5571
|
+
'editor.adopt': '採用',
|
|
5572
|
+
'editor.adoptHint': '提案を訳文に反映し、出所を保ちます',
|
|
5573
|
+
'editor.remaining': '件が未確認',
|
|
5574
|
+
'status.translated': '翻訳済み',
|
|
5575
|
+
'origin.tm': 'TM',
|
|
5576
|
+
'origin.mt': 'MT',
|
|
5577
|
+
'origin.agent': 'Agent',
|
|
5578
|
+
'origin.manual': '手動',
|
|
5579
|
+
'rail.filter.all': 'すべて',
|
|
5580
|
+
'rail.filter.pending': '未翻訳',
|
|
5581
|
+
'rail.filter.translated': '要確認',
|
|
5582
|
+
'rail.filter.confirmed': '確認済み',
|
|
5583
|
+
'rail.confirmAll': 'まとめて確認',
|
|
5584
|
+
'rail.confirmAllHint': '「要確認」の文をまとめて確認し、翻訳メモリに書き込みます',
|
|
5585
|
+
'rail.confirmAllMessage': '要確認の {count} 文をまとめて確認し、翻訳メモリに書き込みます。1 文ずつ見直すことはありません。続けますか?',
|
|
5586
|
+
'toast.confirmedMany': '{count} 文を確認しました',
|
|
5587
|
+
'qa.warn': 'チェック注意',
|
|
5588
|
+
'qa.exportBanner': '書き出し前のチェックで {count} 件の注意が見つかりました(書き出しは可能です):',
|
|
5589
|
+
'qa.counts': 'プレースホルダー {placeholder} · 数字 {number} · 用語 {term} · 空訳 {empty}',
|
|
5590
|
+
'qa.code.PLACEHOLDER_MISSING': 'プレースホルダー欠落',
|
|
5591
|
+
'qa.code.NUMBER_MISMATCH': '数字の不一致',
|
|
5592
|
+
'qa.code.TERM_NOT_USED': '用語が未使用',
|
|
5593
|
+
'qa.code.EMPTY_TARGET': '訳文が空',
|
|
5594
|
+
'toast.suggestionAdopted': '提案を採用しました',
|
|
5595
|
+
'pretranslate.start': '事前翻訳',
|
|
5596
|
+
'editor.noSuggestion': '提案はまだありません。直接翻訳するか、agent に下訳させてください',
|
|
5597
|
+
'editor.baselineHint': '中央の列は機械翻訳/agent の初回出力です。右側で見比べながら直せます',
|
|
5598
|
+
'pretranslate.title': 'TM と機械翻訳で文書全体を下訳し、あとから文ごとに確認します',
|
|
5599
|
+
'pretranslate.running': '事前翻訳中',
|
|
5600
|
+
'pretranslate.cancel': '事前翻訳を停止',
|
|
5601
|
+
'toast.preDone': '事前翻訳が完了しました({count} 文を反映)',
|
|
5602
|
+
'toast.preCancelled': '事前翻訳を停止しました',
|
|
5603
|
+
'toast.preFailed': '事前翻訳が終了しました({count} 文が失敗)',
|
|
3385
5604
|
'empty.title': 'CAT 翻訳を始める',
|
|
3386
5605
|
'empty.desc': 'テキストを貼り付けるか、ファイルをドロップするか、デモ文書を読み込みます。CAIT は原文をセグメントに分割し、翻訳メモリと用語集を同時に検索します。確認した訳文は自動的に TM に保存されます。',
|
|
3387
5606
|
'empty.paste': 'テキストを貼り付け',
|
|
@@ -3430,6 +5649,13 @@ window.__ModuleLoader__.load({
|
|
|
3430
5649
|
'libraries.create': '作成',
|
|
3431
5650
|
'libraries.done': '完了',
|
|
3432
5651
|
'libraries.created': 'ライブラリを作成してマウントしました',
|
|
5652
|
+
'libraries.rename': '名前を変更',
|
|
5653
|
+
'libraries.save': '保存',
|
|
5654
|
+
'libraries.renamed': 'ライブラリ名を変更しました',
|
|
5655
|
+
'libraries.delete': 'ライブラリを削除',
|
|
5656
|
+
'libraries.deleted': 'ライブラリを削除しました',
|
|
5657
|
+
'libraries.deleteTmWarn': '中の TM エントリも一緒に削除され、元に戻せません。',
|
|
5658
|
+
'libraries.deleteGlossaryWarn': '中の用語も一緒に削除され、元に戻せません。',
|
|
3433
5659
|
'libraries.mountSaved': 'マウント設定を保存しました',
|
|
3434
5660
|
'panel.tm': 'TM',
|
|
3435
5661
|
'panel.glossary': '用語集',
|
|
@@ -3447,6 +5673,9 @@ window.__ModuleLoader__.load({
|
|
|
3447
5673
|
'match.high_fuzzy': '高あいまい一致',
|
|
3448
5674
|
'match.medium_fuzzy': '中あいまい一致',
|
|
3449
5675
|
'match.low_fuzzy': '低あいまい一致',
|
|
5676
|
+
'match.context': 'コンテキスト一致',
|
|
5677
|
+
'panel.contextPrev': '前文',
|
|
5678
|
+
'panel.contextNext': '後文',
|
|
3450
5679
|
'panel.apply': '適用',
|
|
3451
5680
|
'panel.importTmx': 'TMX をインポート',
|
|
3452
5681
|
'panel.clearTm': 'TM をクリア',
|
|
@@ -3462,6 +5691,8 @@ window.__ModuleLoader__.load({
|
|
|
3462
5691
|
'panel.saveTerm': '用語を保存',
|
|
3463
5692
|
'panel.cancel': 'キャンセル',
|
|
3464
5693
|
'panel.delete': '削除',
|
|
5694
|
+
'panel.edit': '編集',
|
|
5695
|
+
'panel.saveEntry': '変更を保存',
|
|
3465
5696
|
'panel.noTerms': '用語集は空です。用語を追加するとヒットが自動で強調表示されます。',
|
|
3466
5697
|
'panel.importCsv': 'CSV/TSV をインポート',
|
|
3467
5698
|
'panel.clearGlossary': '用語集をクリア',
|
|
@@ -3540,6 +5771,7 @@ window.__ModuleLoader__.load({
|
|
|
3540
5771
|
'modal.confirm.clearGlossary':
|
|
3541
5772
|
'現在マウント中の用語をクリアしますか?未マウントのライブラリは影響を受けません。すべてのライブラリをクリアするには API に all: true を明示してください。この操作は元に戻せません。',
|
|
3542
5773
|
'modal.confirm.deleteTerm': '用語を削除',
|
|
5774
|
+
'modal.confirm.deleteTm': 'TM エントリを削除',
|
|
3543
5775
|
'modal.confirm.ok': '確認',
|
|
3544
5776
|
'modal.confirm.cancel': 'キャンセル',
|
|
3545
5777
|
'toast.confirmed': 'セグメントを確認して TM に保存しました',
|
|
@@ -3552,6 +5784,8 @@ window.__ModuleLoader__.load({
|
|
|
3552
5784
|
'toast.draftSaved': '下書きを保存しました',
|
|
3553
5785
|
'toast.termSaved': '用語を保存しました',
|
|
3554
5786
|
'toast.termDeleted': '用語を削除しました',
|
|
5787
|
+
'toast.tmUpdated': 'TM エントリを更新しました',
|
|
5788
|
+
'toast.tmDeleted': 'TM エントリを削除しました',
|
|
3555
5789
|
'toast.tmImported': 'TMX をインポートしました',
|
|
3556
5790
|
'toast.tmxMarkupLost': 'TMX をインポートしました。{count} 個のインライン プレースホルダー/書式タグはプレーンテキストとして処理され、書式情報は失われました。',
|
|
3557
5791
|
'toast.glossaryImported': '用語をインポートしました',
|
|
@@ -3562,6 +5796,93 @@ window.__ModuleLoader__.load({
|
|
|
3562
5796
|
'toast.copied': 'クリップボードにコピーしました',
|
|
3563
5797
|
'toast.mtReady': '訳文候補を生成しました',
|
|
3564
5798
|
'toast.error': '操作に失敗しました',
|
|
5799
|
+
'nav.documents': '文書',
|
|
5800
|
+
'nav.libraries': 'ライブラリ',
|
|
5801
|
+
'library.title': '翻訳メモリと用語集',
|
|
5802
|
+
'library.tmTab': '翻訳メモリ',
|
|
5803
|
+
'library.glossaryTab': '用語集',
|
|
5804
|
+
'library.scope': '検索範囲',
|
|
5805
|
+
'library.scopeAll': 'すべてのライブラリ',
|
|
5806
|
+
'library.search': '原文または訳文を検索…',
|
|
5807
|
+
'library.language': 'すべての言語',
|
|
5808
|
+
'library.pageSize': '表示件数',
|
|
5809
|
+
'library.pageInfo': '{start}–{end} 件 / 全 {total} 件',
|
|
5810
|
+
'library.prev': '前へ',
|
|
5811
|
+
'library.next': '次へ',
|
|
5812
|
+
'library.empty': 'このライブラリにはまだエントリがありません。',
|
|
5813
|
+
'library.noMatch': '一致するエントリはありません。',
|
|
5814
|
+
'library.add': '追加',
|
|
5815
|
+
'library.import': 'インポート',
|
|
5816
|
+
'library.export': 'エクスポート',
|
|
5817
|
+
'library.clear': 'クリア',
|
|
5818
|
+
'library.newLibrary': '新しいライブラリ',
|
|
5819
|
+
'library.rename': '名前を変更',
|
|
5820
|
+
'library.deleteLibrary': 'ライブラリを削除',
|
|
5821
|
+
'library.deleteLibraryMessage': '「{name}」を削除しますか?エントリも一緒に削除されます。',
|
|
5822
|
+
'library.colSource': '原文',
|
|
5823
|
+
'library.colTarget': '訳文',
|
|
5824
|
+
'library.colNote': 'メモ',
|
|
5825
|
+
'library.colContext': 'コンテキスト',
|
|
5826
|
+
'library.colLang': '言語',
|
|
5827
|
+
'library.colCase': '大文字小文字を区別',
|
|
5828
|
+
'library.colUpdated': '更新日時',
|
|
5829
|
+
'library.detail': 'エントリ詳細',
|
|
5830
|
+
'library.detailEmpty': '行を選択すると表示・編集できます。',
|
|
5831
|
+
'library.save': '保存',
|
|
5832
|
+
'library.deleteEntry': 'エントリを削除',
|
|
5833
|
+
'library.entryCreated': 'エントリを追加しました',
|
|
5834
|
+
'library.entrySaved': '保存しました',
|
|
5835
|
+
'library.entryDeleted': '削除しました',
|
|
5836
|
+
'library.exported': 'エクスポートしました',
|
|
5837
|
+
'library.exportFormat': '形式',
|
|
5838
|
+
'library.caseSensitiveHint': '大文字小文字を区別すると、完全に同じ表記だけが一致します。',
|
|
5839
|
+
'library.noDocHint': '文書を開かなくてもライブラリを管理できます。',
|
|
5840
|
+
'rail.pageInfo': '{start}–{end} 件 / 全 {total} 件',
|
|
5841
|
+
'rail.goto': '番号へ移動',
|
|
5842
|
+
'rail.gotoInvalid': '番号が範囲外です。',
|
|
5843
|
+
'rail.nextUnconfirmed': '次の未確認へ',
|
|
5844
|
+
'panel.searchInLibrary': 'ライブラリを検索',
|
|
5845
|
+
'panel.viewAll': 'ライブラリで全件表示',
|
|
5846
|
+
'panel.glossaryMatchesOnly': 'ここには現在のセグメントに一致する用語だけを表示します。',
|
|
5847
|
+
'nav.align': '整列',
|
|
5848
|
+
'align.title': '2つのファイルを整列',
|
|
5849
|
+
'align.hint': '原文ファイルと訳文ファイルをそれぞれ選んでください。段落ごとに 1 対 1 で対応づけます。言語が違っても構いません。',
|
|
5850
|
+
'align.sourceFile': '原文ファイル',
|
|
5851
|
+
'align.targetFile': '訳文ファイル',
|
|
5852
|
+
'align.pick': 'ファイル未選択',
|
|
5853
|
+
'align.choose': '選択',
|
|
5854
|
+
'align.chars': '{count} 文字',
|
|
5855
|
+
'align.run': '整列する',
|
|
5856
|
+
'align.running': '整列中…',
|
|
5857
|
+
'align.needFiles': '先に 2 つのファイルを選んでください。',
|
|
5858
|
+
'align.readError': 'ファイルを読み込めませんでした。',
|
|
5859
|
+
'align.sourceLang': '原文言語(任意)',
|
|
5860
|
+
'align.targetLang': '訳文言語(任意)',
|
|
5861
|
+
'align.stats': '高 {high} · 要確認 {medium} · 要注意 {low}',
|
|
5862
|
+
'align.unpairedSource': '未対応の原文',
|
|
5863
|
+
'align.unpairedTarget': '未対応の訳文',
|
|
5864
|
+
'align.blocked': '大きいファイルは段落ブロックごとに整列します。全体整列より不安定になることがあります。',
|
|
5865
|
+
'align.confidence': '信頼度',
|
|
5866
|
+
'align.merge': '次の行と結合',
|
|
5867
|
+
'align.split': '改行で分割',
|
|
5868
|
+
'align.remove': 'この行を削除',
|
|
5869
|
+
'align.write': 'メモリに保存',
|
|
5870
|
+
'align.writeCount': '選択した {count} 件を保存',
|
|
5871
|
+
'align.writeMessage': '{count} 件を保存します。同一の項目はスキップされます。',
|
|
5872
|
+
'align.written': '{written} 件を保存、{skipped} 件をスキップしました。',
|
|
5873
|
+
'align.nothing': '保存する行がありません。',
|
|
5874
|
+
'origin.auto': '自動反映',
|
|
5875
|
+
'rail.propagate': '重複セグメントを埋める',
|
|
5876
|
+
'rail.propagateMessage': '同じ原文の他のセグメントに既存の訳文をコピーします。確定済みは変更しません。',
|
|
5877
|
+
'toast.propagated': '{count} 件のセグメントを埋めました。',
|
|
5878
|
+
'term.title': '用語候補',
|
|
5879
|
+
'term.hint': '出現回数と長さで並んでいます。実際の用語だけを選び、訳語を入れて登録してください。',
|
|
5880
|
+
'term.extract': '文書から用語を抽出',
|
|
5881
|
+
'term.empty': '繰り返し現れる候補はありませんでした。',
|
|
5882
|
+
'term.add': '用語集に追加',
|
|
5883
|
+
'term.added': '{added} 件の用語を追加しました。',
|
|
5884
|
+
'term.noDoc': '先に文書を開いてください。',
|
|
5885
|
+
'term.inLibrary': '登録済み',
|
|
3565
5886
|
}
|
|
3566
5887
|
|
|
3567
5888
|
// UI languages offered by the in-app switch. 'auto' follows the DSH locale
|
|
@@ -3639,6 +5960,19 @@ window.__ModuleLoader__.load({
|
|
|
3639
5960
|
scaleCssLengths,
|
|
3640
5961
|
caitUiBase: CAIT_UI_BASE,
|
|
3641
5962
|
caitUiMaxUnit: CAIT_UI_MAX_UNIT,
|
|
5963
|
+
caitUiDamping: CAIT_UI_DAMPING,
|
|
5964
|
+
caitThemeAttr: CAIT_THEME_ATTR,
|
|
5965
|
+
detectThemeKind,
|
|
5966
|
+
normalizePageSize,
|
|
5967
|
+
readPageSize,
|
|
5968
|
+
writePageSize,
|
|
5969
|
+
pageCount,
|
|
5970
|
+
clampPage,
|
|
5971
|
+
pageBounds,
|
|
5972
|
+
segmentNumberToIndex,
|
|
5973
|
+
pageSizeOptions: PAGE_SIZE_OPTIONS,
|
|
5974
|
+
pageSizeDefault: PAGE_SIZE_DEFAULT,
|
|
5975
|
+
pageSizeStorageKey: PAGE_SIZE_STORAGE_KEY,
|
|
3642
5976
|
cssScaled: CSS_SCALED,
|
|
3643
5977
|
syncPollMs: SYNC_POLL_MS,
|
|
3644
5978
|
makeTranslator,
|