experimental-ciao-react 1.1.10 → 1.1.12
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.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { cloneElement, isValidElement, useCallback, useEffect, useRef } from "react";
|
|
1
|
+
import React, { cloneElement, isValidElement, useCallback, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { create } from "zustand";
|
|
3
3
|
import { persist } from "zustand/middleware";
|
|
4
4
|
|
|
@@ -8,127 +8,32 @@ function CTContextBlock({ children }) {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
//#endregion
|
|
11
|
-
//#region src/
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
24
|
-
request.onerror = () => reject(request.error);
|
|
25
|
-
request.onsuccess = () => resolve(request.result);
|
|
26
|
-
request.onupgradeneeded = (event) => {
|
|
27
|
-
const db = event.target.result;
|
|
28
|
-
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
29
|
-
const store = db.createObjectStore(STORE_NAME, { keyPath: "url" });
|
|
30
|
-
store.createIndex("language", "language", { unique: false });
|
|
31
|
-
store.createIndex("projectId", "projectId", { unique: false });
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
return dbPromise;
|
|
36
|
-
}
|
|
37
|
-
async function getCachedTranslation(url) {
|
|
38
|
-
try {
|
|
39
|
-
const db = await openDB();
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
const request = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(url);
|
|
42
|
-
request.onerror = () => reject(request.error);
|
|
43
|
-
request.onsuccess = () => {
|
|
44
|
-
const entry = request.result;
|
|
45
|
-
resolve(entry?.data ?? null);
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
} catch {
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async function cacheTranslation(url, language, projectId, data) {
|
|
53
|
-
try {
|
|
54
|
-
const db = await openDB();
|
|
55
|
-
return new Promise((resolve, reject) => {
|
|
56
|
-
const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
|
|
57
|
-
const entry = {
|
|
58
|
-
url,
|
|
59
|
-
language,
|
|
60
|
-
projectId,
|
|
61
|
-
data,
|
|
62
|
-
cachedAt: Date.now()
|
|
63
|
-
};
|
|
64
|
-
const request = store.put(entry);
|
|
65
|
-
request.onerror = () => reject(request.error);
|
|
66
|
-
request.onsuccess = () => resolve();
|
|
67
|
-
});
|
|
68
|
-
} catch {}
|
|
69
|
-
}
|
|
70
|
-
async function clearCache(projectId) {
|
|
71
|
-
try {
|
|
72
|
-
const db = await openDB();
|
|
73
|
-
return new Promise((resolve, reject) => {
|
|
74
|
-
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
75
|
-
const store = transaction.objectStore(STORE_NAME);
|
|
76
|
-
if (projectId) {
|
|
77
|
-
const request = store.index("projectId").openCursor(IDBKeyRange.only(projectId));
|
|
78
|
-
request.onsuccess = (event) => {
|
|
79
|
-
const cursor = event.target.result;
|
|
80
|
-
if (cursor) {
|
|
81
|
-
cursor.delete();
|
|
82
|
-
cursor.continue();
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
transaction.oncomplete = () => resolve();
|
|
86
|
-
transaction.onerror = () => reject(transaction.error);
|
|
87
|
-
} else {
|
|
88
|
-
const request = store.clear();
|
|
89
|
-
request.onerror = () => reject(request.error);
|
|
90
|
-
request.onsuccess = () => resolve();
|
|
11
|
+
//#region src/interpolate.ts
|
|
12
|
+
const MAX_CACHE_SIZE = 100;
|
|
13
|
+
function createBoundedCache(maxSize) {
|
|
14
|
+
const cache = /* @__PURE__ */ new Map();
|
|
15
|
+
return {
|
|
16
|
+
get(key) {
|
|
17
|
+
return cache.get(key);
|
|
18
|
+
},
|
|
19
|
+
set(key, value) {
|
|
20
|
+
if (cache.size >= maxSize) {
|
|
21
|
+
const firstKey = cache.keys().next().value;
|
|
22
|
+
if (firstKey !== void 0) cache.delete(firstKey);
|
|
91
23
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const countRequest = store.count();
|
|
102
|
-
const languages = /* @__PURE__ */ new Set();
|
|
103
|
-
const cursorRequest = store.openCursor();
|
|
104
|
-
cursorRequest.onsuccess = (event) => {
|
|
105
|
-
const cursor = event.target.result;
|
|
106
|
-
if (cursor) {
|
|
107
|
-
languages.add(cursor.value.language);
|
|
108
|
-
cursor.continue();
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
transaction.oncomplete = () => {
|
|
112
|
-
resolve({
|
|
113
|
-
count: countRequest.result,
|
|
114
|
-
languages: Array.from(languages)
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
transaction.onerror = () => reject(transaction.error);
|
|
118
|
-
});
|
|
119
|
-
} catch {
|
|
120
|
-
return {
|
|
121
|
-
count: 0,
|
|
122
|
-
languages: []
|
|
123
|
-
};
|
|
124
|
-
}
|
|
24
|
+
cache.set(key, value);
|
|
25
|
+
},
|
|
26
|
+
has(key) {
|
|
27
|
+
return cache.has(key);
|
|
28
|
+
},
|
|
29
|
+
clear() {
|
|
30
|
+
cache.clear();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
125
33
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const numberFormatCache = /* @__PURE__ */ new Map();
|
|
130
|
-
const dateFormatCache = /* @__PURE__ */ new Map();
|
|
131
|
-
const pluralRulesCache = /* @__PURE__ */ new Map();
|
|
34
|
+
const numberFormatCache = createBoundedCache(MAX_CACHE_SIZE);
|
|
35
|
+
const dateFormatCache = createBoundedCache(MAX_CACHE_SIZE);
|
|
36
|
+
const pluralRulesCache = createBoundedCache(MAX_CACHE_SIZE);
|
|
132
37
|
function clearFormatterCache() {
|
|
133
38
|
numberFormatCache.clear();
|
|
134
39
|
dateFormatCache.clear();
|
|
@@ -251,7 +156,7 @@ const useTranslationStore = create()(persist((set) => ({
|
|
|
251
156
|
isLoading: false,
|
|
252
157
|
isReady: false,
|
|
253
158
|
isHydrated: false,
|
|
254
|
-
|
|
159
|
+
storedManifest: null,
|
|
255
160
|
lastVersionCheck: null,
|
|
256
161
|
setLanguage: (language) => {
|
|
257
162
|
set({
|
|
@@ -292,9 +197,9 @@ const useTranslationStore = create()(persist((set) => ({
|
|
|
292
197
|
setReady: (ready) => {
|
|
293
198
|
set({ isReady: ready });
|
|
294
199
|
},
|
|
295
|
-
|
|
200
|
+
setStoredManifest: (manifest) => {
|
|
296
201
|
set({
|
|
297
|
-
|
|
202
|
+
storedManifest: manifest,
|
|
298
203
|
lastVersionCheck: Date.now()
|
|
299
204
|
});
|
|
300
205
|
}
|
|
@@ -302,10 +207,14 @@ const useTranslationStore = create()(persist((set) => ({
|
|
|
302
207
|
name: "ciao-tools-language",
|
|
303
208
|
partialize: (state) => ({
|
|
304
209
|
currentLanguage: state.currentLanguage,
|
|
305
|
-
|
|
210
|
+
storedManifest: state.storedManifest
|
|
306
211
|
}),
|
|
307
212
|
onRehydrateStorage: () => (_, error) => {
|
|
308
|
-
if (
|
|
213
|
+
if (error) console.error("[ciao-tools] Storage hydration failed:", error);
|
|
214
|
+
if (hydrationResolver) {
|
|
215
|
+
hydrationResolver();
|
|
216
|
+
hydrationResolver = null;
|
|
217
|
+
}
|
|
309
218
|
}
|
|
310
219
|
}));
|
|
311
220
|
hydrationPromise.then(() => {
|
|
@@ -317,6 +226,133 @@ function getTranslation(translations, language, text, values) {
|
|
|
317
226
|
return interpolate(translated, values, language);
|
|
318
227
|
}
|
|
319
228
|
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/cache.ts
|
|
231
|
+
const DB_NAME = "ciao-tools-translations";
|
|
232
|
+
const DB_VERSION = 1;
|
|
233
|
+
const STORE_NAME = "translations";
|
|
234
|
+
let dbPromise = null;
|
|
235
|
+
function openDB() {
|
|
236
|
+
if (dbPromise) return dbPromise;
|
|
237
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
238
|
+
if (typeof indexedDB === "undefined") {
|
|
239
|
+
dbPromise = null;
|
|
240
|
+
reject(/* @__PURE__ */ new Error("IndexedDB not available"));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
244
|
+
request.onerror = () => {
|
|
245
|
+
dbPromise = null;
|
|
246
|
+
reject(request.error);
|
|
247
|
+
};
|
|
248
|
+
request.onsuccess = () => resolve(request.result);
|
|
249
|
+
request.onupgradeneeded = (event) => {
|
|
250
|
+
const db = event.target.result;
|
|
251
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
252
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "url" });
|
|
253
|
+
store.createIndex("language", "language", { unique: false });
|
|
254
|
+
store.createIndex("projectId", "projectId", { unique: false });
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
});
|
|
258
|
+
return dbPromise;
|
|
259
|
+
}
|
|
260
|
+
async function getCachedTranslation(url) {
|
|
261
|
+
try {
|
|
262
|
+
const db = await openDB();
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
const request = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(url);
|
|
265
|
+
request.onerror = () => reject(request.error);
|
|
266
|
+
request.onsuccess = () => {
|
|
267
|
+
const entry = request.result;
|
|
268
|
+
resolve(entry?.data ?? null);
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.warn("[ciao-tools] Failed to read from cache:", error);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
async function cacheTranslation(url, language, projectId, data) {
|
|
277
|
+
try {
|
|
278
|
+
const db = await openDB();
|
|
279
|
+
return new Promise((resolve, reject) => {
|
|
280
|
+
const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
|
|
281
|
+
const entry = {
|
|
282
|
+
url,
|
|
283
|
+
language,
|
|
284
|
+
projectId,
|
|
285
|
+
data,
|
|
286
|
+
cachedAt: Date.now()
|
|
287
|
+
};
|
|
288
|
+
const request = store.put(entry);
|
|
289
|
+
request.onerror = () => reject(request.error);
|
|
290
|
+
request.onsuccess = () => resolve();
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.warn("[ciao-tools] Failed to write to cache:", error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function clearCache(projectId) {
|
|
297
|
+
try {
|
|
298
|
+
const db = await openDB();
|
|
299
|
+
return new Promise((resolve, reject) => {
|
|
300
|
+
const transaction = db.transaction(STORE_NAME, "readwrite");
|
|
301
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
302
|
+
if (projectId) {
|
|
303
|
+
const request = store.index("projectId").openCursor(IDBKeyRange.only(projectId));
|
|
304
|
+
request.onsuccess = (event) => {
|
|
305
|
+
const cursor = event.target.result;
|
|
306
|
+
if (cursor) {
|
|
307
|
+
cursor.delete();
|
|
308
|
+
cursor.continue();
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
transaction.oncomplete = () => resolve();
|
|
312
|
+
transaction.onerror = () => reject(transaction.error);
|
|
313
|
+
} else {
|
|
314
|
+
const request = store.clear();
|
|
315
|
+
request.onerror = () => reject(request.error);
|
|
316
|
+
request.onsuccess = () => resolve();
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
} catch (error) {
|
|
320
|
+
console.warn("[ciao-tools] Failed to clear cache:", error);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function getCacheStats() {
|
|
324
|
+
try {
|
|
325
|
+
const db = await openDB();
|
|
326
|
+
return new Promise((resolve, reject) => {
|
|
327
|
+
const transaction = db.transaction(STORE_NAME, "readonly");
|
|
328
|
+
const store = transaction.objectStore(STORE_NAME);
|
|
329
|
+
const countRequest = store.count();
|
|
330
|
+
const languages = /* @__PURE__ */ new Set();
|
|
331
|
+
const cursorRequest = store.openCursor();
|
|
332
|
+
cursorRequest.onsuccess = (event) => {
|
|
333
|
+
const cursor = event.target.result;
|
|
334
|
+
if (cursor) {
|
|
335
|
+
languages.add(cursor.value.language);
|
|
336
|
+
cursor.continue();
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
transaction.oncomplete = () => {
|
|
340
|
+
resolve({
|
|
341
|
+
count: countRequest.result,
|
|
342
|
+
languages: Array.from(languages)
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
transaction.onerror = () => reject(transaction.error);
|
|
346
|
+
});
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.warn("[ciao-tools] Failed to get cache stats:", error);
|
|
349
|
+
return {
|
|
350
|
+
count: 0,
|
|
351
|
+
languages: []
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
320
356
|
//#endregion
|
|
321
357
|
//#region src/hooks/useHotUpdates.ts
|
|
322
358
|
const CDN_BASE_URL = "https://t1.ciao-tools.com";
|
|
@@ -329,7 +365,8 @@ async function fetchLatestManifest(projectId) {
|
|
|
329
365
|
throw new Error(`Failed to fetch latest manifest: ${response.statusText}`);
|
|
330
366
|
}
|
|
331
367
|
return response.json();
|
|
332
|
-
} catch {
|
|
368
|
+
} catch (error) {
|
|
369
|
+
console.warn("[ciao-tools] Failed to fetch latest manifest:", error);
|
|
333
370
|
return null;
|
|
334
371
|
}
|
|
335
372
|
}
|
|
@@ -338,60 +375,81 @@ async function fetchTranslations(url) {
|
|
|
338
375
|
if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
|
|
339
376
|
return response.json();
|
|
340
377
|
}
|
|
341
|
-
function useHotUpdates(config, projectId) {
|
|
378
|
+
function useHotUpdates(config, projectId, sourceLanguage) {
|
|
342
379
|
const isCheckingRef = useRef(false);
|
|
343
380
|
const hasCheckedOnMountRef = useRef(false);
|
|
344
|
-
const
|
|
381
|
+
const configRef = useRef(config);
|
|
382
|
+
const projectIdRef = useRef(projectId);
|
|
383
|
+
const sourceLanguageRef = useRef(sourceLanguage);
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
configRef.current = config;
|
|
386
|
+
projectIdRef.current = projectId;
|
|
387
|
+
sourceLanguageRef.current = sourceLanguage;
|
|
388
|
+
}, [
|
|
389
|
+
config,
|
|
390
|
+
projectId,
|
|
391
|
+
sourceLanguage
|
|
392
|
+
]);
|
|
393
|
+
const { setStoredManifest, addLanguage } = useTranslationStore();
|
|
345
394
|
const checkForUpdates = useCallback(async () => {
|
|
346
|
-
|
|
347
|
-
|
|
395
|
+
const currentConfig = configRef.current;
|
|
396
|
+
const currentProjectId = projectIdRef.current;
|
|
397
|
+
const currentSourceLanguage = sourceLanguageRef.current;
|
|
398
|
+
if (!currentConfig || !currentProjectId || isCheckingRef.current) return;
|
|
399
|
+
if (currentConfig.enabled !== true) return;
|
|
348
400
|
isCheckingRef.current = true;
|
|
349
401
|
try {
|
|
350
402
|
console.log("[ciao-tools] Checking for hot updates...");
|
|
351
|
-
const
|
|
352
|
-
if (!
|
|
403
|
+
const latestManifest = await fetchLatestManifest(currentProjectId);
|
|
404
|
+
if (!latestManifest) {
|
|
353
405
|
console.log("[ciao-tools] No latest.json found (project may not have hot updates yet)");
|
|
354
406
|
return;
|
|
355
407
|
}
|
|
356
|
-
console.log("[ciao-tools] Found latest.json, version:",
|
|
357
|
-
const
|
|
358
|
-
const
|
|
408
|
+
console.log("[ciao-tools] Found latest.json, version:", latestManifest.version);
|
|
409
|
+
const currentManifest = useTranslationStore.getState().storedManifest;
|
|
410
|
+
const currentVersion = currentManifest?.serverVersion ?? null;
|
|
411
|
+
const isFirstCheck = currentVersion === null;
|
|
412
|
+
const hasNewVersion = currentVersion !== null && latestManifest.version > currentVersion;
|
|
359
413
|
if (isFirstCheck || hasNewVersion) {
|
|
360
|
-
if (Object.keys(
|
|
414
|
+
if (Object.keys(latestManifest.urls).length > 0) {
|
|
361
415
|
const reason = hasNewVersion ? "new version" : "first check";
|
|
362
416
|
console.log(`[ciao-tools] Fetching translations (${reason})...`);
|
|
363
|
-
if (hasNewVersion) await clearCache(
|
|
417
|
+
if (hasNewVersion) await clearCache(currentProjectId);
|
|
364
418
|
const updatedLanguages = [];
|
|
365
|
-
for (const [langCode, url] of Object.entries(
|
|
419
|
+
for (const [langCode, url] of Object.entries(latestManifest.urls)) try {
|
|
366
420
|
const translations = await fetchTranslations(url);
|
|
367
421
|
addLanguage(langCode, translations);
|
|
368
|
-
await cacheTranslation(url, langCode,
|
|
422
|
+
await cacheTranslation(url, langCode, currentProjectId, translations);
|
|
369
423
|
updatedLanguages.push(langCode);
|
|
370
424
|
} catch (err) {
|
|
371
425
|
console.error(`[ciao-tools] Failed to fetch ${langCode} translations:`, err);
|
|
372
426
|
}
|
|
373
427
|
if (updatedLanguages.length > 0) {
|
|
374
428
|
console.log("[ciao-tools] Updated translations for:", updatedLanguages);
|
|
375
|
-
if (hasNewVersion &&
|
|
429
|
+
if (hasNewVersion && currentConfig.onTranslationsUpdated) currentConfig.onTranslationsUpdated(updatedLanguages);
|
|
376
430
|
}
|
|
377
431
|
}
|
|
378
|
-
|
|
379
|
-
|
|
432
|
+
setStoredManifest({
|
|
433
|
+
serverVersion: latestManifest.version,
|
|
434
|
+
updatedAt: latestManifest.updatedAt,
|
|
435
|
+
projectId: currentProjectId,
|
|
436
|
+
sourceLanguage: currentManifest?.sourceLanguage ?? currentSourceLanguage ?? "en",
|
|
437
|
+
languages: Object.keys(latestManifest.urls),
|
|
438
|
+
cdnUrls: latestManifest.urls
|
|
439
|
+
});
|
|
440
|
+
} else console.log("[ciao-tools] Already up to date (version " + latestManifest.version + ")");
|
|
380
441
|
} catch (error) {
|
|
381
442
|
console.error("[ciao-tools] Hot update check failed:", error);
|
|
382
443
|
} finally {
|
|
383
444
|
isCheckingRef.current = false;
|
|
384
445
|
}
|
|
385
|
-
}, [
|
|
386
|
-
config,
|
|
387
|
-
projectId,
|
|
388
|
-
serverVersion,
|
|
389
|
-
setServerVersion,
|
|
390
|
-
addLanguage
|
|
391
|
-
]);
|
|
446
|
+
}, [setStoredManifest, addLanguage]);
|
|
392
447
|
useEffect(() => {
|
|
393
|
-
|
|
394
|
-
|
|
448
|
+
const currentConfig = configRef.current;
|
|
449
|
+
const currentProjectId = projectIdRef.current;
|
|
450
|
+
if (!currentConfig || !currentProjectId) return;
|
|
451
|
+
if (currentConfig.enabled !== true) return;
|
|
452
|
+
if (typeof document === "undefined") return;
|
|
395
453
|
if (!hasCheckedOnMountRef.current) {
|
|
396
454
|
hasCheckedOnMountRef.current = true;
|
|
397
455
|
checkForUpdates();
|
|
@@ -403,22 +461,133 @@ function useHotUpdates(config, projectId) {
|
|
|
403
461
|
return () => {
|
|
404
462
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
405
463
|
};
|
|
406
|
-
}, [
|
|
407
|
-
config,
|
|
408
|
-
projectId,
|
|
409
|
-
checkForUpdates
|
|
410
|
-
]);
|
|
464
|
+
}, [checkForUpdates]);
|
|
411
465
|
return { checkForUpdates };
|
|
412
466
|
}
|
|
413
467
|
|
|
414
468
|
//#endregion
|
|
415
|
-
//#region src/
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
469
|
+
//#region src/hooks/useManifest.ts
|
|
470
|
+
function getManifestUrlsHash(manifest) {
|
|
471
|
+
if (!manifest) return "";
|
|
472
|
+
return JSON.stringify(manifest.cdnUrls);
|
|
473
|
+
}
|
|
474
|
+
function useManifest({ manifest }) {
|
|
475
|
+
const storedManifest = useTranslationStore((state) => state.storedManifest);
|
|
476
|
+
const effectiveManifest = useMemo(() => {
|
|
477
|
+
if (storedManifest && storedManifest.projectId === manifest?.projectId) return {
|
|
478
|
+
version: String(storedManifest.serverVersion),
|
|
479
|
+
projectId: storedManifest.projectId,
|
|
480
|
+
sourceLanguage: storedManifest.sourceLanguage,
|
|
481
|
+
languages: storedManifest.languages,
|
|
482
|
+
cdnUrls: storedManifest.cdnUrls,
|
|
483
|
+
generatedAt: storedManifest.updatedAt
|
|
484
|
+
};
|
|
485
|
+
return manifest;
|
|
486
|
+
}, [storedManifest, manifest]);
|
|
487
|
+
const manifestRef = useRef(effectiveManifest);
|
|
488
|
+
const previousManifestHashRef = useRef("");
|
|
489
|
+
const loadedUrlsRef = useRef(/* @__PURE__ */ new Map());
|
|
490
|
+
const hasManifestChangedRef = useRef(false);
|
|
491
|
+
useEffect(() => {
|
|
492
|
+
manifestRef.current = effectiveManifest;
|
|
493
|
+
const newHash = getManifestUrlsHash(effectiveManifest);
|
|
494
|
+
const oldHash = previousManifestHashRef.current;
|
|
495
|
+
if (oldHash && newHash && oldHash !== newHash) {
|
|
496
|
+
loadedUrlsRef.current.clear();
|
|
497
|
+
useTranslationStore.setState({ translations: {} });
|
|
498
|
+
hasManifestChangedRef.current = true;
|
|
499
|
+
} else hasManifestChangedRef.current = false;
|
|
500
|
+
previousManifestHashRef.current = newHash;
|
|
501
|
+
}, [effectiveManifest]);
|
|
502
|
+
return {
|
|
503
|
+
effectiveManifest,
|
|
504
|
+
manifestRef,
|
|
505
|
+
hasManifestChanged: hasManifestChangedRef.current,
|
|
506
|
+
loadedUrlsRef
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/hooks/useTranslationLoader.ts
|
|
512
|
+
async function fetchTranslationsFromCDN(url, signal) {
|
|
513
|
+
const response = await fetch(url, { signal });
|
|
419
514
|
if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
|
|
420
515
|
return response.json();
|
|
421
516
|
}
|
|
517
|
+
function useTranslationLoader({ manifestRef, loadedUrlsRef, hotUpdatesEnabled }) {
|
|
518
|
+
const addLanguage = useTranslationStore((state) => state.addLanguage);
|
|
519
|
+
const setLoading = useTranslationStore((state) => state.setLoading);
|
|
520
|
+
const setReady = useTranslationStore((state) => state.setReady);
|
|
521
|
+
const abortControllerRef = useRef(null);
|
|
522
|
+
return {
|
|
523
|
+
loadLanguage: useCallback(async (language, isPreload = false) => {
|
|
524
|
+
const currentManifest = manifestRef.current;
|
|
525
|
+
if (!currentManifest) {
|
|
526
|
+
if (!isPreload) setReady(true);
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
const cdnUrl = currentManifest.cdnUrls[language];
|
|
530
|
+
if (!cdnUrl) {
|
|
531
|
+
if (!isPreload) setReady(true);
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
if (loadedUrlsRef.current.get(language) === cdnUrl) {
|
|
535
|
+
if (!isPreload) setReady(true);
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if (!isPreload) {
|
|
539
|
+
if (abortControllerRef.current) abortControllerRef.current.abort();
|
|
540
|
+
abortControllerRef.current = new AbortController();
|
|
541
|
+
}
|
|
542
|
+
const controller = isPreload ? void 0 : abortControllerRef.current;
|
|
543
|
+
if (!isPreload) {
|
|
544
|
+
setLoading(true);
|
|
545
|
+
setReady(false);
|
|
546
|
+
}
|
|
547
|
+
const useCache = hotUpdatesEnabled !== true;
|
|
548
|
+
try {
|
|
549
|
+
if (useCache) {
|
|
550
|
+
const cached = await getCachedTranslation(cdnUrl);
|
|
551
|
+
if (cached) {
|
|
552
|
+
if (controller?.signal.aborted) return false;
|
|
553
|
+
addLanguage(language, cached);
|
|
554
|
+
loadedUrlsRef.current.set(language, cdnUrl);
|
|
555
|
+
if (!isPreload) {
|
|
556
|
+
setReady(true);
|
|
557
|
+
setLoading(false);
|
|
558
|
+
}
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const translationData = await fetchTranslationsFromCDN(cdnUrl, controller?.signal);
|
|
563
|
+
if (controller?.signal.aborted) return false;
|
|
564
|
+
addLanguage(language, translationData);
|
|
565
|
+
loadedUrlsRef.current.set(language, cdnUrl);
|
|
566
|
+
if (useCache) await cacheTranslation(cdnUrl, language, currentManifest.projectId, translationData);
|
|
567
|
+
if (!isPreload) setReady(true);
|
|
568
|
+
return true;
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (error instanceof Error && error.name === "AbortError") return false;
|
|
571
|
+
console.error(`[ciao-tools] Failed to load translations for ${language}:`, error);
|
|
572
|
+
if (!isPreload) setReady(true);
|
|
573
|
+
return false;
|
|
574
|
+
} finally {
|
|
575
|
+
if (!isPreload) setLoading(false);
|
|
576
|
+
}
|
|
577
|
+
}, [
|
|
578
|
+
manifestRef,
|
|
579
|
+
loadedUrlsRef,
|
|
580
|
+
hotUpdatesEnabled,
|
|
581
|
+
addLanguage,
|
|
582
|
+
setLoading,
|
|
583
|
+
setReady
|
|
584
|
+
]),
|
|
585
|
+
abortControllerRef
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
//#endregion
|
|
590
|
+
//#region src/utils/browserLanguage.ts
|
|
422
591
|
function detectBrowserLanguage(availableLanguages) {
|
|
423
592
|
if (typeof navigator === "undefined") return null;
|
|
424
593
|
const browserLangs = navigator.languages || [navigator.language];
|
|
@@ -430,43 +599,19 @@ function detectBrowserLanguage(availableLanguages) {
|
|
|
430
599
|
}
|
|
431
600
|
return null;
|
|
432
601
|
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
const manifestRef = useRef(manifest);
|
|
440
|
-
const loadedUrlsRef = useRef(/* @__PURE__ */ new Map());
|
|
441
|
-
const previousManifestHashRef = useRef("");
|
|
602
|
+
|
|
603
|
+
//#endregion
|
|
604
|
+
//#region src/hooks/useLanguageInit.ts
|
|
605
|
+
function useLanguageInit({ effectiveManifest, availableLanguages, defaultLanguage, detectLanguage }) {
|
|
606
|
+
const isHydrated = useTranslationStore((state) => state.isHydrated);
|
|
607
|
+
const setLanguage = useTranslationStore((state) => state.setLanguage);
|
|
442
608
|
const initializedRef = useRef(false);
|
|
443
|
-
const preloadTimeoutRef = useRef(null);
|
|
444
|
-
useEffect(() => {
|
|
445
|
-
manifestRef.current = manifest;
|
|
446
|
-
const newHash = getManifestUrlsHash(manifest);
|
|
447
|
-
const oldHash = previousManifestHashRef.current;
|
|
448
|
-
if (oldHash && newHash && oldHash !== newHash) {
|
|
449
|
-
loadedUrlsRef.current.clear();
|
|
450
|
-
useTranslationStore.setState({ translations: {} });
|
|
451
|
-
}
|
|
452
|
-
previousManifestHashRef.current = newHash;
|
|
453
|
-
}, [manifest]);
|
|
454
|
-
useEffect(() => {
|
|
455
|
-
if (translations) {
|
|
456
|
-
loadTranslations(translations);
|
|
457
|
-
setReady(true);
|
|
458
|
-
}
|
|
459
|
-
}, [
|
|
460
|
-
translations,
|
|
461
|
-
loadTranslations,
|
|
462
|
-
setReady
|
|
463
|
-
]);
|
|
464
609
|
useEffect(() => {
|
|
465
610
|
if (!isHydrated) return;
|
|
466
611
|
if (initializedRef.current) return;
|
|
467
612
|
initializedRef.current = true;
|
|
468
613
|
const store = useTranslationStore.getState();
|
|
469
|
-
const effectiveLanguages =
|
|
614
|
+
const effectiveLanguages = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages || [];
|
|
470
615
|
if (store.currentLanguage && store.currentLanguage !== "en" && effectiveLanguages.includes(store.currentLanguage)) return;
|
|
471
616
|
if (detectLanguage && effectiveLanguages.length > 0) {
|
|
472
617
|
const detected = detectBrowserLanguage(effectiveLanguages);
|
|
@@ -477,18 +622,95 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
|
|
|
477
622
|
}
|
|
478
623
|
if (defaultLanguage && defaultLanguage !== store.currentLanguage) setLanguage(defaultLanguage);
|
|
479
624
|
}, [
|
|
480
|
-
|
|
625
|
+
effectiveManifest,
|
|
481
626
|
availableLanguages,
|
|
482
627
|
defaultLanguage,
|
|
483
628
|
detectLanguage,
|
|
484
629
|
setLanguage,
|
|
485
630
|
isHydrated
|
|
486
631
|
]);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
//#endregion
|
|
635
|
+
//#region src/hooks/usePreloader.ts
|
|
636
|
+
function usePreloader({ effectiveManifest, currentLanguage, delay, enabled, loadLanguage }) {
|
|
637
|
+
const preloadTimeoutRef = useRef(null);
|
|
638
|
+
const isMountedRef = useRef(true);
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
isMountedRef.current = true;
|
|
641
|
+
return () => {
|
|
642
|
+
isMountedRef.current = false;
|
|
643
|
+
};
|
|
644
|
+
}, []);
|
|
645
|
+
useEffect(() => {
|
|
646
|
+
if (!enabled || !effectiveManifest) return;
|
|
647
|
+
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
648
|
+
preloadTimeoutRef.current = setTimeout(async () => {
|
|
649
|
+
if (!isMountedRef.current) return;
|
|
650
|
+
const languages = [...effectiveManifest.languages];
|
|
651
|
+
for (const language of languages) {
|
|
652
|
+
if (!isMountedRef.current) break;
|
|
653
|
+
if (language === effectiveManifest.sourceLanguage) continue;
|
|
654
|
+
if (language === currentLanguage) continue;
|
|
655
|
+
await loadLanguage(language, true);
|
|
656
|
+
}
|
|
657
|
+
}, delay);
|
|
658
|
+
return () => {
|
|
659
|
+
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
660
|
+
};
|
|
661
|
+
}, [
|
|
662
|
+
effectiveManifest,
|
|
663
|
+
currentLanguage,
|
|
664
|
+
enabled,
|
|
665
|
+
delay,
|
|
666
|
+
loadLanguage
|
|
667
|
+
]);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region src/components/CiaoProvider.tsx
|
|
672
|
+
const PRELOAD_DELAY_MS = 5e3;
|
|
673
|
+
function CiaoProvider({ children, translations, manifest, defaultLanguage = "en", availableLanguages, onLanguageChange, detectLanguage = true, blockUntilReady = true, fallback = null, preloadLanguages = true, preloadDelay = PRELOAD_DELAY_MS, hotUpdates }) {
|
|
674
|
+
const loadTranslations = useTranslationStore((state) => state.loadTranslations);
|
|
675
|
+
const setReady = useTranslationStore((state) => state.setReady);
|
|
676
|
+
const currentLanguage = useTranslationStore((state) => state.currentLanguage);
|
|
677
|
+
const isReady = useTranslationStore((state) => state.isReady);
|
|
678
|
+
const isHydrated = useTranslationStore((state) => state.isHydrated);
|
|
679
|
+
const { effectiveManifest, manifestRef, loadedUrlsRef } = useManifest({ manifest });
|
|
680
|
+
const { loadLanguage, abortControllerRef } = useTranslationLoader({
|
|
681
|
+
manifestRef,
|
|
682
|
+
loadedUrlsRef,
|
|
683
|
+
hotUpdatesEnabled: hotUpdates?.enabled
|
|
684
|
+
});
|
|
685
|
+
useLanguageInit({
|
|
686
|
+
effectiveManifest,
|
|
687
|
+
availableLanguages,
|
|
688
|
+
defaultLanguage,
|
|
689
|
+
detectLanguage
|
|
690
|
+
});
|
|
691
|
+
usePreloader({
|
|
692
|
+
effectiveManifest,
|
|
693
|
+
currentLanguage,
|
|
694
|
+
delay: preloadDelay,
|
|
695
|
+
enabled: preloadLanguages,
|
|
696
|
+
loadLanguage
|
|
697
|
+
});
|
|
698
|
+
useHotUpdates(hotUpdates, manifest?.projectId, manifest?.sourceLanguage);
|
|
699
|
+
useEffect(() => {
|
|
700
|
+
if (translations) {
|
|
701
|
+
loadTranslations(translations);
|
|
702
|
+
setReady(true);
|
|
703
|
+
}
|
|
704
|
+
}, [
|
|
705
|
+
translations,
|
|
706
|
+
loadTranslations,
|
|
707
|
+
setReady
|
|
708
|
+
]);
|
|
487
709
|
useEffect(() => {
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
710
|
+
const languagesList = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages;
|
|
711
|
+
if (languagesList) {
|
|
490
712
|
const store = useTranslationStore.getState();
|
|
491
|
-
const merged = [...new Set([...store.availableLanguages, ...
|
|
713
|
+
const merged = [...new Set([...store.availableLanguages, ...languagesList])];
|
|
492
714
|
useTranslationStore.setState({
|
|
493
715
|
availableLanguages: merged,
|
|
494
716
|
defaultLanguage
|
|
@@ -496,97 +718,32 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
|
|
|
496
718
|
}
|
|
497
719
|
}, [
|
|
498
720
|
availableLanguages,
|
|
499
|
-
|
|
721
|
+
effectiveManifest,
|
|
500
722
|
defaultLanguage
|
|
501
723
|
]);
|
|
502
|
-
const loadLanguageFromCDN = useCallback(async (language, isPreload = false) => {
|
|
503
|
-
const currentManifest = manifestRef.current;
|
|
504
|
-
if (!currentManifest) {
|
|
505
|
-
if (!isPreload) setReady(true);
|
|
506
|
-
return false;
|
|
507
|
-
}
|
|
508
|
-
const cdnUrl = currentManifest.cdnUrls[language];
|
|
509
|
-
if (!cdnUrl) {
|
|
510
|
-
if (!isPreload) setReady(true);
|
|
511
|
-
return false;
|
|
512
|
-
}
|
|
513
|
-
if (loadedUrlsRef.current.get(language) === cdnUrl) {
|
|
514
|
-
if (!isPreload) setReady(true);
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
if (!isPreload) {
|
|
518
|
-
setLoading(true);
|
|
519
|
-
setReady(false);
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
const cached = await getCachedTranslation(cdnUrl);
|
|
523
|
-
if (cached) {
|
|
524
|
-
addLanguage(language, cached);
|
|
525
|
-
loadedUrlsRef.current.set(language, cdnUrl);
|
|
526
|
-
if (!isPreload) {
|
|
527
|
-
setReady(true);
|
|
528
|
-
setLoading(false);
|
|
529
|
-
}
|
|
530
|
-
return true;
|
|
531
|
-
}
|
|
532
|
-
const translationData = await fetchTranslationsFromCDN(cdnUrl);
|
|
533
|
-
addLanguage(language, translationData);
|
|
534
|
-
loadedUrlsRef.current.set(language, cdnUrl);
|
|
535
|
-
await cacheTranslation(cdnUrl, language, currentManifest.projectId, translationData);
|
|
536
|
-
if (!isPreload) setReady(true);
|
|
537
|
-
return true;
|
|
538
|
-
} catch (error) {
|
|
539
|
-
console.error(`[ciao-tools] Failed to load translations for ${language}:`, error);
|
|
540
|
-
if (!isPreload) setReady(true);
|
|
541
|
-
return false;
|
|
542
|
-
} finally {
|
|
543
|
-
if (!isPreload) setLoading(false);
|
|
544
|
-
}
|
|
545
|
-
}, [
|
|
546
|
-
addLanguage,
|
|
547
|
-
setLoading,
|
|
548
|
-
setReady
|
|
549
|
-
]);
|
|
550
724
|
useEffect(() => {
|
|
551
725
|
if (!isHydrated) return;
|
|
552
|
-
if (currentLanguage ===
|
|
726
|
+
if (currentLanguage === effectiveManifest?.sourceLanguage) {
|
|
553
727
|
setReady(true);
|
|
554
728
|
return;
|
|
555
729
|
}
|
|
556
|
-
if (
|
|
557
|
-
else if (!
|
|
730
|
+
if (effectiveManifest && currentLanguage) loadLanguage(currentLanguage, false);
|
|
731
|
+
else if (!effectiveManifest) setReady(true);
|
|
558
732
|
}, [
|
|
559
|
-
|
|
733
|
+
effectiveManifest,
|
|
560
734
|
currentLanguage,
|
|
561
|
-
|
|
735
|
+
loadLanguage,
|
|
562
736
|
setReady,
|
|
563
737
|
isHydrated
|
|
564
738
|
]);
|
|
565
|
-
useEffect(() => {
|
|
566
|
-
if (!preloadLanguages || !manifest) return;
|
|
567
|
-
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
568
|
-
preloadTimeoutRef.current = setTimeout(async () => {
|
|
569
|
-
const languages = [...manifest.languages];
|
|
570
|
-
for (const language of languages) {
|
|
571
|
-
if (language === manifest.sourceLanguage) continue;
|
|
572
|
-
if (language === currentLanguage) continue;
|
|
573
|
-
await loadLanguageFromCDN(language, true);
|
|
574
|
-
}
|
|
575
|
-
}, preloadDelay);
|
|
576
|
-
return () => {
|
|
577
|
-
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
578
|
-
};
|
|
579
|
-
}, [
|
|
580
|
-
manifest,
|
|
581
|
-
currentLanguage,
|
|
582
|
-
preloadLanguages,
|
|
583
|
-
preloadDelay,
|
|
584
|
-
loadLanguageFromCDN
|
|
585
|
-
]);
|
|
586
739
|
useEffect(() => {
|
|
587
740
|
if (onLanguageChange) onLanguageChange(currentLanguage);
|
|
588
741
|
}, [currentLanguage, onLanguageChange]);
|
|
589
|
-
|
|
742
|
+
useEffect(() => {
|
|
743
|
+
return () => {
|
|
744
|
+
abortControllerRef.current?.abort();
|
|
745
|
+
};
|
|
746
|
+
}, [abortControllerRef]);
|
|
590
747
|
if (blockUntilReady && (!isReady || !isHydrated)) return /* @__PURE__ */ React.createElement(React.Fragment, null, fallback);
|
|
591
748
|
return /* @__PURE__ */ React.createElement(React.Fragment, null, children);
|
|
592
749
|
}
|
|
@@ -878,6 +1035,7 @@ function useCt() {
|
|
|
878
1035
|
|
|
879
1036
|
//#endregion
|
|
880
1037
|
//#region src/components/Trans.tsx
|
|
1038
|
+
const TAG_REGEX = /<(\d+)>(.*?)<\/\1>/gs;
|
|
881
1039
|
function parseChildren(children) {
|
|
882
1040
|
const elements = [];
|
|
883
1041
|
let template = "";
|
|
@@ -903,11 +1061,10 @@ function reconstructChildren(translated, elements) {
|
|
|
903
1061
|
if (elements.length === 0) return translated;
|
|
904
1062
|
const result = [];
|
|
905
1063
|
let keyCounter = 0;
|
|
906
|
-
const tagRegex = /<(\d+)>(.*?)<\/\1>/gs;
|
|
907
1064
|
let lastIndex = 0;
|
|
908
1065
|
let match;
|
|
909
|
-
|
|
910
|
-
while ((match =
|
|
1066
|
+
TAG_REGEX.lastIndex = 0;
|
|
1067
|
+
while ((match = TAG_REGEX.exec(translated)) !== null) {
|
|
911
1068
|
if (match.index > lastIndex) {
|
|
912
1069
|
const textBefore = translated.slice(lastIndex, match.index);
|
|
913
1070
|
if (textBefore) result.push(textBefore);
|
|
@@ -977,6 +1134,11 @@ function useLanguage() {
|
|
|
977
1134
|
availableLanguagesInfo: availableLanguages.map(getFullLanguageInfo),
|
|
978
1135
|
setLanguage,
|
|
979
1136
|
cycleLanguage: useCallback(() => {
|
|
1137
|
+
if (availableLanguages.length === 0) {
|
|
1138
|
+
console.warn("[ciao-tools] Cannot cycle language: no languages available");
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
if (availableLanguages.length === 1) return;
|
|
980
1142
|
setLanguage(availableLanguages[(availableLanguages.indexOf(currentLanguage) + 1) % availableLanguages.length]);
|
|
981
1143
|
}, [
|
|
982
1144
|
availableLanguages,
|
|
@@ -989,5 +1151,5 @@ function useLanguage() {
|
|
|
989
1151
|
}
|
|
990
1152
|
|
|
991
1153
|
//#endregion
|
|
992
|
-
export { CTContextBlock, CTProvider, LANGUAGE_DATA, LanguageSwitcher, Trans, clearCache, clearFormatterCache, formatLanguageDisplay, getCacheStats, getFullLanguageInfo, getLanguageInfo, interpolate, useAvailableLanguages, useAvailableLanguagesInfo, useCt, useCurrentLanguage, useHotUpdates, useIsLoading, useIsReady, useLanguage, useLanguageInfo, useSetLanguage, useTranslationStore };
|
|
1154
|
+
export { CTContextBlock, CiaoProvider as CTProvider, CiaoProvider, LANGUAGE_DATA, LanguageSwitcher, Trans, clearCache, clearFormatterCache, formatLanguageDisplay, getCacheStats, getFullLanguageInfo, getLanguageInfo, interpolate, useAvailableLanguages, useAvailableLanguagesInfo, useCt, useCurrentLanguage, useHotUpdates, useIsLoading, useIsReady, useLanguage, useLanguageInfo, useSetLanguage, useTranslationStore };
|
|
993
1155
|
//# sourceMappingURL=index.js.map
|