@affectively/aeon-pages 1.3.0
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/CHANGELOG.md +112 -0
- package/README.md +625 -0
- package/examples/basic/aeon.config.ts +39 -0
- package/examples/basic/components/Cursor.tsx +86 -0
- package/examples/basic/components/OfflineIndicator.tsx +103 -0
- package/examples/basic/components/PresenceBar.tsx +77 -0
- package/examples/basic/package.json +20 -0
- package/examples/basic/pages/index.tsx +80 -0
- package/package.json +101 -0
- package/packages/analytics/README.md +309 -0
- package/packages/analytics/build.ts +35 -0
- package/packages/analytics/package.json +50 -0
- package/packages/analytics/src/click-tracker.ts +368 -0
- package/packages/analytics/src/context-bridge.ts +319 -0
- package/packages/analytics/src/data-layer.ts +302 -0
- package/packages/analytics/src/gtm-loader.ts +239 -0
- package/packages/analytics/src/index.ts +230 -0
- package/packages/analytics/src/merkle-tree.ts +489 -0
- package/packages/analytics/src/provider.tsx +300 -0
- package/packages/analytics/src/types.ts +320 -0
- package/packages/analytics/src/use-analytics.ts +296 -0
- package/packages/analytics/tsconfig.json +19 -0
- package/packages/benchmarks/src/benchmark.test.ts +691 -0
- package/packages/cli/dist/index.js +61899 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/commands/build.test.ts +682 -0
- package/packages/cli/src/commands/build.ts +890 -0
- package/packages/cli/src/commands/dev.ts +473 -0
- package/packages/cli/src/commands/init.ts +409 -0
- package/packages/cli/src/commands/start.ts +297 -0
- package/packages/cli/src/index.ts +105 -0
- package/packages/directives/src/use-aeon.ts +272 -0
- package/packages/mcp-server/package.json +51 -0
- package/packages/mcp-server/src/index.ts +178 -0
- package/packages/mcp-server/src/resources.ts +346 -0
- package/packages/mcp-server/src/tools/index.ts +36 -0
- package/packages/mcp-server/src/tools/navigation.ts +545 -0
- package/packages/mcp-server/tsconfig.json +21 -0
- package/packages/react/package.json +40 -0
- package/packages/react/src/Link.tsx +388 -0
- package/packages/react/src/components/InstallPrompt.tsx +286 -0
- package/packages/react/src/components/OfflineDiagnostics.tsx +677 -0
- package/packages/react/src/components/PushNotifications.tsx +453 -0
- package/packages/react/src/hooks/useAeonNavigation.ts +219 -0
- package/packages/react/src/hooks/useConflicts.ts +277 -0
- package/packages/react/src/hooks/useNetworkState.ts +209 -0
- package/packages/react/src/hooks/usePilotNavigation.ts +254 -0
- package/packages/react/src/hooks/useServiceWorker.ts +278 -0
- package/packages/react/src/hooks.ts +195 -0
- package/packages/react/src/index.ts +151 -0
- package/packages/react/src/provider.tsx +467 -0
- package/packages/react/tsconfig.json +19 -0
- package/packages/runtime/README.md +399 -0
- package/packages/runtime/build.ts +48 -0
- package/packages/runtime/package.json +71 -0
- package/packages/runtime/schema.sql +40 -0
- package/packages/runtime/src/api-routes.ts +465 -0
- package/packages/runtime/src/benchmark.ts +171 -0
- package/packages/runtime/src/cache.ts +479 -0
- package/packages/runtime/src/durable-object.ts +1341 -0
- package/packages/runtime/src/index.ts +360 -0
- package/packages/runtime/src/navigation.test.ts +421 -0
- package/packages/runtime/src/navigation.ts +422 -0
- package/packages/runtime/src/nextjs-adapter.ts +272 -0
- package/packages/runtime/src/offline/encrypted-queue.test.ts +607 -0
- package/packages/runtime/src/offline/encrypted-queue.ts +478 -0
- package/packages/runtime/src/offline/encryption.test.ts +412 -0
- package/packages/runtime/src/offline/encryption.ts +397 -0
- package/packages/runtime/src/offline/types.ts +465 -0
- package/packages/runtime/src/predictor.ts +371 -0
- package/packages/runtime/src/registry.ts +351 -0
- package/packages/runtime/src/router/context-extractor.ts +661 -0
- package/packages/runtime/src/router/esi-control-react.tsx +2053 -0
- package/packages/runtime/src/router/esi-control.ts +541 -0
- package/packages/runtime/src/router/esi-cyrano.ts +779 -0
- package/packages/runtime/src/router/esi-format-react.tsx +1744 -0
- package/packages/runtime/src/router/esi-react.tsx +1065 -0
- package/packages/runtime/src/router/esi-translate-observer.ts +476 -0
- package/packages/runtime/src/router/esi-translate-react.tsx +556 -0
- package/packages/runtime/src/router/esi-translate.ts +503 -0
- package/packages/runtime/src/router/esi.ts +666 -0
- package/packages/runtime/src/router/heuristic-adapter.test.ts +295 -0
- package/packages/runtime/src/router/heuristic-adapter.ts +557 -0
- package/packages/runtime/src/router/index.ts +298 -0
- package/packages/runtime/src/router/merkle-capability.ts +473 -0
- package/packages/runtime/src/router/speculation.ts +451 -0
- package/packages/runtime/src/router/types.ts +630 -0
- package/packages/runtime/src/router.test.ts +470 -0
- package/packages/runtime/src/router.ts +302 -0
- package/packages/runtime/src/server.ts +481 -0
- package/packages/runtime/src/service-worker-push.ts +319 -0
- package/packages/runtime/src/service-worker.ts +553 -0
- package/packages/runtime/src/skeleton-hydrate.ts +237 -0
- package/packages/runtime/src/speculation.test.ts +389 -0
- package/packages/runtime/src/speculation.ts +486 -0
- package/packages/runtime/src/storage.test.ts +1297 -0
- package/packages/runtime/src/storage.ts +1048 -0
- package/packages/runtime/src/sync/conflict-resolver.test.ts +528 -0
- package/packages/runtime/src/sync/conflict-resolver.ts +565 -0
- package/packages/runtime/src/sync/coordinator.test.ts +608 -0
- package/packages/runtime/src/sync/coordinator.ts +596 -0
- package/packages/runtime/src/tree-compiler.ts +295 -0
- package/packages/runtime/src/types.ts +728 -0
- package/packages/runtime/src/worker.ts +327 -0
- package/packages/runtime/tsconfig.json +20 -0
- package/packages/runtime/wasm/aeon_pages_runtime.d.ts +504 -0
- package/packages/runtime/wasm/aeon_pages_runtime.js +1657 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime/wasm/aeon_pages_runtime_bg.wasm.d.ts +196 -0
- package/packages/runtime/wasm/package.json +21 -0
- package/packages/runtime/wrangler.toml +41 -0
- package/packages/runtime-wasm/Cargo.lock +436 -0
- package/packages/runtime-wasm/Cargo.toml +29 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.d.ts +480 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime.js +1568 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm +0 -0
- package/packages/runtime-wasm/pkg/aeon_pages_runtime_bg.wasm.d.ts +192 -0
- package/packages/runtime-wasm/pkg/package.json +21 -0
- package/packages/runtime-wasm/src/hydrate.rs +352 -0
- package/packages/runtime-wasm/src/lib.rs +191 -0
- package/packages/runtime-wasm/src/render.rs +629 -0
- package/packages/runtime-wasm/src/router.rs +298 -0
- package/packages/runtime-wasm/src/skeleton.rs +430 -0
- package/rfcs/RFC-001-ZERO-DEPENDENCY-RENDERING.md +1446 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESI Translation Observer
|
|
3
|
+
*
|
|
4
|
+
* MutationObserver-based system for automatically translating elements
|
|
5
|
+
* decorated with data-translate attribute.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```html
|
|
9
|
+
* <!-- Basic translation -->
|
|
10
|
+
* <p data-translate data-target-lang="es">Hello world</p>
|
|
11
|
+
*
|
|
12
|
+
* <!-- With source language hint -->
|
|
13
|
+
* <span data-translate data-source-lang="en" data-target-lang="fr">Welcome</span>
|
|
14
|
+
*
|
|
15
|
+
* <!-- With translation context -->
|
|
16
|
+
* <p data-translate data-translate-context="formal, business">
|
|
17
|
+
* Please review the attached document.
|
|
18
|
+
* </p>
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```tsx
|
|
23
|
+
* // React hook usage
|
|
24
|
+
* function App() {
|
|
25
|
+
* useTranslationObserver({
|
|
26
|
+
* translateAttribute: 'data-translate',
|
|
27
|
+
* batchSize: 10,
|
|
28
|
+
* debounceMs: 100,
|
|
29
|
+
* });
|
|
30
|
+
*
|
|
31
|
+
* return <div data-translate data-target-lang="es">Hello</div>;
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { useEffect, useRef } from 'react';
|
|
37
|
+
import type { TranslationResult } from './types';
|
|
38
|
+
import {
|
|
39
|
+
translateWithAIGateway,
|
|
40
|
+
detectTargetLanguage,
|
|
41
|
+
normalizeLanguageCode,
|
|
42
|
+
} from './esi-translate';
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Types
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
export interface TranslationObserverConfig {
|
|
49
|
+
/** Root element to observe (defaults to document.body) */
|
|
50
|
+
root?: Element;
|
|
51
|
+
|
|
52
|
+
/** Attribute that marks elements for translation (default: 'data-translate') */
|
|
53
|
+
translateAttribute?: string;
|
|
54
|
+
|
|
55
|
+
/** Batch translations for efficiency (default: 10) */
|
|
56
|
+
batchSize?: number;
|
|
57
|
+
|
|
58
|
+
/** Debounce time for mutations in ms (default: 100) */
|
|
59
|
+
debounceMs?: number;
|
|
60
|
+
|
|
61
|
+
/** AI Gateway endpoint */
|
|
62
|
+
endpoint?: string;
|
|
63
|
+
|
|
64
|
+
/** Default target language if not specified on element */
|
|
65
|
+
defaultTargetLanguage?: string;
|
|
66
|
+
|
|
67
|
+
/** Cache TTL in seconds (default: 86400 = 24 hours) */
|
|
68
|
+
cacheTtl?: number;
|
|
69
|
+
|
|
70
|
+
/** Callback when element is translated */
|
|
71
|
+
onTranslate?: (element: Element, result: TranslationResult) => void;
|
|
72
|
+
|
|
73
|
+
/** Callback on translation error */
|
|
74
|
+
onError?: (element: Element, error: string) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface TranslationQueueItem {
|
|
78
|
+
element: Element;
|
|
79
|
+
originalText: string;
|
|
80
|
+
targetLanguage: string;
|
|
81
|
+
sourceLanguage: string;
|
|
82
|
+
context?: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ============================================================================
|
|
86
|
+
// TranslationObserver Class
|
|
87
|
+
// ============================================================================
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* TranslationObserver - Watches DOM for translatable elements
|
|
91
|
+
*
|
|
92
|
+
* Uses MutationObserver to detect elements with data-translate attribute
|
|
93
|
+
* and automatically translates them using the AI Gateway.
|
|
94
|
+
*/
|
|
95
|
+
export class TranslationObserver {
|
|
96
|
+
private observer: MutationObserver | null = null;
|
|
97
|
+
private config: Required<
|
|
98
|
+
Omit<TranslationObserverConfig, 'root' | 'onTranslate' | 'onError'>
|
|
99
|
+
> & {
|
|
100
|
+
root: Element | null;
|
|
101
|
+
onTranslate?: TranslationObserverConfig['onTranslate'];
|
|
102
|
+
onError?: TranslationObserverConfig['onError'];
|
|
103
|
+
};
|
|
104
|
+
private translationQueue: TranslationQueueItem[] = [];
|
|
105
|
+
private isProcessing = false;
|
|
106
|
+
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
107
|
+
private translatedElements = new WeakSet<Element>();
|
|
108
|
+
|
|
109
|
+
constructor(config: TranslationObserverConfig = {}) {
|
|
110
|
+
this.config = {
|
|
111
|
+
root: config.root ?? null,
|
|
112
|
+
translateAttribute: config.translateAttribute ?? 'data-translate',
|
|
113
|
+
batchSize: config.batchSize ?? 10,
|
|
114
|
+
debounceMs: config.debounceMs ?? 100,
|
|
115
|
+
endpoint: config.endpoint ?? 'https://ai-gateway.taylorbuley.workers.dev',
|
|
116
|
+
defaultTargetLanguage: config.defaultTargetLanguage ?? 'en',
|
|
117
|
+
cacheTtl: config.cacheTtl ?? 86400,
|
|
118
|
+
onTranslate: config.onTranslate,
|
|
119
|
+
onError: config.onError,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Start observing for translatable elements
|
|
125
|
+
*/
|
|
126
|
+
observe(): void {
|
|
127
|
+
if (
|
|
128
|
+
typeof window === 'undefined' ||
|
|
129
|
+
typeof MutationObserver === 'undefined'
|
|
130
|
+
) {
|
|
131
|
+
console.warn('[TranslationObserver] MutationObserver not available');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const root = this.config.root ?? document.body;
|
|
136
|
+
if (!root) {
|
|
137
|
+
console.warn('[TranslationObserver] Root element not found');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Create observer
|
|
142
|
+
this.observer = new MutationObserver((mutations) => {
|
|
143
|
+
this.handleMutations(mutations);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Start observing
|
|
147
|
+
this.observer.observe(root, {
|
|
148
|
+
childList: true,
|
|
149
|
+
subtree: true,
|
|
150
|
+
characterData: true,
|
|
151
|
+
attributes: true,
|
|
152
|
+
attributeFilter: [this.config.translateAttribute, 'data-target-lang'],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Translate existing elements
|
|
156
|
+
this.translateAll();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Stop observing
|
|
161
|
+
*/
|
|
162
|
+
disconnect(): void {
|
|
163
|
+
if (this.observer) {
|
|
164
|
+
this.observer.disconnect();
|
|
165
|
+
this.observer = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.debounceTimer) {
|
|
169
|
+
clearTimeout(this.debounceTimer);
|
|
170
|
+
this.debounceTimer = null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Manually translate all current elements
|
|
176
|
+
*/
|
|
177
|
+
async translateAll(): Promise<void> {
|
|
178
|
+
const root = this.config.root ?? document.body;
|
|
179
|
+
if (!root) return;
|
|
180
|
+
|
|
181
|
+
const elements = root.querySelectorAll(
|
|
182
|
+
`[${this.config.translateAttribute}]`,
|
|
183
|
+
);
|
|
184
|
+
Array.from(elements).forEach((element) => {
|
|
185
|
+
if (!this.translatedElements.has(element)) {
|
|
186
|
+
this.queueElement(element);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await this.processQueue();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Translate a specific element
|
|
195
|
+
*/
|
|
196
|
+
async translateElement(element: Element): Promise<TranslationResult | null> {
|
|
197
|
+
const originalText = element.textContent?.trim();
|
|
198
|
+
if (!originalText) return null;
|
|
199
|
+
|
|
200
|
+
const targetLanguage = this.getTargetLanguage(element);
|
|
201
|
+
const sourceLanguage = this.getSourceLanguage(element);
|
|
202
|
+
const context = element.getAttribute('data-translate-context') ?? undefined;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const result = await translateWithAIGateway(
|
|
206
|
+
originalText,
|
|
207
|
+
targetLanguage,
|
|
208
|
+
{
|
|
209
|
+
sourceLanguage,
|
|
210
|
+
context,
|
|
211
|
+
endpoint: this.config.endpoint,
|
|
212
|
+
},
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// Update element content
|
|
216
|
+
if (result.translated !== originalText) {
|
|
217
|
+
element.textContent = result.translated;
|
|
218
|
+
element.setAttribute('data-translated', 'true');
|
|
219
|
+
element.setAttribute('data-original-text', originalText);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.translatedElements.add(element);
|
|
223
|
+
this.config.onTranslate?.(element, result);
|
|
224
|
+
|
|
225
|
+
return result;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const errorMsg =
|
|
228
|
+
error instanceof Error ? error.message : 'Translation failed';
|
|
229
|
+
this.config.onError?.(element, errorMsg);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handle DOM mutations
|
|
236
|
+
*/
|
|
237
|
+
private handleMutations(mutations: MutationRecord[]): void {
|
|
238
|
+
let hasNewElements = false;
|
|
239
|
+
|
|
240
|
+
for (const mutation of mutations) {
|
|
241
|
+
if (mutation.type === 'childList') {
|
|
242
|
+
// Check added nodes
|
|
243
|
+
Array.from(mutation.addedNodes).forEach((node) => {
|
|
244
|
+
if (node instanceof Element) {
|
|
245
|
+
if (node.hasAttribute(this.config.translateAttribute)) {
|
|
246
|
+
if (!this.translatedElements.has(node)) {
|
|
247
|
+
this.queueElement(node);
|
|
248
|
+
hasNewElements = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// Check descendants
|
|
252
|
+
const descendants = node.querySelectorAll(
|
|
253
|
+
`[${this.config.translateAttribute}]`,
|
|
254
|
+
);
|
|
255
|
+
Array.from(descendants).forEach((descendant) => {
|
|
256
|
+
if (!this.translatedElements.has(descendant)) {
|
|
257
|
+
this.queueElement(descendant);
|
|
258
|
+
hasNewElements = true;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
} else if (mutation.type === 'attributes') {
|
|
264
|
+
// Element got data-translate attribute added
|
|
265
|
+
if (mutation.target instanceof Element) {
|
|
266
|
+
if (mutation.target.hasAttribute(this.config.translateAttribute)) {
|
|
267
|
+
if (!this.translatedElements.has(mutation.target)) {
|
|
268
|
+
this.queueElement(mutation.target);
|
|
269
|
+
hasNewElements = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (hasNewElements) {
|
|
277
|
+
this.debouncedProcessQueue();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Queue an element for translation
|
|
283
|
+
*/
|
|
284
|
+
private queueElement(element: Element): void {
|
|
285
|
+
const originalText = element.textContent?.trim();
|
|
286
|
+
if (!originalText) return;
|
|
287
|
+
|
|
288
|
+
// Skip if already in queue
|
|
289
|
+
if (this.translationQueue.some((item) => item.element === element)) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const targetLanguage = this.getTargetLanguage(element);
|
|
294
|
+
const sourceLanguage = this.getSourceLanguage(element);
|
|
295
|
+
const context = element.getAttribute('data-translate-context') ?? undefined;
|
|
296
|
+
|
|
297
|
+
this.translationQueue.push({
|
|
298
|
+
element,
|
|
299
|
+
originalText,
|
|
300
|
+
targetLanguage,
|
|
301
|
+
sourceLanguage,
|
|
302
|
+
context,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Debounced queue processing
|
|
308
|
+
*/
|
|
309
|
+
private debouncedProcessQueue(): void {
|
|
310
|
+
if (this.debounceTimer) {
|
|
311
|
+
clearTimeout(this.debounceTimer);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.debounceTimer = setTimeout(() => {
|
|
315
|
+
this.processQueue();
|
|
316
|
+
}, this.config.debounceMs);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Process queued translations
|
|
321
|
+
*/
|
|
322
|
+
private async processQueue(): Promise<void> {
|
|
323
|
+
if (this.isProcessing || this.translationQueue.length === 0) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
this.isProcessing = true;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
// Process in batches
|
|
331
|
+
while (this.translationQueue.length > 0) {
|
|
332
|
+
const batch = this.translationQueue.splice(0, this.config.batchSize);
|
|
333
|
+
|
|
334
|
+
// Process batch concurrently
|
|
335
|
+
await Promise.all(
|
|
336
|
+
batch.map(async (item) => {
|
|
337
|
+
try {
|
|
338
|
+
const result = await translateWithAIGateway(
|
|
339
|
+
item.originalText,
|
|
340
|
+
item.targetLanguage,
|
|
341
|
+
{
|
|
342
|
+
sourceLanguage: item.sourceLanguage,
|
|
343
|
+
context: item.context,
|
|
344
|
+
endpoint: this.config.endpoint,
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Update element content
|
|
349
|
+
if (result.translated !== item.originalText) {
|
|
350
|
+
item.element.textContent = result.translated;
|
|
351
|
+
item.element.setAttribute('data-translated', 'true');
|
|
352
|
+
item.element.setAttribute(
|
|
353
|
+
'data-original-text',
|
|
354
|
+
item.originalText,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
this.translatedElements.add(item.element);
|
|
359
|
+
this.config.onTranslate?.(item.element, result);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
const errorMsg =
|
|
362
|
+
error instanceof Error ? error.message : 'Translation failed';
|
|
363
|
+
this.config.onError?.(item.element, errorMsg);
|
|
364
|
+
}
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
} finally {
|
|
369
|
+
this.isProcessing = false;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Get target language for an element
|
|
375
|
+
*/
|
|
376
|
+
private getTargetLanguage(element: Element): string {
|
|
377
|
+
const explicit = element.getAttribute('data-target-lang');
|
|
378
|
+
if (explicit) return normalizeLanguageCode(explicit);
|
|
379
|
+
|
|
380
|
+
return detectTargetLanguage(undefined, undefined);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Get source language for an element
|
|
385
|
+
*/
|
|
386
|
+
private getSourceLanguage(element: Element): string {
|
|
387
|
+
const explicit = element.getAttribute('data-source-lang');
|
|
388
|
+
if (explicit) return normalizeLanguageCode(explicit);
|
|
389
|
+
|
|
390
|
+
return 'auto';
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// React Hook
|
|
396
|
+
// ============================================================================
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* React hook for translation observer
|
|
400
|
+
*
|
|
401
|
+
* @example
|
|
402
|
+
* ```tsx
|
|
403
|
+
* function App() {
|
|
404
|
+
* useTranslationObserver({
|
|
405
|
+
* translateAttribute: 'data-translate',
|
|
406
|
+
* onTranslate: (element, result) => {
|
|
407
|
+
* console.log('Translated:', result.translated);
|
|
408
|
+
* },
|
|
409
|
+
* });
|
|
410
|
+
*
|
|
411
|
+
* return (
|
|
412
|
+
* <div>
|
|
413
|
+
* <p data-translate data-target-lang="es">Hello world</p>
|
|
414
|
+
* <p data-translate data-target-lang="fr">Goodbye</p>
|
|
415
|
+
* </div>
|
|
416
|
+
* );
|
|
417
|
+
* }
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
export function useTranslationObserver(
|
|
421
|
+
config?: TranslationObserverConfig,
|
|
422
|
+
): React.RefObject<TranslationObserver | null> {
|
|
423
|
+
const observerRef = useRef<TranslationObserver | null>(null);
|
|
424
|
+
|
|
425
|
+
useEffect(() => {
|
|
426
|
+
observerRef.current = new TranslationObserver(config);
|
|
427
|
+
observerRef.current.observe();
|
|
428
|
+
|
|
429
|
+
return () => {
|
|
430
|
+
observerRef.current?.disconnect();
|
|
431
|
+
};
|
|
432
|
+
}, []);
|
|
433
|
+
|
|
434
|
+
return observerRef;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ============================================================================
|
|
438
|
+
// Auto-initialization
|
|
439
|
+
// ============================================================================
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Auto-initialize translation observer when DOM is ready
|
|
443
|
+
*
|
|
444
|
+
* Call this in your app's entry point to automatically translate
|
|
445
|
+
* all elements with data-translate attribute.
|
|
446
|
+
*
|
|
447
|
+
* @example
|
|
448
|
+
* ```tsx
|
|
449
|
+
* // In your app entry point
|
|
450
|
+
* import { initTranslationObserver } from '@affectively/aeon-flux';
|
|
451
|
+
*
|
|
452
|
+
* initTranslationObserver({
|
|
453
|
+
* defaultTargetLanguage: 'es',
|
|
454
|
+
* onTranslate: (element, result) => {
|
|
455
|
+
* console.log('Translated:', result.translated);
|
|
456
|
+
* },
|
|
457
|
+
* });
|
|
458
|
+
* ```
|
|
459
|
+
*/
|
|
460
|
+
export function initTranslationObserver(
|
|
461
|
+
config?: TranslationObserverConfig,
|
|
462
|
+
): TranslationObserver {
|
|
463
|
+
const observer = new TranslationObserver(config);
|
|
464
|
+
|
|
465
|
+
if (typeof document !== 'undefined') {
|
|
466
|
+
if (document.readyState === 'loading') {
|
|
467
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
468
|
+
observer.observe();
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
observer.observe();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return observer;
|
|
476
|
+
}
|