@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 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
- console.warn("[Lovalingo] updateSeoLinks() failed:", e);
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
- console.warn('localStorage not available:', e);
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
- console.log(`[Lovalingo] Using cached translations for ${targetLocale} on ${currentPath}`);
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
- console.log(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
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
- console.log(`[Lovalingo] Fetching translations for ${targetLocale} on ${currentPath}`);
331
+ logDebug(`[Lovalingo] Fetching translations for ${targetLocale} on ${currentPath}`);
331
332
  setIsLoading(true);
332
333
  try {
333
334
  if (previousLocale && previousLocale !== defaultLocale) {
334
- console.log(`[Lovalingo] Switching from ${previousLocale} to ${targetLocale}`);
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
- console.log(`[Lovalingo] 🔄 Retry scan for late-rendering content`);
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
- console.error('Error loading translations:', error);
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
- console.warn('Failed to save locale to localStorage:', e);
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
- console.warn(`[Lovalingo] Missing public project key: ${action} was skipped. Pass publicAnonKey to <LovalingoProvider ...> (or set VITE_LOVALINGO_PUBLIC_ANON_KEY).`);
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
- console.error(`[Lovalingo] ${context} blocked (HTTP ${response.status}). ` +
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
- console.error('Error fetching translations:', error);
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
- console.error('Error fetching exclusions:', error);
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
- console.error('Error fetching DOM rules:', error);
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
- console.error('Error saving exclusion:', error);
234
+ errorDebug('Error saving exclusion:', error);
234
235
  throw error;
235
236
  }
236
237
  }
@@ -0,0 +1,3 @@
1
+ export declare function logDebug(...args: unknown[]): void;
2
+ export declare function warnDebug(...args: unknown[]): void;
3
+ export declare function errorDebug(...args: unknown[]): void;
@@ -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
- console.warn('[PathNormalizer] Invalid pattern:', rule.pattern, error);
44
+ warnDebug('[PathNormalizer] Invalid pattern:', rule.pattern, error);
44
45
  }
45
46
  }
46
47
  }
@@ -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
- console.error('[Lovalingo] setTranslations expected array, got:', typeof translations);
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
- console.log(`[Lovalingo] ✅ Loaded ${this.translationMap.size} translations`);
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
- console.warn('[Lovalingo] ⚠️ Non-translatable content after extraction:', semanticElement.outerHTML.substring(0, 140));
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
- console.log(`[Lovalingo] 📝 Extracted: "${processedText.substring(0, 80)}..." with ${Object.keys(placeholderMap).length} placeholders`);
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
- console.log(`[Lovalingo] 🔄 Restoring: "${element.textContent?.substring(0, 50)}..."`);
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
- console.log('[Lovalingo] 🔄 Restoring DOM to original state...');
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
- console.log(`[Lovalingo] 🏁 DOM restoration complete in ${elapsed.toFixed(2)}ms`);
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
- console.warn('[Lovalingo] Parent element not in DOM, skipping translation');
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
- console.warn('[Lovalingo] Could not remove child, likely already removed by framework');
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
- console.warn('[Lovalingo] Could not append child:', e);
512
+ warnDebug('[Lovalingo] Could not append child:', e);
512
513
  break;
513
514
  }
514
515
  }
515
516
  }
516
517
  catch (error) {
517
- console.error('[Lovalingo] Error updating element children:', error);
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
- console.log('[Lovalingo] ⏭️ Skipping non-translatable content:', sourceText.substring(0, 50));
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
- console.log(`[Lovalingo] ✅ Translating: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
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
- console.warn('[Lovalingo] ⚠️ Tokens remain after reconstruction, using original HTML');
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
- console.log(`[Lovalingo] ❌ Miss: "${sourceText.substring(0, 80)}..."`);
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
- console.log('[Lovalingo] ⏭️ Skipping non-translatable content (generic):', sourceText.substring(0, 50));
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
- console.log(`[Lovalingo] ✅ Generic semantic: "${sourceText.substring(0, 50)}..." → "${translated.substring(0, 50)}..."`);
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
- console.warn('[Lovalingo] ⚠️ Tokens remain (generic), using original HTML');
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
- console.log(`[Lovalingo] ❌ Miss (generic): "${sourceText.substring(0, 80)}..."`);
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
- console.log(`[Lovalingo] 🔄 translateDOM() called with ${this.translationMap.size} translations`);
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
- console.log(`[Lovalingo] 🏁 translateDOM() complete in ${elapsed.toFixed(2)}ms.`);
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.0";
1
+ export declare const VERSION = "0.1.2";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.1.0";
1
+ export const VERSION = "0.1.2";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lovalingo/lovalingo",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "React translation runtime with i18n routing, deterministic bundles + DOM rules, and zero-flash rendering.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",