@translifycc/angular 0.1.3 → 0.1.5

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 ADDED
@@ -0,0 +1,141 @@
1
+ # @translifycc/angular
2
+
3
+ Angular language selector component for [Translify](https://translify.cc) — add AI-powered translations to any Angular app in minutes. Compatible with Angular 15–19.
4
+
5
+ ## Get your API key
6
+
7
+ Sign up free at **[translify.cc/signup](https://translify.cc/signup)** — no credit card required.
8
+
9
+ **Free tier:** 100,000 unique characters/month. Cached translations are always free and never count against your limit.
10
+
11
+ | Plan | Price | Unique chars/month |
12
+ |---------|--------|--------------------|
13
+ | Free | $0 | 100,000 |
14
+ | Starter | $25/mo | 5,000,000 |
15
+ | Growth | $49/mo | 25,000,000 |
16
+ | Pro | $99/mo | 100,000,000 |
17
+
18
+ > **How billing works:** You're only charged for characters that hit OpenAI (first-time translations). Repeat translations served from cache are always free.
19
+
20
+ ---
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install @translifycc/angular
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ ### Standalone component (Angular 15+)
31
+
32
+ ```ts
33
+ // app.component.ts
34
+ import { Component } from '@angular/core';
35
+ import { TranslifySelectorComponent } from '@translifycc/angular';
36
+
37
+ @Component({
38
+ standalone: true,
39
+ imports: [TranslifySelectorComponent],
40
+ template: `
41
+ <nav>
42
+ <translify-selector apiKey="YOUR_API_KEY" />
43
+ </nav>
44
+ `
45
+ })
46
+ export class AppComponent {}
47
+ ```
48
+
49
+ ### NgModule-based app
50
+
51
+ ```ts
52
+ // app.module.ts
53
+ import { NgModule } from '@angular/core';
54
+ import { TranslifyModule } from '@translifycc/angular';
55
+
56
+ @NgModule({
57
+ imports: [TranslifyModule],
58
+ })
59
+ export class AppModule {}
60
+ ```
61
+
62
+ ```html
63
+ <!-- any template -->
64
+ <translify-selector apiKey="YOUR_API_KEY" domain="myapp.com" />
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Inputs
70
+
71
+ | Input | Type | Default | Description |
72
+ |--------------|--------|----------------------------|--------------------------------------|
73
+ | `apiKey` | string | **required** | Your Translify API key |
74
+ | `apiBase` | string | `https://translify.cc` | API base URL |
75
+ | `domain` | string | `window.location.hostname` | Your site domain |
76
+ | `classNames` | object | `{}` | Override class names for any element |
77
+ | `labels` | object | `{}` | Override UI text |
78
+
79
+ ### `classNames` keys
80
+
81
+ `trigger` · `dropdown` · `dropdownHeader` · `option` · `optionActive` · `optionNative` · `optionName` · `optionRtl` · `restore` · `overlay` · `overlaySpinner` · `overlayTitle` · `overlaySub` · `toast`
82
+
83
+ ### `labels` keys
84
+
85
+ `trigger` · `dropdownHeader` · `restore` · `overlayTitle` · `overlaySub`
86
+
87
+ ---
88
+
89
+ ## Customisation
90
+
91
+ ```html
92
+ <translify-selector
93
+ apiKey="YOUR_API_KEY"
94
+ [classNames]="{
95
+ trigger: 'btn btn-outline',
96
+ dropdown: 'my-dropdown',
97
+ option: 'my-option',
98
+ optionActive: 'my-option--selected',
99
+ restore: 'my-restore'
100
+ }"
101
+ [labels]="{
102
+ trigger: 'Language',
103
+ dropdownHeader: 'Choose language',
104
+ restore: 'Back to English',
105
+ overlayTitle: 'Translating…',
106
+ overlaySub: 'First time only — instant on next visit.'
107
+ }"
108
+ />
109
+ ```
110
+
111
+ ---
112
+
113
+ ## Angular version compatibility
114
+
115
+ | Version | Support |
116
+ |------------|-----------------------|
117
+ | Angular 15 | ✅ Standalone + NgModule |
118
+ | Angular 16 | ✅ Standalone + NgModule |
119
+ | Angular 17 | ✅ Standalone + NgModule |
120
+ | Angular 18 | ✅ Standalone + NgModule |
121
+ | Angular 19 | ✅ Standalone + NgModule |
122
+
123
+ ---
124
+
125
+ ## Supported languages
126
+
127
+ Arabic · Chinese · Dutch · French · German · Hebrew · Hindi · Italian · Japanese · Korean · Persian · Polish · Portuguese · Russian · Spanish · Swedish · Turkish · Urdu
128
+
129
+ RTL languages (Arabic, Hebrew, Persian, Urdu) automatically set `dir="rtl"` on the `<html>` element — no extra config needed.
130
+
131
+ ---
132
+
133
+ ## Links
134
+
135
+ - [Sign up free](https://translify.cc/signup)
136
+ - [Dashboard](https://translify.cc/dashboard)
137
+ - [Full docs](https://translify.cc/docs)
138
+
139
+ ## License
140
+
141
+ MIT
@@ -0,0 +1,547 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Input, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
3
+ import { NgIf, NgFor, NgClass } from '@angular/common';
4
+
5
+ const SKIP_TAGS = new Set([
6
+ 'SCRIPT', 'STYLE', 'NOSCRIPT', 'IFRAME',
7
+ 'CODE', 'PRE', 'SVG', 'MATH', 'INPUT', 'TEXTAREA',
8
+ ]);
9
+ function getSavedLang() {
10
+ return localStorage.getItem('translify_lang');
11
+ }
12
+ function saveLang(lang) {
13
+ localStorage.setItem('translify_lang', lang);
14
+ }
15
+ function clearSavedLang() {
16
+ localStorage.removeItem('translify_lang');
17
+ }
18
+ function loadTranslationCache(lang) {
19
+ try {
20
+ const stored = localStorage.getItem(`translify_cache_${lang}`);
21
+ return stored ? new Map(Object.entries(JSON.parse(stored))) : new Map();
22
+ }
23
+ catch {
24
+ return new Map();
25
+ }
26
+ }
27
+ function saveTranslationCache(lang, cache) {
28
+ try {
29
+ const obj = {};
30
+ cache.forEach((v, k) => { obj[k] = v; });
31
+ localStorage.setItem(`translify_cache_${lang}`, JSON.stringify(obj));
32
+ }
33
+ catch { }
34
+ }
35
+ function getTextNodes(root) {
36
+ const nodes = [];
37
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
38
+ acceptNode(node) {
39
+ const text = node;
40
+ if (!text.textContent?.trim())
41
+ return NodeFilter.FILTER_REJECT;
42
+ if (SKIP_TAGS.has((text.parentElement?.tagName ?? '')))
43
+ return NodeFilter.FILTER_REJECT;
44
+ if ((text.textContent?.trim().length ?? 0) > 5000)
45
+ return NodeFilter.FILTER_REJECT;
46
+ if (text.parentElement?.closest('[translate="no"]'))
47
+ return NodeFilter.FILTER_REJECT;
48
+ return NodeFilter.FILTER_ACCEPT;
49
+ },
50
+ });
51
+ let n;
52
+ while ((n = walker.nextNode()))
53
+ nodes.push(n);
54
+ return nodes;
55
+ }
56
+ function collectNewTextNodes(node, originalTexts) {
57
+ const nodes = [];
58
+ if (node.nodeType === Node.TEXT_NODE) {
59
+ const t = node;
60
+ if (!originalTexts.has(t) && t.textContent?.trim())
61
+ nodes.push(t);
62
+ }
63
+ else if (node.nodeType === Node.ELEMENT_NODE) {
64
+ const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
65
+ let n;
66
+ while ((n = walker.nextNode())) {
67
+ const t = n;
68
+ if (!originalTexts.has(t) &&
69
+ t.textContent?.trim() &&
70
+ !SKIP_TAGS.has(t.parentElement?.tagName ?? '') &&
71
+ !t.parentElement?.closest('[translate="no"]')) {
72
+ nodes.push(t);
73
+ }
74
+ }
75
+ }
76
+ return nodes;
77
+ }
78
+ async function fetchLanguages(apiBase) {
79
+ const res = await fetch(`${apiBase}/translate/languages`);
80
+ const json = await res.json();
81
+ return json.data.languages;
82
+ }
83
+ async function callTranslateApi(apiBase, apiKey, domain, texts, targetLang) {
84
+ const results = [];
85
+ for (let i = 0; i < texts.length; i += 100) {
86
+ const chunk = texts.slice(i, i + 100);
87
+ const res = await fetch(`${apiBase}/translate`, {
88
+ method: 'POST',
89
+ headers: {
90
+ 'Content-Type': 'application/json',
91
+ 'x-api-key': apiKey,
92
+ 'x-domain': domain,
93
+ },
94
+ body: JSON.stringify({ texts: chunk, targetLanguage: targetLang, sourceLanguage: 'auto' }),
95
+ });
96
+ const json = await res.json();
97
+ if (!json.success)
98
+ throw new Error(json.message || 'Translation failed');
99
+ results.push(...json.data.translations);
100
+ }
101
+ return results;
102
+ }
103
+
104
+ class TranslifySelectorComponent {
105
+ ngZone;
106
+ cdr;
107
+ apiKey;
108
+ apiBase = 'https://translify.cc';
109
+ domain = window.location.hostname || 'localhost';
110
+ classNames = {};
111
+ labels = {};
112
+ languages = [];
113
+ activeLang = getSavedLang();
114
+ isOpen = false;
115
+ loadingState = 'idle';
116
+ overlayVisible = false;
117
+ toastVisible = false;
118
+ // All mutable translation state — no change detection needed
119
+ originalTexts = new Map();
120
+ translationCache = new Map();
121
+ currentCacheLang = null;
122
+ isTranslating = false;
123
+ pendingNodes = [];
124
+ observer = null;
125
+ debounceTimer = null;
126
+ overlayTimer = null;
127
+ constructor(ngZone, cdr) {
128
+ this.ngZone = ngZone;
129
+ this.cdr = cdr;
130
+ }
131
+ get activeLangObj() {
132
+ return this.languages.find((l) => l.code === this.activeLang);
133
+ }
134
+ async ngOnInit() {
135
+ if (!this.apiKey) {
136
+ console.error('[Translify] apiKey is required.');
137
+ return;
138
+ }
139
+ try {
140
+ this.languages = await fetchLanguages(this.apiBase);
141
+ this.cdr.markForCheck();
142
+ }
143
+ catch (e) {
144
+ console.error('[Translify] Failed to fetch languages', e);
145
+ return;
146
+ }
147
+ this.ngZone.runOutsideAngular(() => {
148
+ this.startObserver();
149
+ this.patchHistory();
150
+ });
151
+ if (this.activeLang && this.languages.length > 0) {
152
+ await this.translate(this.activeLang);
153
+ }
154
+ }
155
+ ngOnDestroy() {
156
+ this.stopObserver();
157
+ clearTimeout(this.overlayTimer);
158
+ }
159
+ // ── Cache helpers ───────────────────────────────────────────────────────────
160
+ ensureCacheLoaded(lang) {
161
+ if (this.currentCacheLang === lang)
162
+ return;
163
+ this.translationCache = loadTranslationCache(lang);
164
+ this.currentCacheLang = lang;
165
+ }
166
+ applyCache(nodes) {
167
+ const uncached = [];
168
+ nodes.forEach((node) => {
169
+ const src = this.originalTexts.get(node) ?? node.textContent ?? '';
170
+ const hit = this.translationCache.get(src);
171
+ if (hit !== undefined) {
172
+ node.textContent = hit;
173
+ }
174
+ else {
175
+ uncached.push(node);
176
+ }
177
+ });
178
+ return uncached;
179
+ }
180
+ // ── Overlay / toast ─────────────────────────────────────────────────────────
181
+ showOverlay() {
182
+ clearTimeout(this.overlayTimer);
183
+ this.ngZone.run(() => {
184
+ this.overlayVisible = false;
185
+ this.loadingState = 'overlay';
186
+ requestAnimationFrame(() => { this.overlayVisible = true; this.cdr.markForCheck(); });
187
+ this.cdr.markForCheck();
188
+ });
189
+ }
190
+ hideOverlay() {
191
+ this.overlayTimer = setTimeout(() => {
192
+ this.ngZone.run(() => { this.overlayVisible = false; this.cdr.markForCheck(); });
193
+ this.overlayTimer = setTimeout(() => {
194
+ this.ngZone.run(() => { this.loadingState = 'idle'; this.cdr.markForCheck(); });
195
+ }, 400);
196
+ }, 600);
197
+ }
198
+ showToast() {
199
+ clearTimeout(this.overlayTimer);
200
+ this.ngZone.run(() => {
201
+ this.loadingState = 'toast';
202
+ requestAnimationFrame(() => { this.toastVisible = true; this.cdr.markForCheck(); });
203
+ this.cdr.markForCheck();
204
+ });
205
+ }
206
+ hideToast() {
207
+ this.ngZone.run(() => {
208
+ this.toastVisible = false;
209
+ this.overlayTimer = setTimeout(() => {
210
+ this.ngZone.run(() => { this.loadingState = 'idle'; this.cdr.markForCheck(); });
211
+ }, 350);
212
+ this.cdr.markForCheck();
213
+ });
214
+ }
215
+ // ── flushNodes ──────────────────────────────────────────────────────────────
216
+ async flushNodes(nodes, targetLang) {
217
+ if (!nodes.length)
218
+ return;
219
+ if (this.isTranslating) {
220
+ this.pendingNodes.push(...nodes);
221
+ return;
222
+ }
223
+ this.isTranslating = true;
224
+ try {
225
+ nodes.forEach((n) => {
226
+ if (!this.originalTexts.has(n))
227
+ this.originalTexts.set(n, n.textContent ?? '');
228
+ });
229
+ const uncached = this.applyCache(nodes);
230
+ if (uncached.length > 0) {
231
+ const texts = uncached.map((n) => this.originalTexts.get(n));
232
+ const translations = await callTranslateApi(this.apiBase, this.apiKey, this.domain, texts, targetLang);
233
+ uncached.forEach((node, i) => {
234
+ if (translations[i] !== undefined) {
235
+ this.translationCache.set(this.originalTexts.get(node), translations[i]);
236
+ node.textContent = translations[i];
237
+ }
238
+ });
239
+ saveTranslationCache(targetLang, this.translationCache);
240
+ }
241
+ }
242
+ catch (e) {
243
+ console.error('[Translify] Dynamic translation error', e);
244
+ }
245
+ finally {
246
+ this.isTranslating = false;
247
+ if (this.pendingNodes.length > 0) {
248
+ const next = this.pendingNodes.splice(0);
249
+ this.flushNodes(next, targetLang);
250
+ }
251
+ }
252
+ }
253
+ // ── MutationObserver ────────────────────────────────────────────────────────
254
+ startObserver() {
255
+ if (this.observer)
256
+ return;
257
+ this.observer = new MutationObserver((mutations) => {
258
+ if (!this.activeLang)
259
+ return;
260
+ const targetLang = this.activeLang;
261
+ const newNodes = [];
262
+ mutations.forEach((mutation) => {
263
+ if (mutation.type !== 'childList')
264
+ return;
265
+ mutation.addedNodes.forEach((node) => {
266
+ newNodes.push(...collectNewTextNodes(node, this.originalTexts));
267
+ });
268
+ });
269
+ if (!newNodes.length)
270
+ return;
271
+ const uncached = [];
272
+ newNodes.forEach((node) => {
273
+ const src = node.textContent ?? '';
274
+ const hit = this.translationCache.get(src);
275
+ if (hit !== undefined) {
276
+ this.originalTexts.set(node, src);
277
+ node.textContent = hit;
278
+ }
279
+ else {
280
+ uncached.push(node);
281
+ }
282
+ });
283
+ if (!uncached.length)
284
+ return;
285
+ this.pendingNodes.push(...uncached);
286
+ if (!this.isTranslating) {
287
+ clearTimeout(this.debounceTimer);
288
+ this.debounceTimer = setTimeout(() => {
289
+ const toFlush = this.pendingNodes.splice(0);
290
+ this.flushNodes(toFlush, targetLang);
291
+ }, 50);
292
+ }
293
+ });
294
+ this.observer.observe(document.body, { childList: true, subtree: true });
295
+ }
296
+ stopObserver() {
297
+ if (this.observer) {
298
+ this.observer.disconnect();
299
+ this.observer = null;
300
+ }
301
+ clearTimeout(this.debounceTimer);
302
+ }
303
+ // ── SPA route change detection ──────────────────────────────────────────────
304
+ patchHistory() {
305
+ const orig = history.pushState.bind(history);
306
+ const self = this;
307
+ history.pushState = function (state, unused, url) {
308
+ orig(state, unused, url);
309
+ self.onRouteChange();
310
+ };
311
+ window.addEventListener('popstate', () => this.onRouteChange());
312
+ }
313
+ onRouteChange() {
314
+ if (!this.activeLang)
315
+ return;
316
+ const targetLang = this.activeLang;
317
+ setTimeout(() => {
318
+ const nodes = getTextNodes(document.body).filter((n) => !this.originalTexts.has(n));
319
+ if (!nodes.length)
320
+ return;
321
+ this.pendingNodes.push(...nodes);
322
+ if (!this.isTranslating) {
323
+ const toFlush = this.pendingNodes.splice(0);
324
+ this.flushNodes(toFlush, targetLang);
325
+ }
326
+ }, 100);
327
+ }
328
+ // ── Main translate ──────────────────────────────────────────────────────────
329
+ async translate(targetLang) {
330
+ if (this.isTranslating)
331
+ return;
332
+ this.isTranslating = true;
333
+ try {
334
+ this.ensureCacheLoaded(targetLang);
335
+ const isFirstEver = this.translationCache.size === 0;
336
+ if (this.originalTexts.size > 0) {
337
+ this.originalTexts.forEach((text, node) => { node.textContent = text; });
338
+ }
339
+ const nodes = getTextNodes(document.body);
340
+ if (!nodes.length)
341
+ return;
342
+ nodes.forEach((node) => {
343
+ if (!this.originalTexts.has(node))
344
+ this.originalTexts.set(node, node.textContent ?? '');
345
+ });
346
+ const uncached = this.applyCache(nodes);
347
+ const lang = this.languages.find((l) => l.code === targetLang);
348
+ document.documentElement.setAttribute('dir', lang?.isRTL ? 'rtl' : 'ltr');
349
+ saveLang(targetLang);
350
+ this.ngZone.run(() => { this.activeLang = targetLang; this.cdr.markForCheck(); });
351
+ if (uncached.length > 0) {
352
+ if (isFirstEver)
353
+ this.showOverlay();
354
+ else
355
+ this.showToast();
356
+ const texts = uncached.map((n) => this.originalTexts.get(n));
357
+ const translations = await callTranslateApi(this.apiBase, this.apiKey, this.domain, texts, targetLang);
358
+ uncached.forEach((node, i) => {
359
+ if (translations[i] !== undefined) {
360
+ this.translationCache.set(this.originalTexts.get(node), translations[i]);
361
+ node.textContent = translations[i];
362
+ }
363
+ });
364
+ saveTranslationCache(targetLang, this.translationCache);
365
+ if (isFirstEver)
366
+ this.hideOverlay();
367
+ else
368
+ this.hideToast();
369
+ }
370
+ }
371
+ catch (e) {
372
+ console.error('[Translify] Translation error', e);
373
+ this.hideOverlay();
374
+ this.hideToast();
375
+ }
376
+ finally {
377
+ this.isTranslating = false;
378
+ if (this.pendingNodes.length > 0) {
379
+ const next = this.pendingNodes.splice(0);
380
+ this.flushNodes(next, this.activeLang);
381
+ }
382
+ }
383
+ }
384
+ // ── Restore ─────────────────────────────────────────────────────────────────
385
+ restore() {
386
+ this.stopObserver();
387
+ this.originalTexts.forEach((text, node) => { node.textContent = text; });
388
+ this.originalTexts.clear();
389
+ document.documentElement.setAttribute('dir', 'ltr');
390
+ clearSavedLang();
391
+ this.activeLang = null;
392
+ this.isOpen = false;
393
+ this.cdr.markForCheck();
394
+ this.ngZone.runOutsideAngular(() => this.startObserver());
395
+ }
396
+ // ── UI handlers ─────────────────────────────────────────────────────────────
397
+ handleSelect(langCode) {
398
+ this.isOpen = false;
399
+ this.translate(langCode);
400
+ }
401
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TranslifySelectorComponent, deps: [{ token: i0.NgZone }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
402
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.5", type: TranslifySelectorComponent, isStandalone: true, selector: "translify-selector", inputs: { apiKey: "apiKey", apiBase: "apiBase", domain: "domain", classNames: "classNames", labels: "labels" }, ngImport: i0, template: `
403
+ <div class="translify-selector" (clickOutside)="isOpen = false">
404
+ <button
405
+ class="translify-trigger"
406
+ [class]="classNames.trigger || ''"
407
+ (click)="isOpen = !isOpen"
408
+ [disabled]="loadingState !== 'idle' || languages.length === 0"
409
+ [attr.aria-expanded]="isOpen"
410
+ >
411
+ <span>🌐</span>
412
+ <span>{{ activeLangObj ? activeLangObj.nativeName : (labels.trigger || 'Translate') }}</span>
413
+ <span class="translify-trigger-chevron" [class.open]="isOpen">▾</span>
414
+ </button>
415
+
416
+ <div *ngIf="isOpen" class="translify-dropdown" [class]="classNames.dropdown || ''">
417
+ <div class="translify-dropdown-header">{{ labels.dropdownHeader || 'Select Language' }}</div>
418
+
419
+ <button
420
+ *ngFor="let lang of languages"
421
+ class="translify-option"
422
+ [class.active]="activeLang === lang.code"
423
+ (click)="handleSelect(lang.code)"
424
+ >
425
+ <span>{{ lang.nativeName }}</span>
426
+ <span class="translify-option-name">{{ lang.name }}</span>
427
+ <span *ngIf="lang.isRTL" class="translify-option-rtl">RTL</span>
428
+ </button>
429
+
430
+ <button *ngIf="activeLang" class="translify-restore" (click)="restore()">
431
+ ↩ {{ labels.restore || 'Show original' }}
432
+ </button>
433
+ </div>
434
+ </div>
435
+
436
+ <div
437
+ *ngIf="loadingState === 'overlay'"
438
+ class="translify-overlay"
439
+ [style.opacity]="overlayVisible ? '1' : '0'"
440
+ >
441
+ <div class="translify-overlay-spinner"></div>
442
+ <div>
443
+ <div class="translify-overlay-title">{{ labels.overlayTitle || 'Translating page…' }}</div>
444
+ <div class="translify-overlay-sub">{{ labels.overlaySub || 'First time only — instant on next visit.' }}</div>
445
+ </div>
446
+ </div>
447
+
448
+ <div
449
+ *ngIf="loadingState === 'toast'"
450
+ class="translify-toast"
451
+ [style.opacity]="toastVisible ? '1' : '0'"
452
+ [style.transform]="toastVisible ? 'translateY(0)' : 'translateY(8px)'"
453
+ >
454
+ ⏳ Translating…
455
+ </div>
456
+ `, isInline: true, styles: [".translify-selector{position:relative;display:inline-block}.translify-trigger{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;cursor:pointer;font-size:14px;color:#374151;transition:border-color .15s}.translify-trigger:hover{border-color:#9ca3af}.translify-trigger:disabled{opacity:.5;cursor:not-allowed}.translify-trigger-chevron{font-size:10px;transition:transform .2s}.translify-trigger-chevron.open{transform:rotate(180deg)}.translify-dropdown{position:absolute;top:calc(100% + 6px);left:0;z-index:9999;background:#fff;border:1px solid #e5e7eb;border-radius:12px;min-width:220px;max-height:320px;overflow-y:auto;box-shadow:0 8px 24px #0000001f;display:flex;flex-direction:column}.translify-dropdown-header{padding:10px 14px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #f3f4f6;flex-shrink:0}.translify-option{display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:14px;color:#111827;border:none;background:none;width:100%;text-align:left;transition:background .1s}.translify-option:hover{background:#f9fafb}.translify-option.active{background:#eff6ff;color:#2563eb;font-weight:600}.translify-option-name{font-size:12px;color:#9ca3af;margin-left:auto}.translify-option-rtl{font-size:10px;color:#9ca3af;background:#f3f4f6;padding:2px 5px;border-radius:4px}.translify-restore{display:flex;align-items:center;gap:6px;padding:10px 14px;cursor:pointer;font-size:13px;color:#ef4444;border:none;background:none;width:100%;text-align:left;border-top:1px solid #f3f4f6;flex-shrink:0;transition:background .1s}.translify-restore:hover{background:#fff5f5}.translify-toast{position:fixed;bottom:84px;right:24px;z-index:2147483647;background:#1e293b;color:#fff;padding:10px 16px;border-radius:10px;font-size:13px;pointer-events:none;transition:opacity .3s ease,transform .3s ease}.translify-overlay{position:fixed;inset:0;z-index:2147483646;background:#ffffffd9;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;transition:opacity .35s ease}.translify-overlay-spinner{width:40px;height:40px;border-radius:50%;border:3px solid #e5e7eb;border-top-color:#2563eb;animation:tlfy-spin .7s linear infinite}@keyframes tlfy-spin{to{transform:rotate(360deg)}}.translify-overlay-title{font-size:15px;font-weight:600;color:#111827;text-align:center}.translify-overlay-sub{font-size:12px;color:#6b7280;margin-top:4px;text-align:center}\n"], dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
457
+ }
458
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TranslifySelectorComponent, decorators: [{
459
+ type: Component,
460
+ args: [{ selector: 'translify-selector', standalone: true, imports: [NgIf, NgFor, NgClass], changeDetection: ChangeDetectionStrategy.OnPush, template: `
461
+ <div class="translify-selector" (clickOutside)="isOpen = false">
462
+ <button
463
+ class="translify-trigger"
464
+ [class]="classNames.trigger || ''"
465
+ (click)="isOpen = !isOpen"
466
+ [disabled]="loadingState !== 'idle' || languages.length === 0"
467
+ [attr.aria-expanded]="isOpen"
468
+ >
469
+ <span>🌐</span>
470
+ <span>{{ activeLangObj ? activeLangObj.nativeName : (labels.trigger || 'Translate') }}</span>
471
+ <span class="translify-trigger-chevron" [class.open]="isOpen">▾</span>
472
+ </button>
473
+
474
+ <div *ngIf="isOpen" class="translify-dropdown" [class]="classNames.dropdown || ''">
475
+ <div class="translify-dropdown-header">{{ labels.dropdownHeader || 'Select Language' }}</div>
476
+
477
+ <button
478
+ *ngFor="let lang of languages"
479
+ class="translify-option"
480
+ [class.active]="activeLang === lang.code"
481
+ (click)="handleSelect(lang.code)"
482
+ >
483
+ <span>{{ lang.nativeName }}</span>
484
+ <span class="translify-option-name">{{ lang.name }}</span>
485
+ <span *ngIf="lang.isRTL" class="translify-option-rtl">RTL</span>
486
+ </button>
487
+
488
+ <button *ngIf="activeLang" class="translify-restore" (click)="restore()">
489
+ ↩ {{ labels.restore || 'Show original' }}
490
+ </button>
491
+ </div>
492
+ </div>
493
+
494
+ <div
495
+ *ngIf="loadingState === 'overlay'"
496
+ class="translify-overlay"
497
+ [style.opacity]="overlayVisible ? '1' : '0'"
498
+ >
499
+ <div class="translify-overlay-spinner"></div>
500
+ <div>
501
+ <div class="translify-overlay-title">{{ labels.overlayTitle || 'Translating page…' }}</div>
502
+ <div class="translify-overlay-sub">{{ labels.overlaySub || 'First time only — instant on next visit.' }}</div>
503
+ </div>
504
+ </div>
505
+
506
+ <div
507
+ *ngIf="loadingState === 'toast'"
508
+ class="translify-toast"
509
+ [style.opacity]="toastVisible ? '1' : '0'"
510
+ [style.transform]="toastVisible ? 'translateY(0)' : 'translateY(8px)'"
511
+ >
512
+ ⏳ Translating…
513
+ </div>
514
+ `, styles: [".translify-selector{position:relative;display:inline-block}.translify-trigger{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;border:1px solid #e5e7eb;background:#fff;cursor:pointer;font-size:14px;color:#374151;transition:border-color .15s}.translify-trigger:hover{border-color:#9ca3af}.translify-trigger:disabled{opacity:.5;cursor:not-allowed}.translify-trigger-chevron{font-size:10px;transition:transform .2s}.translify-trigger-chevron.open{transform:rotate(180deg)}.translify-dropdown{position:absolute;top:calc(100% + 6px);left:0;z-index:9999;background:#fff;border:1px solid #e5e7eb;border-radius:12px;min-width:220px;max-height:320px;overflow-y:auto;box-shadow:0 8px 24px #0000001f;display:flex;flex-direction:column}.translify-dropdown-header{padding:10px 14px;font-size:11px;font-weight:700;color:#6b7280;text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid #f3f4f6;flex-shrink:0}.translify-option{display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:14px;color:#111827;border:none;background:none;width:100%;text-align:left;transition:background .1s}.translify-option:hover{background:#f9fafb}.translify-option.active{background:#eff6ff;color:#2563eb;font-weight:600}.translify-option-name{font-size:12px;color:#9ca3af;margin-left:auto}.translify-option-rtl{font-size:10px;color:#9ca3af;background:#f3f4f6;padding:2px 5px;border-radius:4px}.translify-restore{display:flex;align-items:center;gap:6px;padding:10px 14px;cursor:pointer;font-size:13px;color:#ef4444;border:none;background:none;width:100%;text-align:left;border-top:1px solid #f3f4f6;flex-shrink:0;transition:background .1s}.translify-restore:hover{background:#fff5f5}.translify-toast{position:fixed;bottom:84px;right:24px;z-index:2147483647;background:#1e293b;color:#fff;padding:10px 16px;border-radius:10px;font-size:13px;pointer-events:none;transition:opacity .3s ease,transform .3s ease}.translify-overlay{position:fixed;inset:0;z-index:2147483646;background:#ffffffd9;-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;transition:opacity .35s ease}.translify-overlay-spinner{width:40px;height:40px;border-radius:50%;border:3px solid #e5e7eb;border-top-color:#2563eb;animation:tlfy-spin .7s linear infinite}@keyframes tlfy-spin{to{transform:rotate(360deg)}}.translify-overlay-title{font-size:15px;font-weight:600;color:#111827;text-align:center}.translify-overlay-sub{font-size:12px;color:#6b7280;margin-top:4px;text-align:center}\n"] }]
515
+ }], ctorParameters: () => [{ type: i0.NgZone }, { type: i0.ChangeDetectorRef }], propDecorators: { apiKey: [{
516
+ type: Input
517
+ }], apiBase: [{
518
+ type: Input
519
+ }], domain: [{
520
+ type: Input
521
+ }], classNames: [{
522
+ type: Input
523
+ }], labels: [{
524
+ type: Input
525
+ }] } });
526
+
527
+ // For NgModule-based apps (Angular 15+)
528
+ // Standalone component apps can import TranslifySelectorComponent directly
529
+ class TranslifyModule {
530
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TranslifyModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
531
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.5", ngImport: i0, type: TranslifyModule, imports: [TranslifySelectorComponent], exports: [TranslifySelectorComponent] });
532
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TranslifyModule });
533
+ }
534
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: TranslifyModule, decorators: [{
535
+ type: NgModule,
536
+ args: [{
537
+ imports: [TranslifySelectorComponent],
538
+ exports: [TranslifySelectorComponent],
539
+ }]
540
+ }] });
541
+
542
+ /**
543
+ * Generated bundle index. Do not edit.
544
+ */
545
+
546
+ export { TranslifyModule, TranslifySelectorComponent };
547
+ //# sourceMappingURL=translifycc-angular.mjs.map