@lovalingo/lovalingo 0.1.0 → 0.1.2
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 +11 -0
- package/dist/components/AixsterProvider.js +10 -9
- package/dist/utils/api.js +7 -6
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.js +23 -0
- package/dist/utils/pathNormalizer.js +2 -1
- package/dist/utils/translator.js +22 -21
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,16 @@ It does **not** generate translations in the browser.
|
|
|
14
14
|
|
|
15
15
|
All artifacts are produced server-side by the pipeline (render → audit → deterministic translate → optional fix loop).
|
|
16
16
|
|
|
17
|
+
## Marker engine (v0.1+)
|
|
18
|
+
|
|
19
|
+
The runtime now marks translatable text nodes deterministically and exposes marker stats to the pipeline.
|
|
20
|
+
|
|
21
|
+
- The pipeline waits for `window.__lovalingoMarkersReady` before capturing HTML.
|
|
22
|
+
- Coverage is enforced server-side (jobs fail if marker coverage is below the threshold).
|
|
23
|
+
- This is a breaking change: older runtimes will be rejected by the pipeline.
|
|
24
|
+
|
|
25
|
+
Debug (runtime logs): set `window.__lovalingoDebug = true` before initializing `LovalingoProvider`.
|
|
26
|
+
|
|
17
27
|
## Installation
|
|
18
28
|
|
|
19
29
|
```bash
|
|
@@ -163,6 +173,7 @@ function MyComponent() {
|
|
|
163
173
|
- ✅ Zero-flash translations (rendered before browser paint)
|
|
164
174
|
- ✅ Automatic route change detection
|
|
165
175
|
- ✅ MutationObserver for dynamic content
|
|
176
|
+
- ✅ Deterministic marker engine + pipeline coverage gate (v0.1+)
|
|
166
177
|
- ✅ SEO: automatic `canonical` + `hreflang` (can be disabled via `seo={false}`)
|
|
167
178
|
- ✅ Customizable language switcher
|
|
168
179
|
- ✅ TypeScript support
|
|
@@ -4,6 +4,7 @@ import { LovalingoAPI } from '../utils/api';
|
|
|
4
4
|
import { Translator } from '../utils/translator';
|
|
5
5
|
import { applyDomRules } from '../utils/domRules';
|
|
6
6
|
import { startMarkerEngine } from '../utils/markerEngine';
|
|
7
|
+
import { logDebug, warnDebug, errorDebug } from '../utils/logger';
|
|
7
8
|
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
8
9
|
import { NavigationOverlay } from './NavigationOverlay';
|
|
9
10
|
const LOCALE_STORAGE_KEY = 'Lovalingo_locale';
|
|
@@ -206,7 +207,7 @@ navigateRef, // For path mode routing
|
|
|
206
207
|
}
|
|
207
208
|
}
|
|
208
209
|
catch (e) {
|
|
209
|
-
|
|
210
|
+
warnDebug("[Lovalingo] updateSeoLinks() failed:", e);
|
|
210
211
|
}
|
|
211
212
|
}, [isSeoActive]);
|
|
212
213
|
// Marker engine: always mark full DOM content for deterministic pipeline extraction.
|
|
@@ -241,7 +242,7 @@ navigateRef, // For path mode routing
|
|
|
241
242
|
}
|
|
242
243
|
catch (e) {
|
|
243
244
|
// localStorage might be unavailable (SSR, private browsing)
|
|
244
|
-
|
|
245
|
+
warnDebug('localStorage not available:', e);
|
|
245
246
|
}
|
|
246
247
|
// 3. Default locale
|
|
247
248
|
return defaultLocale;
|
|
@@ -289,7 +290,7 @@ navigateRef, // For path mode routing
|
|
|
289
290
|
const cachedDomRules = domRulesCacheRef.current.get(cacheKey);
|
|
290
291
|
if (cachedEntry && cachedExclusions) {
|
|
291
292
|
// CACHE HIT - Use cached data immediately (FAST!)
|
|
292
|
-
|
|
293
|
+
logDebug(`[Lovalingo] Using cached translations for ${targetLocale} on ${currentPath}`);
|
|
293
294
|
translatorRef.current.setTranslations(cachedEntry.translations);
|
|
294
295
|
translatorRef.current.setExclusions(cachedExclusions);
|
|
295
296
|
if (mode === 'dom') {
|
|
@@ -311,7 +312,7 @@ navigateRef, // For path mode routing
|
|
|
311
312
|
if (isNavigatingRef.current) {
|
|
312
313
|
return;
|
|
313
314
|
}
|
|
314
|
-
|
|
315
|
+
logDebug(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
|
|
315
316
|
if (mode === 'dom') {
|
|
316
317
|
translatorRef.current.translateDOM();
|
|
317
318
|
}
|
|
@@ -327,11 +328,11 @@ navigateRef, // For path mode routing
|
|
|
327
328
|
return;
|
|
328
329
|
}
|
|
329
330
|
// CACHE MISS - Fetch from API
|
|
330
|
-
|
|
331
|
+
logDebug(`[Lovalingo] Fetching translations for ${targetLocale} on ${currentPath}`);
|
|
331
332
|
setIsLoading(true);
|
|
332
333
|
try {
|
|
333
334
|
if (previousLocale && previousLocale !== defaultLocale) {
|
|
334
|
-
|
|
335
|
+
logDebug(`[Lovalingo] Switching from ${previousLocale} to ${targetLocale}`);
|
|
335
336
|
}
|
|
336
337
|
const [bundle, exclusions, domRules] = await Promise.all([
|
|
337
338
|
apiRef.current.fetchBundle(targetLocale),
|
|
@@ -369,7 +370,7 @@ navigateRef, // For path mode routing
|
|
|
369
370
|
if (isNavigatingRef.current) {
|
|
370
371
|
return;
|
|
371
372
|
}
|
|
372
|
-
|
|
373
|
+
logDebug(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
|
|
373
374
|
if (mode === "dom") {
|
|
374
375
|
translatorRef.current.translateDOM();
|
|
375
376
|
}
|
|
@@ -383,7 +384,7 @@ navigateRef, // For path mode routing
|
|
|
383
384
|
}
|
|
384
385
|
}
|
|
385
386
|
catch (error) {
|
|
386
|
-
|
|
387
|
+
errorDebug('Error loading translations:', error);
|
|
387
388
|
if (showOverlay)
|
|
388
389
|
setIsNavigationLoading(false);
|
|
389
390
|
}
|
|
@@ -403,7 +404,7 @@ navigateRef, // For path mode routing
|
|
|
403
404
|
localStorage.setItem(LOCALE_STORAGE_KEY, newLocale);
|
|
404
405
|
}
|
|
405
406
|
catch (e) {
|
|
406
|
-
|
|
407
|
+
warnDebug('Failed to save locale to localStorage:', e);
|
|
407
408
|
}
|
|
408
409
|
isInternalNavigationRef.current = true;
|
|
409
410
|
// Show navigation overlay immediately (only when a non-default locale is involved)
|
package/dist/utils/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { processPath } from './pathNormalizer';
|
|
2
|
+
import { warnDebug, errorDebug } from './logger';
|
|
2
3
|
export class LovalingoAPI {
|
|
3
4
|
constructor(apiKey, apiBase, pathConfig) {
|
|
4
5
|
this.entitlements = null;
|
|
@@ -11,10 +12,10 @@ export class LovalingoAPI {
|
|
|
11
12
|
}
|
|
12
13
|
warnMissingApiKey(action) {
|
|
13
14
|
// Avoid hard-crashing apps; make the failure mode obvious.
|
|
14
|
-
|
|
15
|
+
warnDebug(`[Lovalingo] Missing public project key: ${action} was skipped. Pass publicAnonKey to <LovalingoProvider ...> (or set VITE_LOVALINGO_PUBLIC_ANON_KEY).`);
|
|
15
16
|
}
|
|
16
17
|
logActivationRequired(context, response) {
|
|
17
|
-
|
|
18
|
+
errorDebug(`[Lovalingo] ${context} blocked (HTTP ${response.status}). ` +
|
|
18
19
|
`This project is not activated yet. ` +
|
|
19
20
|
`Publish a public manifest at "/.well-known/lovalingo.json" on your domain, ` +
|
|
20
21
|
`then verify it in the Lovalingo dashboard to activate translations + SEO.`);
|
|
@@ -129,7 +130,7 @@ export class LovalingoAPI {
|
|
|
129
130
|
}));
|
|
130
131
|
}
|
|
131
132
|
catch (error) {
|
|
132
|
-
|
|
133
|
+
errorDebug('Error fetching translations:', error);
|
|
133
134
|
return [];
|
|
134
135
|
}
|
|
135
136
|
}
|
|
@@ -184,7 +185,7 @@ export class LovalingoAPI {
|
|
|
184
185
|
return Array.isArray(data.exclusions) ? data.exclusions : [];
|
|
185
186
|
}
|
|
186
187
|
catch (error) {
|
|
187
|
-
|
|
188
|
+
errorDebug('Error fetching exclusions:', error);
|
|
188
189
|
return [];
|
|
189
190
|
}
|
|
190
191
|
}
|
|
@@ -210,7 +211,7 @@ export class LovalingoAPI {
|
|
|
210
211
|
return Array.isArray(data?.rules) ? data.rules : [];
|
|
211
212
|
}
|
|
212
213
|
catch (error) {
|
|
213
|
-
|
|
214
|
+
errorDebug('Error fetching DOM rules:', error);
|
|
214
215
|
return [];
|
|
215
216
|
}
|
|
216
217
|
}
|
|
@@ -230,7 +231,7 @@ export class LovalingoAPI {
|
|
|
230
231
|
}
|
|
231
232
|
}
|
|
232
233
|
catch (error) {
|
|
233
|
-
|
|
234
|
+
errorDebug('Error saving exclusion:', error);
|
|
234
235
|
throw error;
|
|
235
236
|
}
|
|
236
237
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function isDebugEnabled() {
|
|
2
|
+
if (typeof globalThis === "undefined")
|
|
3
|
+
return false;
|
|
4
|
+
const value = globalThis.__lovalingoDebug;
|
|
5
|
+
if (value === true || value === "true" || value === 1)
|
|
6
|
+
return true;
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
export function logDebug(...args) {
|
|
10
|
+
if (!isDebugEnabled())
|
|
11
|
+
return;
|
|
12
|
+
console.log(...args);
|
|
13
|
+
}
|
|
14
|
+
export function warnDebug(...args) {
|
|
15
|
+
if (!isDebugEnabled())
|
|
16
|
+
return;
|
|
17
|
+
console.warn(...args);
|
|
18
|
+
}
|
|
19
|
+
export function errorDebug(...args) {
|
|
20
|
+
if (!isDebugEnabled())
|
|
21
|
+
return;
|
|
22
|
+
console.error(...args);
|
|
23
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* /dashboard/projects/4458eb10-608c-4622-a92e-ec3ed2eeb524/setup
|
|
9
9
|
* → /dashboard/projects/:id/setup
|
|
10
10
|
*/
|
|
11
|
+
import { warnDebug } from "./logger";
|
|
11
12
|
// Common patterns for dynamic segments
|
|
12
13
|
const UUID_PATTERN = /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/gi;
|
|
13
14
|
const NUMERIC_ID_PATTERN = /\/\d+(?=\/|$)/g;
|
|
@@ -40,7 +41,7 @@ export function normalizePath(path, config) {
|
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
catch (error) {
|
|
43
|
-
|
|
44
|
+
warnDebug('[PathNormalizer] Invalid pattern:', rule.pattern, error);
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
package/dist/utils/translator.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logDebug, warnDebug, errorDebug } from './logger';
|
|
1
2
|
export class Translator {
|
|
2
3
|
constructor() {
|
|
3
4
|
this.translationMap = new Map();
|
|
@@ -113,7 +114,7 @@ export class Translator {
|
|
|
113
114
|
setTranslations(translations) {
|
|
114
115
|
this.translationMap.clear();
|
|
115
116
|
if (!Array.isArray(translations)) {
|
|
116
|
-
|
|
117
|
+
errorDebug('[Lovalingo] setTranslations expected array, got:', typeof translations);
|
|
117
118
|
return;
|
|
118
119
|
}
|
|
119
120
|
translations.forEach(t => {
|
|
@@ -122,7 +123,7 @@ export class Translator {
|
|
|
122
123
|
this.translationMap.set(t.source_text.trim(), t.translated_text);
|
|
123
124
|
}
|
|
124
125
|
});
|
|
125
|
-
|
|
126
|
+
logDebug(`[Lovalingo] ✅ Loaded ${this.translationMap.size} translations`);
|
|
126
127
|
}
|
|
127
128
|
setExclusions(exclusions) {
|
|
128
129
|
this.exclusions = exclusions || [];
|
|
@@ -334,7 +335,7 @@ export class Translator {
|
|
|
334
335
|
const hadLettersInRaw = /[a-zA-Z\u00C0-\u024F\u1E00-\u1EFF]/.test(rawHTML);
|
|
335
336
|
const hadPlaceholders = Object.keys(placeholderMap).length > 0;
|
|
336
337
|
if (hadLettersInRaw || hadPlaceholders) {
|
|
337
|
-
|
|
338
|
+
warnDebug('[Lovalingo] ⚠️ Non-translatable content after extraction:', semanticElement.outerHTML.substring(0, 140));
|
|
338
339
|
}
|
|
339
340
|
return {
|
|
340
341
|
rawHTML,
|
|
@@ -343,7 +344,7 @@ export class Translator {
|
|
|
343
344
|
semanticContext
|
|
344
345
|
};
|
|
345
346
|
}
|
|
346
|
-
|
|
347
|
+
logDebug(`[Lovalingo] 📝 Extracted: "${processedText.substring(0, 80)}..." with ${Object.keys(placeholderMap).length} placeholders`);
|
|
347
348
|
return {
|
|
348
349
|
rawHTML,
|
|
349
350
|
processedText,
|
|
@@ -394,7 +395,7 @@ export class Translator {
|
|
|
394
395
|
if (SEMANTIC_BOUNDARIES.has(element.tagName)) {
|
|
395
396
|
const originalHTML = element.getAttribute('data-Lovalingo-original-html');
|
|
396
397
|
if (originalHTML) {
|
|
397
|
-
|
|
398
|
+
logDebug(`[Lovalingo] 🔄 Restoring: "${element.textContent?.substring(0, 50)}..."`);
|
|
398
399
|
// Mark current live elements before restoring
|
|
399
400
|
const markedElements = this.markElements(element);
|
|
400
401
|
// Use safe update that preserves live elements
|
|
@@ -422,11 +423,11 @@ export class Translator {
|
|
|
422
423
|
* Restore all elements to original state
|
|
423
424
|
*/
|
|
424
425
|
restoreDOM() {
|
|
425
|
-
|
|
426
|
+
logDebug('[Lovalingo] 🔄 Restoring DOM to original state...');
|
|
426
427
|
const startTime = performance.now();
|
|
427
428
|
this.restoreElement(document.body);
|
|
428
429
|
const elapsed = performance.now() - startTime;
|
|
429
|
-
|
|
430
|
+
logDebug(`[Lovalingo] 🏁 DOM restoration complete in ${elapsed.toFixed(2)}ms`);
|
|
430
431
|
}
|
|
431
432
|
/**
|
|
432
433
|
* Mark all descendant elements with unique IDs before translation
|
|
@@ -481,7 +482,7 @@ export class Translator {
|
|
|
481
482
|
updateElementChildren(parent, newHTML, markedElements) {
|
|
482
483
|
// Safety check: Ensure parent is still in the DOM
|
|
483
484
|
if (!parent.isConnected || !document.body.contains(parent)) {
|
|
484
|
-
|
|
485
|
+
warnDebug('[Lovalingo] Parent element not in DOM, skipping translation');
|
|
485
486
|
return;
|
|
486
487
|
}
|
|
487
488
|
try {
|
|
@@ -498,7 +499,7 @@ export class Translator {
|
|
|
498
499
|
}
|
|
499
500
|
catch (e) {
|
|
500
501
|
// Node might have been removed by React already, skip
|
|
501
|
-
|
|
502
|
+
warnDebug('[Lovalingo] Could not remove child, likely already removed by framework');
|
|
502
503
|
break;
|
|
503
504
|
}
|
|
504
505
|
}
|
|
@@ -508,13 +509,13 @@ export class Translator {
|
|
|
508
509
|
parent.appendChild(temp.firstChild);
|
|
509
510
|
}
|
|
510
511
|
catch (e) {
|
|
511
|
-
|
|
512
|
+
warnDebug('[Lovalingo] Could not append child:', e);
|
|
512
513
|
break;
|
|
513
514
|
}
|
|
514
515
|
}
|
|
515
516
|
}
|
|
516
517
|
catch (error) {
|
|
517
|
-
|
|
518
|
+
errorDebug('[Lovalingo] Error updating element children:', error);
|
|
518
519
|
}
|
|
519
520
|
}
|
|
520
521
|
/**
|
|
@@ -595,7 +596,7 @@ export class Translator {
|
|
|
595
596
|
const sourceText = originalTextAttr || content.processedText;
|
|
596
597
|
// If boundary isn't translatable, recurse to children.
|
|
597
598
|
if (!this.isTranslatableText(sourceText)) {
|
|
598
|
-
|
|
599
|
+
logDebug('[Lovalingo] ⏭️ Skipping non-translatable content:', sourceText.substring(0, 50));
|
|
599
600
|
Array.from(element.children).forEach((child) => {
|
|
600
601
|
if (child instanceof HTMLElement)
|
|
601
602
|
this.translateElement(child);
|
|
@@ -604,10 +605,10 @@ export class Translator {
|
|
|
604
605
|
}
|
|
605
606
|
const translated = this.translationMap.get(sourceText);
|
|
606
607
|
if (translated) {
|
|
607
|
-
|
|
608
|
+
logDebug(`[Lovalingo] ✅ Translating: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
|
|
608
609
|
let reconstructedHTML = this.reconstructHTML(translated, content.placeholderMap);
|
|
609
610
|
if (/__[A-Z0-9_]+__/.test(reconstructedHTML)) {
|
|
610
|
-
|
|
611
|
+
warnDebug('[Lovalingo] ⚠️ Tokens remain after reconstruction, using original HTML');
|
|
611
612
|
reconstructedHTML = content.rawHTML;
|
|
612
613
|
}
|
|
613
614
|
// Mark for MutationObserver suppression (prevents feedback loops on DOM updates).
|
|
@@ -625,7 +626,7 @@ export class Translator {
|
|
|
625
626
|
});
|
|
626
627
|
}
|
|
627
628
|
else {
|
|
628
|
-
|
|
629
|
+
logDebug(`[Lovalingo] ❌ Miss: "${sourceText.substring(0, 80)}..."`);
|
|
629
630
|
}
|
|
630
631
|
}
|
|
631
632
|
finally {
|
|
@@ -661,15 +662,15 @@ export class Translator {
|
|
|
661
662
|
// Use stored original text
|
|
662
663
|
const sourceText = originalTextAttr || content.processedText;
|
|
663
664
|
if (!this.isTranslatableText(sourceText)) {
|
|
664
|
-
|
|
665
|
+
logDebug('[Lovalingo] ⏭️ Skipping non-translatable content (generic):', sourceText.substring(0, 50));
|
|
665
666
|
return;
|
|
666
667
|
}
|
|
667
668
|
const translated = this.translationMap.get(sourceText);
|
|
668
669
|
if (translated) {
|
|
669
|
-
|
|
670
|
+
logDebug(`[Lovalingo] ✅ Generic semantic: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
|
|
670
671
|
let reconstructedHTML = this.reconstructHTML(translated, content.placeholderMap);
|
|
671
672
|
if (/__[A-Z0-9_]+__/.test(reconstructedHTML)) {
|
|
672
|
-
|
|
673
|
+
warnDebug('[Lovalingo] ⚠️ Tokens remain (generic), using original HTML');
|
|
673
674
|
reconstructedHTML = content.rawHTML;
|
|
674
675
|
}
|
|
675
676
|
this.updateElementChildren(element, reconstructedHTML, markedElements);
|
|
@@ -681,7 +682,7 @@ export class Translator {
|
|
|
681
682
|
});
|
|
682
683
|
}
|
|
683
684
|
else {
|
|
684
|
-
|
|
685
|
+
logDebug(`[Lovalingo] ❌ Miss (generic): "${sourceText.substring(0, 80)}..."`);
|
|
685
686
|
}
|
|
686
687
|
}
|
|
687
688
|
finally {
|
|
@@ -792,10 +793,10 @@ export class Translator {
|
|
|
792
793
|
}
|
|
793
794
|
}
|
|
794
795
|
translateDOM() {
|
|
795
|
-
|
|
796
|
+
logDebug(`[Lovalingo] 🔄 translateDOM() called with ${this.translationMap.size} translations`);
|
|
796
797
|
const startTime = performance.now();
|
|
797
798
|
this.translateElement(document.body);
|
|
798
799
|
const elapsed = performance.now() - startTime;
|
|
799
|
-
|
|
800
|
+
logDebug(`[Lovalingo] 🏁 translateDOM() complete in ${elapsed.toFixed(2)}ms.`);
|
|
800
801
|
}
|
|
801
802
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.1.
|
|
1
|
+
export declare const VERSION = "0.1.2";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.1.
|
|
1
|
+
export const VERSION = "0.1.2";
|
package/package.json
CHANGED