experimental-ciao-react 1.1.10 → 1.1.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.
- package/README.md +4 -4
- package/dist/index.cjs +437 -274
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +19 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +437 -275
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -36,127 +36,32 @@ function CTContextBlock({ children }) {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
//#endregion
|
|
39
|
-
//#region src/
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
52
|
-
request.onerror = () => reject(request.error);
|
|
53
|
-
request.onsuccess = () => resolve(request.result);
|
|
54
|
-
request.onupgradeneeded = (event) => {
|
|
55
|
-
const db = event.target.result;
|
|
56
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
57
|
-
const store = db.createObjectStore(STORE_NAME, { keyPath: "url" });
|
|
58
|
-
store.createIndex("language", "language", { unique: false });
|
|
59
|
-
store.createIndex("projectId", "projectId", { unique: false });
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
});
|
|
63
|
-
return dbPromise;
|
|
64
|
-
}
|
|
65
|
-
async function getCachedTranslation(url) {
|
|
66
|
-
try {
|
|
67
|
-
const db = await openDB();
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
const request = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(url);
|
|
70
|
-
request.onerror = () => reject(request.error);
|
|
71
|
-
request.onsuccess = () => {
|
|
72
|
-
const entry = request.result;
|
|
73
|
-
resolve(entry?.data ?? null);
|
|
74
|
-
};
|
|
75
|
-
});
|
|
76
|
-
} catch {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
async function cacheTranslation(url, language, projectId, data) {
|
|
81
|
-
try {
|
|
82
|
-
const db = await openDB();
|
|
83
|
-
return new Promise((resolve, reject) => {
|
|
84
|
-
const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
|
|
85
|
-
const entry = {
|
|
86
|
-
url,
|
|
87
|
-
language,
|
|
88
|
-
projectId,
|
|
89
|
-
data,
|
|
90
|
-
cachedAt: Date.now()
|
|
91
|
-
};
|
|
92
|
-
const request = store.put(entry);
|
|
93
|
-
request.onerror = () => reject(request.error);
|
|
94
|
-
request.onsuccess = () => resolve();
|
|
95
|
-
});
|
|
96
|
-
} catch {}
|
|
97
|
-
}
|
|
98
|
-
async function clearCache(projectId) {
|
|
99
|
-
try {
|
|
100
|
-
const db = await openDB();
|
|
101
|
-
return new Promise((resolve, reject) => {
|
|
102
|
-
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
103
|
-
const store = transaction.objectStore(STORE_NAME);
|
|
104
|
-
if (projectId) {
|
|
105
|
-
const request = store.index("projectId").openCursor(IDBKeyRange.only(projectId));
|
|
106
|
-
request.onsuccess = (event) => {
|
|
107
|
-
const cursor = event.target.result;
|
|
108
|
-
if (cursor) {
|
|
109
|
-
cursor.delete();
|
|
110
|
-
cursor.continue();
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
transaction.oncomplete = () => resolve();
|
|
114
|
-
transaction.onerror = () => reject(transaction.error);
|
|
115
|
-
} else {
|
|
116
|
-
const request = store.clear();
|
|
117
|
-
request.onerror = () => reject(request.error);
|
|
118
|
-
request.onsuccess = () => resolve();
|
|
39
|
+
//#region src/interpolate.ts
|
|
40
|
+
const MAX_CACHE_SIZE = 100;
|
|
41
|
+
function createBoundedCache(maxSize) {
|
|
42
|
+
const cache = /* @__PURE__ */ new Map();
|
|
43
|
+
return {
|
|
44
|
+
get(key) {
|
|
45
|
+
return cache.get(key);
|
|
46
|
+
},
|
|
47
|
+
set(key, value) {
|
|
48
|
+
if (cache.size >= maxSize) {
|
|
49
|
+
const firstKey = cache.keys().next().value;
|
|
50
|
+
if (firstKey !== void 0) cache.delete(firstKey);
|
|
119
51
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const countRequest = store.count();
|
|
130
|
-
const languages = /* @__PURE__ */ new Set();
|
|
131
|
-
const cursorRequest = store.openCursor();
|
|
132
|
-
cursorRequest.onsuccess = (event) => {
|
|
133
|
-
const cursor = event.target.result;
|
|
134
|
-
if (cursor) {
|
|
135
|
-
languages.add(cursor.value.language);
|
|
136
|
-
cursor.continue();
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
transaction.oncomplete = () => {
|
|
140
|
-
resolve({
|
|
141
|
-
count: countRequest.result,
|
|
142
|
-
languages: Array.from(languages)
|
|
143
|
-
});
|
|
144
|
-
};
|
|
145
|
-
transaction.onerror = () => reject(transaction.error);
|
|
146
|
-
});
|
|
147
|
-
} catch {
|
|
148
|
-
return {
|
|
149
|
-
count: 0,
|
|
150
|
-
languages: []
|
|
151
|
-
};
|
|
152
|
-
}
|
|
52
|
+
cache.set(key, value);
|
|
53
|
+
},
|
|
54
|
+
has(key) {
|
|
55
|
+
return cache.has(key);
|
|
56
|
+
},
|
|
57
|
+
clear() {
|
|
58
|
+
cache.clear();
|
|
59
|
+
}
|
|
60
|
+
};
|
|
153
61
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const numberFormatCache = /* @__PURE__ */ new Map();
|
|
158
|
-
const dateFormatCache = /* @__PURE__ */ new Map();
|
|
159
|
-
const pluralRulesCache = /* @__PURE__ */ new Map();
|
|
62
|
+
const numberFormatCache = createBoundedCache(MAX_CACHE_SIZE);
|
|
63
|
+
const dateFormatCache = createBoundedCache(MAX_CACHE_SIZE);
|
|
64
|
+
const pluralRulesCache = createBoundedCache(MAX_CACHE_SIZE);
|
|
160
65
|
function clearFormatterCache() {
|
|
161
66
|
numberFormatCache.clear();
|
|
162
67
|
dateFormatCache.clear();
|
|
@@ -279,7 +184,7 @@ const useTranslationStore = (0, zustand.create)()((0, zustand_middleware.persist
|
|
|
279
184
|
isLoading: false,
|
|
280
185
|
isReady: false,
|
|
281
186
|
isHydrated: false,
|
|
282
|
-
|
|
187
|
+
storedManifest: null,
|
|
283
188
|
lastVersionCheck: null,
|
|
284
189
|
setLanguage: (language) => {
|
|
285
190
|
set({
|
|
@@ -320,9 +225,9 @@ const useTranslationStore = (0, zustand.create)()((0, zustand_middleware.persist
|
|
|
320
225
|
setReady: (ready) => {
|
|
321
226
|
set({ isReady: ready });
|
|
322
227
|
},
|
|
323
|
-
|
|
228
|
+
setStoredManifest: (manifest) => {
|
|
324
229
|
set({
|
|
325
|
-
|
|
230
|
+
storedManifest: manifest,
|
|
326
231
|
lastVersionCheck: Date.now()
|
|
327
232
|
});
|
|
328
233
|
}
|
|
@@ -330,10 +235,14 @@ const useTranslationStore = (0, zustand.create)()((0, zustand_middleware.persist
|
|
|
330
235
|
name: "ciao-tools-language",
|
|
331
236
|
partialize: (state) => ({
|
|
332
237
|
currentLanguage: state.currentLanguage,
|
|
333
|
-
|
|
238
|
+
storedManifest: state.storedManifest
|
|
334
239
|
}),
|
|
335
240
|
onRehydrateStorage: () => (_, error) => {
|
|
336
|
-
if (
|
|
241
|
+
if (error) console.error("[ciao-tools] Storage hydration failed:", error);
|
|
242
|
+
if (hydrationResolver) {
|
|
243
|
+
hydrationResolver();
|
|
244
|
+
hydrationResolver = null;
|
|
245
|
+
}
|
|
337
246
|
}
|
|
338
247
|
}));
|
|
339
248
|
hydrationPromise.then(() => {
|
|
@@ -345,6 +254,133 @@ function getTranslation(translations, language, text, values) {
|
|
|
345
254
|
return interpolate(translated, values, language);
|
|
346
255
|
}
|
|
347
256
|
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/cache.ts
|
|
259
|
+
const DB_NAME = "ciao-tools-translations";
|
|
260
|
+
const DB_VERSION = 1;
|
|
261
|
+
const STORE_NAME = "translations";
|
|
262
|
+
let dbPromise = null;
|
|
263
|
+
function openDB() {
|
|
264
|
+
if (dbPromise) return dbPromise;
|
|
265
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
266
|
+
if (typeof indexedDB === "undefined") {
|
|
267
|
+
dbPromise = null;
|
|
268
|
+
reject(/* @__PURE__ */ new Error("IndexedDB not available"));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
272
|
+
request.onerror = () => {
|
|
273
|
+
dbPromise = null;
|
|
274
|
+
reject(request.error);
|
|
275
|
+
};
|
|
276
|
+
request.onsuccess = () => resolve(request.result);
|
|
277
|
+
request.onupgradeneeded = (event) => {
|
|
278
|
+
const db = event.target.result;
|
|
279
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
280
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "url" });
|
|
281
|
+
store.createIndex("language", "language", { unique: false });
|
|
282
|
+
store.createIndex("projectId", "projectId", { unique: false });
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
});
|
|
286
|
+
return dbPromise;
|
|
287
|
+
}
|
|
288
|
+
async function getCachedTranslation(url) {
|
|
289
|
+
try {
|
|
290
|
+
const db = await openDB();
|
|
291
|
+
return new Promise((resolve, reject) => {
|
|
292
|
+
const request = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(url);
|
|
293
|
+
request.onerror = () => reject(request.error);
|
|
294
|
+
request.onsuccess = () => {
|
|
295
|
+
const entry = request.result;
|
|
296
|
+
resolve(entry?.data ?? null);
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.warn("[ciao-tools] Failed to read from cache:", error);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function cacheTranslation(url, language, projectId, data) {
|
|
305
|
+
try {
|
|
306
|
+
const db = await openDB();
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
|
|
309
|
+
const entry = {
|
|
310
|
+
url,
|
|
311
|
+
language,
|
|
312
|
+
projectId,
|
|
313
|
+
data,
|
|
314
|
+
cachedAt: Date.now()
|
|
315
|
+
};
|
|
316
|
+
const request = store.put(entry);
|
|
317
|
+
request.onerror = () => reject(request.error);
|
|
318
|
+
request.onsuccess = () => resolve();
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.warn("[ciao-tools] Failed to write to cache:", error);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function clearCache(projectId) {
|
|
325
|
+
try {
|
|
326
|
+
const db = await openDB();
|
|
327
|
+
return new Promise((resolve, reject) => {
|
|
328
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
329
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
330
|
+
if (projectId) {
|
|
331
|
+
const request = store.index("projectId").openCursor(IDBKeyRange.only(projectId));
|
|
332
|
+
request.onsuccess = (event) => {
|
|
333
|
+
const cursor = event.target.result;
|
|
334
|
+
if (cursor) {
|
|
335
|
+
cursor.delete();
|
|
336
|
+
cursor.continue();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
transaction.oncomplete = () => resolve();
|
|
340
|
+
transaction.onerror = () => reject(transaction.error);
|
|
341
|
+
} else {
|
|
342
|
+
const request = store.clear();
|
|
343
|
+
request.onerror = () => reject(request.error);
|
|
344
|
+
request.onsuccess = () => resolve();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.warn("[ciao-tools] Failed to clear cache:", error);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async function getCacheStats() {
|
|
352
|
+
try {
|
|
353
|
+
const db = await openDB();
|
|
354
|
+
return new Promise((resolve, reject) => {
|
|
355
|
+
const transaction = db.transaction(STORE_NAME, "readonly");
|
|
356
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
357
|
+
const countRequest = store.count();
|
|
358
|
+
const languages = /* @__PURE__ */ new Set();
|
|
359
|
+
const cursorRequest = store.openCursor();
|
|
360
|
+
cursorRequest.onsuccess = (event) => {
|
|
361
|
+
const cursor = event.target.result;
|
|
362
|
+
if (cursor) {
|
|
363
|
+
languages.add(cursor.value.language);
|
|
364
|
+
cursor.continue();
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
transaction.oncomplete = () => {
|
|
368
|
+
resolve({
|
|
369
|
+
count: countRequest.result,
|
|
370
|
+
languages: Array.from(languages)
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
transaction.onerror = () => reject(transaction.error);
|
|
374
|
+
});
|
|
375
|
+
} catch (error) {
|
|
376
|
+
console.warn("[ciao-tools] Failed to get cache stats:", error);
|
|
377
|
+
return {
|
|
378
|
+
count: 0,
|
|
379
|
+
languages: []
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
348
384
|
//#endregion
|
|
349
385
|
//#region src/hooks/useHotUpdates.ts
|
|
350
386
|
const CDN_BASE_URL = "https://t1.ciao-tools.com";
|
|
@@ -357,7 +393,8 @@ async function fetchLatestManifest(projectId) {
|
|
|
357
393
|
throw new Error(`Failed to fetch latest manifest: ${response.statusText}`);
|
|
358
394
|
}
|
|
359
395
|
return response.json();
|
|
360
|
-
} catch {
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.warn("[ciao-tools] Failed to fetch latest manifest:", error);
|
|
361
398
|
return null;
|
|
362
399
|
}
|
|
363
400
|
}
|
|
@@ -366,60 +403,81 @@ async function fetchTranslations(url) {
|
|
|
366
403
|
if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
|
|
367
404
|
return response.json();
|
|
368
405
|
}
|
|
369
|
-
function useHotUpdates(config, projectId) {
|
|
406
|
+
function useHotUpdates(config, projectId, sourceLanguage) {
|
|
370
407
|
const isCheckingRef = (0, react.useRef)(false);
|
|
371
408
|
const hasCheckedOnMountRef = (0, react.useRef)(false);
|
|
372
|
-
const
|
|
409
|
+
const configRef = (0, react.useRef)(config);
|
|
410
|
+
const projectIdRef = (0, react.useRef)(projectId);
|
|
411
|
+
const sourceLanguageRef = (0, react.useRef)(sourceLanguage);
|
|
412
|
+
(0, react.useEffect)(() => {
|
|
413
|
+
configRef.current = config;
|
|
414
|
+
projectIdRef.current = projectId;
|
|
415
|
+
sourceLanguageRef.current = sourceLanguage;
|
|
416
|
+
}, [
|
|
417
|
+
config,
|
|
418
|
+
projectId,
|
|
419
|
+
sourceLanguage
|
|
420
|
+
]);
|
|
421
|
+
const { setStoredManifest, addLanguage } = useTranslationStore();
|
|
373
422
|
const checkForUpdates = (0, react.useCallback)(async () => {
|
|
374
|
-
|
|
375
|
-
|
|
423
|
+
const currentConfig = configRef.current;
|
|
424
|
+
const currentProjectId = projectIdRef.current;
|
|
425
|
+
const currentSourceLanguage = sourceLanguageRef.current;
|
|
426
|
+
if (!currentConfig || !currentProjectId || isCheckingRef.current) return;
|
|
427
|
+
if (currentConfig.enabled !== true) return;
|
|
376
428
|
isCheckingRef.current = true;
|
|
377
429
|
try {
|
|
378
430
|
console.log("[ciao-tools] Checking for hot updates...");
|
|
379
|
-
const
|
|
380
|
-
if (!
|
|
431
|
+
const latestManifest = await fetchLatestManifest(currentProjectId);
|
|
432
|
+
if (!latestManifest) {
|
|
381
433
|
console.log("[ciao-tools] No latest.json found (project may not have hot updates yet)");
|
|
382
434
|
return;
|
|
383
435
|
}
|
|
384
|
-
console.log("[ciao-tools] Found latest.json, version:",
|
|
385
|
-
const
|
|
386
|
-
const
|
|
436
|
+
console.log("[ciao-tools] Found latest.json, version:", latestManifest.version);
|
|
437
|
+
const currentManifest = useTranslationStore.getState().storedManifest;
|
|
438
|
+
const currentVersion = currentManifest?.serverVersion ?? null;
|
|
439
|
+
const isFirstCheck = currentVersion === null;
|
|
440
|
+
const hasNewVersion = currentVersion !== null && latestManifest.version > currentVersion;
|
|
387
441
|
if (isFirstCheck || hasNewVersion) {
|
|
388
|
-
if (Object.keys(
|
|
442
|
+
if (Object.keys(latestManifest.urls).length > 0) {
|
|
389
443
|
const reason = hasNewVersion ? "new version" : "first check";
|
|
390
444
|
console.log(`[ciao-tools] Fetching translations (${reason})...`);
|
|
391
|
-
if (hasNewVersion) await clearCache(
|
|
445
|
+
if (hasNewVersion) await clearCache(currentProjectId);
|
|
392
446
|
const updatedLanguages = [];
|
|
393
|
-
for (const [langCode, url] of Object.entries(
|
|
447
|
+
for (const [langCode, url] of Object.entries(latestManifest.urls)) try {
|
|
394
448
|
const translations = await fetchTranslations(url);
|
|
395
449
|
addLanguage(langCode, translations);
|
|
396
|
-
await cacheTranslation(url, langCode,
|
|
450
|
+
await cacheTranslation(url, langCode, currentProjectId, translations);
|
|
397
451
|
updatedLanguages.push(langCode);
|
|
398
452
|
} catch (err) {
|
|
399
453
|
console.error(`[ciao-tools] Failed to fetch ${langCode} translations:`, err);
|
|
400
454
|
}
|
|
401
455
|
if (updatedLanguages.length > 0) {
|
|
402
456
|
console.log("[ciao-tools] Updated translations for:", updatedLanguages);
|
|
403
|
-
if (hasNewVersion &&
|
|
457
|
+
if (hasNewVersion && currentConfig.onTranslationsUpdated) currentConfig.onTranslationsUpdated(updatedLanguages);
|
|
404
458
|
}
|
|
405
459
|
}
|
|
406
|
-
|
|
407
|
-
|
|
460
|
+
setStoredManifest({
|
|
461
|
+
serverVersion: latestManifest.version,
|
|
462
|
+
updatedAt: latestManifest.updatedAt,
|
|
463
|
+
projectId: currentProjectId,
|
|
464
|
+
sourceLanguage: currentManifest?.sourceLanguage ?? currentSourceLanguage ?? "en",
|
|
465
|
+
languages: Object.keys(latestManifest.urls),
|
|
466
|
+
cdnUrls: latestManifest.urls
|
|
467
|
+
});
|
|
468
|
+
} else console.log("[ciao-tools] Already up to date (version " + latestManifest.version + ")");
|
|
408
469
|
} catch (error) {
|
|
409
470
|
console.error("[ciao-tools] Hot update check failed:", error);
|
|
410
471
|
} finally {
|
|
411
472
|
isCheckingRef.current = false;
|
|
412
473
|
}
|
|
413
|
-
}, [
|
|
414
|
-
config,
|
|
415
|
-
projectId,
|
|
416
|
-
serverVersion,
|
|
417
|
-
setServerVersion,
|
|
418
|
-
addLanguage
|
|
419
|
-
]);
|
|
474
|
+
}, [setStoredManifest, addLanguage]);
|
|
420
475
|
(0, react.useEffect)(() => {
|
|
421
|
-
|
|
422
|
-
|
|
476
|
+
const currentConfig = configRef.current;
|
|
477
|
+
const currentProjectId = projectIdRef.current;
|
|
478
|
+
if (!currentConfig || !currentProjectId) return;
|
|
479
|
+
if (currentConfig.enabled !== true) return;
|
|
480
|
+
if (typeof document === "undefined") return;
|
|
423
481
|
if (!hasCheckedOnMountRef.current) {
|
|
424
482
|
hasCheckedOnMountRef.current = true;
|
|
425
483
|
checkForUpdates();
|
|
@@ -431,22 +489,133 @@ function useHotUpdates(config, projectId) {
|
|
|
431
489
|
return () => {
|
|
432
490
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
433
491
|
};
|
|
434
|
-
}, [
|
|
435
|
-
config,
|
|
436
|
-
projectId,
|
|
437
|
-
checkForUpdates
|
|
438
|
-
]);
|
|
492
|
+
}, [checkForUpdates]);
|
|
439
493
|
return { checkForUpdates };
|
|
440
494
|
}
|
|
441
495
|
|
|
442
496
|
//#endregion
|
|
443
|
-
//#region src/
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
497
|
+
//#region src/hooks/useManifest.ts
|
|
498
|
+
function getManifestUrlsHash(manifest) {
|
|
499
|
+
if (!manifest) return "";
|
|
500
|
+
return JSON.stringify(manifest.cdnUrls);
|
|
501
|
+
}
|
|
502
|
+
function useManifest({ manifest }) {
|
|
503
|
+
const storedManifest = useTranslationStore((state) => state.storedManifest);
|
|
504
|
+
const effectiveManifest = (0, react.useMemo)(() => {
|
|
505
|
+
if (storedManifest && storedManifest.projectId === manifest?.projectId) return {
|
|
506
|
+
version: String(storedManifest.serverVersion),
|
|
507
|
+
projectId: storedManifest.projectId,
|
|
508
|
+
sourceLanguage: storedManifest.sourceLanguage,
|
|
509
|
+
languages: storedManifest.languages,
|
|
510
|
+
cdnUrls: storedManifest.cdnUrls,
|
|
511
|
+
generatedAt: storedManifest.updatedAt
|
|
512
|
+
};
|
|
513
|
+
return manifest;
|
|
514
|
+
}, [storedManifest, manifest]);
|
|
515
|
+
const manifestRef = (0, react.useRef)(effectiveManifest);
|
|
516
|
+
const previousManifestHashRef = (0, react.useRef)("");
|
|
517
|
+
const loadedUrlsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
518
|
+
const hasManifestChangedRef = (0, react.useRef)(false);
|
|
519
|
+
(0, react.useEffect)(() => {
|
|
520
|
+
manifestRef.current = effectiveManifest;
|
|
521
|
+
const newHash = getManifestUrlsHash(effectiveManifest);
|
|
522
|
+
const oldHash = previousManifestHashRef.current;
|
|
523
|
+
if (oldHash && newHash && oldHash !== newHash) {
|
|
524
|
+
loadedUrlsRef.current.clear();
|
|
525
|
+
useTranslationStore.setState({ translations: {} });
|
|
526
|
+
hasManifestChangedRef.current = true;
|
|
527
|
+
} else hasManifestChangedRef.current = false;
|
|
528
|
+
previousManifestHashRef.current = newHash;
|
|
529
|
+
}, [effectiveManifest]);
|
|
530
|
+
return {
|
|
531
|
+
effectiveManifest,
|
|
532
|
+
manifestRef,
|
|
533
|
+
hasManifestChanged: hasManifestChangedRef.current,
|
|
534
|
+
loadedUrlsRef
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
//#endregion
|
|
539
|
+
//#region src/hooks/useTranslationLoader.ts
|
|
540
|
+
async function fetchTranslationsFromCDN(url, signal) {
|
|
541
|
+
const response = await fetch(url, { signal });
|
|
447
542
|
if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
|
|
448
543
|
return response.json();
|
|
449
544
|
}
|
|
545
|
+
function useTranslationLoader({ manifestRef, loadedUrlsRef, hotUpdatesEnabled }) {
|
|
546
|
+
const addLanguage = useTranslationStore((state) => state.addLanguage);
|
|
547
|
+
const setLoading = useTranslationStore((state) => state.setLoading);
|
|
548
|
+
const setReady = useTranslationStore((state) => state.setReady);
|
|
549
|
+
const abortControllerRef = (0, react.useRef)(null);
|
|
550
|
+
return {
|
|
551
|
+
loadLanguage: (0, react.useCallback)(async (language, isPreload = false) => {
|
|
552
|
+
const currentManifest = manifestRef.current;
|
|
553
|
+
if (!currentManifest) {
|
|
554
|
+
if (!isPreload) setReady(true);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
const cdnUrl = currentManifest.cdnUrls[language];
|
|
558
|
+
if (!cdnUrl) {
|
|
559
|
+
if (!isPreload) setReady(true);
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
if (loadedUrlsRef.current.get(language) === cdnUrl) {
|
|
563
|
+
if (!isPreload) setReady(true);
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
if (!isPreload) {
|
|
567
|
+
if (abortControllerRef.current) abortControllerRef.current.abort();
|
|
568
|
+
abortControllerRef.current = new AbortController();
|
|
569
|
+
}
|
|
570
|
+
const controller = isPreload ? void 0 : abortControllerRef.current;
|
|
571
|
+
if (!isPreload) {
|
|
572
|
+
setLoading(true);
|
|
573
|
+
setReady(false);
|
|
574
|
+
}
|
|
575
|
+
const useCache = hotUpdatesEnabled !== true;
|
|
576
|
+
try {
|
|
577
|
+
if (useCache) {
|
|
578
|
+
const cached = await getCachedTranslation(cdnUrl);
|
|
579
|
+
if (cached) {
|
|
580
|
+
if (controller?.signal.aborted) return false;
|
|
581
|
+
addLanguage(language, cached);
|
|
582
|
+
loadedUrlsRef.current.set(language, cdnUrl);
|
|
583
|
+
if (!isPreload) {
|
|
584
|
+
setReady(true);
|
|
585
|
+
setLoading(false);
|
|
586
|
+
}
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
const translationData = await fetchTranslationsFromCDN(cdnUrl, controller?.signal);
|
|
591
|
+
if (controller?.signal.aborted) return false;
|
|
592
|
+
addLanguage(language, translationData);
|
|
593
|
+
loadedUrlsRef.current.set(language, cdnUrl);
|
|
594
|
+
if (useCache) await cacheTranslation(cdnUrl, language, currentManifest.projectId, translationData);
|
|
595
|
+
if (!isPreload) setReady(true);
|
|
596
|
+
return true;
|
|
597
|
+
} catch (error) {
|
|
598
|
+
if (error instanceof Error && error.name === "AbortError") return false;
|
|
599
|
+
console.error(`[ciao-tools] Failed to load translations for ${language}:`, error);
|
|
600
|
+
if (!isPreload) setReady(true);
|
|
601
|
+
return false;
|
|
602
|
+
} finally {
|
|
603
|
+
if (!isPreload) setLoading(false);
|
|
604
|
+
}
|
|
605
|
+
}, [
|
|
606
|
+
manifestRef,
|
|
607
|
+
loadedUrlsRef,
|
|
608
|
+
hotUpdatesEnabled,
|
|
609
|
+
addLanguage,
|
|
610
|
+
setLoading,
|
|
611
|
+
setReady
|
|
612
|
+
]),
|
|
613
|
+
abortControllerRef
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/utils/browserLanguage.ts
|
|
450
619
|
function detectBrowserLanguage(availableLanguages) {
|
|
451
620
|
if (typeof navigator === "undefined") return null;
|
|
452
621
|
const browserLangs = navigator.languages || [navigator.language];
|
|
@@ -458,43 +627,19 @@ function detectBrowserLanguage(availableLanguages) {
|
|
|
458
627
|
}
|
|
459
628
|
return null;
|
|
460
629
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
const manifestRef = (0, react.useRef)(manifest);
|
|
468
|
-
const loadedUrlsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
469
|
-
const previousManifestHashRef = (0, react.useRef)("");
|
|
630
|
+
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/hooks/useLanguageInit.ts
|
|
633
|
+
function useLanguageInit({ effectiveManifest, availableLanguages, defaultLanguage, detectLanguage }) {
|
|
634
|
+
const isHydrated = useTranslationStore((state) => state.isHydrated);
|
|
635
|
+
const setLanguage = useTranslationStore((state) => state.setLanguage);
|
|
470
636
|
const initializedRef = (0, react.useRef)(false);
|
|
471
|
-
const preloadTimeoutRef = (0, react.useRef)(null);
|
|
472
|
-
(0, react.useEffect)(() => {
|
|
473
|
-
manifestRef.current = manifest;
|
|
474
|
-
const newHash = getManifestUrlsHash(manifest);
|
|
475
|
-
const oldHash = previousManifestHashRef.current;
|
|
476
|
-
if (oldHash && newHash && oldHash !== newHash) {
|
|
477
|
-
loadedUrlsRef.current.clear();
|
|
478
|
-
useTranslationStore.setState({ translations: {} });
|
|
479
|
-
}
|
|
480
|
-
previousManifestHashRef.current = newHash;
|
|
481
|
-
}, [manifest]);
|
|
482
|
-
(0, react.useEffect)(() => {
|
|
483
|
-
if (translations) {
|
|
484
|
-
loadTranslations(translations);
|
|
485
|
-
setReady(true);
|
|
486
|
-
}
|
|
487
|
-
}, [
|
|
488
|
-
translations,
|
|
489
|
-
loadTranslations,
|
|
490
|
-
setReady
|
|
491
|
-
]);
|
|
492
637
|
(0, react.useEffect)(() => {
|
|
493
638
|
if (!isHydrated) return;
|
|
494
639
|
if (initializedRef.current) return;
|
|
495
640
|
initializedRef.current = true;
|
|
496
641
|
const store = useTranslationStore.getState();
|
|
497
|
-
const effectiveLanguages =
|
|
642
|
+
const effectiveLanguages = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages || [];
|
|
498
643
|
if (store.currentLanguage && store.currentLanguage !== "en" && effectiveLanguages.includes(store.currentLanguage)) return;
|
|
499
644
|
if (detectLanguage && effectiveLanguages.length > 0) {
|
|
500
645
|
const detected = detectBrowserLanguage(effectiveLanguages);
|
|
@@ -505,18 +650,95 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
|
|
|
505
650
|
}
|
|
506
651
|
if (defaultLanguage && defaultLanguage !== store.currentLanguage) setLanguage(defaultLanguage);
|
|
507
652
|
}, [
|
|
508
|
-
|
|
653
|
+
effectiveManifest,
|
|
509
654
|
availableLanguages,
|
|
510
655
|
defaultLanguage,
|
|
511
656
|
detectLanguage,
|
|
512
657
|
setLanguage,
|
|
513
658
|
isHydrated
|
|
514
659
|
]);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
663
|
+
//#region src/hooks/usePreloader.ts
|
|
664
|
+
function usePreloader({ effectiveManifest, currentLanguage, delay, enabled, loadLanguage }) {
|
|
665
|
+
const preloadTimeoutRef = (0, react.useRef)(null);
|
|
666
|
+
const isMountedRef = (0, react.useRef)(true);
|
|
667
|
+
(0, react.useEffect)(() => {
|
|
668
|
+
isMountedRef.current = true;
|
|
669
|
+
return () => {
|
|
670
|
+
isMountedRef.current = false;
|
|
671
|
+
};
|
|
672
|
+
}, []);
|
|
673
|
+
(0, react.useEffect)(() => {
|
|
674
|
+
if (!enabled || !effectiveManifest) return;
|
|
675
|
+
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
676
|
+
preloadTimeoutRef.current = setTimeout(async () => {
|
|
677
|
+
if (!isMountedRef.current) return;
|
|
678
|
+
const languages = [...effectiveManifest.languages];
|
|
679
|
+
for (const language of languages) {
|
|
680
|
+
if (!isMountedRef.current) break;
|
|
681
|
+
if (language === effectiveManifest.sourceLanguage) continue;
|
|
682
|
+
if (language === currentLanguage) continue;
|
|
683
|
+
await loadLanguage(language, true);
|
|
684
|
+
}
|
|
685
|
+
}, delay);
|
|
686
|
+
return () => {
|
|
687
|
+
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
688
|
+
};
|
|
689
|
+
}, [
|
|
690
|
+
effectiveManifest,
|
|
691
|
+
currentLanguage,
|
|
692
|
+
enabled,
|
|
693
|
+
delay,
|
|
694
|
+
loadLanguage
|
|
695
|
+
]);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
//#endregion
|
|
699
|
+
//#region src/components/CiaoProvider.tsx
|
|
700
|
+
const PRELOAD_DELAY_MS = 5e3;
|
|
701
|
+
function CiaoProvider({ children, translations, manifest, defaultLanguage = "en", availableLanguages, onLanguageChange, detectLanguage = true, blockUntilReady = true, fallback = null, preloadLanguages = true, preloadDelay = PRELOAD_DELAY_MS, hotUpdates }) {
|
|
702
|
+
const loadTranslations = useTranslationStore((state) => state.loadTranslations);
|
|
703
|
+
const setReady = useTranslationStore((state) => state.setReady);
|
|
704
|
+
const currentLanguage = useTranslationStore((state) => state.currentLanguage);
|
|
705
|
+
const isReady = useTranslationStore((state) => state.isReady);
|
|
706
|
+
const isHydrated = useTranslationStore((state) => state.isHydrated);
|
|
707
|
+
const { effectiveManifest, manifestRef, loadedUrlsRef } = useManifest({ manifest });
|
|
708
|
+
const { loadLanguage, abortControllerRef } = useTranslationLoader({
|
|
709
|
+
manifestRef,
|
|
710
|
+
loadedUrlsRef,
|
|
711
|
+
hotUpdatesEnabled: hotUpdates?.enabled
|
|
712
|
+
});
|
|
713
|
+
useLanguageInit({
|
|
714
|
+
effectiveManifest,
|
|
715
|
+
availableLanguages,
|
|
716
|
+
defaultLanguage,
|
|
717
|
+
detectLanguage
|
|
718
|
+
});
|
|
719
|
+
usePreloader({
|
|
720
|
+
effectiveManifest,
|
|
721
|
+
currentLanguage,
|
|
722
|
+
delay: preloadDelay,
|
|
723
|
+
enabled: preloadLanguages,
|
|
724
|
+
loadLanguage
|
|
725
|
+
});
|
|
726
|
+
useHotUpdates(hotUpdates, manifest?.projectId, manifest?.sourceLanguage);
|
|
727
|
+
(0, react.useEffect)(() => {
|
|
728
|
+
if (translations) {
|
|
729
|
+
loadTranslations(translations);
|
|
730
|
+
setReady(true);
|
|
731
|
+
}
|
|
732
|
+
}, [
|
|
733
|
+
translations,
|
|
734
|
+
loadTranslations,
|
|
735
|
+
setReady
|
|
736
|
+
]);
|
|
515
737
|
(0, react.useEffect)(() => {
|
|
516
|
-
const
|
|
517
|
-
if (
|
|
738
|
+
const languagesList = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages;
|
|
739
|
+
if (languagesList) {
|
|
518
740
|
const store = useTranslationStore.getState();
|
|
519
|
-
const merged = [...new Set([...store.availableLanguages, ...
|
|
741
|
+
const merged = [...new Set([...store.availableLanguages, ...languagesList])];
|
|
520
742
|
useTranslationStore.setState({
|
|
521
743
|
availableLanguages: merged,
|
|
522
744
|
defaultLanguage
|
|
@@ -524,97 +746,32 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
|
|
|
524
746
|
}
|
|
525
747
|
}, [
|
|
526
748
|
availableLanguages,
|
|
527
|
-
|
|
749
|
+
effectiveManifest,
|
|
528
750
|
defaultLanguage
|
|
529
751
|
]);
|
|
530
|
-
const loadLanguageFromCDN = (0, react.useCallback)(async (language, isPreload = false) => {
|
|
531
|
-
const currentManifest = manifestRef.current;
|
|
532
|
-
if (!currentManifest) {
|
|
533
|
-
if (!isPreload) setReady(true);
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
const cdnUrl = currentManifest.cdnUrls[language];
|
|
537
|
-
if (!cdnUrl) {
|
|
538
|
-
if (!isPreload) setReady(true);
|
|
539
|
-
return false;
|
|
540
|
-
}
|
|
541
|
-
if (loadedUrlsRef.current.get(language) === cdnUrl) {
|
|
542
|
-
if (!isPreload) setReady(true);
|
|
543
|
-
return true;
|
|
544
|
-
}
|
|
545
|
-
if (!isPreload) {
|
|
546
|
-
setLoading(true);
|
|
547
|
-
setReady(false);
|
|
548
|
-
}
|
|
549
|
-
try {
|
|
550
|
-
const cached = await getCachedTranslation(cdnUrl);
|
|
551
|
-
if (cached) {
|
|
552
|
-
addLanguage(language, cached);
|
|
553
|
-
loadedUrlsRef.current.set(language, cdnUrl);
|
|
554
|
-
if (!isPreload) {
|
|
555
|
-
setReady(true);
|
|
556
|
-
setLoading(false);
|
|
557
|
-
}
|
|
558
|
-
return true;
|
|
559
|
-
}
|
|
560
|
-
const translationData = await fetchTranslationsFromCDN(cdnUrl);
|
|
561
|
-
addLanguage(language, translationData);
|
|
562
|
-
loadedUrlsRef.current.set(language, cdnUrl);
|
|
563
|
-
await cacheTranslation(cdnUrl, language, currentManifest.projectId, translationData);
|
|
564
|
-
if (!isPreload) setReady(true);
|
|
565
|
-
return true;
|
|
566
|
-
} catch (error) {
|
|
567
|
-
console.error(`[ciao-tools] Failed to load translations for ${language}:`, error);
|
|
568
|
-
if (!isPreload) setReady(true);
|
|
569
|
-
return false;
|
|
570
|
-
} finally {
|
|
571
|
-
if (!isPreload) setLoading(false);
|
|
572
|
-
}
|
|
573
|
-
}, [
|
|
574
|
-
addLanguage,
|
|
575
|
-
setLoading,
|
|
576
|
-
setReady
|
|
577
|
-
]);
|
|
578
752
|
(0, react.useEffect)(() => {
|
|
579
753
|
if (!isHydrated) return;
|
|
580
|
-
if (currentLanguage ===
|
|
754
|
+
if (currentLanguage === effectiveManifest?.sourceLanguage) {
|
|
581
755
|
setReady(true);
|
|
582
756
|
return;
|
|
583
757
|
}
|
|
584
|
-
if (
|
|
585
|
-
else if (!
|
|
758
|
+
if (effectiveManifest && currentLanguage) loadLanguage(currentLanguage, false);
|
|
759
|
+
else if (!effectiveManifest) setReady(true);
|
|
586
760
|
}, [
|
|
587
|
-
|
|
761
|
+
effectiveManifest,
|
|
588
762
|
currentLanguage,
|
|
589
|
-
|
|
763
|
+
loadLanguage,
|
|
590
764
|
setReady,
|
|
591
765
|
isHydrated
|
|
592
766
|
]);
|
|
593
|
-
(0, react.useEffect)(() => {
|
|
594
|
-
if (!preloadLanguages || !manifest) return;
|
|
595
|
-
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
596
|
-
preloadTimeoutRef.current = setTimeout(async () => {
|
|
597
|
-
const languages = [...manifest.languages];
|
|
598
|
-
for (const language of languages) {
|
|
599
|
-
if (language === manifest.sourceLanguage) continue;
|
|
600
|
-
if (language === currentLanguage) continue;
|
|
601
|
-
await loadLanguageFromCDN(language, true);
|
|
602
|
-
}
|
|
603
|
-
}, preloadDelay);
|
|
604
|
-
return () => {
|
|
605
|
-
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
606
|
-
};
|
|
607
|
-
}, [
|
|
608
|
-
manifest,
|
|
609
|
-
currentLanguage,
|
|
610
|
-
preloadLanguages,
|
|
611
|
-
preloadDelay,
|
|
612
|
-
loadLanguageFromCDN
|
|
613
|
-
]);
|
|
614
767
|
(0, react.useEffect)(() => {
|
|
615
768
|
if (onLanguageChange) onLanguageChange(currentLanguage);
|
|
616
769
|
}, [currentLanguage, onLanguageChange]);
|
|
617
|
-
|
|
770
|
+
(0, react.useEffect)(() => {
|
|
771
|
+
return () => {
|
|
772
|
+
abortControllerRef.current?.abort();
|
|
773
|
+
};
|
|
774
|
+
}, [abortControllerRef]);
|
|
618
775
|
if (blockUntilReady && (!isReady || !isHydrated)) return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, fallback);
|
|
619
776
|
return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, children);
|
|
620
777
|
}
|
|
@@ -906,6 +1063,7 @@ function useCt() {
|
|
|
906
1063
|
|
|
907
1064
|
//#endregion
|
|
908
1065
|
//#region src/components/Trans.tsx
|
|
1066
|
+
const TAG_REGEX = /<(\d+)>(.*?)<\/\1>/gs;
|
|
909
1067
|
function parseChildren(children) {
|
|
910
1068
|
const elements = [];
|
|
911
1069
|
let template = "";
|
|
@@ -931,11 +1089,10 @@ function reconstructChildren(translated, elements) {
|
|
|
931
1089
|
if (elements.length === 0) return translated;
|
|
932
1090
|
const result = [];
|
|
933
1091
|
let keyCounter = 0;
|
|
934
|
-
const tagRegex = /<(\d+)>(.*?)<\/\1>/gs;
|
|
935
1092
|
let lastIndex = 0;
|
|
936
1093
|
let match;
|
|
937
|
-
|
|
938
|
-
while ((match =
|
|
1094
|
+
TAG_REGEX.lastIndex = 0;
|
|
1095
|
+
while ((match = TAG_REGEX.exec(translated)) !== null) {
|
|
939
1096
|
if (match.index > lastIndex) {
|
|
940
1097
|
const textBefore = translated.slice(lastIndex, match.index);
|
|
941
1098
|
if (textBefore) result.push(textBefore);
|
|
@@ -1005,6 +1162,11 @@ function useLanguage() {
|
|
|
1005
1162
|
availableLanguagesInfo: availableLanguages.map(getFullLanguageInfo),
|
|
1006
1163
|
setLanguage,
|
|
1007
1164
|
cycleLanguage: (0, react.useCallback)(() => {
|
|
1165
|
+
if (availableLanguages.length === 0) {
|
|
1166
|
+
console.warn("[ciao-tools] Cannot cycle language: no languages available");
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
if (availableLanguages.length === 1) return;
|
|
1008
1170
|
setLanguage(availableLanguages[(availableLanguages.indexOf(currentLanguage) + 1) % availableLanguages.length]);
|
|
1009
1171
|
}, [
|
|
1010
1172
|
availableLanguages,
|
|
@@ -1018,7 +1180,8 @@ function useLanguage() {
|
|
|
1018
1180
|
|
|
1019
1181
|
//#endregion
|
|
1020
1182
|
exports.CTContextBlock = CTContextBlock;
|
|
1021
|
-
exports.CTProvider =
|
|
1183
|
+
exports.CTProvider = CiaoProvider;
|
|
1184
|
+
exports.CiaoProvider = CiaoProvider;
|
|
1022
1185
|
exports.LANGUAGE_DATA = LANGUAGE_DATA;
|
|
1023
1186
|
exports.LanguageSwitcher = LanguageSwitcher;
|
|
1024
1187
|
exports.Trans = Trans;
|