@walkinissue/angy 0.2.17

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.
@@ -0,0 +1,146 @@
1
+ <script lang="ts">
2
+ let {
3
+ text,
4
+ position = 'top',
5
+ disabled = false,
6
+ delay = 120
7
+ }: {
8
+ text: string;
9
+ position?: 'top' | 'bottom' | 'left' | 'right';
10
+ disabled?: boolean;
11
+ delay?: number;
12
+ } = $props();
13
+
14
+ let visible = $state(false);
15
+ let timeout: ReturnType<typeof setTimeout> | null = null;
16
+ const id = `tooltip-${Math.random().toString(36).slice(2, 10)}`;
17
+
18
+ function show() {
19
+ if (disabled || !text) return;
20
+ clearDelay();
21
+ timeout = setTimeout(() => {
22
+ visible = true;
23
+ }, delay);
24
+ }
25
+
26
+ function hide() {
27
+ clearDelay();
28
+ visible = false;
29
+ }
30
+
31
+ function clearDelay() {
32
+ if (timeout) {
33
+ clearTimeout(timeout);
34
+ timeout = null;
35
+ }
36
+ }
37
+ </script>
38
+
39
+ <div
40
+ class="tooltip-wrap"
41
+ onmouseenter={show}
42
+ onmouseleave={hide}
43
+ onfocusin={show}
44
+ onfocusout={hide}
45
+ >
46
+ <slot />
47
+
48
+ {#if visible && !disabled && text}
49
+ <div
50
+ id={id}
51
+ role="tooltip"
52
+ class={`tooltip tooltip--${position}`}
53
+ >
54
+ {text}
55
+ </div>
56
+ {/if}
57
+ </div>
58
+
59
+ <style>
60
+ .tooltip-wrap {
61
+ position: relative;
62
+ display: inline-flex;
63
+ width: fit-content;
64
+ }
65
+
66
+ .tooltip {
67
+ position: absolute;
68
+ z-index: 1000;
69
+ /*max-width: 18rem; /*:S*/
70
+ padding: 0.45rem 0.65rem;
71
+ border-radius: 0.5rem;
72
+ background: rgba(20, 20, 20, 0.96);
73
+ color: white;
74
+ font-size: 0.8rem;
75
+ line-height: 1.2;
76
+ white-space: nowrap;
77
+ pointer-events: none;
78
+ box-shadow:
79
+ 0 8px 24px rgba(0, 0, 0, 0.22),
80
+ 0 2px 8px rgba(0, 0, 0, 0.14);
81
+ }
82
+
83
+ .tooltip::after {
84
+ content: '';
85
+ position: absolute;
86
+ width: 0;
87
+ height: 0;
88
+ border-style: solid;
89
+ }
90
+
91
+ .tooltip--top {
92
+ bottom: calc(100% + 0.5rem);
93
+ left: 50%;
94
+ transform: translateX(-50%);
95
+ }
96
+
97
+ .tooltip--top::after {
98
+ top: 100%;
99
+ left: 50%;
100
+ transform: translateX(-50%);
101
+ border-width: 0.35rem 0.35rem 0 0.35rem;
102
+ border-color: rgba(20, 20, 20, 0.96) transparent transparent transparent;
103
+ }
104
+
105
+ .tooltip--bottom {
106
+ top: calc(100% + 0.5rem);
107
+ left: 50%;
108
+ transform: translateX(-50%);
109
+ }
110
+
111
+ .tooltip--bottom::after {
112
+ bottom: 100%;
113
+ left: 50%;
114
+ transform: translateX(-50%);
115
+ border-width: 0 0.35rem 0.35rem 0.35rem;
116
+ border-color: transparent transparent rgba(20, 20, 20, 0.96) transparent;
117
+ }
118
+
119
+ .tooltip--left {
120
+ right: calc(100% + 0.5rem);
121
+ top: 50%;
122
+ transform: translateY(-50%);
123
+ }
124
+
125
+ .tooltip--left::after {
126
+ left: 100%;
127
+ top: 50%;
128
+ transform: translateY(-50%);
129
+ border-width: 0.35rem 0 0.35rem 0.35rem;
130
+ border-color: transparent transparent transparent rgba(20, 20, 20, 0.96);
131
+ }
132
+
133
+ .tooltip--right {
134
+ left: calc(100% + 0.5rem);
135
+ top: 50%;
136
+ transform: translateY(-50%);
137
+ }
138
+
139
+ .tooltip--right::after {
140
+ right: 100%;
141
+ top: 50%;
142
+ transform: translateY(-50%);
143
+ border-width: 0.35rem 0.35rem 0.35rem 0;
144
+ border-color: transparent rgba(20, 20, 20, 0.96) transparent transparent;
145
+ }
146
+ </style>
@@ -0,0 +1,50 @@
1
+
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
+ function down(event: PointerEvent) {
11
+ dragging = param.drag();
12
+ offsetX = event.clientX - x;
13
+ offsetY = event.clientY - y;
14
+
15
+ window.addEventListener('pointermove', move);
16
+ window.addEventListener('pointerup', up);
17
+ param.active(false)
18
+ }
19
+
20
+ function move(event: PointerEvent) {
21
+ if (!dragging) return;
22
+ x = event.clientX - offsetX;
23
+ y = event.clientY - offsetY;
24
+ node.style.left = `${x}px`;
25
+ node.style.top = `${y}px`;
26
+ param.active(true)
27
+ }
28
+
29
+ function up(event: Event) {
30
+ dragging = false;
31
+ window.removeEventListener('pointermove', move);
32
+ window.removeEventListener('pointerup', up);
33
+
34
+ }
35
+
36
+ node.style.position = 'fixed';
37
+ node.style.left = `${x}px`;
38
+ node.style.top = `${y}px`;
39
+ node.style.touchAction = 'none';
40
+
41
+ node.addEventListener('pointerdown', down);
42
+
43
+ return {
44
+ destroy() {
45
+ node.removeEventListener('pointerdown', down);
46
+ window.removeEventListener('pointermove', move);
47
+ window.removeEventListener('pointerup', up);
48
+ }
49
+ };
50
+ }
@@ -0,0 +1,164 @@
1
+ export type TranslationOrigin = "base" | "working";
2
+
3
+ export type ResolvedKey = {
4
+ msgid: string;
5
+ msgctxt: string | null;
6
+ };
7
+
8
+ export type TranslationEntry = {
9
+ msgid: string;
10
+ msgctxt: string | null;
11
+ msgidPlural: string | null;
12
+ msgstr: string[];
13
+ references: string[];
14
+ extractedComments: string[];
15
+ flags: string[];
16
+ previous: string[];
17
+ obsolete: boolean;
18
+ hasTranslation?: boolean;
19
+ isFuzzy?: boolean | null;
20
+ isCommittedToWorking?: boolean;
21
+ matchesTargetTranslation?: boolean;
22
+ translationOrigin?: TranslationOrigin | null;
23
+ };
24
+
25
+ export type TranslationAlternative = {
26
+ msgid: string;
27
+ msgctxt: string | null;
28
+ score: number;
29
+ references: string[];
30
+ msgstr: string[];
31
+ hasTranslation: boolean;
32
+ isFuzzy: boolean | null | undefined;
33
+ isCommittedToWorking?: boolean;
34
+ matchesTargetTranslation?: boolean;
35
+ translationOrigin?: TranslationOrigin | null;
36
+ };
37
+
38
+ export type TranslationContextResult = {
39
+ match: {
40
+ score: number;
41
+ via: string;
42
+ };
43
+ entry: TranslationEntry;
44
+ alternatives: TranslationAlternative[];
45
+ };
46
+
47
+ export type DraftTranslation = {
48
+ msgid: string;
49
+ msgctxt: string | null;
50
+ value: string;
51
+ origin: TranslationOrigin | null;
52
+ originalValue: string;
53
+ isDirty: boolean;
54
+ };
55
+
56
+ export type TranslationCandidate = Pick<
57
+ TranslationEntry,
58
+ "msgid" | "msgctxt" | "hasTranslation" | "translationOrigin" | "isFuzzy"
59
+ >;
60
+
61
+ export const TRANSLATION_STATUS_TOOLTIP =
62
+ "'\u{1F914}' fuzzy translation needs review. '\u2705' target and working agree. '\u274C' no translation. '\u{1F6E0}\uFE0F' working differs from target.";
63
+
64
+ export function translationKey(msgid: string, msgctxt: string | null) {
65
+ return `${msgctxt ?? ""}::${msgid}`;
66
+ }
67
+
68
+ export function sameResolvedKey(
69
+ left: Pick<ResolvedKey, "msgid" | "msgctxt"> | null | undefined,
70
+ right: Pick<ResolvedKey, "msgid" | "msgctxt"> | null | undefined
71
+ ) {
72
+ return (
73
+ Boolean(left && right) &&
74
+ left?.msgid === right?.msgid &&
75
+ (left?.msgctxt ?? null) === (right?.msgctxt ?? null)
76
+ );
77
+ }
78
+
79
+ export function getEffectiveResolvedKey(
80
+ selectedResolvedKey: ResolvedKey | null,
81
+ contextResult: TranslationContextResult | null
82
+ ) {
83
+ return (
84
+ selectedResolvedKey ??
85
+ (contextResult?.entry
86
+ ? {
87
+ msgid: contextResult.entry.msgid,
88
+ msgctxt: contextResult.entry.msgctxt
89
+ }
90
+ : null)
91
+ );
92
+ }
93
+
94
+ export function findContextEntry(
95
+ contextResult: TranslationContextResult | null,
96
+ msgid: string,
97
+ msgctxt: string | null
98
+ ) {
99
+ if (!contextResult) return null;
100
+
101
+ if (
102
+ contextResult.entry.msgid === msgid &&
103
+ (contextResult.entry.msgctxt ?? null) === (msgctxt ?? null)
104
+ ) {
105
+ return contextResult.entry;
106
+ }
107
+
108
+ return (
109
+ contextResult.alternatives.find(
110
+ (alternative) =>
111
+ alternative.msgid === msgid && (alternative.msgctxt ?? null) === (msgctxt ?? null)
112
+ ) ?? null
113
+ );
114
+ }
115
+
116
+ export function getEntryOriginalValue(
117
+ contextResult: TranslationContextResult | null,
118
+ msgid: string,
119
+ msgctxt: string | null
120
+ ) {
121
+ return findContextEntry(contextResult, msgid, msgctxt)?.msgstr?.[0] ?? "";
122
+ }
123
+
124
+ export function getEntryOrigin(
125
+ contextResult: TranslationContextResult | null,
126
+ msgid: string,
127
+ msgctxt: string | null
128
+ ): TranslationOrigin | null {
129
+ return findContextEntry(contextResult, msgid, msgctxt)?.translationOrigin ?? null;
130
+ }
131
+
132
+ export function getCandidateList(contextResult: TranslationContextResult | null): TranslationCandidate[] {
133
+ if (!contextResult) return [];
134
+
135
+ return [
136
+ {
137
+ msgid: contextResult.entry.msgid,
138
+ msgctxt: contextResult.entry.msgctxt,
139
+ translationOrigin: contextResult.entry.translationOrigin ?? null,
140
+ hasTranslation: contextResult.entry.hasTranslation ?? false,
141
+ isFuzzy: contextResult.entry.isFuzzy ?? null
142
+ },
143
+ ...contextResult.alternatives.map((alternative) => ({
144
+ msgid: alternative.msgid,
145
+ msgctxt: alternative.msgctxt,
146
+ translationOrigin: alternative.translationOrigin ?? null,
147
+ hasTranslation: alternative.hasTranslation,
148
+ isFuzzy: alternative.isFuzzy ?? null
149
+ }))
150
+ ];
151
+ }
152
+
153
+ export function getTranslationStatus(item: {
154
+ isCommittedToWorking?: boolean;
155
+ matchesTargetTranslation?: boolean;
156
+ hasTranslation?: boolean;
157
+ isFuzzy?: boolean | null;
158
+ }) {
159
+ if (item.hasTranslation && item.matchesTargetTranslation) return "\u2705";
160
+ if (item.isCommittedToWorking) return "\u{1F6E0}\uFE0F";
161
+ if (!item.hasTranslation) return "\u274C";
162
+ if (item.isFuzzy) return "\u{1F914}";
163
+ return "\u2705";
164
+ }
@@ -0,0 +1,100 @@
1
+ import { translationKey, type TranslationContextResult } from "./toggleQA.shared";
2
+
3
+ const STORAGE_KEY = "i18n-translation-suggestions-v1";
4
+
5
+
6
+ type SuggestionMap = Record<string, string>;
7
+
8
+ type SuggestionItem = {
9
+ msgid: string;
10
+ msgctxt: string | null;
11
+ suggestion: string;
12
+ };
13
+
14
+ function isBrowser() {
15
+ return typeof window !== "undefined" && typeof localStorage !== "undefined";
16
+ }
17
+
18
+ export function readSuggestionCache(): SuggestionMap {
19
+ if (!isBrowser()) return {};
20
+
21
+ try {
22
+ const raw = localStorage.getItem(STORAGE_KEY);
23
+ if (!raw) return {};
24
+ const parsed = JSON.parse(raw);
25
+ return typeof parsed === "object" && parsed ? parsed : {};
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+
31
+ export function writeSuggestionCache(cache: SuggestionMap) {
32
+ if (!isBrowser()) return;
33
+
34
+ try {
35
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(cache));
36
+ } catch {
37
+ // Ignore storage failures in this dev-only helper.
38
+ }
39
+ }
40
+
41
+ function getEntriesForSuggestions(contextResult: TranslationContextResult) {
42
+ return [contextResult.entry, ...contextResult.alternatives];
43
+ }
44
+
45
+ function getUntranslatedItems(contextResult: TranslationContextResult, cache: SuggestionMap) {
46
+ return getEntriesForSuggestions(contextResult)
47
+ .filter((entry) => !entry.hasTranslation)
48
+ .filter((entry) => !cache[translationKey(entry.msgid, entry.msgctxt)])
49
+ .map((entry) => ({
50
+ msgid: entry.msgid,
51
+ msgctxt: entry.msgctxt
52
+ }));
53
+ }
54
+
55
+ function mergeSuggestionItems(cache: SuggestionMap, items: SuggestionItem[]) {
56
+ const nextCache = { ...cache };
57
+ for (const item of items) {
58
+ nextCache[translationKey(item.msgid, item.msgctxt)] = item.suggestion.trim();
59
+ }
60
+ return nextCache;
61
+ }
62
+
63
+ export async function requestTranslationSuggestions(
64
+ contextResult: TranslationContextResult,
65
+ endpoint: string
66
+ ): Promise<SuggestionMap> {
67
+ const cache = readSuggestionCache();
68
+ const untranslatedItems = getUntranslatedItems(contextResult, cache);
69
+ if (!untranslatedItems.length) {
70
+ return cache;
71
+ }
72
+
73
+ const response = await fetch(`${endpoint}?intent=suggestions`, {
74
+ method: "POST",
75
+ headers: {
76
+ "content-type": "application/json"
77
+ },
78
+ body: JSON.stringify({
79
+ context: contextResult,
80
+ items: untranslatedItems
81
+ })
82
+ });
83
+
84
+ if (!response.ok) {
85
+ throw new Error(`Suggestion request failed: ${response.status}`);
86
+ }
87
+
88
+ const json = await response.json();
89
+ if (json?.disabled) {
90
+ return cache;
91
+ }
92
+ const items = Array.isArray(json?.items) ? (json.items as SuggestionItem[]) : [];
93
+ if (!items.length) {
94
+ return cache;
95
+ }
96
+
97
+ const nextCache = mergeSuggestionItems(cache, items);
98
+ writeSuggestionCache(nextCache);
99
+ return nextCache;
100
+ }
@@ -0,0 +1 @@
1
+ export { default as Angy } from "./client/Angy.svelte";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { default as Angy } from "./client/Angy.svelte";
@@ -0,0 +1,26 @@
1
+ import type { Plugin } from "vite";
2
+ import {
3
+ defineAngyConfig,
4
+ type AngyConfigInput,
5
+ type AngyResolvedConfig,
6
+ type SuggestionProvider,
7
+ type SuggestionProviderInput,
8
+ type SuggestionRequestItem,
9
+ type SuggestionResponseItem
10
+ } from "./server";
11
+
12
+ export type AngyPluginOptions = {
13
+ config?: Partial<AngyResolvedConfig>;
14
+ };
15
+
16
+ export declare function angy(options?: AngyPluginOptions): Plugin;
17
+
18
+ export {
19
+ defineAngyConfig,
20
+ type AngyConfigInput,
21
+ type AngyResolvedConfig,
22
+ type SuggestionProvider,
23
+ type SuggestionProviderInput,
24
+ type SuggestionRequestItem,
25
+ type SuggestionResponseItem
26
+ };