@lovalingo/lovalingo 0.0.11

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.
Files changed (49) hide show
  1. package/LICENSE +148 -0
  2. package/README.md +376 -0
  3. package/dist/components/AixsterProvider.d.ts +9 -0
  4. package/dist/components/AixsterProvider.js +495 -0
  5. package/dist/components/AutoTranslate.d.ts +10 -0
  6. package/dist/components/AutoTranslate.js +77 -0
  7. package/dist/components/LangLink.d.ts +20 -0
  8. package/dist/components/LangLink.js +28 -0
  9. package/dist/components/LangRouter.d.ts +34 -0
  10. package/dist/components/LangRouter.js +60 -0
  11. package/dist/components/LanguageSwitcher.d.ts +10 -0
  12. package/dist/components/LanguageSwitcher.js +162 -0
  13. package/dist/components/LovalingoProvider.d.ts +1 -0
  14. package/dist/components/LovalingoProvider.js +1 -0
  15. package/dist/components/NavigationOverlay.d.ts +6 -0
  16. package/dist/components/NavigationOverlay.js +47 -0
  17. package/dist/context/AixsterContext.d.ts +3 -0
  18. package/dist/context/AixsterContext.js +2 -0
  19. package/dist/context/LovalingoContext.d.ts +1 -0
  20. package/dist/context/LovalingoContext.js +1 -0
  21. package/dist/hooks/useAixster.d.ts +6 -0
  22. package/dist/hooks/useAixster.js +14 -0
  23. package/dist/hooks/useAixsterEdit.d.ts +5 -0
  24. package/dist/hooks/useAixsterEdit.js +13 -0
  25. package/dist/hooks/useAixsterTranslate.d.ts +4 -0
  26. package/dist/hooks/useAixsterTranslate.js +12 -0
  27. package/dist/hooks/useLang.d.ts +16 -0
  28. package/dist/hooks/useLang.js +20 -0
  29. package/dist/hooks/useLangNavigate.d.ts +24 -0
  30. package/dist/hooks/useLangNavigate.js +30 -0
  31. package/dist/hooks/useLovalingo.d.ts +1 -0
  32. package/dist/hooks/useLovalingo.js +1 -0
  33. package/dist/hooks/useLovalingoEdit.d.ts +1 -0
  34. package/dist/hooks/useLovalingoEdit.js +1 -0
  35. package/dist/hooks/useLovalingoTranslate.d.ts +1 -0
  36. package/dist/hooks/useLovalingoTranslate.js +1 -0
  37. package/dist/index.d.ts +25 -0
  38. package/dist/index.js +27 -0
  39. package/dist/types.d.ts +65 -0
  40. package/dist/types.js +1 -0
  41. package/dist/utils/api.d.ts +17 -0
  42. package/dist/utils/api.js +145 -0
  43. package/dist/utils/hash.d.ts +9 -0
  44. package/dist/utils/hash.js +27 -0
  45. package/dist/utils/pathNormalizer.d.ts +49 -0
  46. package/dist/utils/pathNormalizer.js +114 -0
  47. package/dist/utils/translator.d.ts +80 -0
  48. package/dist/utils/translator.js +766 -0
  49. package/package.json +50 -0
@@ -0,0 +1,145 @@
1
+ import { processPath } from './pathNormalizer';
2
+ export class LovalingoAPI {
3
+ constructor(apiKey, apiBase, pathConfig) {
4
+ this.apiKey = apiKey;
5
+ this.apiBase = apiBase;
6
+ this.pathConfig = pathConfig;
7
+ }
8
+ async fetchTranslations(sourceLocale, targetLocale) {
9
+ try {
10
+ // Use path normalization utility
11
+ const normalizedPath = processPath(window.location.pathname, this.pathConfig);
12
+ const response = await fetch(`${this.apiBase}/functions/v1/bundle?key=${this.apiKey}&locale=${targetLocale}&path=${normalizedPath}`);
13
+ if (!response.ok)
14
+ throw new Error('Failed to fetch translations');
15
+ const data = await response.json();
16
+ // Convert map to array of Translation objects
17
+ if (data.map && typeof data.map === 'object') {
18
+ return Object.entries(data.map).map(([source_text, translated_text]) => ({
19
+ source_text,
20
+ translated_text: translated_text,
21
+ source_locale: sourceLocale,
22
+ target_locale: targetLocale,
23
+ }));
24
+ }
25
+ return [];
26
+ }
27
+ catch (error) {
28
+ console.error('Error fetching translations:', error);
29
+ return [];
30
+ }
31
+ }
32
+ async fetchExclusions() {
33
+ try {
34
+ const response = await fetch(`${this.apiBase}/functions/v1/exclusions?key=${this.apiKey}`);
35
+ if (!response.ok)
36
+ throw new Error('Failed to fetch exclusions');
37
+ const data = await response.json();
38
+ // Handle response format { exclusions: [...] }
39
+ return Array.isArray(data.exclusions) ? data.exclusions : [];
40
+ }
41
+ catch (error) {
42
+ console.error('Error fetching exclusions:', error);
43
+ return [];
44
+ }
45
+ }
46
+ async reportMisses(misses, sourceLocale, targetLocale) {
47
+ try {
48
+ // Use path normalization utility
49
+ const normalizedPath = processPath(window.location.pathname, this.pathConfig);
50
+ // CRITICAL: Filter out invalid misses
51
+ const validMisses = misses.filter(m => {
52
+ const isValid = m?.text &&
53
+ typeof m.text === 'string' &&
54
+ m.text.trim().length > 1;
55
+ if (!isValid) {
56
+ console.warn('[Lovalingo] ⚠️ Filtered invalid miss:', m);
57
+ }
58
+ return isValid;
59
+ });
60
+ if (validMisses.length === 0) {
61
+ console.log('[Lovalingo] ℹ️ No valid misses to report');
62
+ return;
63
+ }
64
+ // Format for API
65
+ const formattedMisses = validMisses.map(m => ({
66
+ source_text: m.text.trim(), // Tokenized text
67
+ source_text_raw: m.raw, // Original HTML
68
+ placeholder_map: m.placeholderMap, // Token → HTML mapping
69
+ semantic_context: m.semanticContext // Element type
70
+ }));
71
+ console.log(`[Lovalingo] 📤 Reporting ${formattedMisses.length} misses to API...`);
72
+ console.log('[Lovalingo] Sample miss:', formattedMisses[0]);
73
+ const response = await fetch(`${this.apiBase}/functions/v1/misses`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json' },
76
+ body: JSON.stringify({
77
+ key: this.apiKey,
78
+ locale: targetLocale,
79
+ misses: formattedMisses,
80
+ path: normalizedPath,
81
+ }),
82
+ });
83
+ if (!response.ok) {
84
+ const errorText = await response.text();
85
+ console.error(`[Lovalingo] ❌ API Error ${response.status}:`, errorText);
86
+ throw new Error(`Miss reporting failed: ${response.status}`);
87
+ }
88
+ const result = await response.json();
89
+ console.log('[Lovalingo] ✅ Misses reported:', result);
90
+ }
91
+ catch (error) {
92
+ console.error('[Lovalingo] ❌ Error reporting misses:', error);
93
+ }
94
+ }
95
+ async saveExclusion(selector, type) {
96
+ try {
97
+ await fetch(`${this.apiBase}/functions/v1/exclusions?key=${this.apiKey}`, {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify({ selector, type }),
101
+ });
102
+ }
103
+ catch (error) {
104
+ console.error('Error saving exclusion:', error);
105
+ throw error;
106
+ }
107
+ }
108
+ /**
109
+ * Real-time translation - calls Groq directly for instant translation
110
+ * Used when translation cache misses
111
+ */
112
+ async translateRealtime(contentHash, sourceText, sourceLocale, targetLocale) {
113
+ try {
114
+ console.log(`[Lovalingo] 🚀 Real-time translation: "${sourceText.substring(0, 40)}..."`);
115
+ const response = await fetch(`${this.apiBase}/functions/v1/translate-realtime`, {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Content-Type': 'application/json',
119
+ 'x-api-key': this.apiKey,
120
+ },
121
+ body: JSON.stringify({
122
+ contentHash,
123
+ sourceText,
124
+ sourceLocale,
125
+ targetLocale,
126
+ }),
127
+ });
128
+ if (!response.ok) {
129
+ const errorText = await response.text();
130
+ console.error(`[Lovalingo] ❌ Real-time translation failed:`, errorText);
131
+ return null;
132
+ }
133
+ const result = await response.json();
134
+ if (result.success && result.translation) {
135
+ console.log(`[Lovalingo] ✅ Translated: "${result.translation.substring(0, 40)}..." ${result.cached ? '(cached)' : '(new)'}`);
136
+ return result.translation;
137
+ }
138
+ return null;
139
+ }
140
+ catch (error) {
141
+ console.error('[Lovalingo] ❌ Real-time translation error:', error);
142
+ return null;
143
+ }
144
+ }
145
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Simple, fast hash function for content addressing
3
+ * Uses djb2 algorithm - good distribution, very fast
4
+ */
5
+ export declare function hashContent(text: string): string;
6
+ /**
7
+ * Hash with context (includes placeholders info for uniqueness)
8
+ */
9
+ export declare function hashWithContext(text: string, placeholders?: Map<string, string>): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Simple, fast hash function for content addressing
3
+ * Uses djb2 algorithm - good distribution, very fast
4
+ */
5
+ export function hashContent(text) {
6
+ if (!text || text.length === 0) {
7
+ return '0';
8
+ }
9
+ let hash = 5381;
10
+ for (let i = 0; i < text.length; i++) {
11
+ hash = ((hash << 5) + hash) + text.charCodeAt(i); // hash * 33 + c
12
+ }
13
+ // Convert to positive base36 string (compact representation)
14
+ return Math.abs(hash).toString(36);
15
+ }
16
+ /**
17
+ * Hash with context (includes placeholders info for uniqueness)
18
+ */
19
+ export function hashWithContext(text, placeholders) {
20
+ const baseHash = hashContent(text);
21
+ if (!placeholders || placeholders.size === 0) {
22
+ return baseHash;
23
+ }
24
+ // Include placeholder keys in hash for uniqueness
25
+ const placeholderKeys = Array.from(placeholders.keys()).sort().join(',');
26
+ return hashContent(`${text}:${placeholderKeys}`);
27
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Path Normalization Utility
3
+ *
4
+ * Normalizes URL paths by replacing dynamic segments (UUIDs, IDs, slugs)
5
+ * with placeholders to enable translation bundle sharing across similar pages.
6
+ *
7
+ * Example:
8
+ * /dashboard/projects/4458eb10-608c-4622-a92e-ec3ed2eeb524/setup
9
+ * → /dashboard/projects/:id/setup
10
+ */
11
+ export interface PathNormalizationRule {
12
+ pattern: string;
13
+ replacement: string;
14
+ includeSubpaths?: boolean;
15
+ }
16
+ export interface PathNormalizationConfig {
17
+ enabled: boolean;
18
+ rules?: PathNormalizationRule[];
19
+ supportedLocales?: string[];
20
+ }
21
+ /**
22
+ * Normalizes a path by replacing dynamic segments with placeholders
23
+ */
24
+ export declare function normalizePath(path: string, config?: PathNormalizationConfig): string;
25
+ /**
26
+ * Cleans path by removing locale prefix and trailing slashes
27
+ */
28
+ export declare function cleanPath(path: string, supportedLocales?: string[]): string;
29
+ /**
30
+ * Combined path processing: clean + normalize
31
+ */
32
+ export declare function processPath(path: string, config?: PathNormalizationConfig): string;
33
+ /**
34
+ * Detects if a path segment looks like a dynamic ID
35
+ */
36
+ export declare function isDynamicSegment(segment: string): boolean;
37
+ /**
38
+ * Analyzes a path and returns which segments are dynamic
39
+ */
40
+ export interface PathAnalysis {
41
+ original: string;
42
+ normalized: string;
43
+ segments: Array<{
44
+ original: string;
45
+ normalized: string;
46
+ isDynamic: boolean;
47
+ }>;
48
+ }
49
+ export declare function analyzePath(path: string, supportedLocales?: string[]): PathAnalysis;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Path Normalization Utility
3
+ *
4
+ * Normalizes URL paths by replacing dynamic segments (UUIDs, IDs, slugs)
5
+ * with placeholders to enable translation bundle sharing across similar pages.
6
+ *
7
+ * Example:
8
+ * /dashboard/projects/4458eb10-608c-4622-a92e-ec3ed2eeb524/setup
9
+ * → /dashboard/projects/:id/setup
10
+ */
11
+ // Common patterns for dynamic segments
12
+ 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
+ const NUMERIC_ID_PATTERN = /\/\d+(?=\/|$)/g;
14
+ const HASH_PATTERN = /\/[a-f0-9]{32,}(?=\/|$)/gi; // MD5, SHA hashes
15
+ const ALPHANUMERIC_ID_PATTERN = /\/[a-z0-9]{20,}(?=\/|$)/gi; // Long random IDs
16
+ /**
17
+ * Normalizes a path by replacing dynamic segments with placeholders
18
+ */
19
+ export function normalizePath(path, config) {
20
+ if (config?.enabled === false) {
21
+ return path;
22
+ }
23
+ let normalized = path;
24
+ let shouldIncludeSubpaths = false;
25
+ // Apply automatic detection patterns
26
+ normalized = normalized.replace(UUID_PATTERN, ':id');
27
+ normalized = normalized.replace(HASH_PATTERN, ':hash');
28
+ normalized = normalized.replace(ALPHANUMERIC_ID_PATTERN, ':id');
29
+ normalized = normalized.replace(NUMERIC_ID_PATTERN, '/:id');
30
+ // Apply custom user-defined rules
31
+ if (config?.rules) {
32
+ for (const rule of config.rules) {
33
+ try {
34
+ const regex = new RegExp(rule.pattern, 'gi');
35
+ const beforeReplace = normalized;
36
+ normalized = normalized.replace(regex, rule.replacement);
37
+ // Track if this rule was applied and has includeSubpaths enabled
38
+ if (beforeReplace !== normalized && rule.includeSubpaths) {
39
+ shouldIncludeSubpaths = true;
40
+ }
41
+ }
42
+ catch (error) {
43
+ console.warn('[PathNormalizer] Invalid pattern:', rule.pattern, error);
44
+ }
45
+ }
46
+ }
47
+ // Collapse consecutive :id placeholders (e.g., /:id/:id → /:id)
48
+ normalized = normalized.replace(/\/:id(\/):id/g, '/:id$1*');
49
+ // If includeSubpaths is enabled, replace everything after the first placeholder with /*
50
+ if (shouldIncludeSubpaths) {
51
+ // Find the first placeholder (:id, :hash, :slug, etc.)
52
+ const placeholderMatch = normalized.match(/(:[a-z]+)/);
53
+ if (placeholderMatch) {
54
+ const placeholderIndex = normalized.indexOf(placeholderMatch[0]);
55
+ const beforePlaceholder = normalized.substring(0, placeholderIndex + placeholderMatch[0].length);
56
+ normalized = beforePlaceholder + '/*';
57
+ }
58
+ }
59
+ return normalized;
60
+ }
61
+ /**
62
+ * Cleans path by removing locale prefix and trailing slashes
63
+ */
64
+ export function cleanPath(path, supportedLocales) {
65
+ let cleaned = path;
66
+ // Strip locale prefix from path if it matches a supported locale
67
+ // e.g., /fr/pricing -> /pricing, /en -> /
68
+ if (supportedLocales && supportedLocales.length > 0) {
69
+ const segments = cleaned.split('/').filter(Boolean);
70
+ if (segments.length > 0 && supportedLocales.includes(segments[0])) {
71
+ // First segment is a valid locale - remove it
72
+ cleaned = '/' + segments.slice(1).join('/');
73
+ if (cleaned === '')
74
+ cleaned = '/';
75
+ }
76
+ }
77
+ // Remove trailing slash except for root
78
+ if (cleaned !== '/' && cleaned.endsWith('/')) {
79
+ cleaned = cleaned.slice(0, -1);
80
+ }
81
+ return cleaned;
82
+ }
83
+ /**
84
+ * Combined path processing: clean + normalize
85
+ */
86
+ export function processPath(path, config) {
87
+ const cleaned = cleanPath(path, config?.supportedLocales);
88
+ const normalized = normalizePath(cleaned, config);
89
+ return normalized;
90
+ }
91
+ /**
92
+ * Detects if a path segment looks like a dynamic ID
93
+ */
94
+ export function isDynamicSegment(segment) {
95
+ return (UUID_PATTERN.test(segment) ||
96
+ /^\d+$/.test(segment) ||
97
+ HASH_PATTERN.test(segment) ||
98
+ ALPHANUMERIC_ID_PATTERN.test(segment));
99
+ }
100
+ export function analyzePath(path, supportedLocales) {
101
+ const cleaned = cleanPath(path, supportedLocales);
102
+ const normalized = normalizePath(cleaned);
103
+ const originalSegments = cleaned.split('/').filter(Boolean);
104
+ const normalizedSegments = normalized.split('/').filter(Boolean);
105
+ return {
106
+ original: cleaned,
107
+ normalized,
108
+ segments: originalSegments.map((seg, idx) => ({
109
+ original: seg,
110
+ normalized: normalizedSegments[idx] || seg,
111
+ isDynamic: seg !== normalizedSegments[idx],
112
+ })),
113
+ };
114
+ }
@@ -0,0 +1,80 @@
1
+ import { Translation, Exclusion, MissedTranslation } from '../types';
2
+ export declare class Translator {
3
+ private translationMap;
4
+ private exclusions;
5
+ private missedStrings;
6
+ private nonTranslatableTerms;
7
+ constructor();
8
+ /**
9
+ * Check if element is interactive (should be preserved as island)
10
+ */
11
+ private isInteractive;
12
+ /**
13
+ * Check if text contains actual translatable content
14
+ */
15
+ private isTranslatableText;
16
+ /**
17
+ * Check if element should be treated as semantic boundary
18
+ * even though it's not in the predefined SEMANTIC_BOUNDARIES set
19
+ *
20
+ * Phase 1: Conservative approach - only leaf elements (no element children)
21
+ * This catches cases like: <span>text</span>, <div>text</div>, etc.
22
+ */
23
+ private shouldTreatAsSemantic;
24
+ setTranslations(translations: Translation[]): void;
25
+ setExclusions(exclusions: Exclusion[]): void;
26
+ getMissedStrings(): MissedTranslation[];
27
+ clearMissedStrings(): void;
28
+ private isExcluded;
29
+ /**
30
+ * DOM-BASED EXTRACTION
31
+ * Uses DOM APIs to reliably parse and tokenize HTML
32
+ */
33
+ private extractTranslatableContent;
34
+ /**
35
+ * RECONSTRUCTION: Replace tokens with original HTML
36
+ */
37
+ private reconstructHTML;
38
+ /**
39
+ * Restore element to original HTML while preserving event listeners
40
+ */
41
+ private restoreElement;
42
+ /**
43
+ * Restore all elements to original state
44
+ */
45
+ restoreDOM(): void;
46
+ /**
47
+ * Mark all descendant elements with unique IDs before translation
48
+ * This allows us to reuse original DOM nodes (preserving event listeners)
49
+ */
50
+ private markElements;
51
+ /**
52
+ * After reconstruction, transplant original LIVE elements into new structure
53
+ * This preserves event listeners and framework connections
54
+ */
55
+ private transplantOriginalElements;
56
+ /**
57
+ * Directly update the parent element's children by transplanting from temp
58
+ * This avoids using innerHTML which would destroy event listeners
59
+ */
60
+ private updateElementChildren;
61
+ /**
62
+ * Update only text nodes within an element, preserving child elements
63
+ * Enhanced to handle nested structures recursively
64
+ */
65
+ private updateTextNodesOnly;
66
+ translateElement(element: HTMLElement): void;
67
+ /**
68
+ * Restore attribute to original value
69
+ */
70
+ private restoreAttribute;
71
+ /**
72
+ * Translate interactive element safely (preserves event handlers)
73
+ */
74
+ private translateInteractive;
75
+ /**
76
+ * Translate individual attributes
77
+ */
78
+ private translateAttribute;
79
+ translateDOM(): void;
80
+ }