dsh-cait 1.0.4 → 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 +202 -0
- package/README.ja.md +147 -25
- package/README.md +176 -27
- package/README.zh-CN.md +137 -26
- package/client/components.js +2047 -340
- package/client/entry.js +13 -0
- package/client/helpers.js +110 -7
- package/client/locales.js +435 -6
- package/client/rpc.js +1 -1
- package/client/styles.js +137 -3
- package/client.js +2742 -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/docx.js +12 -9
- package/lib/epub.js +355 -0
- package/lib/formats.js +88 -33
- package/lib/glossary.js +4 -4
- package/lib/html.js +271 -0
- package/lib/import-export.js +208 -95
- 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 +92 -7
- package/lib/pre-translate.js +263 -0
- package/lib/preprocess.js +65 -0
- package/lib/propagate.js +133 -0
- package/lib/qa.js +167 -0
- package/lib/store-utils.js +48 -6
- package/lib/store.js +143 -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 +2 -2
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
|
`
|
|
@@ -421,7 +658,7 @@ window.__ModuleLoader__.load({
|
|
|
421
658
|
|
|
422
659
|
function filePayload(file) {
|
|
423
660
|
const extension = (file.name.match(/\.([a-z0-9]+)$/i)?.[1] || '').toLowerCase()
|
|
424
|
-
if (extension === 'docx' || extension === 'xlsx' || extension === 'pptx') {
|
|
661
|
+
if (extension === 'docx' || extension === 'xlsx' || extension === 'pptx' || extension === 'epub') {
|
|
425
662
|
return file.arrayBuffer().then((buffer) => {
|
|
426
663
|
const bytes = new Uint8Array(buffer)
|
|
427
664
|
let binary = ''
|
|
@@ -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' },
|
|
@@ -1528,7 +2131,7 @@ window.__ModuleLoader__.load({
|
|
|
1528
2131
|
h('input', {
|
|
1529
2132
|
ref: inputRef,
|
|
1530
2133
|
type: 'file',
|
|
1531
|
-
accept: '.txt,.md,.markdown,.tmx,.docx,.csv,.srt,.vtt,.po,.pot,.xliff,.xlf,.sdlxliff,.html,.htm,.json,.rtf,.xlsx,.pptx',
|
|
2134
|
+
accept: '.txt,.md,.markdown,.tmx,.docx,.epub,.csv,.srt,.vtt,.po,.pot,.xliff,.xlf,.sdlxliff,.html,.htm,.json,.rtf,.xlsx,.pptx',
|
|
1532
2135
|
hidden: true,
|
|
1533
2136
|
onChange: (event) => pickFile(event.target.files?.[0]),
|
|
1534
2137
|
}),
|
|
@@ -1550,21 +2153,1185 @@ window.__ModuleLoader__.load({
|
|
|
1550
2153
|
)
|
|
1551
2154
|
}
|
|
1552
2155
|
|
|
1553
|
-
function NewDocModal({ t, settings, busy, defaultMode, onClose, onCreate }) {
|
|
1554
|
-
return h(
|
|
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
|
-
|
|
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
|
+
),
|
|
3194
|
+
h(
|
|
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')),
|
|
3281
|
+
),
|
|
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,
|
|
3330
|
+
)
|
|
3331
|
+
}
|
|
3332
|
+
|
|
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'],
|
|
@@ -1572,6 +3339,7 @@ window.__ModuleLoader__.load({
|
|
|
1572
3339
|
['target-txt', t('modal.export.targetTxt'), t('modal.export.target'), 'download'],
|
|
1573
3340
|
['target-md', t('modal.export.targetMd'), t('modal.export.target'), 'download'],
|
|
1574
3341
|
['docx', t('modal.export.docx'), t('modal.export.docxGroup'), 'file'],
|
|
3342
|
+
['epub', t('modal.export.epub'), t('modal.export.epubGroup'), 'file'],
|
|
1575
3343
|
['srt', t('modal.export.srt'), t('modal.export.subtitleGroup'), 'file'],
|
|
1576
3344
|
['vtt', t('modal.export.vtt'), t('modal.export.subtitleGroup'), 'file'],
|
|
1577
3345
|
['po', t('modal.export.po'), t('modal.export.localizationGroup'), 'file'],
|
|
@@ -1589,6 +3357,23 @@ window.__ModuleLoader__.load({
|
|
|
1589
3357
|
Modal,
|
|
1590
3358
|
{ title: t('modal.export.title'), onClose, wide: true },
|
|
1591
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,
|
|
1592
3377
|
h(
|
|
1593
3378
|
'div',
|
|
1594
3379
|
{ className: 'cait-export-grid' },
|
|
@@ -1611,6 +3396,11 @@ window.__ModuleLoader__.load({
|
|
|
1611
3396
|
fuzzyThreshold: settings.fuzzyThreshold,
|
|
1612
3397
|
tmLimit: settings.tmLimit,
|
|
1613
3398
|
uiLang: settings.uiLang || 'auto',
|
|
3399
|
+
preprocess: {
|
|
3400
|
+
normalizeWhitespace: settings.preprocess?.normalizeWhitespace !== false,
|
|
3401
|
+
stripInvisible: settings.preprocess?.stripInvisible !== false,
|
|
3402
|
+
collapseBlankLines: settings.preprocess?.collapseBlankLines !== false,
|
|
3403
|
+
},
|
|
1614
3404
|
mt: {
|
|
1615
3405
|
provider: settings.mt.provider,
|
|
1616
3406
|
model: settings.mt.model,
|
|
@@ -1623,6 +3413,8 @@ window.__ModuleLoader__.load({
|
|
|
1623
3413
|
const routeMissing = form.mt.provider !== 'mock' && !route
|
|
1624
3414
|
|
|
1625
3415
|
const setMt = (patch) => setForm((current) => ({ ...current, mt: { ...current.mt, ...patch } }))
|
|
3416
|
+
const setPre = (patch) =>
|
|
3417
|
+
setForm((current) => ({ ...current, preprocess: { ...current.preprocess, ...patch } }))
|
|
1626
3418
|
const selectProvider = (provider) => {
|
|
1627
3419
|
const nextRoute = routes.find((item) => item.id === provider)
|
|
1628
3420
|
setForm((current) => ({
|
|
@@ -1646,6 +3438,11 @@ window.__ModuleLoader__.load({
|
|
|
1646
3438
|
fuzzyThreshold: Number(form.fuzzyThreshold),
|
|
1647
3439
|
tmLimit: Number(form.tmLimit),
|
|
1648
3440
|
uiLang: form.uiLang,
|
|
3441
|
+
preprocess: {
|
|
3442
|
+
normalizeWhitespace: Boolean(form.preprocess.normalizeWhitespace),
|
|
3443
|
+
stripInvisible: Boolean(form.preprocess.stripInvisible),
|
|
3444
|
+
collapseBlankLines: Boolean(form.preprocess.collapseBlankLines),
|
|
3445
|
+
},
|
|
1649
3446
|
mt: {
|
|
1650
3447
|
provider: form.mt.provider,
|
|
1651
3448
|
model: form.mt.model,
|
|
@@ -1712,6 +3509,24 @@ window.__ModuleLoader__.load({
|
|
|
1712
3509
|
),
|
|
1713
3510
|
),
|
|
1714
3511
|
),
|
|
3512
|
+
h(
|
|
3513
|
+
'div',
|
|
3514
|
+
{ className: 'cait-settings-section' },
|
|
3515
|
+
h('h4', { className: 'cait-settings-title' }, t('modal.settings.preprocess')),
|
|
3516
|
+
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('modal.settings.preprocessHint')),
|
|
3517
|
+
['normalizeWhitespace', 'stripInvisible', 'collapseBlankLines'].map((key) =>
|
|
3518
|
+
h(
|
|
3519
|
+
'label',
|
|
3520
|
+
{ key, className: 'cait-checkline' },
|
|
3521
|
+
h('input', {
|
|
3522
|
+
type: 'checkbox',
|
|
3523
|
+
checked: Boolean(form.preprocess[key]),
|
|
3524
|
+
onChange: (event) => setPre({ [key]: event.target.checked }),
|
|
3525
|
+
}),
|
|
3526
|
+
t(`modal.settings.pre.${key}`),
|
|
3527
|
+
),
|
|
3528
|
+
),
|
|
3529
|
+
),
|
|
1715
3530
|
h(
|
|
1716
3531
|
'div',
|
|
1717
3532
|
{ className: 'cait-settings-section' },
|
|
@@ -1761,7 +3576,13 @@ window.__ModuleLoader__.load({
|
|
|
1761
3576
|
},
|
|
1762
3577
|
h('option', { value: 'mock' }, t('mt.mock')),
|
|
1763
3578
|
routes.map((item) =>
|
|
1764
|
-
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
|
+
),
|
|
1765
3586
|
),
|
|
1766
3587
|
routeMissing
|
|
1767
3588
|
? h('option', { value: form.mt.provider }, t('modal.settings.routeMissing'))
|
|
@@ -1826,139 +3647,6 @@ window.__ModuleLoader__.load({
|
|
|
1826
3647
|
)
|
|
1827
3648
|
}
|
|
1828
3649
|
|
|
1829
|
-
function LibrariesModal({ t, data, busy, onClose, onSetMounts, onCreate }) {
|
|
1830
|
-
const libraries = data.libraries || { tm: [], glossary: [] }
|
|
1831
|
-
const mounts = data.mounts || {
|
|
1832
|
-
tmIds: [],
|
|
1833
|
-
glossaryIds: [],
|
|
1834
|
-
defaultTmId: '',
|
|
1835
|
-
defaultGlossaryId: '',
|
|
1836
|
-
}
|
|
1837
|
-
const [tmName, setTmName] = useState('')
|
|
1838
|
-
const [glossaryName, setGlossaryName] = useState('')
|
|
1839
|
-
|
|
1840
|
-
const toggle = (kind, id, checked) => {
|
|
1841
|
-
const current = kind === 'tm' ? mounts.tmIds : mounts.glossaryIds
|
|
1842
|
-
const next = computeNextMountIds(current, id, checked)
|
|
1843
|
-
if (!next) return
|
|
1844
|
-
onSetMounts(kind === 'tm' ? { tmIds: next } : { glossaryIds: next })
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
const create = async (kind, name, reset) => {
|
|
1848
|
-
if (!name.trim()) return
|
|
1849
|
-
const ok = await onCreate({ kind, name: name.trim() })
|
|
1850
|
-
if (ok) reset('')
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
const section = (kind, title, list, defaultId, ids) =>
|
|
1854
|
-
h(
|
|
1855
|
-
'div',
|
|
1856
|
-
{ className: 'cait-libsection' },
|
|
1857
|
-
h(
|
|
1858
|
-
'div',
|
|
1859
|
-
{ className: 'cait-libhead' },
|
|
1860
|
-
h('h4', { className: 'cait-settings-title' }, title),
|
|
1861
|
-
h('span', { className: 'cait-count' }, `${ids.length}/${list.length}`),
|
|
1862
|
-
),
|
|
1863
|
-
list.length
|
|
1864
|
-
? list.map((library) => {
|
|
1865
|
-
const id = kind === 'tm' ? library.tmId : library.glossaryId
|
|
1866
|
-
const mounted = ids.includes(id)
|
|
1867
|
-
return h(
|
|
1868
|
-
'div',
|
|
1869
|
-
{ className: 'cait-librow', key: id },
|
|
1870
|
-
h(
|
|
1871
|
-
'label',
|
|
1872
|
-
{ className: 'cait-libcheck' },
|
|
1873
|
-
h('input', {
|
|
1874
|
-
type: 'checkbox',
|
|
1875
|
-
checked: mounted,
|
|
1876
|
-
disabled: isMountToggleDisabled({
|
|
1877
|
-
mounted,
|
|
1878
|
-
mountedCount: ids.length,
|
|
1879
|
-
busy: Boolean(busy),
|
|
1880
|
-
}),
|
|
1881
|
-
onChange: (event) => toggle(kind, id, event.target.checked),
|
|
1882
|
-
}),
|
|
1883
|
-
h(
|
|
1884
|
-
'span',
|
|
1885
|
-
{ className: 'cait-libmeta' },
|
|
1886
|
-
h('strong', null, library.name),
|
|
1887
|
-
h(
|
|
1888
|
-
'small',
|
|
1889
|
-
null,
|
|
1890
|
-
`${library.entryCount || 0} ${t('libraries.entries')}${
|
|
1891
|
-
library.mounted ? '' : ` · ${t('libraries.notMounted')}`
|
|
1892
|
-
}`,
|
|
1893
|
-
),
|
|
1894
|
-
),
|
|
1895
|
-
),
|
|
1896
|
-
h(
|
|
1897
|
-
'label',
|
|
1898
|
-
{ className: 'cait-libdefault' },
|
|
1899
|
-
h('input', {
|
|
1900
|
-
type: 'radio',
|
|
1901
|
-
name: `cait-default-${kind}`,
|
|
1902
|
-
checked: defaultId === id,
|
|
1903
|
-
disabled: Boolean(busy) || !mounted,
|
|
1904
|
-
onChange: () =>
|
|
1905
|
-
onSetMounts(
|
|
1906
|
-
kind === 'tm'
|
|
1907
|
-
? { defaultTmId: id }
|
|
1908
|
-
: { defaultGlossaryId: id },
|
|
1909
|
-
),
|
|
1910
|
-
}),
|
|
1911
|
-
t(kind === 'tm' ? 'libraries.defaultTm' : 'libraries.defaultGlossary'),
|
|
1912
|
-
),
|
|
1913
|
-
)
|
|
1914
|
-
})
|
|
1915
|
-
: h('p', { className: 'cait-empty-note' }, t('libraries.empty')),
|
|
1916
|
-
h(
|
|
1917
|
-
'form',
|
|
1918
|
-
{
|
|
1919
|
-
className: 'cait-libcreate',
|
|
1920
|
-
onSubmit: (event) => {
|
|
1921
|
-
event.preventDefault()
|
|
1922
|
-
create(kind, kind === 'tm' ? tmName : glossaryName, kind === 'tm' ? setTmName : setGlossaryName)
|
|
1923
|
-
},
|
|
1924
|
-
},
|
|
1925
|
-
h('input', {
|
|
1926
|
-
className: 'cait-input',
|
|
1927
|
-
value: kind === 'tm' ? tmName : glossaryName,
|
|
1928
|
-
onChange: (event) =>
|
|
1929
|
-
kind === 'tm' ? setTmName(event.target.value) : setGlossaryName(event.target.value),
|
|
1930
|
-
placeholder: t('libraries.namePlaceholder'),
|
|
1931
|
-
disabled: Boolean(busy),
|
|
1932
|
-
}),
|
|
1933
|
-
h(
|
|
1934
|
-
'button',
|
|
1935
|
-
{ className: 'cait-btn', type: 'submit', disabled: Boolean(busy) },
|
|
1936
|
-
h(Icon, { name: 'plus', size: 12 }),
|
|
1937
|
-
t('libraries.create'),
|
|
1938
|
-
),
|
|
1939
|
-
),
|
|
1940
|
-
)
|
|
1941
|
-
|
|
1942
|
-
return h(
|
|
1943
|
-
Modal,
|
|
1944
|
-
{ title: t('libraries.title'), onClose, wide: true },
|
|
1945
|
-
h('p', { className: 'cait-mt-hint', style: { marginTop: 0 } }, t('libraries.desc')),
|
|
1946
|
-
section('tm', t('libraries.tm'), libraries.tm, mounts.defaultTmId, mounts.tmIds),
|
|
1947
|
-
section(
|
|
1948
|
-
'glossary',
|
|
1949
|
-
t('libraries.glossary'),
|
|
1950
|
-
libraries.glossary,
|
|
1951
|
-
mounts.defaultGlossaryId,
|
|
1952
|
-
mounts.glossaryIds,
|
|
1953
|
-
),
|
|
1954
|
-
h(
|
|
1955
|
-
'div',
|
|
1956
|
-
{ className: 'cait-modal-actions' },
|
|
1957
|
-
h('button', { className: 'cait-btn cait-btn-primary', onClick: onClose }, t('libraries.done')),
|
|
1958
|
-
),
|
|
1959
|
-
)
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
3650
|
function ConfirmModal({ t, state, busy, onClose, onConfirm }) {
|
|
1963
3651
|
return h(
|
|
1964
3652
|
Modal,
|
|
@@ -2011,8 +3699,22 @@ window.__ModuleLoader__.load({
|
|
|
2011
3699
|
const [dirty, setDirty] = useState(false)
|
|
2012
3700
|
const [tab, setTab] = useState('tm')
|
|
2013
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)
|
|
2014
3715
|
const [tmQuery, setTmQuery] = useState('')
|
|
2015
3716
|
const [tmResults, setTmResults] = useState(null)
|
|
3717
|
+
const [tmRefresh, setTmRefresh] = useState(0)
|
|
2016
3718
|
const [tmError, setTmError] = useState('')
|
|
2017
3719
|
const [mt, setMt] = useState(null)
|
|
2018
3720
|
const [mtError, setMtError] = useState('')
|
|
@@ -2127,6 +3829,61 @@ window.__ModuleLoader__.load({
|
|
|
2127
3829
|
}
|
|
2128
3830
|
}
|
|
2129
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
|
+
|
|
2130
3887
|
useEffect(() => {
|
|
2131
3888
|
let alive = true
|
|
2132
3889
|
setLoading(true)
|
|
@@ -2183,6 +3940,26 @@ window.__ModuleLoader__.load({
|
|
|
2183
3940
|
}
|
|
2184
3941
|
}, [data?.revision])
|
|
2185
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
|
+
|
|
2186
3963
|
useEffect(() => {
|
|
2187
3964
|
let alive = true
|
|
2188
3965
|
rpc('listMtRoutes')
|
|
@@ -2240,7 +4017,7 @@ window.__ModuleLoader__.load({
|
|
|
2240
4017
|
cancelled = true
|
|
2241
4018
|
clearTimeout(timer)
|
|
2242
4019
|
}
|
|
2243
|
-
}, [tmQuery, doc?.id, doc?.sourceLang, doc?.targetLang, settings.fuzzyThreshold, settings.tmLimit])
|
|
4020
|
+
}, [tmQuery, tmRefresh, doc?.id, doc?.sourceLang, doc?.targetLang, settings.fuzzyThreshold, settings.tmLimit])
|
|
2244
4021
|
|
|
2245
4022
|
const saveDraft = async (silent = true) => {
|
|
2246
4023
|
if (!doc || !dirty) return true
|
|
@@ -2297,6 +4074,24 @@ window.__ModuleLoader__.load({
|
|
|
2297
4074
|
}
|
|
2298
4075
|
}
|
|
2299
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
|
+
|
|
2300
4095
|
const moveSegment = async (delta) => {
|
|
2301
4096
|
if (!doc || busy) return
|
|
2302
4097
|
setBusy('move')
|
|
@@ -2417,8 +4212,72 @@ window.__ModuleLoader__.load({
|
|
|
2417
4212
|
setTimeout(() => textareaRef.current?.focus(), 0)
|
|
2418
4213
|
}
|
|
2419
4214
|
|
|
2420
|
-
const
|
|
2421
|
-
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')
|
|
2422
4281
|
try {
|
|
2423
4282
|
const result = await rpc('addGlossary', {
|
|
2424
4283
|
...form,
|
|
@@ -2558,6 +4417,37 @@ window.__ModuleLoader__.load({
|
|
|
2558
4417
|
}
|
|
2559
4418
|
}
|
|
2560
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
|
+
|
|
2561
4451
|
const exportAs = async (format) => {
|
|
2562
4452
|
if (!doc) return
|
|
2563
4453
|
setBusy('export')
|
|
@@ -2673,13 +4563,40 @@ window.__ModuleLoader__.load({
|
|
|
2673
4563
|
element.style.setProperty(CAIT_UNIT_VAR, String(unit))
|
|
2674
4564
|
}
|
|
2675
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
|
+
|
|
2676
4587
|
if (typeof ResizeObserver === 'undefined') {
|
|
2677
4588
|
window.addEventListener('resize', measure)
|
|
2678
|
-
return () =>
|
|
4589
|
+
return () => {
|
|
4590
|
+
window.removeEventListener('resize', measure)
|
|
4591
|
+
stopTheme()
|
|
4592
|
+
}
|
|
2679
4593
|
}
|
|
2680
4594
|
const observer = new ResizeObserver(measure)
|
|
2681
4595
|
observer.observe(element)
|
|
2682
|
-
return () =>
|
|
4596
|
+
return () => {
|
|
4597
|
+
observer.disconnect()
|
|
4598
|
+
stopTheme()
|
|
4599
|
+
}
|
|
2683
4600
|
}, [])
|
|
2684
4601
|
|
|
2685
4602
|
if (loading) {
|
|
@@ -2726,12 +4643,45 @@ window.__ModuleLoader__.load({
|
|
|
2726
4643
|
onCloseDoc: closeDocumentTab,
|
|
2727
4644
|
onNewTab: () => setModal({ type: 'new', defaultMode: 'file' }),
|
|
2728
4645
|
onCleanClosed: () => setModal({ type: 'recent' }),
|
|
4646
|
+
pre,
|
|
4647
|
+
onPreTranslate: runPreTranslate,
|
|
4648
|
+
onCancelPreTranslate: cancelPreTranslate,
|
|
4649
|
+
view,
|
|
4650
|
+
onView: setView,
|
|
2729
4651
|
}),
|
|
2730
4652
|
h(SaveBanner, { t, storage: data?.storage, stale, onRefresh: refreshState }),
|
|
2731
|
-
|
|
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(
|
|
2732
4682
|
'div',
|
|
2733
4683
|
{ className: `cait-body${doc ? '' : ' cait-no-doc'}` },
|
|
2734
|
-
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,
|
|
2735
4685
|
h(
|
|
2736
4686
|
'div',
|
|
2737
4687
|
{ className: `cait-main${doc ? '' : ' cait-main-empty'}` },
|
|
@@ -2753,6 +4703,7 @@ window.__ModuleLoader__.load({
|
|
|
2753
4703
|
onNext: () => moveSegment(1),
|
|
2754
4704
|
onPrev: () => moveSegment(-1),
|
|
2755
4705
|
onSave: () => saveDraft(false),
|
|
4706
|
+
onAdopt: adoptSuggestion,
|
|
2756
4707
|
onApplyTerm: applyTerm,
|
|
2757
4708
|
textareaRef,
|
|
2758
4709
|
})
|
|
@@ -2771,6 +4722,8 @@ window.__ModuleLoader__.load({
|
|
|
2771
4722
|
tmResults,
|
|
2772
4723
|
tmError,
|
|
2773
4724
|
onApplyTm: applyTm,
|
|
4725
|
+
onEditTm: editTm,
|
|
4726
|
+
onDeleteTm: deleteTm,
|
|
2774
4727
|
onImportTmx: importTmxFile,
|
|
2775
4728
|
onClearTm: clearTm,
|
|
2776
4729
|
onAddTerm: addTerm,
|
|
@@ -2791,12 +4744,13 @@ window.__ModuleLoader__.load({
|
|
|
2791
4744
|
},
|
|
2792
4745
|
onCopy: copy,
|
|
2793
4746
|
onOpenSettings: () => setModal({ type: 'settings' }),
|
|
2794
|
-
|
|
4747
|
+
onSearchLibrary: openLibrary,
|
|
2795
4748
|
busy,
|
|
2796
4749
|
})
|
|
2797
4750
|
: null,
|
|
2798
4751
|
),
|
|
2799
|
-
)
|
|
4752
|
+
)
|
|
4753
|
+
: null,
|
|
2800
4754
|
modal?.type === 'new'
|
|
2801
4755
|
? h(NewDocModal, {
|
|
2802
4756
|
t,
|
|
@@ -2808,21 +4762,11 @@ window.__ModuleLoader__.load({
|
|
|
2808
4762
|
})
|
|
2809
4763
|
: null,
|
|
2810
4764
|
modal?.type === 'export'
|
|
2811
|
-
? h(ExportModal, { t, busy, onClose: () => setModal(null), onExport: exportAs })
|
|
4765
|
+
? h(ExportModal, { t, busy, qa, onClose: () => setModal(null), onExport: exportAs })
|
|
2812
4766
|
: null,
|
|
2813
4767
|
modal?.type === 'settings'
|
|
2814
4768
|
? h(SettingsModal, { t, settings, mtRoutes, busy, onClose: () => setModal(null), onSave: saveSettings })
|
|
2815
4769
|
: null,
|
|
2816
|
-
modal?.type === 'libraries'
|
|
2817
|
-
? h(LibrariesModal, {
|
|
2818
|
-
t,
|
|
2819
|
-
data,
|
|
2820
|
-
busy,
|
|
2821
|
-
onClose: () => setModal(null),
|
|
2822
|
-
onSetMounts: setMounts,
|
|
2823
|
-
onCreate: createLibrary,
|
|
2824
|
-
})
|
|
2825
|
-
: null,
|
|
2826
4770
|
modal?.type === 'recent'
|
|
2827
4771
|
? h(RecentDocsModal, {
|
|
2828
4772
|
t,
|
|
@@ -2889,6 +4833,40 @@ window.__ModuleLoader__.load({
|
|
|
2889
4833
|
'editor.emptyTarget': '请先输入译文',
|
|
2890
4834
|
'editor.done': '全部句段已确认',
|
|
2891
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} 段失败',
|
|
2892
4870
|
'empty.title': '开始一次 CAT 翻译',
|
|
2893
4871
|
'empty.desc': '粘贴文本、拖入文件,或加载演示文档。CAIT 会把源文切成句段,并同时查询翻译记忆与术语库;确认译文后自动入库。',
|
|
2894
4872
|
'empty.paste': '粘贴文本',
|
|
@@ -2899,7 +4877,7 @@ window.__ModuleLoader__.load({
|
|
|
2899
4877
|
'empty.demo': '加载演示文档',
|
|
2900
4878
|
'empty.demoHint': '没有素材?',
|
|
2901
4879
|
'empty.drop': '拖入文件,或点击选择',
|
|
2902
|
-
'empty.formats': '支持 TXT · Markdown · DOCX · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
4880
|
+
'empty.formats': '支持 TXT · Markdown · DOCX · EPUB · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
2903
4881
|
'empty.features.tm': '翻译记忆',
|
|
2904
4882
|
'empty.features.tmDesc': '精确 + 模糊匹配',
|
|
2905
4883
|
'empty.features.glossary': '术语库',
|
|
@@ -2913,7 +4891,7 @@ window.__ModuleLoader__.load({
|
|
|
2913
4891
|
'modal.new.targetLang': '目标语言',
|
|
2914
4892
|
'modal.new.create': '创建文档',
|
|
2915
4893
|
'modal.new.importing': '正在导入…',
|
|
2916
|
-
'modal.new.dropHint': 'TXT · Markdown · DOCX · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
4894
|
+
'modal.new.dropHint': 'TXT · Markdown · DOCX · EPUB · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
2917
4895
|
'modal.new.csvSource': 'CSV 源文列',
|
|
2918
4896
|
'modal.new.csvTarget': 'CSV 译文列',
|
|
2919
4897
|
'modal.new.csvColumnPlaceholder': '列名或列号,例如 Source / 2',
|
|
@@ -2937,6 +4915,13 @@ window.__ModuleLoader__.load({
|
|
|
2937
4915
|
'libraries.create': '新建',
|
|
2938
4916
|
'libraries.done': '完成',
|
|
2939
4917
|
'libraries.created': '库已创建并自动挂载',
|
|
4918
|
+
'libraries.rename': '重命名',
|
|
4919
|
+
'libraries.save': '保存',
|
|
4920
|
+
'libraries.renamed': '库已重命名',
|
|
4921
|
+
'libraries.delete': '删除库',
|
|
4922
|
+
'libraries.deleted': '库已删除',
|
|
4923
|
+
'libraries.deleteTmWarn': '库里的记忆条目也会一起删除,无法撤销。',
|
|
4924
|
+
'libraries.deleteGlossaryWarn': '库里的术语也会一起删除,无法撤销。',
|
|
2940
4925
|
'libraries.mountSaved': '挂载设置已保存',
|
|
2941
4926
|
'panel.tm': '记忆库',
|
|
2942
4927
|
'panel.glossary': '术语库',
|
|
@@ -2954,6 +4939,9 @@ window.__ModuleLoader__.load({
|
|
|
2954
4939
|
'match.high_fuzzy': '高模糊',
|
|
2955
4940
|
'match.medium_fuzzy': '中模糊',
|
|
2956
4941
|
'match.low_fuzzy': '低模糊',
|
|
4942
|
+
'match.context': '上下文匹配',
|
|
4943
|
+
'panel.contextPrev': '上文',
|
|
4944
|
+
'panel.contextNext': '下文',
|
|
2957
4945
|
'panel.apply': '应用',
|
|
2958
4946
|
'panel.importTmx': '导入 TMX',
|
|
2959
4947
|
'panel.clearTm': '清空记忆库',
|
|
@@ -2969,6 +4957,8 @@ window.__ModuleLoader__.load({
|
|
|
2969
4957
|
'panel.saveTerm': '保存术语',
|
|
2970
4958
|
'panel.cancel': '取消',
|
|
2971
4959
|
'panel.delete': '删除',
|
|
4960
|
+
'panel.edit': '编辑',
|
|
4961
|
+
'panel.saveEntry': '保存修改',
|
|
2972
4962
|
'panel.noTerms': '术语库为空。添加术语后,命中会自动高亮。',
|
|
2973
4963
|
'panel.importCsv': '导入 CSV/TSV',
|
|
2974
4964
|
'panel.clearGlossary': '清空术语库',
|
|
@@ -2998,6 +4988,8 @@ window.__ModuleLoader__.load({
|
|
|
2998
4988
|
'modal.export.targetMd': 'Markdown',
|
|
2999
4989
|
'modal.export.docx': 'Word DOCX',
|
|
3000
4990
|
'modal.export.docxGroup': '保留源格式',
|
|
4991
|
+
'modal.export.epub': 'EPUB 电子书',
|
|
4992
|
+
'modal.export.epubGroup': '保留源排版',
|
|
3001
4993
|
'modal.export.json': '文档 JSON',
|
|
3002
4994
|
'modal.export.subtitleGroup': '字幕',
|
|
3003
4995
|
'modal.export.localizationGroup': '本地化交换',
|
|
@@ -3020,6 +5012,11 @@ window.__ModuleLoader__.load({
|
|
|
3020
5012
|
'modal.settings.uiAuto': '跟随 DSH 语言',
|
|
3021
5013
|
'modal.settings.uiHint': '只影响 CAIT 界面,不会改变文档的源语言和目标语言。',
|
|
3022
5014
|
'modal.settings.languages': '语言',
|
|
5015
|
+
'modal.settings.preprocess': '预处理',
|
|
5016
|
+
'modal.settings.preprocessHint': '在导入切句之前清理源文,占位符与标点不会被改写。',
|
|
5017
|
+
'modal.settings.pre.normalizeWhitespace': '折叠多余空格与制表符',
|
|
5018
|
+
'modal.settings.pre.stripInvisible': '移除零宽字符与控制字符',
|
|
5019
|
+
'modal.settings.pre.collapseBlankLines': '合并连续空行',
|
|
3023
5020
|
'modal.settings.tm': '翻译记忆',
|
|
3024
5021
|
'modal.settings.fuzzy': '模糊匹配阈值',
|
|
3025
5022
|
'modal.settings.limit': '返回匹配数',
|
|
@@ -3040,6 +5037,7 @@ window.__ModuleLoader__.load({
|
|
|
3040
5037
|
'modal.confirm.clearGlossary':
|
|
3041
5038
|
'清空当前挂载的术语?未挂载库不会受影响;如要清空全部库,请在 API 调用中显式传 all: true。此操作无法撤销。',
|
|
3042
5039
|
'modal.confirm.deleteTerm': '删除术语',
|
|
5040
|
+
'modal.confirm.deleteTm': '删除记忆库条目',
|
|
3043
5041
|
'modal.confirm.ok': '确认',
|
|
3044
5042
|
'modal.confirm.cancel': '取消',
|
|
3045
5043
|
'toast.confirmed': '句段已确认并写入记忆库',
|
|
@@ -3052,6 +5050,8 @@ window.__ModuleLoader__.load({
|
|
|
3052
5050
|
'toast.draftSaved': '草稿已保存',
|
|
3053
5051
|
'toast.termSaved': '术语已保存',
|
|
3054
5052
|
'toast.termDeleted': '术语已删除',
|
|
5053
|
+
'toast.tmUpdated': '记忆库条目已更新',
|
|
5054
|
+
'toast.tmDeleted': '记忆库条目已删除',
|
|
3055
5055
|
'toast.tmImported': 'TMX 已导入',
|
|
3056
5056
|
'toast.tmxMarkupLost': 'TMX 已导入;{count} 个内联占位符/格式标签已按纯文本处理,格式信息丢失。',
|
|
3057
5057
|
'toast.glossaryImported': '术语已导入',
|
|
@@ -3062,6 +5062,93 @@ window.__ModuleLoader__.load({
|
|
|
3062
5062
|
'toast.copied': '已复制到剪贴板',
|
|
3063
5063
|
'toast.mtReady': '译文建议已生成',
|
|
3064
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': '已有',
|
|
3065
5152
|
}
|
|
3066
5153
|
|
|
3067
5154
|
const EN = {
|
|
@@ -3113,6 +5200,40 @@ window.__ModuleLoader__.load({
|
|
|
3113
5200
|
'editor.emptyTarget': 'Enter a translation first',
|
|
3114
5201
|
'editor.done': 'All segments confirmed',
|
|
3115
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',
|
|
3116
5237
|
'empty.title': 'Start a CAT translation',
|
|
3117
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.',
|
|
3118
5239
|
'empty.paste': 'Paste text',
|
|
@@ -3123,7 +5244,7 @@ window.__ModuleLoader__.load({
|
|
|
3123
5244
|
'empty.demo': 'Load demo document',
|
|
3124
5245
|
'empty.demoHint': 'No material?',
|
|
3125
5246
|
'empty.drop': 'Drop a file, or click to choose',
|
|
3126
|
-
'empty.formats': 'TXT · Markdown · DOCX · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
5247
|
+
'empty.formats': 'TXT · Markdown · DOCX · EPUB · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
3127
5248
|
'empty.features.tm': 'Translation memory',
|
|
3128
5249
|
'empty.features.tmDesc': 'Exact + fuzzy matching',
|
|
3129
5250
|
'empty.features.glossary': 'Glossary',
|
|
@@ -3137,7 +5258,7 @@ window.__ModuleLoader__.load({
|
|
|
3137
5258
|
'modal.new.targetLang': 'Target language',
|
|
3138
5259
|
'modal.new.create': 'Create document',
|
|
3139
5260
|
'modal.new.importing': 'Importing…',
|
|
3140
|
-
'modal.new.dropHint': 'TXT · Markdown · DOCX · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
5261
|
+
'modal.new.dropHint': 'TXT · Markdown · DOCX · EPUB · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
3141
5262
|
'modal.new.csvSource': 'CSV source column',
|
|
3142
5263
|
'modal.new.csvTarget': 'CSV target column',
|
|
3143
5264
|
'modal.new.csvColumnPlaceholder': 'Column name or number, e.g. Source / 2',
|
|
@@ -3161,6 +5282,13 @@ window.__ModuleLoader__.load({
|
|
|
3161
5282
|
'libraries.create': 'Create',
|
|
3162
5283
|
'libraries.done': 'Done',
|
|
3163
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.',
|
|
3164
5292
|
'libraries.mountSaved': 'Mount settings saved',
|
|
3165
5293
|
'panel.tm': 'TM',
|
|
3166
5294
|
'panel.glossary': 'Glossary',
|
|
@@ -3178,6 +5306,9 @@ window.__ModuleLoader__.load({
|
|
|
3178
5306
|
'match.high_fuzzy': 'High fuzzy',
|
|
3179
5307
|
'match.medium_fuzzy': 'Medium fuzzy',
|
|
3180
5308
|
'match.low_fuzzy': 'Low fuzzy',
|
|
5309
|
+
'match.context': 'Context match',
|
|
5310
|
+
'panel.contextPrev': 'Prev',
|
|
5311
|
+
'panel.contextNext': 'Next',
|
|
3181
5312
|
'panel.apply': 'Apply',
|
|
3182
5313
|
'panel.importTmx': 'Import TMX',
|
|
3183
5314
|
'panel.clearTm': 'Clear TM',
|
|
@@ -3193,6 +5324,8 @@ window.__ModuleLoader__.load({
|
|
|
3193
5324
|
'panel.saveTerm': 'Save term',
|
|
3194
5325
|
'panel.cancel': 'Cancel',
|
|
3195
5326
|
'panel.delete': 'Delete',
|
|
5327
|
+
'panel.edit': 'Edit',
|
|
5328
|
+
'panel.saveEntry': 'Save changes',
|
|
3196
5329
|
'panel.noTerms': 'The glossary is empty. Hits will be highlighted automatically.',
|
|
3197
5330
|
'panel.importCsv': 'Import CSV/TSV',
|
|
3198
5331
|
'panel.clearGlossary': 'Clear glossary',
|
|
@@ -3222,6 +5355,8 @@ window.__ModuleLoader__.load({
|
|
|
3222
5355
|
'modal.export.targetMd': 'Markdown',
|
|
3223
5356
|
'modal.export.docx': 'Word DOCX',
|
|
3224
5357
|
'modal.export.docxGroup': 'Preserve source formatting',
|
|
5358
|
+
'modal.export.epub': 'EPUB ebook',
|
|
5359
|
+
'modal.export.epubGroup': 'Preserve source layout',
|
|
3225
5360
|
'modal.export.json': 'Document JSON',
|
|
3226
5361
|
'modal.export.subtitleGroup': 'Subtitles',
|
|
3227
5362
|
'modal.export.localizationGroup': 'Localization exchange',
|
|
@@ -3244,6 +5379,11 @@ window.__ModuleLoader__.load({
|
|
|
3244
5379
|
'modal.settings.uiAuto': 'Follow DSH language',
|
|
3245
5380
|
'modal.settings.uiHint': 'Changes only the CAIT interface; document source and target languages are unaffected.',
|
|
3246
5381
|
'modal.settings.languages': 'Languages',
|
|
5382
|
+
'modal.settings.preprocess': 'Preprocessing',
|
|
5383
|
+
'modal.settings.preprocessHint': 'Cleans imported source text before segmentation. Placeholders and punctuation are left untouched.',
|
|
5384
|
+
'modal.settings.pre.normalizeWhitespace': 'Collapse extra spaces and tabs',
|
|
5385
|
+
'modal.settings.pre.stripInvisible': 'Remove zero-width and control characters',
|
|
5386
|
+
'modal.settings.pre.collapseBlankLines': 'Merge consecutive blank lines',
|
|
3247
5387
|
'modal.settings.tm': 'Translation memory',
|
|
3248
5388
|
'modal.settings.fuzzy': 'Fuzzy-match threshold',
|
|
3249
5389
|
'modal.settings.limit': 'Matches returned',
|
|
@@ -3264,6 +5404,7 @@ window.__ModuleLoader__.load({
|
|
|
3264
5404
|
'modal.confirm.clearGlossary':
|
|
3265
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.',
|
|
3266
5406
|
'modal.confirm.deleteTerm': 'Delete term',
|
|
5407
|
+
'modal.confirm.deleteTm': 'Delete TM entry',
|
|
3267
5408
|
'modal.confirm.ok': 'Confirm',
|
|
3268
5409
|
'modal.confirm.cancel': 'Cancel',
|
|
3269
5410
|
'toast.confirmed': 'Segment confirmed and written to TM',
|
|
@@ -3276,6 +5417,8 @@ window.__ModuleLoader__.load({
|
|
|
3276
5417
|
'toast.draftSaved': 'Draft saved',
|
|
3277
5418
|
'toast.termSaved': 'Term saved',
|
|
3278
5419
|
'toast.termDeleted': 'Term deleted',
|
|
5420
|
+
'toast.tmUpdated': 'TM entry updated',
|
|
5421
|
+
'toast.tmDeleted': 'TM entry deleted',
|
|
3279
5422
|
'toast.tmImported': 'TMX imported',
|
|
3280
5423
|
'toast.tmxMarkupLost': 'TMX imported; {count} inline placeholder/format tags were flattened to plain text.',
|
|
3281
5424
|
'toast.glossaryImported': 'Glossary imported',
|
|
@@ -3286,6 +5429,93 @@ window.__ModuleLoader__.load({
|
|
|
3286
5429
|
'toast.copied': 'Copied to clipboard',
|
|
3287
5430
|
'toast.mtReady': 'Suggestion ready',
|
|
3288
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',
|
|
3289
5519
|
}
|
|
3290
5520
|
|
|
3291
5521
|
const JA = {
|
|
@@ -3337,6 +5567,40 @@ window.__ModuleLoader__.load({
|
|
|
3337
5567
|
'editor.emptyTarget': '先に訳文を入力してください',
|
|
3338
5568
|
'editor.done': 'すべてのセグメントを確認済み',
|
|
3339
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} 文が失敗)',
|
|
3340
5604
|
'empty.title': 'CAT 翻訳を始める',
|
|
3341
5605
|
'empty.desc': 'テキストを貼り付けるか、ファイルをドロップするか、デモ文書を読み込みます。CAIT は原文をセグメントに分割し、翻訳メモリと用語集を同時に検索します。確認した訳文は自動的に TM に保存されます。',
|
|
3342
5606
|
'empty.paste': 'テキストを貼り付け',
|
|
@@ -3347,7 +5611,7 @@ window.__ModuleLoader__.load({
|
|
|
3347
5611
|
'empty.demo': 'デモ文書を読み込む',
|
|
3348
5612
|
'empty.demoHint': '素材がありませんか?',
|
|
3349
5613
|
'empty.drop': 'ファイルをドロップ、またはクリックして選択',
|
|
3350
|
-
'empty.formats': 'TXT · Markdown · DOCX · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX に対応',
|
|
5614
|
+
'empty.formats': 'TXT · Markdown · DOCX · EPUB · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX に対応',
|
|
3351
5615
|
'empty.features.tm': '翻訳メモリ',
|
|
3352
5616
|
'empty.features.tmDesc': '完全一致+あいまい一致',
|
|
3353
5617
|
'empty.features.glossary': '用語集',
|
|
@@ -3361,7 +5625,7 @@ window.__ModuleLoader__.load({
|
|
|
3361
5625
|
'modal.new.targetLang': '訳文言語',
|
|
3362
5626
|
'modal.new.create': '文書を作成',
|
|
3363
5627
|
'modal.new.importing': 'インポート中…',
|
|
3364
|
-
'modal.new.dropHint': 'TXT · Markdown · DOCX · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
5628
|
+
'modal.new.dropHint': 'TXT · Markdown · DOCX · EPUB · TMX · CSV · SRT · VTT · PO · XLIFF · HTML · JSON · RTF · XLSX · PPTX',
|
|
3365
5629
|
'modal.new.csvSource': 'CSV 原文列',
|
|
3366
5630
|
'modal.new.csvTarget': 'CSV 訳文列',
|
|
3367
5631
|
'modal.new.csvColumnPlaceholder': '列名または列番号(例:Source / 2)',
|
|
@@ -3385,6 +5649,13 @@ window.__ModuleLoader__.load({
|
|
|
3385
5649
|
'libraries.create': '作成',
|
|
3386
5650
|
'libraries.done': '完了',
|
|
3387
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': '中の用語も一緒に削除され、元に戻せません。',
|
|
3388
5659
|
'libraries.mountSaved': 'マウント設定を保存しました',
|
|
3389
5660
|
'panel.tm': 'TM',
|
|
3390
5661
|
'panel.glossary': '用語集',
|
|
@@ -3402,6 +5673,9 @@ window.__ModuleLoader__.load({
|
|
|
3402
5673
|
'match.high_fuzzy': '高あいまい一致',
|
|
3403
5674
|
'match.medium_fuzzy': '中あいまい一致',
|
|
3404
5675
|
'match.low_fuzzy': '低あいまい一致',
|
|
5676
|
+
'match.context': 'コンテキスト一致',
|
|
5677
|
+
'panel.contextPrev': '前文',
|
|
5678
|
+
'panel.contextNext': '後文',
|
|
3405
5679
|
'panel.apply': '適用',
|
|
3406
5680
|
'panel.importTmx': 'TMX をインポート',
|
|
3407
5681
|
'panel.clearTm': 'TM をクリア',
|
|
@@ -3417,6 +5691,8 @@ window.__ModuleLoader__.load({
|
|
|
3417
5691
|
'panel.saveTerm': '用語を保存',
|
|
3418
5692
|
'panel.cancel': 'キャンセル',
|
|
3419
5693
|
'panel.delete': '削除',
|
|
5694
|
+
'panel.edit': '編集',
|
|
5695
|
+
'panel.saveEntry': '変更を保存',
|
|
3420
5696
|
'panel.noTerms': '用語集は空です。用語を追加するとヒットが自動で強調表示されます。',
|
|
3421
5697
|
'panel.importCsv': 'CSV/TSV をインポート',
|
|
3422
5698
|
'panel.clearGlossary': '用語集をクリア',
|
|
@@ -3446,6 +5722,8 @@ window.__ModuleLoader__.load({
|
|
|
3446
5722
|
'modal.export.targetMd': 'Markdown',
|
|
3447
5723
|
'modal.export.docx': 'Word DOCX',
|
|
3448
5724
|
'modal.export.docxGroup': '元の書式を保持',
|
|
5725
|
+
'modal.export.epub': 'EPUB 電子書',
|
|
5726
|
+
'modal.export.epubGroup': '元のレイアウトを保持',
|
|
3449
5727
|
'modal.export.json': '文書 JSON',
|
|
3450
5728
|
'modal.export.subtitleGroup': '字幕',
|
|
3451
5729
|
'modal.export.localizationGroup': 'ローカライズ交換',
|
|
@@ -3468,6 +5746,11 @@ window.__ModuleLoader__.load({
|
|
|
3468
5746
|
'modal.settings.uiAuto': 'DSH の言語に合わせる',
|
|
3469
5747
|
'modal.settings.uiHint': 'CAIT の表示だけを切り替えます。文書の原文言語・訳文言語には影響しません。',
|
|
3470
5748
|
'modal.settings.languages': '言語',
|
|
5749
|
+
'modal.settings.preprocess': '前処理',
|
|
5750
|
+
'modal.settings.preprocessHint': '取り込みと分割の前に原文を整えます。プレースホルダーと句読点は変更しません。',
|
|
5751
|
+
'modal.settings.pre.normalizeWhitespace': '余分な空白とタブをまとめる',
|
|
5752
|
+
'modal.settings.pre.stripInvisible': 'ゼロ幅文字と制御文字を除去',
|
|
5753
|
+
'modal.settings.pre.collapseBlankLines': '連続する空行をまとめる',
|
|
3471
5754
|
'modal.settings.tm': '翻訳メモリ',
|
|
3472
5755
|
'modal.settings.fuzzy': 'あいまい一致のしきい値',
|
|
3473
5756
|
'modal.settings.limit': '返す一致件数',
|
|
@@ -3488,6 +5771,7 @@ window.__ModuleLoader__.load({
|
|
|
3488
5771
|
'modal.confirm.clearGlossary':
|
|
3489
5772
|
'現在マウント中の用語をクリアしますか?未マウントのライブラリは影響を受けません。すべてのライブラリをクリアするには API に all: true を明示してください。この操作は元に戻せません。',
|
|
3490
5773
|
'modal.confirm.deleteTerm': '用語を削除',
|
|
5774
|
+
'modal.confirm.deleteTm': 'TM エントリを削除',
|
|
3491
5775
|
'modal.confirm.ok': '確認',
|
|
3492
5776
|
'modal.confirm.cancel': 'キャンセル',
|
|
3493
5777
|
'toast.confirmed': 'セグメントを確認して TM に保存しました',
|
|
@@ -3500,6 +5784,8 @@ window.__ModuleLoader__.load({
|
|
|
3500
5784
|
'toast.draftSaved': '下書きを保存しました',
|
|
3501
5785
|
'toast.termSaved': '用語を保存しました',
|
|
3502
5786
|
'toast.termDeleted': '用語を削除しました',
|
|
5787
|
+
'toast.tmUpdated': 'TM エントリを更新しました',
|
|
5788
|
+
'toast.tmDeleted': 'TM エントリを削除しました',
|
|
3503
5789
|
'toast.tmImported': 'TMX をインポートしました',
|
|
3504
5790
|
'toast.tmxMarkupLost': 'TMX をインポートしました。{count} 個のインライン プレースホルダー/書式タグはプレーンテキストとして処理され、書式情報は失われました。',
|
|
3505
5791
|
'toast.glossaryImported': '用語をインポートしました',
|
|
@@ -3510,6 +5796,93 @@ window.__ModuleLoader__.load({
|
|
|
3510
5796
|
'toast.copied': 'クリップボードにコピーしました',
|
|
3511
5797
|
'toast.mtReady': '訳文候補を生成しました',
|
|
3512
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': '登録済み',
|
|
3513
5886
|
}
|
|
3514
5887
|
|
|
3515
5888
|
// UI languages offered by the in-app switch. 'auto' follows the DSH locale
|
|
@@ -3587,6 +5960,19 @@ window.__ModuleLoader__.load({
|
|
|
3587
5960
|
scaleCssLengths,
|
|
3588
5961
|
caitUiBase: CAIT_UI_BASE,
|
|
3589
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,
|
|
3590
5976
|
cssScaled: CSS_SCALED,
|
|
3591
5977
|
syncPollMs: SYNC_POLL_MS,
|
|
3592
5978
|
makeTranslator,
|