@walkinissue/angy 0.2.17 → 0.2.18
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 +120 -11
- 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 +31 -8
- package/dist/server/types.ts +30 -8
- package/dist/server.d.ts +2 -2
- package/dist/server.js +383 -96
- package/dist/server.js.map +3 -3
- package/package.json +2 -2
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
let {
|
|
3
|
+
pending = false,
|
|
4
|
+
affected,
|
|
5
|
+
onCancel,
|
|
6
|
+
onConfirm
|
|
7
|
+
}: {
|
|
8
|
+
pending?: boolean;
|
|
9
|
+
affected: Array<{
|
|
10
|
+
msgid: string;
|
|
11
|
+
msgctxt: string | null;
|
|
12
|
+
baseValue: string;
|
|
13
|
+
workingValue: string;
|
|
14
|
+
}>;
|
|
15
|
+
onCancel: () => void;
|
|
16
|
+
onConfirm: () => void;
|
|
17
|
+
} = $props();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<div class="dialog-backdrop" role="presentation">
|
|
21
|
+
<div
|
|
22
|
+
class="dialog"
|
|
23
|
+
role="dialog"
|
|
24
|
+
aria-modal="true"
|
|
25
|
+
aria-labelledby="rotation-warning-title"
|
|
26
|
+
aria-describedby="rotation-warning-description"
|
|
27
|
+
>
|
|
28
|
+
<h2 id="rotation-warning-title">Catalog rotation warning</h2>
|
|
29
|
+
<p id="rotation-warning-description">
|
|
30
|
+
These working translations differ from base. Rotating now will promote the working catalog
|
|
31
|
+
and effectively overwrite the current base values for the keys below.
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<ul class="impact-list">
|
|
35
|
+
{#each affected as item}
|
|
36
|
+
<li>
|
|
37
|
+
<div class="impact-key">
|
|
38
|
+
<code>{item.msgid}</code>
|
|
39
|
+
{#if item.msgctxt}
|
|
40
|
+
<code>{item.msgctxt}</code>
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
43
|
+
<div class="impact-values">
|
|
44
|
+
<div>
|
|
45
|
+
<span>Base</span>
|
|
46
|
+
<code>{item.baseValue || "(empty)"}</code>
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<span>Working</span>
|
|
50
|
+
<code>{item.workingValue || "(empty)"}</code>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</li>
|
|
54
|
+
{/each}
|
|
55
|
+
</ul>
|
|
56
|
+
|
|
57
|
+
<div class="actions">
|
|
58
|
+
<button type="button" class="secondary-btn" onclick={onCancel}>Cancel</button>
|
|
59
|
+
<button type="button" class="primary-btn" disabled={pending} onclick={onConfirm}>
|
|
60
|
+
{pending ? "Rotating..." : "Rotate anyway"}
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<style>
|
|
67
|
+
.dialog-backdrop {
|
|
68
|
+
position: fixed;
|
|
69
|
+
inset: 0;
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
padding: 1rem;
|
|
74
|
+
background: rgba(0, 0, 0, 0.55);
|
|
75
|
+
z-index: 500100;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.dialog {
|
|
79
|
+
width: min(42rem, calc(100vw - 2rem));
|
|
80
|
+
max-height: min(80vh, 48rem);
|
|
81
|
+
overflow: auto;
|
|
82
|
+
padding: 1rem;
|
|
83
|
+
border-radius: 1rem;
|
|
84
|
+
border: 1px solid rgba(255, 196, 120, 0.22);
|
|
85
|
+
background: linear-gradient(180deg, rgba(25, 27, 34, 0.98) 0%, rgba(17, 19, 25, 0.99) 100%);
|
|
86
|
+
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.45);
|
|
87
|
+
color: rgba(248, 250, 252, 0.96);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
h2 {
|
|
91
|
+
margin: 0 0 0.5rem;
|
|
92
|
+
font-size: 1rem;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
p {
|
|
96
|
+
margin: 0 0 1rem;
|
|
97
|
+
color: rgba(255, 255, 255, 0.74);
|
|
98
|
+
line-height: 1.45;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.impact-list {
|
|
102
|
+
list-style: none;
|
|
103
|
+
margin: 0;
|
|
104
|
+
padding: 0;
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
gap: 0.75rem;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.impact-list li {
|
|
111
|
+
padding: 0.75rem;
|
|
112
|
+
border-radius: 0.8rem;
|
|
113
|
+
background: rgba(255, 255, 255, 0.035);
|
|
114
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.impact-key,
|
|
118
|
+
.impact-values {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 0.4rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.impact-values {
|
|
125
|
+
margin-top: 0.6rem;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.impact-values span {
|
|
129
|
+
display: inline-block;
|
|
130
|
+
min-width: 4rem;
|
|
131
|
+
font-size: 0.72rem;
|
|
132
|
+
font-weight: 700;
|
|
133
|
+
text-transform: uppercase;
|
|
134
|
+
letter-spacing: 0.05em;
|
|
135
|
+
color: rgba(255, 255, 255, 0.58);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
code {
|
|
139
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
140
|
+
font-size: 0.76rem;
|
|
141
|
+
word-break: break-word;
|
|
142
|
+
color: rgba(232, 243, 255, 0.94);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.actions {
|
|
146
|
+
display: flex;
|
|
147
|
+
justify-content: flex-end;
|
|
148
|
+
gap: 0.6rem;
|
|
149
|
+
margin-top: 1rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.primary-btn,
|
|
153
|
+
.secondary-btn {
|
|
154
|
+
min-height: 2.5rem;
|
|
155
|
+
padding: 0.7rem 1rem;
|
|
156
|
+
border-radius: 0.8rem;
|
|
157
|
+
font: inherit;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.primary-btn {
|
|
162
|
+
border: 1px solid rgba(255, 196, 120, 0.35);
|
|
163
|
+
background: rgba(255, 172, 80, 0.14);
|
|
164
|
+
color: rgba(255, 242, 220, 0.98);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.secondary-btn {
|
|
168
|
+
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
169
|
+
background: rgba(255, 255, 255, 0.05);
|
|
170
|
+
color: rgba(248, 250, 252, 0.96);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
button:disabled {
|
|
174
|
+
opacity: 0.5;
|
|
175
|
+
cursor: not-allowed;
|
|
176
|
+
}
|
|
177
|
+
</style>
|
|
@@ -20,14 +20,17 @@
|
|
|
20
20
|
focusedAltKey,
|
|
21
21
|
translatedValue = $bindable(),
|
|
22
22
|
stagedTranslations,
|
|
23
|
-
hasActiveSuggestion,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
hasActiveSuggestion,
|
|
24
|
+
hasDraft,
|
|
25
|
+
pending,
|
|
26
|
+
previewOnly,
|
|
27
|
+
error,
|
|
28
|
+
success,
|
|
27
29
|
onClose,
|
|
28
30
|
onSubmit,
|
|
29
|
-
onStage,
|
|
30
|
-
|
|
31
|
+
onStage,
|
|
32
|
+
onInputValue,
|
|
33
|
+
onAltFocusChange,
|
|
31
34
|
onAltKeydown,
|
|
32
35
|
onSelectAlt,
|
|
33
36
|
isStaged,
|
|
@@ -43,14 +46,17 @@
|
|
|
43
46
|
focusedAltKey: string | null;
|
|
44
47
|
translatedValue: string;
|
|
45
48
|
stagedTranslations: Record<string, DraftTranslation>;
|
|
46
|
-
hasActiveSuggestion: boolean;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
hasActiveSuggestion: boolean;
|
|
50
|
+
hasDraft: boolean;
|
|
51
|
+
pending: boolean;
|
|
52
|
+
previewOnly: boolean;
|
|
53
|
+
error: string | null;
|
|
54
|
+
success: string | null;
|
|
50
55
|
onClose: () => void;
|
|
51
56
|
onSubmit: (event: SubmitEvent) => void;
|
|
52
|
-
onStage: (event?: Event) => void;
|
|
53
|
-
|
|
57
|
+
onStage: (event?: Event) => void;
|
|
58
|
+
onInputValue: () => void;
|
|
59
|
+
onAltFocusChange: (key: string | null) => void;
|
|
54
60
|
onAltKeydown: (event: KeyboardEvent, msgid: string, msgctxt: string | null) => void;
|
|
55
61
|
onSelectAlt: (event: Event, msgid: string, msgctxt: string | null) => void;
|
|
56
62
|
isStaged: (msgid: string, msgctxt: string | null) => boolean;
|
|
@@ -152,7 +158,14 @@
|
|
|
152
158
|
<div class="context-row">
|
|
153
159
|
<span class="context-label">Match score</span>
|
|
154
160
|
<span>{contextResult.match.score.toFixed(3)}</span>
|
|
155
|
-
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{#if contextResult.catalogState?.status === "out_of_sync"}
|
|
164
|
+
<div class="catalog-warning">
|
|
165
|
+
<span class="context-label">Warning</span>
|
|
166
|
+
<div>Catalogs are out of sync. Rotation can overwrite working-only translations.</div>
|
|
167
|
+
</div>
|
|
168
|
+
{/if}
|
|
156
169
|
|
|
157
170
|
{#if contextResult.entry.flags.length}
|
|
158
171
|
<div class="context-row">
|
|
@@ -214,27 +227,36 @@
|
|
|
214
227
|
<label class="field">
|
|
215
228
|
<span class="field-label">Selected text</span>
|
|
216
229
|
<div class="selected-copy">{capturedSelection}</div>
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
230
|
+
{#if previewOnly}
|
|
231
|
+
<div class="preview-note">
|
|
232
|
+
Working locale preview mode is active. Rotate back to a non-working locale to stage or
|
|
233
|
+
commit translations.
|
|
234
|
+
</div>
|
|
235
|
+
{:else}
|
|
236
|
+
<textarea
|
|
237
|
+
bind:this={translationInputElement}
|
|
238
|
+
name="translationValue"
|
|
239
|
+
class="translation-input"
|
|
240
|
+
class:suggested={hasActiveSuggestion}
|
|
241
|
+
class:draft={hasDraft}
|
|
242
|
+
rows="4"
|
|
243
|
+
bind:value={translatedValue}
|
|
244
|
+
placeholder={capturedSelection}
|
|
245
|
+
oninput={onInputValue}
|
|
246
|
+
onkeydown={(event) => {
|
|
247
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
onStage(event);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (event.key === "Tab") {
|
|
254
|
+
event.preventDefault();
|
|
255
|
+
moveSelection(event.shiftKey ? -1 : 1);
|
|
256
|
+
}
|
|
257
|
+
}}
|
|
258
|
+
></textarea>
|
|
259
|
+
{/if}
|
|
238
260
|
</label>
|
|
239
261
|
|
|
240
262
|
<div class="meta-row">
|
|
@@ -248,27 +270,29 @@
|
|
|
248
270
|
<p>Failed: {error}</p>
|
|
249
271
|
{/if}
|
|
250
272
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
event
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
{#if !previewOnly}
|
|
274
|
+
<button
|
|
275
|
+
type="button"
|
|
276
|
+
class="tool-btn"
|
|
277
|
+
disabled={!translatedValue.trim() || !commitTarget}
|
|
278
|
+
onclick={(event) => {
|
|
279
|
+
event.preventDefault();
|
|
280
|
+
event.stopPropagation();
|
|
281
|
+
onStage(event);
|
|
282
|
+
}}
|
|
283
|
+
>
|
|
284
|
+
Stage
|
|
285
|
+
</button>
|
|
286
|
+
|
|
287
|
+
<button
|
|
288
|
+
type="submit"
|
|
289
|
+
class="tool-btn primary"
|
|
290
|
+
disabled={!hasDirtyTranslations || pending}
|
|
291
|
+
aria-disabled={!hasDirtyTranslations || pending}
|
|
292
|
+
>
|
|
293
|
+
{pending ? "Committing..." : "Commit all"}
|
|
294
|
+
</button>
|
|
295
|
+
{/if}
|
|
272
296
|
</div>
|
|
273
297
|
</form>
|
|
274
298
|
|
|
@@ -455,12 +479,38 @@
|
|
|
455
479
|
box-shadow: 0 0 0 1px rgba(255, 214, 102, 0.18);
|
|
456
480
|
}
|
|
457
481
|
|
|
458
|
-
.translation-input.suggested:hover,
|
|
459
|
-
.translation-input.suggested:focus {
|
|
460
|
-
background: rgba(255, 214, 102, 0.16);
|
|
461
|
-
border-color: rgba(255, 214, 102, 0.48);
|
|
462
|
-
box-shadow: 0 0 0 4px rgba(255, 214, 102, 0.12);
|
|
463
|
-
}
|
|
482
|
+
.translation-input.suggested:hover,
|
|
483
|
+
.translation-input.suggested:focus {
|
|
484
|
+
background: rgba(255, 214, 102, 0.16);
|
|
485
|
+
border-color: rgba(255, 214, 102, 0.48);
|
|
486
|
+
box-shadow: 0 0 0 4px rgba(255, 214, 102, 0.12);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.translation-input.draft {
|
|
490
|
+
background: rgba(255, 120, 120, 0.12);
|
|
491
|
+
border-color: rgba(255, 130, 130, 0.38);
|
|
492
|
+
box-shadow: 0 0 0 1px rgba(255, 130, 130, 0.16);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.preview-note {
|
|
496
|
+
padding: 0.8rem 0.9rem;
|
|
497
|
+
border-radius: 0.8rem;
|
|
498
|
+
background: rgba(255, 196, 120, 0.08);
|
|
499
|
+
border: 1px solid rgba(255, 196, 120, 0.24);
|
|
500
|
+
color: rgba(255, 242, 220, 0.96);
|
|
501
|
+
font-size: 0.85rem;
|
|
502
|
+
line-height: 1.45;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.catalog-warning {
|
|
506
|
+
padding: 0.75rem 0.8rem;
|
|
507
|
+
border-radius: 0.8rem;
|
|
508
|
+
background: rgba(255, 188, 88, 0.1);
|
|
509
|
+
border: 1px solid rgba(255, 188, 88, 0.26);
|
|
510
|
+
color: rgba(255, 243, 220, 0.96);
|
|
511
|
+
font-size: 0.82rem;
|
|
512
|
+
line-height: 1.45;
|
|
513
|
+
}
|
|
464
514
|
|
|
465
515
|
.meta-row {
|
|
466
516
|
display: flex;
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
text,
|
|
6
|
+
position = 'top',
|
|
7
|
+
disabled = false,
|
|
8
|
+
delay = 120,
|
|
9
|
+
children
|
|
10
|
+
}: {
|
|
11
|
+
text: string;
|
|
12
|
+
position?: 'top' | 'bottom' | 'left' | 'right';
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
delay?: number;
|
|
15
|
+
children?: Snippet;
|
|
16
|
+
} = $props();
|
|
13
17
|
|
|
14
18
|
let visible = $state(false);
|
|
15
19
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -42,8 +46,8 @@
|
|
|
42
46
|
onmouseleave={hide}
|
|
43
47
|
onfocusin={show}
|
|
44
48
|
onfocusout={hide}
|
|
45
|
-
>
|
|
46
|
-
|
|
49
|
+
>
|
|
50
|
+
{@render children?.()}
|
|
47
51
|
|
|
48
52
|
{#if visible && !disabled && text}
|
|
49
53
|
<div
|
package/dist/client/dragItem.ts
CHANGED
|
@@ -1,48 +1,74 @@
|
|
|
1
1
|
|
|
2
2
|
export function draggable(node: HTMLElement, param:{active: (activate:boolean) => void, drag: ()=> boolean}) {
|
|
3
|
-
|
|
4
|
-
let x = 100;
|
|
5
|
-
let y = 100;
|
|
6
|
-
let offsetX = 0;
|
|
7
|
-
let offsetY = 0;
|
|
8
|
-
let dragging = false;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
3
|
+
|
|
4
|
+
let x = 100;
|
|
5
|
+
let y = 100;
|
|
6
|
+
let offsetX = 0;
|
|
7
|
+
let offsetY = 0;
|
|
8
|
+
let dragging = false;
|
|
9
|
+
let pointerStartX = 0;
|
|
10
|
+
let pointerStartY = 0;
|
|
11
|
+
const dragThreshold = 6;
|
|
12
|
+
let suppressClick = false;
|
|
13
|
+
|
|
14
|
+
function down(event: PointerEvent) {
|
|
15
|
+
dragging = false;
|
|
16
|
+
pointerStartX = event.clientX;
|
|
17
|
+
pointerStartY = event.clientY;
|
|
18
|
+
offsetX = event.clientX - x;
|
|
19
|
+
offsetY = event.clientY - y;
|
|
20
|
+
|
|
21
|
+
window.addEventListener('pointermove', move);
|
|
22
|
+
window.addEventListener('pointerup', up);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function move(event: PointerEvent) {
|
|
26
|
+
if (!dragging) {
|
|
27
|
+
if (!param.drag()) return;
|
|
28
|
+
const movedX = Math.abs(event.clientX - pointerStartX);
|
|
29
|
+
const movedY = Math.abs(event.clientY - pointerStartY);
|
|
30
|
+
if (Math.max(movedX, movedY) < dragThreshold) return;
|
|
31
|
+
dragging = true;
|
|
32
|
+
param.active(true);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
x = event.clientX - offsetX;
|
|
36
|
+
y = event.clientY - offsetY;
|
|
37
|
+
node.style.left = `${x}px`;
|
|
38
|
+
node.style.top = `${y}px`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function up(event: Event) {
|
|
42
|
+
if (dragging) {
|
|
43
|
+
suppressClick = true;
|
|
44
|
+
param.active(false);
|
|
45
|
+
}
|
|
46
|
+
dragging = false;
|
|
47
|
+
window.removeEventListener('pointermove', move);
|
|
48
|
+
window.removeEventListener('pointerup', up);
|
|
49
|
+
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onClick(event: MouseEvent) {
|
|
53
|
+
if (!suppressClick) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
event.stopPropagation();
|
|
56
|
+
suppressClick = false;
|
|
57
|
+
}
|
|
35
58
|
|
|
36
|
-
node.style.position = 'fixed';
|
|
37
|
-
node.style.left = `${x}px`;
|
|
38
|
-
node.style.top = `${y}px`;
|
|
39
|
-
node.style.touchAction = 'none';
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
node.style.position = 'fixed';
|
|
60
|
+
node.style.left = `${x}px`;
|
|
61
|
+
node.style.top = `${y}px`;
|
|
62
|
+
node.style.touchAction = 'none';
|
|
63
|
+
param.active(false);
|
|
64
|
+
|
|
65
|
+
node.addEventListener('pointerdown', down);
|
|
66
|
+
node.addEventListener('click', onClick, true);
|
|
67
|
+
|
|
43
68
|
return {
|
|
44
69
|
destroy() {
|
|
45
70
|
node.removeEventListener('pointerdown', down);
|
|
71
|
+
node.removeEventListener('click', onClick, true);
|
|
46
72
|
window.removeEventListener('pointermove', move);
|
|
47
73
|
window.removeEventListener('pointerup', up);
|
|
48
74
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type TranslationOrigin = "base" | "working";
|
|
1
|
+
export type TranslationOrigin = "base" | "working";
|
|
2
|
+
export type TranslationStatus = "base" | "working" | "none" | "fuzzy" | "out_of_sync";
|
|
2
3
|
|
|
3
4
|
export type ResolvedKey = {
|
|
4
5
|
msgid: string;
|
|
@@ -20,6 +21,7 @@ export type TranslationEntry = {
|
|
|
20
21
|
isCommittedToWorking?: boolean;
|
|
21
22
|
matchesTargetTranslation?: boolean;
|
|
22
23
|
translationOrigin?: TranslationOrigin | null;
|
|
24
|
+
translationStatus?: TranslationStatus | null;
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
export type TranslationAlternative = {
|
|
@@ -33,16 +35,21 @@ export type TranslationAlternative = {
|
|
|
33
35
|
isCommittedToWorking?: boolean;
|
|
34
36
|
matchesTargetTranslation?: boolean;
|
|
35
37
|
translationOrigin?: TranslationOrigin | null;
|
|
38
|
+
translationStatus?: TranslationStatus | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type TranslationContextResult = {
|
|
42
|
+
match: {
|
|
43
|
+
score: number;
|
|
44
|
+
via: string;
|
|
45
|
+
};
|
|
46
|
+
catalogState?: {
|
|
47
|
+
status: "ok" | "out_of_sync";
|
|
48
|
+
affectedCount: number;
|
|
49
|
+
};
|
|
50
|
+
entry: TranslationEntry;
|
|
51
|
+
alternatives: TranslationAlternative[];
|
|
36
52
|
};
|
|
37
|
-
|
|
38
|
-
export type TranslationContextResult = {
|
|
39
|
-
match: {
|
|
40
|
-
score: number;
|
|
41
|
-
via: string;
|
|
42
|
-
};
|
|
43
|
-
entry: TranslationEntry;
|
|
44
|
-
alternatives: TranslationAlternative[];
|
|
45
|
-
};
|
|
46
53
|
|
|
47
54
|
export type DraftTranslation = {
|
|
48
55
|
msgid: string;
|
|
@@ -59,7 +66,7 @@ export type TranslationCandidate = Pick<
|
|
|
59
66
|
>;
|
|
60
67
|
|
|
61
68
|
export const TRANSLATION_STATUS_TOOLTIP =
|
|
62
|
-
"'\u{1F914}' fuzzy translation needs review. '\u2705'
|
|
69
|
+
"'\u{1F914}' fuzzy translation needs review. '\u2705' translation is in base. '\u274C' no translation. '\u{1F6E0}\uFE0F' working differs from base. '\u26A0\uFE0F' catalogs are out of sync.";
|
|
63
70
|
|
|
64
71
|
export function translationKey(msgid: string, msgctxt: string | null) {
|
|
65
72
|
return `${msgctxt ?? ""}::${msgid}`;
|
|
@@ -155,10 +162,11 @@ export function getTranslationStatus(item: {
|
|
|
155
162
|
matchesTargetTranslation?: boolean;
|
|
156
163
|
hasTranslation?: boolean;
|
|
157
164
|
isFuzzy?: boolean | null;
|
|
165
|
+
translationStatus?: TranslationStatus | null;
|
|
158
166
|
}) {
|
|
159
|
-
if (item.
|
|
160
|
-
if (item.
|
|
161
|
-
if (
|
|
162
|
-
if (item.
|
|
167
|
+
if (item.translationStatus === "out_of_sync") return "\u26A0\uFE0F";
|
|
168
|
+
if (item.translationStatus === "fuzzy" || item.isFuzzy) return "\u{1F914}";
|
|
169
|
+
if (item.translationStatus === "working" || item.isCommittedToWorking) return "\u{1F6E0}\uFE0F";
|
|
170
|
+
if (item.translationStatus === "none" || !item.hasTranslation) return "\u274C";
|
|
163
171
|
return "\u2705";
|
|
164
172
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { translationKey } from "./toggleQA.shared";
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = "angy-translation-drafts-v1";
|
|
4
|
+
|
|
5
|
+
export type DraftCacheItem = {
|
|
6
|
+
value: string;
|
|
7
|
+
isDirty: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type DraftCache = Record<string, DraftCacheItem>;
|
|
11
|
+
|
|
12
|
+
function isBrowser() {
|
|
13
|
+
return typeof window !== "undefined" && typeof localStorage !== "undefined";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readDraftCache(): DraftCache {
|
|
17
|
+
if (!isBrowser()) return {};
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
21
|
+
if (!raw) return {};
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return typeof parsed === "object" && parsed ? parsed : {};
|
|
24
|
+
} catch {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function writeDraftCache(cache: DraftCache) {
|
|
30
|
+
if (!isBrowser()) return;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
|
|
34
|
+
} catch {
|
|
35
|
+
// Ignore storage failures in this dev-only helper.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function setDraftValue(
|
|
40
|
+
cache: DraftCache,
|
|
41
|
+
msgid: string,
|
|
42
|
+
msgctxt: string | null,
|
|
43
|
+
item: DraftCacheItem
|
|
44
|
+
) {
|
|
45
|
+
const next = { ...cache, [translationKey(msgid, msgctxt)]: item };
|
|
46
|
+
writeDraftCache(next);
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function clearDraftValue(cache: DraftCache, msgid: string, msgctxt: string | null) {
|
|
51
|
+
const next = { ...cache };
|
|
52
|
+
delete next[translationKey(msgid, msgctxt)];
|
|
53
|
+
writeDraftCache(next);
|
|
54
|
+
return next;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getDraftValue(cache: DraftCache, msgid: string, msgctxt: string | null) {
|
|
58
|
+
return cache[translationKey(msgid, msgctxt)] ?? null;
|
|
59
|
+
}
|