@translifycc/angular 0.1.2 → 0.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.
@@ -0,0 +1,3 @@
1
+ export { TranslifySelectorComponent } from './translify-selector.component';
2
+ export { TranslifyModule } from './translify.module';
3
+ export type { Language } from './translate';
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { TranslifySelectorComponent } from './translify-selector.component';
2
+ export { TranslifyModule } from './translify.module';
@@ -0,0 +1,15 @@
1
+ export interface Language {
2
+ code: string;
3
+ name: string;
4
+ nativeName: string;
5
+ isRTL: boolean;
6
+ }
7
+ export declare function getSavedLang(): string | null;
8
+ export declare function saveLang(lang: string): void;
9
+ export declare function clearSavedLang(): void;
10
+ export declare function loadTranslationCache(lang: string): Map<string, string>;
11
+ export declare function saveTranslationCache(lang: string, cache: Map<string, string>): void;
12
+ export declare function getTextNodes(root: Node): Text[];
13
+ export declare function collectNewTextNodes(node: Node, originalTexts: Map<Node, string>): Text[];
14
+ export declare function fetchLanguages(apiBase: string): Promise<Language[]>;
15
+ export declare function callTranslateApi(apiBase: string, apiKey: string, domain: string, texts: string[], targetLang: string): Promise<string[]>;
@@ -0,0 +1,100 @@
1
+ const SKIP_TAGS = new Set([
2
+ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME',
3
+ 'CODE', 'PRE', 'SVG', 'MATH', 'INPUT', 'TEXTAREA',
4
+ ]);
5
+ export function getSavedLang() {
6
+ return localStorage.getItem('translify_lang');
7
+ }
8
+ export function saveLang(lang) {
9
+ localStorage.setItem('translify_lang', lang);
10
+ }
11
+ export function clearSavedLang() {
12
+ localStorage.removeItem('translify_lang');
13
+ }
14
+ export function loadTranslationCache(lang) {
15
+ try {
16
+ const stored = localStorage.getItem(`translify_cache_${lang}`);
17
+ return stored ? new Map(Object.entries(JSON.parse(stored))) : new Map();
18
+ }
19
+ catch (_a) {
20
+ return new Map();
21
+ }
22
+ }
23
+ export function saveTranslationCache(lang, cache) {
24
+ try {
25
+ const obj = {};
26
+ cache.forEach((v, k) => { obj[k] = v; });
27
+ localStorage.setItem(`translify_cache_${lang}`, JSON.stringify(obj));
28
+ }
29
+ catch (_a) { }
30
+ }
31
+ export function getTextNodes(root) {
32
+ const nodes = [];
33
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
34
+ acceptNode(node) {
35
+ var _a, _b, _c, _d, _e, _f;
36
+ const text = node;
37
+ if (!((_a = text.textContent) === null || _a === void 0 ? void 0 : _a.trim()))
38
+ return NodeFilter.FILTER_REJECT;
39
+ if (SKIP_TAGS.has(((_c = (_b = text.parentElement) === null || _b === void 0 ? void 0 : _b.tagName) !== null && _c !== void 0 ? _c : '')))
40
+ return NodeFilter.FILTER_REJECT;
41
+ if (((_e = (_d = text.textContent) === null || _d === void 0 ? void 0 : _d.trim().length) !== null && _e !== void 0 ? _e : 0) > 5000)
42
+ return NodeFilter.FILTER_REJECT;
43
+ if ((_f = text.parentElement) === null || _f === void 0 ? void 0 : _f.closest('[translate="no"]'))
44
+ return NodeFilter.FILTER_REJECT;
45
+ return NodeFilter.FILTER_ACCEPT;
46
+ },
47
+ });
48
+ let n;
49
+ while ((n = walker.nextNode()))
50
+ nodes.push(n);
51
+ return nodes;
52
+ }
53
+ export function collectNewTextNodes(node, originalTexts) {
54
+ var _a, _b, _c, _d, _e;
55
+ const nodes = [];
56
+ if (node.nodeType === Node.TEXT_NODE) {
57
+ const t = node;
58
+ if (!originalTexts.has(t) && ((_a = t.textContent) === null || _a === void 0 ? void 0 : _a.trim()))
59
+ nodes.push(t);
60
+ }
61
+ else if (node.nodeType === Node.ELEMENT_NODE) {
62
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
63
+ let n;
64
+ while ((n = walker.nextNode())) {
65
+ const t = n;
66
+ if (!originalTexts.has(t) &&
67
+ ((_b = t.textContent) === null || _b === void 0 ? void 0 : _b.trim()) &&
68
+ !SKIP_TAGS.has((_d = (_c = t.parentElement) === null || _c === void 0 ? void 0 : _c.tagName) !== null && _d !== void 0 ? _d : '') &&
69
+ !((_e = t.parentElement) === null || _e === void 0 ? void 0 : _e.closest('[translate="no"]'))) {
70
+ nodes.push(t);
71
+ }
72
+ }
73
+ }
74
+ return nodes;
75
+ }
76
+ export async function fetchLanguages(apiBase) {
77
+ const res = await fetch(`${apiBase}/translate/languages`);
78
+ const json = await res.json();
79
+ return json.data.languages;
80
+ }
81
+ export async function callTranslateApi(apiBase, apiKey, domain, texts, targetLang) {
82
+ const results = [];
83
+ for (let i = 0; i < texts.length; i += 100) {
84
+ const chunk = texts.slice(i, i + 100);
85
+ const res = await fetch(`${apiBase}/translate`, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ 'x-api-key': apiKey,
90
+ 'x-domain': domain,
91
+ },
92
+ body: JSON.stringify({ texts: chunk, targetLanguage: targetLang, sourceLanguage: 'auto' }),
93
+ });
94
+ const json = await res.json();
95
+ if (!json.success)
96
+ throw new Error(json.message || 'Translation failed');
97
+ results.push(...json.data.translations);
98
+ }
99
+ return results;
100
+ }
@@ -0,0 +1,64 @@
1
+ import { OnInit, OnDestroy, NgZone, ChangeDetectorRef } from '@angular/core';
2
+ import { Language } from './translate';
3
+ export declare class TranslifySelectorComponent implements OnInit, OnDestroy {
4
+ private ngZone;
5
+ private cdr;
6
+ apiKey: string;
7
+ apiBase: string;
8
+ domain: string;
9
+ classNames: {
10
+ trigger?: string;
11
+ dropdown?: string;
12
+ dropdownHeader?: string;
13
+ option?: string;
14
+ optionActive?: string;
15
+ optionNative?: string;
16
+ optionName?: string;
17
+ optionRtl?: string;
18
+ restore?: string;
19
+ overlay?: string;
20
+ overlaySpinner?: string;
21
+ overlayTitle?: string;
22
+ overlaySub?: string;
23
+ toast?: string;
24
+ };
25
+ labels: {
26
+ trigger?: string;
27
+ dropdownHeader?: string;
28
+ restore?: string;
29
+ overlayTitle?: string;
30
+ overlaySub?: string;
31
+ };
32
+ languages: Language[];
33
+ activeLang: string | null;
34
+ isOpen: boolean;
35
+ loadingState: 'idle' | 'overlay' | 'toast';
36
+ overlayVisible: boolean;
37
+ toastVisible: boolean;
38
+ private originalTexts;
39
+ private translationCache;
40
+ private currentCacheLang;
41
+ private isTranslating;
42
+ private pendingNodes;
43
+ private observer;
44
+ private debounceTimer;
45
+ private overlayTimer;
46
+ constructor(ngZone: NgZone, cdr: ChangeDetectorRef);
47
+ get activeLangObj(): Language | undefined;
48
+ ngOnInit(): Promise<void>;
49
+ ngOnDestroy(): void;
50
+ private ensureCacheLoaded;
51
+ private applyCache;
52
+ private showOverlay;
53
+ private hideOverlay;
54
+ private showToast;
55
+ private hideToast;
56
+ private flushNodes;
57
+ private startObserver;
58
+ private stopObserver;
59
+ private patchHistory;
60
+ private onRouteChange;
61
+ translate(targetLang: string): Promise<void>;
62
+ restore(): void;
63
+ handleSelect(langCode: string): void;
64
+ }
@@ -0,0 +1,461 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Component, Input, NgZone, ChangeDetectionStrategy, ChangeDetectorRef, } from '@angular/core';
11
+ import { NgIf, NgFor, NgClass } from '@angular/common';
12
+ import { getSavedLang, saveLang, clearSavedLang, loadTranslationCache, saveTranslationCache, getTextNodes, collectNewTextNodes, fetchLanguages, callTranslateApi, } from './translate';
13
+ let TranslifySelectorComponent = class TranslifySelectorComponent {
14
+ constructor(ngZone, cdr) {
15
+ this.ngZone = ngZone;
16
+ this.cdr = cdr;
17
+ this.apiBase = 'https://translify.cc';
18
+ this.domain = window.location.hostname || 'localhost';
19
+ this.classNames = {};
20
+ this.labels = {};
21
+ this.languages = [];
22
+ this.activeLang = getSavedLang();
23
+ this.isOpen = false;
24
+ this.loadingState = 'idle';
25
+ this.overlayVisible = false;
26
+ this.toastVisible = false;
27
+ // All mutable translation state — no change detection needed
28
+ this.originalTexts = new Map();
29
+ this.translationCache = new Map();
30
+ this.currentCacheLang = null;
31
+ this.isTranslating = false;
32
+ this.pendingNodes = [];
33
+ this.observer = null;
34
+ this.debounceTimer = null;
35
+ this.overlayTimer = null;
36
+ }
37
+ get activeLangObj() {
38
+ return this.languages.find((l) => l.code === this.activeLang);
39
+ }
40
+ async ngOnInit() {
41
+ if (!this.apiKey) {
42
+ console.error('[Translify] apiKey is required.');
43
+ return;
44
+ }
45
+ try {
46
+ this.languages = await fetchLanguages(this.apiBase);
47
+ this.cdr.markForCheck();
48
+ }
49
+ catch (e) {
50
+ console.error('[Translify] Failed to fetch languages', e);
51
+ return;
52
+ }
53
+ this.ngZone.runOutsideAngular(() => {
54
+ this.startObserver();
55
+ this.patchHistory();
56
+ });
57
+ if (this.activeLang && this.languages.length > 0) {
58
+ await this.translate(this.activeLang);
59
+ }
60
+ }
61
+ ngOnDestroy() {
62
+ this.stopObserver();
63
+ clearTimeout(this.overlayTimer);
64
+ }
65
+ // ── Cache helpers ───────────────────────────────────────────────────────────
66
+ ensureCacheLoaded(lang) {
67
+ if (this.currentCacheLang === lang)
68
+ return;
69
+ this.translationCache = loadTranslationCache(lang);
70
+ this.currentCacheLang = lang;
71
+ }
72
+ applyCache(nodes) {
73
+ const uncached = [];
74
+ nodes.forEach((node) => {
75
+ var _a, _b;
76
+ const src = (_b = (_a = this.originalTexts.get(node)) !== null && _a !== void 0 ? _a : node.textContent) !== null && _b !== void 0 ? _b : '';
77
+ const hit = this.translationCache.get(src);
78
+ if (hit !== undefined) {
79
+ node.textContent = hit;
80
+ }
81
+ else {
82
+ uncached.push(node);
83
+ }
84
+ });
85
+ return uncached;
86
+ }
87
+ // ── Overlay / toast ─────────────────────────────────────────────────────────
88
+ showOverlay() {
89
+ clearTimeout(this.overlayTimer);
90
+ this.ngZone.run(() => {
91
+ this.overlayVisible = false;
92
+ this.loadingState = 'overlay';
93
+ requestAnimationFrame(() => { this.overlayVisible = true; this.cdr.markForCheck(); });
94
+ this.cdr.markForCheck();
95
+ });
96
+ }
97
+ hideOverlay() {
98
+ this.overlayTimer = setTimeout(() => {
99
+ this.ngZone.run(() => { this.overlayVisible = false; this.cdr.markForCheck(); });
100
+ this.overlayTimer = setTimeout(() => {
101
+ this.ngZone.run(() => { this.loadingState = 'idle'; this.cdr.markForCheck(); });
102
+ }, 400);
103
+ }, 600);
104
+ }
105
+ showToast() {
106
+ clearTimeout(this.overlayTimer);
107
+ this.ngZone.run(() => {
108
+ this.loadingState = 'toast';
109
+ requestAnimationFrame(() => { this.toastVisible = true; this.cdr.markForCheck(); });
110
+ this.cdr.markForCheck();
111
+ });
112
+ }
113
+ hideToast() {
114
+ this.ngZone.run(() => {
115
+ this.toastVisible = false;
116
+ this.overlayTimer = setTimeout(() => {
117
+ this.ngZone.run(() => { this.loadingState = 'idle'; this.cdr.markForCheck(); });
118
+ }, 350);
119
+ this.cdr.markForCheck();
120
+ });
121
+ }
122
+ // ── flushNodes ──────────────────────────────────────────────────────────────
123
+ async flushNodes(nodes, targetLang) {
124
+ if (!nodes.length)
125
+ return;
126
+ if (this.isTranslating) {
127
+ this.pendingNodes.push(...nodes);
128
+ return;
129
+ }
130
+ this.isTranslating = true;
131
+ try {
132
+ nodes.forEach((n) => {
133
+ var _a;
134
+ if (!this.originalTexts.has(n))
135
+ this.originalTexts.set(n, (_a = n.textContent) !== null && _a !== void 0 ? _a : '');
136
+ });
137
+ const uncached = this.applyCache(nodes);
138
+ if (uncached.length > 0) {
139
+ const texts = uncached.map((n) => this.originalTexts.get(n));
140
+ const translations = await callTranslateApi(this.apiBase, this.apiKey, this.domain, texts, targetLang);
141
+ uncached.forEach((node, i) => {
142
+ if (translations[i] !== undefined) {
143
+ this.translationCache.set(this.originalTexts.get(node), translations[i]);
144
+ node.textContent = translations[i];
145
+ }
146
+ });
147
+ saveTranslationCache(targetLang, this.translationCache);
148
+ }
149
+ }
150
+ catch (e) {
151
+ console.error('[Translify] Dynamic translation error', e);
152
+ }
153
+ finally {
154
+ this.isTranslating = false;
155
+ if (this.pendingNodes.length > 0) {
156
+ const next = this.pendingNodes.splice(0);
157
+ this.flushNodes(next, targetLang);
158
+ }
159
+ }
160
+ }
161
+ // ── MutationObserver ────────────────────────────────────────────────────────
162
+ startObserver() {
163
+ if (this.observer)
164
+ return;
165
+ this.observer = new MutationObserver((mutations) => {
166
+ if (!this.activeLang)
167
+ return;
168
+ const targetLang = this.activeLang;
169
+ const newNodes = [];
170
+ mutations.forEach((mutation) => {
171
+ if (mutation.type !== 'childList')
172
+ return;
173
+ mutation.addedNodes.forEach((node) => {
174
+ newNodes.push(...collectNewTextNodes(node, this.originalTexts));
175
+ });
176
+ });
177
+ if (!newNodes.length)
178
+ return;
179
+ const uncached = [];
180
+ newNodes.forEach((node) => {
181
+ var _a;
182
+ const src = (_a = node.textContent) !== null && _a !== void 0 ? _a : '';
183
+ const hit = this.translationCache.get(src);
184
+ if (hit !== undefined) {
185
+ this.originalTexts.set(node, src);
186
+ node.textContent = hit;
187
+ }
188
+ else {
189
+ uncached.push(node);
190
+ }
191
+ });
192
+ if (!uncached.length)
193
+ return;
194
+ this.pendingNodes.push(...uncached);
195
+ if (!this.isTranslating) {
196
+ clearTimeout(this.debounceTimer);
197
+ this.debounceTimer = setTimeout(() => {
198
+ const toFlush = this.pendingNodes.splice(0);
199
+ this.flushNodes(toFlush, targetLang);
200
+ }, 50);
201
+ }
202
+ });
203
+ this.observer.observe(document.body, { childList: true, subtree: true });
204
+ }
205
+ stopObserver() {
206
+ if (this.observer) {
207
+ this.observer.disconnect();
208
+ this.observer = null;
209
+ }
210
+ clearTimeout(this.debounceTimer);
211
+ }
212
+ // ── SPA route change detection ──────────────────────────────────────────────
213
+ patchHistory() {
214
+ const orig = history.pushState.bind(history);
215
+ const self = this;
216
+ history.pushState = function (state, unused, url) {
217
+ orig(state, unused, url);
218
+ self.onRouteChange();
219
+ };
220
+ window.addEventListener('popstate', () => this.onRouteChange());
221
+ }
222
+ onRouteChange() {
223
+ if (!this.activeLang)
224
+ return;
225
+ const targetLang = this.activeLang;
226
+ setTimeout(() => {
227
+ const nodes = getTextNodes(document.body).filter((n) => !this.originalTexts.has(n));
228
+ if (!nodes.length)
229
+ return;
230
+ this.pendingNodes.push(...nodes);
231
+ if (!this.isTranslating) {
232
+ const toFlush = this.pendingNodes.splice(0);
233
+ this.flushNodes(toFlush, targetLang);
234
+ }
235
+ }, 100);
236
+ }
237
+ // ── Main translate ──────────────────────────────────────────────────────────
238
+ async translate(targetLang) {
239
+ if (this.isTranslating)
240
+ return;
241
+ this.isTranslating = true;
242
+ try {
243
+ this.ensureCacheLoaded(targetLang);
244
+ const isFirstEver = this.translationCache.size === 0;
245
+ if (this.originalTexts.size > 0) {
246
+ this.originalTexts.forEach((text, node) => { node.textContent = text; });
247
+ }
248
+ const nodes = getTextNodes(document.body);
249
+ if (!nodes.length)
250
+ return;
251
+ nodes.forEach((node) => {
252
+ var _a;
253
+ if (!this.originalTexts.has(node))
254
+ this.originalTexts.set(node, (_a = node.textContent) !== null && _a !== void 0 ? _a : '');
255
+ });
256
+ const uncached = this.applyCache(nodes);
257
+ const lang = this.languages.find((l) => l.code === targetLang);
258
+ document.documentElement.setAttribute('dir', (lang === null || lang === void 0 ? void 0 : lang.isRTL) ? 'rtl' : 'ltr');
259
+ saveLang(targetLang);
260
+ this.ngZone.run(() => { this.activeLang = targetLang; this.cdr.markForCheck(); });
261
+ if (uncached.length > 0) {
262
+ if (isFirstEver)
263
+ this.showOverlay();
264
+ else
265
+ this.showToast();
266
+ const texts = uncached.map((n) => this.originalTexts.get(n));
267
+ const translations = await callTranslateApi(this.apiBase, this.apiKey, this.domain, texts, targetLang);
268
+ uncached.forEach((node, i) => {
269
+ if (translations[i] !== undefined) {
270
+ this.translationCache.set(this.originalTexts.get(node), translations[i]);
271
+ node.textContent = translations[i];
272
+ }
273
+ });
274
+ saveTranslationCache(targetLang, this.translationCache);
275
+ if (isFirstEver)
276
+ this.hideOverlay();
277
+ else
278
+ this.hideToast();
279
+ }
280
+ }
281
+ catch (e) {
282
+ console.error('[Translify] Translation error', e);
283
+ this.hideOverlay();
284
+ this.hideToast();
285
+ }
286
+ finally {
287
+ this.isTranslating = false;
288
+ if (this.pendingNodes.length > 0) {
289
+ const next = this.pendingNodes.splice(0);
290
+ this.flushNodes(next, this.activeLang);
291
+ }
292
+ }
293
+ }
294
+ // ── Restore ─────────────────────────────────────────────────────────────────
295
+ restore() {
296
+ this.stopObserver();
297
+ this.originalTexts.forEach((text, node) => { node.textContent = text; });
298
+ this.originalTexts.clear();
299
+ document.documentElement.setAttribute('dir', 'ltr');
300
+ clearSavedLang();
301
+ this.activeLang = null;
302
+ this.isOpen = false;
303
+ this.cdr.markForCheck();
304
+ this.ngZone.runOutsideAngular(() => this.startObserver());
305
+ }
306
+ // ── UI handlers ─────────────────────────────────────────────────────────────
307
+ handleSelect(langCode) {
308
+ this.isOpen = false;
309
+ this.translate(langCode);
310
+ }
311
+ };
312
+ __decorate([
313
+ Input(),
314
+ __metadata("design:type", String)
315
+ ], TranslifySelectorComponent.prototype, "apiKey", void 0);
316
+ __decorate([
317
+ Input(),
318
+ __metadata("design:type", Object)
319
+ ], TranslifySelectorComponent.prototype, "apiBase", void 0);
320
+ __decorate([
321
+ Input(),
322
+ __metadata("design:type", Object)
323
+ ], TranslifySelectorComponent.prototype, "domain", void 0);
324
+ __decorate([
325
+ Input(),
326
+ __metadata("design:type", Object)
327
+ ], TranslifySelectorComponent.prototype, "classNames", void 0);
328
+ __decorate([
329
+ Input(),
330
+ __metadata("design:type", Object)
331
+ ], TranslifySelectorComponent.prototype, "labels", void 0);
332
+ TranslifySelectorComponent = __decorate([
333
+ Component({
334
+ selector: 'translify-selector',
335
+ standalone: true,
336
+ imports: [NgIf, NgFor, NgClass],
337
+ changeDetection: ChangeDetectionStrategy.OnPush,
338
+ styles: [`
339
+ .translify-selector { position: relative; display: inline-block; }
340
+ .translify-trigger {
341
+ display: inline-flex; align-items: center; gap: 6px;
342
+ padding: 8px 14px; border-radius: 8px; border: 1px solid #e5e7eb;
343
+ background: #fff; cursor: pointer; font-size: 14px; color: #374151;
344
+ transition: border-color 0.15s;
345
+ }
346
+ .translify-trigger:hover { border-color: #9ca3af; }
347
+ .translify-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
348
+ .translify-trigger-chevron { font-size: 10px; transition: transform 0.2s; }
349
+ .translify-trigger-chevron.open { transform: rotate(180deg); }
350
+ .translify-dropdown {
351
+ position: absolute; top: calc(100% + 6px); left: 0; z-index: 9999;
352
+ background: #fff; border: 1px solid #e5e7eb; border-radius: 12px;
353
+ min-width: 220px; max-height: 320px; overflow-y: auto;
354
+ box-shadow: 0 8px 24px rgba(0,0,0,0.12); display: flex; flex-direction: column;
355
+ }
356
+ .translify-dropdown-header {
357
+ padding: 10px 14px; font-size: 11px; font-weight: 700;
358
+ color: #6b7280; text-transform: uppercase; letter-spacing: 0.06em;
359
+ border-bottom: 1px solid #f3f4f6; flex-shrink: 0;
360
+ }
361
+ .translify-option {
362
+ display: flex; align-items: center; gap: 8px;
363
+ padding: 10px 14px; cursor: pointer; font-size: 14px;
364
+ color: #111827; border: none; background: none; width: 100%; text-align: left;
365
+ transition: background 0.1s;
366
+ }
367
+ .translify-option:hover { background: #f9fafb; }
368
+ .translify-option.active { background: #eff6ff; color: #2563eb; font-weight: 600; }
369
+ .translify-option-name { font-size: 12px; color: #9ca3af; margin-left: auto; }
370
+ .translify-option-rtl {
371
+ font-size: 10px; color: #9ca3af;
372
+ background: #f3f4f6; padding: 2px 5px; border-radius: 4px;
373
+ }
374
+ .translify-restore {
375
+ display: flex; align-items: center; gap: 6px;
376
+ padding: 10px 14px; cursor: pointer; font-size: 13px; color: #ef4444;
377
+ border: none; background: none; width: 100%; text-align: left;
378
+ border-top: 1px solid #f3f4f6; flex-shrink: 0; transition: background 0.1s;
379
+ }
380
+ .translify-restore:hover { background: #fff5f5; }
381
+ .translify-toast {
382
+ position: fixed; bottom: 84px; right: 24px; z-index: 2147483647;
383
+ background: #1e293b; color: #fff; padding: 10px 16px;
384
+ border-radius: 10px; font-size: 13px; pointer-events: none;
385
+ transition: opacity 0.3s ease, transform 0.3s ease;
386
+ }
387
+ .translify-overlay {
388
+ position: fixed; inset: 0; z-index: 2147483646;
389
+ background: rgba(255,255,255,0.85); backdrop-filter: blur(3px);
390
+ display: flex; flex-direction: column;
391
+ align-items: center; justify-content: center; gap: 14px;
392
+ transition: opacity 0.35s ease;
393
+ }
394
+ .translify-overlay-spinner {
395
+ width: 40px; height: 40px; border-radius: 50%;
396
+ border: 3px solid #e5e7eb; border-top-color: #2563eb;
397
+ animation: tlfy-spin 0.7s linear infinite;
398
+ }
399
+ @keyframes tlfy-spin { to { transform: rotate(360deg); } }
400
+ .translify-overlay-title { font-size: 15px; font-weight: 600; color: #111827; text-align: center; }
401
+ .translify-overlay-sub { font-size: 12px; color: #6b7280; margin-top: 4px; text-align: center; }
402
+ `],
403
+ template: `
404
+ <div class="translify-selector" (clickOutside)="isOpen = false">
405
+ <button
406
+ class="translify-trigger"
407
+ [class]="classNames.trigger || ''"
408
+ (click)="isOpen = !isOpen"
409
+ [disabled]="loadingState !== 'idle' || languages.length === 0"
410
+ [attr.aria-expanded]="isOpen"
411
+ >
412
+ <span>🌐</span>
413
+ <span>{{ activeLangObj ? activeLangObj.nativeName : (labels.trigger || 'Translate') }}</span>
414
+ <span class="translify-trigger-chevron" [class.open]="isOpen">▾</span>
415
+ </button>
416
+
417
+ <div *ngIf="isOpen" class="translify-dropdown" [class]="classNames.dropdown || ''">
418
+ <div class="translify-dropdown-header">{{ labels.dropdownHeader || 'Select Language' }}</div>
419
+
420
+ <button
421
+ *ngFor="let lang of languages"
422
+ class="translify-option"
423
+ [class.active]="activeLang === lang.code"
424
+ (click)="handleSelect(lang.code)"
425
+ >
426
+ <span>{{ lang.nativeName }}</span>
427
+ <span class="translify-option-name">{{ lang.name }}</span>
428
+ <span *ngIf="lang.isRTL" class="translify-option-rtl">RTL</span>
429
+ </button>
430
+
431
+ <button *ngIf="activeLang" class="translify-restore" (click)="restore()">
432
+ ↩ {{ labels.restore || 'Show original' }}
433
+ </button>
434
+ </div>
435
+ </div>
436
+
437
+ <div
438
+ *ngIf="loadingState === 'overlay'"
439
+ class="translify-overlay"
440
+ [style.opacity]="overlayVisible ? '1' : '0'"
441
+ >
442
+ <div class="translify-overlay-spinner"></div>
443
+ <div>
444
+ <div class="translify-overlay-title">{{ labels.overlayTitle || 'Translating page…' }}</div>
445
+ <div class="translify-overlay-sub">{{ labels.overlaySub || 'First time only — instant on next visit.' }}</div>
446
+ </div>
447
+ </div>
448
+
449
+ <div
450
+ *ngIf="loadingState === 'toast'"
451
+ class="translify-toast"
452
+ [style.opacity]="toastVisible ? '1' : '0'"
453
+ [style.transform]="toastVisible ? 'translateY(0)' : 'translateY(8px)'"
454
+ >
455
+ ⏳ Translating…
456
+ </div>
457
+ `,
458
+ }),
459
+ __metadata("design:paramtypes", [NgZone, ChangeDetectorRef])
460
+ ], TranslifySelectorComponent);
461
+ export { TranslifySelectorComponent };
@@ -0,0 +1,2 @@
1
+ export declare class TranslifyModule {
2
+ }
@@ -0,0 +1,19 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { NgModule } from '@angular/core';
8
+ import { TranslifySelectorComponent } from './translify-selector.component';
9
+ // For NgModule-based apps (Angular 15+)
10
+ // Standalone component apps can import TranslifySelectorComponent directly
11
+ let TranslifyModule = class TranslifyModule {
12
+ };
13
+ TranslifyModule = __decorate([
14
+ NgModule({
15
+ imports: [TranslifySelectorComponent],
16
+ exports: [TranslifySelectorComponent],
17
+ })
18
+ ], TranslifyModule);
19
+ export { TranslifyModule };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@translifycc/angular",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Angular language selector component for Translify",
5
- "main": "src/index.js",
6
- "types": "src/index.d.ts",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
7
  "peerDependencies": {
8
8
  "@angular/common": ">=15.0.0",
9
9
  "@angular/core": ">=15.0.0"
@@ -14,5 +14,10 @@
14
14
  "translation",
15
15
  "angular"
16
16
  ],
17
- "license": "MIT"
17
+ "license": "MIT",
18
+ "devDependencies": {
19
+ "@angular/common": "^15.2.10",
20
+ "@angular/core": "^15.2.10",
21
+ "typescript": "^5.9.3"
22
+ }
18
23
  }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "module": "ES2020",
5
+ "moduleResolution": "node",
6
+ "declaration": true,
7
+ "declarationMap": false,
8
+ "sourceMap": false,
9
+ "outDir": "./dist",
10
+ "strict": false,
11
+ "experimentalDecorators": true,
12
+ "emitDecoratorMetadata": true,
13
+ "skipLibCheck": true,
14
+ "lib": ["ES2017", "DOM"]
15
+ },
16
+ "include": ["src/**/*.ts"],
17
+ "exclude": ["node_modules"]
18
+ }