@walkinissue/angy 0.2.17 → 0.2.19
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/README.md +72 -189
- package/dist/client/Angy.svelte +751 -546
- package/dist/client/RotationWarningDialog.svelte +177 -0
- package/dist/client/TranslationHelperForm.svelte +111 -61
- package/dist/client/VibeTooltip.svelte +18 -14
- package/dist/client/dragItem.ts +65 -39
- package/dist/client/toggleQA.shared.ts +23 -15
- package/dist/client/translationDrafts.ts +59 -0
- package/dist/plugin.js +102 -10
- package/dist/server/types.ts +30 -8
- package/dist/server.d.ts +30 -5
- package/dist/server.js +519 -142
- package/dist/server.js.map +3 -3
- package/package.json +2 -2
package/dist/client/Angy.svelte
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
|
-
<!-- @wc-ignore-file -->
|
|
1
|
+
<!-- @wc-ignore-file -->
|
|
2
2
|
<script lang="ts">
|
|
3
3
|
import { browser, dev } from "$app/environment";
|
|
4
4
|
import { page } from "$app/state";
|
|
5
5
|
import { loadLocale } from "wuchale/load-utils";
|
|
6
|
-
import { onMount } from "svelte";
|
|
7
|
-
import { draggable } from "./dragItem";
|
|
8
|
-
import PendingChangesDialog from "./PendingChangesDialog.svelte";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
6
|
+
import { onMount } from "svelte";
|
|
7
|
+
import { draggable } from "./dragItem";
|
|
8
|
+
import PendingChangesDialog from "./PendingChangesDialog.svelte";
|
|
9
|
+
import RotationWarningDialog from "./RotationWarningDialog.svelte";
|
|
10
|
+
import TranslationHelperForm from "./TranslationHelperForm.svelte";
|
|
11
|
+
import {
|
|
12
|
+
clearDraftValue,
|
|
13
|
+
getDraftValue,
|
|
14
|
+
readDraftCache,
|
|
15
|
+
setDraftValue,
|
|
16
|
+
type DraftCacheItem
|
|
17
|
+
} from "./translationDrafts";
|
|
18
|
+
import {
|
|
19
|
+
readSuggestionCache,
|
|
20
|
+
requestTranslationSuggestions
|
|
21
|
+
} from "./translationSuggestions";
|
|
14
22
|
import {
|
|
15
23
|
getCandidateList,
|
|
16
|
-
getEffectiveResolvedKey,
|
|
17
|
-
getEntryOrigin,
|
|
18
|
-
getEntryOriginalValue,
|
|
19
|
-
sameResolvedKey,
|
|
20
|
-
translationKey,
|
|
21
|
-
type DraftTranslation,
|
|
24
|
+
getEffectiveResolvedKey,
|
|
25
|
+
getEntryOrigin,
|
|
26
|
+
getEntryOriginalValue,
|
|
27
|
+
sameResolvedKey,
|
|
28
|
+
translationKey,
|
|
29
|
+
type DraftTranslation,
|
|
22
30
|
type ResolvedKey,
|
|
23
31
|
type TranslationContextResult
|
|
24
32
|
} from "./toggleQA.shared";
|
|
@@ -27,6 +35,8 @@
|
|
|
27
35
|
__ANGY_ROUTE_PATH__?: string;
|
|
28
36
|
__ANGY_LOCALES__?: string[];
|
|
29
37
|
};
|
|
38
|
+
const LOCALE_STORAGE_KEY = "angy:locale";
|
|
39
|
+
const LOCALE_COOKIE_KEY = "locale";
|
|
30
40
|
|
|
31
41
|
const defaultEndpoint =
|
|
32
42
|
typeof runtimeConfig.__ANGY_ROUTE_PATH__ !== "undefined"
|
|
@@ -40,109 +50,175 @@
|
|
|
40
50
|
let { endpoint = defaultEndpoint } = $props<{
|
|
41
51
|
endpoint?: string;
|
|
42
52
|
}>();
|
|
43
|
-
|
|
44
|
-
let dragQua = $state(true);
|
|
45
|
-
let selectedResolvedKey = $state<ResolvedKey | null>(null);
|
|
46
|
-
let focusedAltKey = $state<string | null>(null);
|
|
47
|
-
|
|
48
|
-
let contextPending = $state(false);
|
|
49
|
-
let contextError = $state<string | null>(null);
|
|
50
|
-
let contextResult = $state<TranslationContextResult | null>(null);
|
|
51
|
-
|
|
52
|
-
let stagedTranslations = $state<Record<string, DraftTranslation>>({});
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
let dragQua = $state(true);
|
|
55
|
+
let selectedResolvedKey = $state<ResolvedKey | null>(null);
|
|
56
|
+
let focusedAltKey = $state<string | null>(null);
|
|
57
|
+
|
|
58
|
+
let contextPending = $state(false);
|
|
59
|
+
let contextError = $state<string | null>(null);
|
|
60
|
+
let contextResult = $state<TranslationContextResult | null>(null);
|
|
61
|
+
|
|
62
|
+
let stagedTranslations = $state<Record<string, DraftTranslation>>({});
|
|
63
|
+
let draftLookup = $state<Record<string, DraftCacheItem>>(readDraftCache());
|
|
64
|
+
|
|
54
65
|
let currentLocale = $state(defaultLocales);
|
|
55
66
|
let locale = 0;
|
|
56
|
-
|
|
57
|
-
let activeSwitchLocale = $state(
|
|
58
|
-
let selectionStarted = false;
|
|
59
|
-
let capturedSelection: string | undefined = $state(undefined);
|
|
60
|
-
let spawnTranslation = $state(false);
|
|
61
|
-
let translatedValue = $state("");
|
|
62
|
-
|
|
67
|
+
|
|
68
|
+
let activeSwitchLocale = $state(false);
|
|
69
|
+
let selectionStarted = false;
|
|
70
|
+
let capturedSelection: string | undefined = $state(undefined);
|
|
71
|
+
let spawnTranslation = $state(false);
|
|
72
|
+
let translatedValue = $state("");
|
|
73
|
+
|
|
63
74
|
let pending = $state(false);
|
|
64
75
|
let rotatePending = $state(false);
|
|
65
76
|
let error = $state<string | null>(null);
|
|
66
77
|
let success = $state<string | null>(null);
|
|
67
78
|
let renderedLocale = $state<string>(currentLocale[locale] ?? "en");
|
|
68
|
-
let showPendingChangesDialog = $state(false);
|
|
69
|
-
let
|
|
70
|
-
let
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
let showPendingChangesDialog = $state(false);
|
|
80
|
+
let showRotationWarningDialog = $state(false);
|
|
81
|
+
let rotationImpact = $state<
|
|
82
|
+
Array<{ msgid: string; msgctxt: string | null; baseValue: string; workingValue: string }>
|
|
83
|
+
>([]);
|
|
84
|
+
let suggestionLookup = $state<Record<string, string>>(readSuggestionCache());
|
|
85
|
+
let suggestionPending = $state(false);
|
|
86
|
+
|
|
87
|
+
let panelEl: HTMLDivElement | null = null;
|
|
88
|
+
let translationInputEl: HTMLTextAreaElement | null = null;
|
|
89
|
+
|
|
90
|
+
function setTranslationInputEl(element: HTMLTextAreaElement | null) {
|
|
91
|
+
translationInputEl = element;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getLocale(increase = true) {
|
|
95
|
+
if (increase) {
|
|
96
|
+
locale = locale >= currentLocale.length - 1 ? 0 : locale + 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
renderedLocale = currentLocale[locale];
|
|
100
|
+
return currentLocale[locale];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function peekNextLocale() {
|
|
104
|
+
const nextIndex = locale >= currentLocale.length - 1 ? 0 : locale + 1;
|
|
105
|
+
return currentLocale[nextIndex];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function persistLocaleSelection(nextLocale: string) {
|
|
109
|
+
if (!browser) return;
|
|
110
|
+
localStorage.setItem(LOCALE_STORAGE_KEY, nextLocale);
|
|
111
|
+
document.cookie = `${LOCALE_COOKIE_KEY}=${nextLocale}; path=/; SameSite=Lax`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isWorkingLocaleName(value: string | null | undefined) {
|
|
115
|
+
return Boolean(value?.endsWith("-working"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const previewOnly = $derived(isWorkingLocaleName(renderedLocale));
|
|
119
|
+
|
|
88
120
|
function focusTranslationInput() {
|
|
89
|
-
queueMicrotask(() => {
|
|
90
|
-
translationInputEl?.focus();
|
|
91
|
-
translationInputEl?.select();
|
|
92
|
-
});
|
|
121
|
+
queueMicrotask(() => {
|
|
122
|
+
translationInputEl?.focus();
|
|
123
|
+
translationInputEl?.select();
|
|
124
|
+
});
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
function handleLocaleToggle(event: MouseEvent) {
|
|
96
|
-
if (activeSwitchLocale) return;
|
|
127
|
+
async function handleLocaleToggle(event: MouseEvent) {
|
|
128
|
+
if (activeSwitchLocale || pending) return;
|
|
97
129
|
event.preventDefault();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
const nextLocale = peekNextLocale();
|
|
131
|
+
persistLocaleSelection(nextLocale);
|
|
132
|
+
|
|
133
|
+
if (isWorkingLocaleName(nextLocale)) {
|
|
134
|
+
resetFeedback();
|
|
135
|
+
pending = true;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(`${endpoint}?intent=promote-working-preview`, {
|
|
139
|
+
method: "POST"
|
|
140
|
+
});
|
|
141
|
+
const payload = await response.json().catch(() => null);
|
|
142
|
+
if (!response.ok) {
|
|
143
|
+
error = payload?.error ?? "Failed to prepare working preview";
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
} catch {
|
|
147
|
+
error = "Failed to prepare working preview";
|
|
148
|
+
return;
|
|
149
|
+
} finally {
|
|
150
|
+
pending = false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await loadLocale(getLocale());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getStaged(msgid: string, msgctxt: string | null) {
|
|
158
|
+
return stagedTranslations[translationKey(msgid, msgctxt)] ?? null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getSuggestion(msgid: string, msgctxt: string | null) {
|
|
162
|
+
return suggestionLookup[translationKey(msgid, msgctxt)] ?? "";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getDraft(msgid: string, msgctxt: string | null) {
|
|
166
|
+
return getDraftValue(draftLookup, msgid, msgctxt);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isStaged(msgid: string, msgctxt: string | null) {
|
|
170
|
+
return Boolean(getStaged(msgid, msgctxt));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isSelectedAlt(msgid: string, msgctxt: string | null) {
|
|
174
|
+
return sameResolvedKey(selectedResolvedKey, { msgid, msgctxt });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function setFocusedAltKey(key: string | null) {
|
|
178
|
+
focusedAltKey = key;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function persistDraft(msgid: string, msgctxt: string | null, item: DraftCacheItem) {
|
|
182
|
+
draftLookup = setDraftValue(draftLookup, msgid, msgctxt, item);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function clearDraft(msgid: string, msgctxt: string | null) {
|
|
186
|
+
draftLookup = clearDraftValue(draftLookup, msgid, msgctxt);
|
|
187
|
+
}
|
|
188
|
+
|
|
121
189
|
function resetFeedback() {
|
|
122
190
|
error = null;
|
|
123
191
|
success = null;
|
|
124
192
|
}
|
|
125
193
|
|
|
126
|
-
async function
|
|
127
|
-
if (rotatePending || pending) return;
|
|
128
|
-
if (!confirm("Rotate catalogs? This will back up both catalogs and promote the working catalog into the base catalog.")) {
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
194
|
+
async function runRotation(confirmDestructive = false) {
|
|
132
195
|
rotatePending = true;
|
|
133
196
|
resetFeedback();
|
|
134
197
|
|
|
135
198
|
try {
|
|
136
199
|
const response = await fetch(`${endpoint}?intent=rotate-catalogs`, {
|
|
137
|
-
method: "POST"
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: {
|
|
202
|
+
"content-type": "application/json"
|
|
203
|
+
},
|
|
204
|
+
body: JSON.stringify({ confirmDestructive })
|
|
138
205
|
});
|
|
139
206
|
const payload = await response.json().catch(() => null);
|
|
140
207
|
|
|
141
208
|
if (!response.ok) {
|
|
209
|
+
if (payload?.code === "rotation_confirmation_required") {
|
|
210
|
+
rotationImpact = Array.isArray(payload?.affected) ? payload.affected : [];
|
|
211
|
+
showRotationWarningDialog = true;
|
|
212
|
+
error = payload?.error ?? "Catalog rotation requires confirmation";
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
142
216
|
error = payload?.error ?? "Catalog rotation failed";
|
|
143
217
|
return;
|
|
144
218
|
}
|
|
145
219
|
|
|
220
|
+
showRotationWarningDialog = false;
|
|
221
|
+
rotationImpact = [];
|
|
146
222
|
success = payload?.message ?? "Catalogs rotated";
|
|
147
223
|
} catch {
|
|
148
224
|
error = "Catalog rotation failed";
|
|
@@ -150,423 +226,534 @@
|
|
|
150
226
|
rotatePending = false;
|
|
151
227
|
}
|
|
152
228
|
}
|
|
153
|
-
|
|
154
|
-
function
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
229
|
+
|
|
230
|
+
async function rotateCatalogs() {
|
|
231
|
+
if (rotatePending || pending) return;
|
|
232
|
+
try {
|
|
233
|
+
const response = await fetch(`${endpoint}?intent=rotate-preflight`, {
|
|
234
|
+
method: "POST"
|
|
235
|
+
});
|
|
236
|
+
const payload = await response.json().catch(() => null);
|
|
237
|
+
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
error = payload?.error ?? "Catalog rotation failed";
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (Array.isArray(payload?.affected) && payload.affected.length) {
|
|
244
|
+
rotationImpact = Array.isArray(payload?.affected) ? payload.affected : [];
|
|
245
|
+
showRotationWarningDialog = true;
|
|
246
|
+
resetFeedback();
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
!confirm(
|
|
252
|
+
"Rotate catalogs? This will back up both catalogs and promote the working catalog into the base catalog."
|
|
253
|
+
)
|
|
254
|
+
) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await runRotation(false);
|
|
259
|
+
} catch {
|
|
260
|
+
error = "Catalog rotation failed";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function resetContextState() {
|
|
265
|
+
contextError = null;
|
|
266
|
+
contextResult = null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function hasDirtyStagedTranslations() {
|
|
270
|
+
return Object.values(stagedTranslations).some((item) => item.isDirty);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getResolvedInputValue(msgid: string, msgctxt: string | null) {
|
|
274
|
+
return (
|
|
275
|
+
getStaged(msgid, msgctxt)?.value ||
|
|
276
|
+
getDraft(msgid, msgctxt)?.value ||
|
|
277
|
+
getSuggestion(msgid, msgctxt) ||
|
|
278
|
+
getEntryOriginalValue(contextResult, msgid, msgctxt)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function syncDraftForResolved(msgid: string, msgctxt: string | null, value: string) {
|
|
283
|
+
const originalValue = getEntryOriginalValue(contextResult, msgid, msgctxt);
|
|
284
|
+
const trimmedValue = value.trim();
|
|
285
|
+
if (!trimmedValue || trimmedValue === originalValue) {
|
|
286
|
+
clearDraft(msgid, msgctxt);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
persistDraft(msgid, msgctxt, {
|
|
291
|
+
value,
|
|
292
|
+
isDirty: trimmedValue !== originalValue
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function handleTranslationInput() {
|
|
297
|
+
const resolved = getEffectiveResolvedKey(selectedResolvedKey, contextResult);
|
|
298
|
+
if (!resolved) return;
|
|
299
|
+
syncDraftForResolved(resolved.msgid, resolved.msgctxt, translatedValue);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function refreshContextAndReconcile(payload: DraftTranslation[]) {
|
|
303
|
+
if (!capturedSelection) return true;
|
|
304
|
+
|
|
305
|
+
await fetchContext();
|
|
306
|
+
|
|
307
|
+
let allReconciled = true;
|
|
308
|
+
for (const item of payload) {
|
|
309
|
+
const latest = getEntryOriginalValue(contextResult, item.msgid, item.msgctxt);
|
|
310
|
+
if (latest === item.value) {
|
|
311
|
+
clearDraft(item.msgid, item.msgctxt);
|
|
312
|
+
} else {
|
|
313
|
+
allReconciled = false;
|
|
314
|
+
persistDraft(item.msgid, item.msgctxt, { value: item.value, isDirty: true });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return allReconciled;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function selectResolvedKey(msgid: string, msgctxt: string | null, focusInput = false) {
|
|
322
|
+
selectedResolvedKey = { msgid, msgctxt };
|
|
323
|
+
translatedValue = getResolvedInputValue(msgid, msgctxt);
|
|
324
|
+
|
|
325
|
+
resetFeedback();
|
|
326
|
+
|
|
327
|
+
if (focusInput) {
|
|
328
|
+
focusTranslationInput();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function hasActiveSuggestion() {
|
|
333
|
+
const resolved = getEffectiveResolvedKey(selectedResolvedKey, contextResult);
|
|
334
|
+
if (!resolved) return false;
|
|
335
|
+
|
|
336
|
+
if (getStaged(resolved.msgid, resolved.msgctxt)) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const suggestion = getSuggestion(resolved.msgid, resolved.msgctxt);
|
|
341
|
+
if (!suggestion) return false;
|
|
342
|
+
|
|
343
|
+
const originalValue = getEntryOriginalValue(contextResult, resolved.msgid, resolved.msgctxt);
|
|
344
|
+
return translatedValue === suggestion && suggestion !== originalValue;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function handleSelectAlt(event: Event, msgid: string, msgctxt: string | null) {
|
|
348
|
+
event.preventDefault();
|
|
349
|
+
event.stopPropagation();
|
|
350
|
+
selectResolvedKey(msgid, msgctxt, true);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function moveSelection(direction: 1 | -1) {
|
|
354
|
+
const items = getCandidateList(contextResult);
|
|
355
|
+
if (!items.length) return false;
|
|
356
|
+
|
|
357
|
+
const current = getEffectiveResolvedKey(selectedResolvedKey, contextResult);
|
|
358
|
+
const currentIndex = current
|
|
359
|
+
? items.findIndex(
|
|
360
|
+
(item) =>
|
|
361
|
+
item.msgid === current.msgid &&
|
|
362
|
+
(item.msgctxt ?? null) === (current.msgctxt ?? null)
|
|
363
|
+
)
|
|
364
|
+
: -1;
|
|
365
|
+
|
|
366
|
+
for (let step = 1; step <= items.length; step++) {
|
|
367
|
+
const nextIndex =
|
|
368
|
+
((Math.max(currentIndex, 0) + direction * step) % items.length + items.length) %
|
|
369
|
+
items.length;
|
|
370
|
+
const next = items[nextIndex];
|
|
371
|
+
const skipForKeyboard = next.hasTranslation && !next.isFuzzy;
|
|
372
|
+
|
|
373
|
+
if (skipForKeyboard) continue;
|
|
374
|
+
|
|
375
|
+
selectResolvedKey(next.msgid, next.msgctxt, true);
|
|
376
|
+
return true;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const nextIndex =
|
|
380
|
+
((Math.max(currentIndex, 0) + direction) % items.length + items.length) % items.length;
|
|
381
|
+
const next = items[nextIndex];
|
|
382
|
+
selectResolvedKey(next.msgid, next.msgctxt, true);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function onAltKeydown(event: KeyboardEvent, msgid: string, msgctxt: string | null) {
|
|
387
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
388
|
+
event.preventDefault();
|
|
389
|
+
selectResolvedKey(msgid, msgctxt, true);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (event.key === "Tab") {
|
|
394
|
+
event.preventDefault();
|
|
395
|
+
moveSelection(event.shiftKey ? -1 : 1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function stageCurrentTranslation(event?: Event) {
|
|
400
|
+
event?.preventDefault();
|
|
401
|
+
if (previewOnly) {
|
|
402
|
+
error = "Working locale is preview-only. Rotate back to a non-working locale before staging.";
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const resolved = getEffectiveResolvedKey(selectedResolvedKey, contextResult);
|
|
407
|
+
if (!resolved?.msgid) {
|
|
408
|
+
error = "No resolved translation key selected";
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const value = translatedValue.trim();
|
|
413
|
+
if (!value) {
|
|
414
|
+
error = "No translation";
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const originalValue = getEntryOriginalValue(contextResult, resolved.msgid, resolved.msgctxt);
|
|
419
|
+
stagedTranslations = {
|
|
420
|
+
...stagedTranslations,
|
|
421
|
+
[translationKey(resolved.msgid, resolved.msgctxt)]: {
|
|
422
|
+
msgid: resolved.msgid,
|
|
423
|
+
msgctxt: resolved.msgctxt,
|
|
424
|
+
value,
|
|
425
|
+
origin: getEntryOrigin(contextResult, resolved.msgid, resolved.msgctxt),
|
|
426
|
+
originalValue,
|
|
427
|
+
isDirty: value !== originalValue
|
|
428
|
+
}
|
|
188
429
|
};
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const value = translatedValue.trim();
|
|
288
|
-
if (!value) {
|
|
289
|
-
error = "No translation";
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const originalValue = getEntryOriginalValue(contextResult, resolved.msgid, resolved.msgctxt);
|
|
294
|
-
stagedTranslations = {
|
|
295
|
-
...stagedTranslations,
|
|
296
|
-
[translationKey(resolved.msgid, resolved.msgctxt)]: {
|
|
297
|
-
msgid: resolved.msgid,
|
|
298
|
-
msgctxt: resolved.msgctxt,
|
|
299
|
-
value,
|
|
300
|
-
origin: getEntryOrigin(contextResult, resolved.msgid, resolved.msgctxt),
|
|
301
|
-
originalValue,
|
|
302
|
-
isDirty: value !== originalValue
|
|
303
|
-
}
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
error = null;
|
|
307
|
-
success = "Staged";
|
|
308
|
-
|
|
309
|
-
if (!moveSelection(1)) {
|
|
310
|
-
focusTranslationInput();
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async function commitStagedTranslations() {
|
|
315
|
-
const payload = Object.values(stagedTranslations).filter((item) => item.isDirty);
|
|
316
|
-
if (!payload.length) {
|
|
317
|
-
error = "No changed translations to submit";
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
pending = true;
|
|
322
|
-
resetFeedback();
|
|
323
|
-
|
|
324
|
-
const res = await fetch(`${endpoint}?intent=commit-batch`, {
|
|
325
|
-
method: "POST",
|
|
326
|
-
headers: {
|
|
327
|
-
"content-type": "application/json"
|
|
328
|
-
},
|
|
329
|
-
body: JSON.stringify({
|
|
330
|
-
items: payload.map((item) => ({
|
|
331
|
-
resolvedMsgid: item.msgid,
|
|
332
|
-
resolvedMsgctxt: item.msgctxt,
|
|
333
|
-
translationValue: item.value
|
|
334
|
-
}))
|
|
335
|
-
})
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
const json = await res.json();
|
|
339
|
-
pending = false;
|
|
340
|
-
|
|
341
|
-
if (!res.ok) {
|
|
342
|
-
error = json.error ?? "Batch commit failed";
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
success = json.message ?? "Translations committed";
|
|
347
|
-
|
|
348
|
-
const next = { ...stagedTranslations };
|
|
349
|
-
for (const item of payload) {
|
|
350
|
-
delete next[translationKey(item.msgid, item.msgctxt)];
|
|
351
|
-
}
|
|
352
|
-
stagedTranslations = next;
|
|
353
|
-
contextResult = applyCommittedTranslations(contextResult, payload);
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async function submitStagedTranslations(event: SubmitEvent) {
|
|
358
|
-
if (!event.defaultPrevented) {
|
|
359
|
-
event.preventDefault();
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
await commitStagedTranslations();
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function openTranslation(text: string) {
|
|
366
|
-
capturedSelection = text;
|
|
367
|
-
translatedValue = "";
|
|
368
|
-
resetFeedback();
|
|
369
|
-
resetContextState();
|
|
370
|
-
spawnTranslation = true;
|
|
371
|
-
showPendingChangesDialog = false;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
async function fetchContext() {
|
|
375
|
-
if (!capturedSelection) return;
|
|
376
|
-
|
|
377
|
-
contextPending = true;
|
|
378
|
-
resetContextState();
|
|
379
|
-
resetFeedback();
|
|
380
|
-
|
|
381
|
-
const body = new FormData();
|
|
382
|
-
body.set("translationKey", capturedSelection);
|
|
383
|
-
body.set("currentPath", page.url.pathname);
|
|
384
|
-
|
|
430
|
+
persistDraft(resolved.msgid, resolved.msgctxt, {
|
|
431
|
+
value,
|
|
432
|
+
isDirty: value !== originalValue
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
error = null;
|
|
436
|
+
success = "Staged";
|
|
437
|
+
|
|
438
|
+
if (!moveSelection(1)) {
|
|
439
|
+
focusTranslationInput();
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
async function commitStagedTranslations() {
|
|
444
|
+
if (previewOnly) {
|
|
445
|
+
error = "Working locale is preview-only. Commit from the base or target locale instead.";
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const payload = Object.values(stagedTranslations).filter((item) => item.isDirty);
|
|
450
|
+
if (!payload.length) {
|
|
451
|
+
error = "No changed translations to submit";
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
pending = true;
|
|
456
|
+
resetFeedback();
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
const res = await fetch(`${endpoint}?intent=commit-batch`, {
|
|
460
|
+
method: "POST",
|
|
461
|
+
headers: {
|
|
462
|
+
"content-type": "application/json"
|
|
463
|
+
},
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
items: payload.map((item) => ({
|
|
466
|
+
resolvedMsgid: item.msgid,
|
|
467
|
+
resolvedMsgctxt: item.msgctxt,
|
|
468
|
+
translationValue: item.value
|
|
469
|
+
}))
|
|
470
|
+
})
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const json = await res.json().catch(() => null);
|
|
474
|
+
|
|
475
|
+
if (!res.ok) {
|
|
476
|
+
error = json?.error ?? "Batch commit failed";
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const reconciled = await refreshContextAndReconcile(payload);
|
|
481
|
+
const next = { ...stagedTranslations };
|
|
482
|
+
for (const item of payload) {
|
|
483
|
+
if (reconciled || getEntryOriginalValue(contextResult, item.msgid, item.msgctxt) === item.value) {
|
|
484
|
+
delete next[translationKey(item.msgid, item.msgctxt)];
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
stagedTranslations = next;
|
|
488
|
+
success = reconciled
|
|
489
|
+
? (json?.message ?? "Translations committed")
|
|
490
|
+
: "Commit may have landed; verifying catalog state. Your edits were kept locally.";
|
|
491
|
+
return true;
|
|
492
|
+
} finally {
|
|
493
|
+
pending = false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function submitStagedTranslations(event: SubmitEvent) {
|
|
498
|
+
if (!event.defaultPrevented) {
|
|
499
|
+
event.preventDefault();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
await commitStagedTranslations();
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function openTranslation(text: string) {
|
|
506
|
+
capturedSelection = text;
|
|
507
|
+
translatedValue = "";
|
|
508
|
+
resetFeedback();
|
|
509
|
+
resetContextState();
|
|
510
|
+
spawnTranslation = true;
|
|
511
|
+
showPendingChangesDialog = false;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function fetchContext() {
|
|
515
|
+
if (!capturedSelection) return;
|
|
516
|
+
|
|
517
|
+
contextPending = true;
|
|
518
|
+
resetContextState();
|
|
519
|
+
resetFeedback();
|
|
520
|
+
|
|
521
|
+
const body = new FormData();
|
|
522
|
+
body.set("translationKey", capturedSelection);
|
|
523
|
+
body.set("currentPath", page.url.pathname);
|
|
524
|
+
|
|
385
525
|
const res = await fetch(`${endpoint}?intent=context`, {
|
|
386
|
-
method: "POST",
|
|
387
|
-
body
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
const json = await res.json();
|
|
391
|
-
contextPending = false;
|
|
392
|
-
|
|
393
|
-
if (!res.ok) {
|
|
394
|
-
contextError = json.error ?? "Context lookup failed";
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
contextResult = json;
|
|
399
|
-
selectedResolvedKey = {
|
|
400
|
-
msgid: json.entry.msgid,
|
|
401
|
-
msgctxt: json.entry.msgctxt
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
526
|
+
method: "POST",
|
|
527
|
+
body
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const json = await res.json();
|
|
531
|
+
contextPending = false;
|
|
532
|
+
|
|
533
|
+
if (!res.ok) {
|
|
534
|
+
contextError = json.error ?? "Context lookup failed";
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
contextResult = json;
|
|
539
|
+
selectedResolvedKey = {
|
|
540
|
+
msgid: json.entry.msgid,
|
|
541
|
+
msgctxt: json.entry.msgctxt
|
|
542
|
+
};
|
|
543
|
+
const currentDraft = getDraft(json.entry.msgid, json.entry.msgctxt);
|
|
544
|
+
translatedValue =
|
|
545
|
+
getStaged(json.entry.msgid, json.entry.msgctxt)?.value ||
|
|
546
|
+
currentDraft?.value ||
|
|
547
|
+
getSuggestion(json.entry.msgid, json.entry.msgctxt) ||
|
|
548
|
+
json.entry.msgstr?.[0] ||
|
|
549
|
+
"";
|
|
550
|
+
|
|
551
|
+
if (!suggestionPending) {
|
|
552
|
+
suggestionPending = true;
|
|
411
553
|
void requestTranslationSuggestions(json, endpoint)
|
|
412
|
-
.then((nextSuggestions) => {
|
|
413
|
-
suggestionLookup = nextSuggestions;
|
|
414
|
-
|
|
415
|
-
const resolved = getEffectiveResolvedKey(selectedResolvedKey, contextResult);
|
|
416
|
-
if (!resolved) return;
|
|
417
|
-
|
|
418
|
-
const staged = getStaged(resolved.msgid, resolved.msgctxt);
|
|
419
|
-
const original = getEntryOriginalValue(contextResult, resolved.msgid, resolved.msgctxt);
|
|
420
|
-
const currentSuggestion = getSuggestion(resolved.msgid, resolved.msgctxt);
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function
|
|
473
|
-
const
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
554
|
+
.then((nextSuggestions) => {
|
|
555
|
+
suggestionLookup = nextSuggestions;
|
|
556
|
+
|
|
557
|
+
const resolved = getEffectiveResolvedKey(selectedResolvedKey, contextResult);
|
|
558
|
+
if (!resolved) return;
|
|
559
|
+
|
|
560
|
+
const staged = getStaged(resolved.msgid, resolved.msgctxt);
|
|
561
|
+
const original = getEntryOriginalValue(contextResult, resolved.msgid, resolved.msgctxt);
|
|
562
|
+
const currentSuggestion = getSuggestion(resolved.msgid, resolved.msgctxt);
|
|
563
|
+
|
|
564
|
+
const draft = getDraft(resolved.msgid, resolved.msgctxt);
|
|
565
|
+
if (!staged && !draft && translatedValue === original && currentSuggestion) {
|
|
566
|
+
translatedValue = currentSuggestion;
|
|
567
|
+
}
|
|
568
|
+
})
|
|
569
|
+
.catch(() => {
|
|
570
|
+
// Suggestion lookup is best-effort in this dev tool.
|
|
571
|
+
})
|
|
572
|
+
.finally(() => {
|
|
573
|
+
suggestionPending = false;
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!previewOnly) {
|
|
578
|
+
focusTranslationInput();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function closeTranslation() {
|
|
583
|
+
spawnTranslation = false;
|
|
584
|
+
capturedSelection = undefined;
|
|
585
|
+
selectionStarted = false;
|
|
586
|
+
translatedValue = "";
|
|
587
|
+
selectedResolvedKey = null;
|
|
588
|
+
focusedAltKey = null;
|
|
589
|
+
showPendingChangesDialog = false;
|
|
590
|
+
showRotationWarningDialog = false;
|
|
591
|
+
resetFeedback();
|
|
592
|
+
resetContextState();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function requestCloseTranslation() {
|
|
596
|
+
if (pending) return;
|
|
597
|
+
|
|
598
|
+
if (hasDirtyStagedTranslations()) {
|
|
599
|
+
showPendingChangesDialog = true;
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
closeTranslation();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function discardAndCloseTranslation() {
|
|
607
|
+
for (const item of Object.values(stagedTranslations)) {
|
|
608
|
+
clearDraft(item.msgid, item.msgctxt);
|
|
609
|
+
}
|
|
610
|
+
stagedTranslations = {};
|
|
611
|
+
closeTranslation();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async function submitAndCloseTranslation() {
|
|
615
|
+
const committed = await commitStagedTranslations();
|
|
616
|
+
if (committed) {
|
|
617
|
+
closeTranslation();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function getElementTranslationCandidate(target: EventTarget | null): string | null {
|
|
622
|
+
const el =
|
|
623
|
+
target instanceof Element
|
|
624
|
+
? target.closest(
|
|
625
|
+
'button, a, [role="button"], input[type="button"], input[type="submit"]'
|
|
626
|
+
)
|
|
627
|
+
: null;
|
|
628
|
+
|
|
629
|
+
if (!el) return null;
|
|
630
|
+
|
|
631
|
+
if (el instanceof HTMLInputElement) {
|
|
632
|
+
return (
|
|
633
|
+
el.getAttribute("aria-label")?.trim() ||
|
|
634
|
+
el.value?.trim() ||
|
|
635
|
+
el.title?.trim() ||
|
|
636
|
+
null
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function getFirstDescendantTextCandidate(root: Element) {
|
|
641
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
642
|
+
let current = walker.nextNode();
|
|
643
|
+
|
|
644
|
+
while (current) {
|
|
645
|
+
if (current instanceof HTMLElement) {
|
|
646
|
+
const text = current.innerText?.trim() || current.textContent?.trim() || "";
|
|
647
|
+
if (text) {
|
|
648
|
+
return text;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
current = walker.nextNode();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return "";
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const directText = [...el.childNodes]
|
|
659
|
+
.filter((node) => node.nodeType === Node.TEXT_NODE)
|
|
660
|
+
.map((node) => node.textContent?.trim() ?? "")
|
|
661
|
+
.filter(Boolean)
|
|
662
|
+
.slice(0,1)[0]
|
|
663
|
+
.trim();
|
|
664
|
+
const descendantText = getFirstDescendantTextCandidate(el);
|
|
665
|
+
|
|
666
|
+
return (
|
|
667
|
+
el.getAttribute("aria-label")?.trim() ||
|
|
668
|
+
el.getAttribute("title")?.trim() ||
|
|
669
|
+
directText ||
|
|
670
|
+
descendantText ||
|
|
671
|
+
null
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function translationForm() {
|
|
676
|
+
if (!browser || !dev) return;
|
|
677
|
+
|
|
678
|
+
const onSelectStart = () => {
|
|
679
|
+
selectionStarted = dragQua;
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
function onCaptureInteractivePointerDown(event: PointerEvent) {
|
|
683
|
+
if (!event.altKey) return;
|
|
684
|
+
|
|
685
|
+
const text = getElementTranslationCandidate(event.target);
|
|
686
|
+
if (!text) return;
|
|
687
|
+
|
|
688
|
+
event.preventDefault();
|
|
689
|
+
event.stopPropagation();
|
|
690
|
+
event.stopImmediatePropagation?.();
|
|
691
|
+
|
|
692
|
+
openTranslation(text);
|
|
693
|
+
void fetchContext();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const onMouseUp = () => {
|
|
697
|
+
if (!selectionStarted) return;
|
|
698
|
+
|
|
699
|
+
const text = document.getSelection()?.toString().trim();
|
|
700
|
+
if (text) {
|
|
701
|
+
openTranslation(text);
|
|
702
|
+
void fetchContext();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
selectionStarted = false;
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const onDocumentMouseDown = (event: MouseEvent) => {
|
|
709
|
+
if (!spawnTranslation) return;
|
|
710
|
+
if (showPendingChangesDialog) return;
|
|
711
|
+
|
|
712
|
+
const target = event.target as Node | null;
|
|
713
|
+
if (panelEl && target && panelEl.contains(target)) return;
|
|
714
|
+
|
|
715
|
+
requestCloseTranslation();
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
function onCtrlDown(event: KeyboardEvent) {
|
|
719
|
+
dragQua = !event.ctrlKey;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function onCtrlUp() {
|
|
723
|
+
dragQua = true;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
document.addEventListener("selectstart", onSelectStart);
|
|
727
|
+
document.addEventListener("mouseup", onMouseUp);
|
|
728
|
+
document.addEventListener("mousedown", onDocumentMouseDown);
|
|
729
|
+
document.addEventListener("keydown", onCtrlDown);
|
|
730
|
+
document.addEventListener("keyup", onCtrlUp);
|
|
731
|
+
document.addEventListener("pointerdown", onCaptureInteractivePointerDown, true);
|
|
732
|
+
|
|
733
|
+
return () => {
|
|
734
|
+
document.removeEventListener("selectstart", onSelectStart);
|
|
735
|
+
document.removeEventListener("mouseup", onMouseUp);
|
|
736
|
+
document.removeEventListener("mousedown", onDocumentMouseDown);
|
|
737
|
+
document.removeEventListener("keydown", onCtrlDown);
|
|
738
|
+
document.removeEventListener("keyup", onCtrlUp);
|
|
739
|
+
document.removeEventListener("pointerdown", onCaptureInteractivePointerDown, true);
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
onMount(translationForm);
|
|
744
|
+
|
|
745
|
+
onMount(() => {
|
|
746
|
+
if (!browser) return;
|
|
747
|
+
const storedLocale = localStorage.getItem(LOCALE_STORAGE_KEY);
|
|
748
|
+
if (!storedLocale) return;
|
|
749
|
+
const nextIndex = currentLocale.findIndex((item) => item === storedLocale);
|
|
750
|
+
if (nextIndex >= 0) {
|
|
751
|
+
locale = nextIndex;
|
|
752
|
+
renderedLocale = currentLocale[nextIndex];
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
</script>
|
|
756
|
+
|
|
570
757
|
<div class="translator-actions sticky-rotate">
|
|
571
758
|
<button
|
|
572
759
|
type="button"
|
|
@@ -602,51 +789,69 @@
|
|
|
602
789
|
class="translator-toggle-button"
|
|
603
790
|
onclick={handleLocaleToggle}
|
|
604
791
|
title="qa-button"
|
|
605
|
-
disabled={activeSwitchLocale}
|
|
606
|
-
aria-disabled={activeSwitchLocale}
|
|
792
|
+
disabled={activeSwitchLocale || pending}
|
|
793
|
+
aria-disabled={activeSwitchLocale || pending}
|
|
607
794
|
>
|
|
608
795
|
[-QA-] {renderedLocale}
|
|
609
796
|
</button>
|
|
610
797
|
</div>
|
|
611
|
-
|
|
612
|
-
{#if spawnTranslation}
|
|
613
|
-
<TranslationHelperForm
|
|
614
|
-
{capturedSelection}
|
|
615
|
-
{contextPending}
|
|
616
|
-
{contextError}
|
|
617
|
-
{contextResult}
|
|
618
|
-
{selectedResolvedKey}
|
|
619
|
-
{focusedAltKey}
|
|
620
|
-
bind:translatedValue
|
|
621
|
-
{stagedTranslations}
|
|
622
|
-
hasActiveSuggestion={hasActiveSuggestion()}
|
|
623
|
-
{pending}
|
|
624
|
-
{error}
|
|
625
|
-
{success}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
{
|
|
633
|
-
{
|
|
634
|
-
{
|
|
635
|
-
{
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
{
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
798
|
+
|
|
799
|
+
{#if spawnTranslation}
|
|
800
|
+
<TranslationHelperForm
|
|
801
|
+
{capturedSelection}
|
|
802
|
+
{contextPending}
|
|
803
|
+
{contextError}
|
|
804
|
+
{contextResult}
|
|
805
|
+
{selectedResolvedKey}
|
|
806
|
+
{focusedAltKey}
|
|
807
|
+
bind:translatedValue
|
|
808
|
+
{stagedTranslations}
|
|
809
|
+
hasActiveSuggestion={hasActiveSuggestion()}
|
|
810
|
+
{pending}
|
|
811
|
+
{error}
|
|
812
|
+
{success}
|
|
813
|
+
{previewOnly}
|
|
814
|
+
hasDraft={Boolean(
|
|
815
|
+
selectedResolvedKey &&
|
|
816
|
+
getDraft(selectedResolvedKey.msgid, selectedResolvedKey.msgctxt)?.isDirty
|
|
817
|
+
)}
|
|
818
|
+
onClose={requestCloseTranslation}
|
|
819
|
+
onSubmit={submitStagedTranslations}
|
|
820
|
+
onStage={stageCurrentTranslation}
|
|
821
|
+
onInputValue={handleTranslationInput}
|
|
822
|
+
onAltFocusChange={setFocusedAltKey}
|
|
823
|
+
{onAltKeydown}
|
|
824
|
+
onSelectAlt={handleSelectAlt}
|
|
825
|
+
{isStaged}
|
|
826
|
+
{isSelectedAlt}
|
|
827
|
+
{setTranslationInputEl}
|
|
828
|
+
{moveSelection}
|
|
829
|
+
/>
|
|
830
|
+
{/if}
|
|
831
|
+
|
|
832
|
+
{#if showPendingChangesDialog}
|
|
833
|
+
<PendingChangesDialog
|
|
834
|
+
{pending}
|
|
835
|
+
onCancel={() => (showPendingChangesDialog = false)}
|
|
836
|
+
onDiscard={discardAndCloseTranslation}
|
|
837
|
+
onSubmit={() => void submitAndCloseTranslation()}
|
|
838
|
+
/>
|
|
839
|
+
{/if}
|
|
840
|
+
|
|
841
|
+
{#if showRotationWarningDialog}
|
|
842
|
+
<RotationWarningDialog
|
|
843
|
+
pending={rotatePending}
|
|
844
|
+
affected={rotationImpact}
|
|
845
|
+
onCancel={() => {
|
|
846
|
+
showRotationWarningDialog = false;
|
|
847
|
+
rotationImpact = [];
|
|
848
|
+
}}
|
|
849
|
+
onConfirm={() => void runRotation(true)}
|
|
850
|
+
/>
|
|
851
|
+
{/if}
|
|
852
|
+
</div>
|
|
853
|
+
|
|
854
|
+
<style>
|
|
650
855
|
.translator-shell {
|
|
651
856
|
display: flex;
|
|
652
857
|
flex-direction: column;
|
|
@@ -713,7 +918,7 @@
|
|
|
713
918
|
border: 1px solid rgba(255, 130, 130, 0.28);
|
|
714
919
|
color: rgba(255, 224, 224, 0.98);
|
|
715
920
|
}
|
|
716
|
-
|
|
921
|
+
|
|
717
922
|
.translator-toggle {
|
|
718
923
|
display: inline-flex;
|
|
719
924
|
opacity: 0.72;
|
|
@@ -756,7 +961,7 @@
|
|
|
756
961
|
.translator-toggle:hover {
|
|
757
962
|
opacity: 1;
|
|
758
963
|
}
|
|
759
|
-
|
|
964
|
+
|
|
760
965
|
.sticky {
|
|
761
966
|
position: fixed;
|
|
762
967
|
top: 10%;
|