@translifycc/angular 0.1.3 → 0.1.4
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/fesm2022/translifycc-angular.mjs +547 -0
- package/fesm2022/translifycc-angular.mjs.map +1 -0
- package/package.json +17 -7
- package/{dist/translify-selector.component.d.ts → types/translifycc-angular.d.ts} +21 -2
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -2
- package/dist/translate.d.ts +0 -15
- package/dist/translate.js +0 -100
- package/dist/translify-selector.component.js +0 -461
- package/dist/translify.module.d.ts +0 -2
- package/dist/translify.module.js +0 -19
- package/src/index.d.ts +0 -9
- package/src/index.ts +0 -3
- package/src/translate.ts +0 -112
- package/src/translify-selector.component.ts +0 -463
- package/src/translify.module.ts +0 -10
- package/tsconfig.json +0 -18
package/package.json
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@translifycc/angular",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Angular language selector component for Translify",
|
|
5
|
-
"main": "dist/
|
|
5
|
+
"main": "dist/fesm2022/translifycc-angular.mjs",
|
|
6
|
+
"module": "fesm2022/translifycc-angular.mjs",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"default": "./fesm2022/translifycc-angular.mjs",
|
|
11
|
+
"types": "./types/translifycc-angular.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": {
|
|
14
|
+
"default": "./package.json"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
7
17
|
"peerDependencies": {
|
|
8
18
|
"@angular/common": ">=15.0.0",
|
|
9
19
|
"@angular/core": ">=15.0.0"
|
|
@@ -15,9 +25,9 @@
|
|
|
15
25
|
"angular"
|
|
16
26
|
],
|
|
17
27
|
"license": "MIT",
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
"
|
|
28
|
+
"typings": "types/translifycc-angular.d.ts",
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"tslib": "^2.3.0"
|
|
22
32
|
}
|
|
23
|
-
}
|
|
33
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
1
2
|
import { OnInit, OnDestroy, NgZone, ChangeDetectorRef } from '@angular/core';
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
interface Language {
|
|
5
|
+
code: string;
|
|
6
|
+
name: string;
|
|
7
|
+
nativeName: string;
|
|
8
|
+
isRTL: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
declare class TranslifySelectorComponent implements OnInit, OnDestroy {
|
|
4
12
|
private ngZone;
|
|
5
13
|
private cdr;
|
|
6
14
|
apiKey: string;
|
|
@@ -61,4 +69,15 @@ export declare class TranslifySelectorComponent implements OnInit, OnDestroy {
|
|
|
61
69
|
translate(targetLang: string): Promise<void>;
|
|
62
70
|
restore(): void;
|
|
63
71
|
handleSelect(langCode: string): void;
|
|
72
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<TranslifySelectorComponent, never>;
|
|
73
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<TranslifySelectorComponent, "translify-selector", never, { "apiKey": { "alias": "apiKey"; "required": false; }; "apiBase": { "alias": "apiBase"; "required": false; }; "domain": { "alias": "domain"; "required": false; }; "classNames": { "alias": "classNames"; "required": false; }; "labels": { "alias": "labels"; "required": false; }; }, {}, never, never, true, never>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
declare class TranslifyModule {
|
|
77
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<TranslifyModule, never>;
|
|
78
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<TranslifyModule, never, [typeof TranslifySelectorComponent], [typeof TranslifySelectorComponent]>;
|
|
79
|
+
static ɵinj: i0.ɵɵInjectorDeclaration<TranslifyModule>;
|
|
64
80
|
}
|
|
81
|
+
|
|
82
|
+
export { TranslifyModule, TranslifySelectorComponent };
|
|
83
|
+
export type { Language };
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
package/dist/translate.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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[]>;
|
package/dist/translate.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,461 +0,0 @@
|
|
|
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 };
|
package/dist/translify.module.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
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 };
|