experimental-ciao-react 1.1.9 → 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 +439 -285
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -13
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +19 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +439 -286
- 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,9 +184,8 @@ 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
|
-
cachedLanguages: [],
|
|
285
189
|
setLanguage: (language) => {
|
|
286
190
|
set({
|
|
287
191
|
currentLanguage: language,
|
|
@@ -321,24 +225,24 @@ const useTranslationStore = (0, zustand.create)()((0, zustand_middleware.persist
|
|
|
321
225
|
setReady: (ready) => {
|
|
322
226
|
set({ isReady: ready });
|
|
323
227
|
},
|
|
324
|
-
|
|
228
|
+
setStoredManifest: (manifest) => {
|
|
325
229
|
set({
|
|
326
|
-
|
|
230
|
+
storedManifest: manifest,
|
|
327
231
|
lastVersionCheck: Date.now()
|
|
328
232
|
});
|
|
329
|
-
},
|
|
330
|
-
setCachedLanguages: (languages) => {
|
|
331
|
-
set({ cachedLanguages: languages });
|
|
332
233
|
}
|
|
333
234
|
}), {
|
|
334
235
|
name: "ciao-tools-language",
|
|
335
236
|
partialize: (state) => ({
|
|
336
237
|
currentLanguage: state.currentLanguage,
|
|
337
|
-
|
|
338
|
-
cachedLanguages: state.cachedLanguages
|
|
238
|
+
storedManifest: state.storedManifest
|
|
339
239
|
}),
|
|
340
240
|
onRehydrateStorage: () => (_, error) => {
|
|
341
|
-
if (
|
|
241
|
+
if (error) console.error("[ciao-tools] Storage hydration failed:", error);
|
|
242
|
+
if (hydrationResolver) {
|
|
243
|
+
hydrationResolver();
|
|
244
|
+
hydrationResolver = null;
|
|
245
|
+
}
|
|
342
246
|
}
|
|
343
247
|
}));
|
|
344
248
|
hydrationPromise.then(() => {
|
|
@@ -350,6 +254,133 @@ function getTranslation(translations, language, text, values) {
|
|
|
350
254
|
return interpolate(translated, values, language);
|
|
351
255
|
}
|
|
352
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
|
+
|
|
353
384
|
//#endregion
|
|
354
385
|
//#region src/hooks/useHotUpdates.ts
|
|
355
386
|
const CDN_BASE_URL = "https://t1.ciao-tools.com";
|
|
@@ -362,7 +393,8 @@ async function fetchLatestManifest(projectId) {
|
|
|
362
393
|
throw new Error(`Failed to fetch latest manifest: ${response.statusText}`);
|
|
363
394
|
}
|
|
364
395
|
return response.json();
|
|
365
|
-
} catch {
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.warn("[ciao-tools] Failed to fetch latest manifest:", error);
|
|
366
398
|
return null;
|
|
367
399
|
}
|
|
368
400
|
}
|
|
@@ -371,64 +403,81 @@ async function fetchTranslations(url) {
|
|
|
371
403
|
if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
|
|
372
404
|
return response.json();
|
|
373
405
|
}
|
|
374
|
-
function useHotUpdates(config, projectId) {
|
|
406
|
+
function useHotUpdates(config, projectId, sourceLanguage) {
|
|
375
407
|
const isCheckingRef = (0, react.useRef)(false);
|
|
376
408
|
const hasCheckedOnMountRef = (0, react.useRef)(false);
|
|
377
|
-
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();
|
|
378
422
|
const checkForUpdates = (0, react.useCallback)(async () => {
|
|
379
|
-
|
|
380
|
-
|
|
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;
|
|
381
428
|
isCheckingRef.current = true;
|
|
382
429
|
try {
|
|
383
430
|
console.log("[ciao-tools] Checking for hot updates...");
|
|
384
|
-
const
|
|
385
|
-
if (!
|
|
431
|
+
const latestManifest = await fetchLatestManifest(currentProjectId);
|
|
432
|
+
if (!latestManifest) {
|
|
386
433
|
console.log("[ciao-tools] No latest.json found (project may not have hot updates yet)");
|
|
387
434
|
return;
|
|
388
435
|
}
|
|
389
|
-
console.log("[ciao-tools] Found latest.json, version:",
|
|
390
|
-
const
|
|
391
|
-
const
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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;
|
|
441
|
+
if (isFirstCheck || hasNewVersion) {
|
|
442
|
+
if (Object.keys(latestManifest.urls).length > 0) {
|
|
443
|
+
const reason = hasNewVersion ? "new version" : "first check";
|
|
396
444
|
console.log(`[ciao-tools] Fetching translations (${reason})...`);
|
|
397
|
-
await clearCache(
|
|
445
|
+
if (hasNewVersion) await clearCache(currentProjectId);
|
|
398
446
|
const updatedLanguages = [];
|
|
399
|
-
for (const [langCode, url] of Object.entries(
|
|
447
|
+
for (const [langCode, url] of Object.entries(latestManifest.urls)) try {
|
|
400
448
|
const translations = await fetchTranslations(url);
|
|
401
449
|
addLanguage(langCode, translations);
|
|
402
|
-
await cacheTranslation(url, langCode,
|
|
450
|
+
await cacheTranslation(url, langCode, currentProjectId, translations);
|
|
403
451
|
updatedLanguages.push(langCode);
|
|
404
452
|
} catch (err) {
|
|
405
453
|
console.error(`[ciao-tools] Failed to fetch ${langCode} translations:`, err);
|
|
406
454
|
}
|
|
407
455
|
if (updatedLanguages.length > 0) {
|
|
408
|
-
setCachedLanguages(updatedLanguages);
|
|
409
456
|
console.log("[ciao-tools] Updated translations for:", updatedLanguages);
|
|
410
|
-
if (hasNewVersion &&
|
|
457
|
+
if (hasNewVersion && currentConfig.onTranslationsUpdated) currentConfig.onTranslationsUpdated(updatedLanguages);
|
|
411
458
|
}
|
|
412
459
|
}
|
|
413
|
-
|
|
414
|
-
|
|
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 + ")");
|
|
415
469
|
} catch (error) {
|
|
416
470
|
console.error("[ciao-tools] Hot update check failed:", error);
|
|
417
471
|
} finally {
|
|
418
472
|
isCheckingRef.current = false;
|
|
419
473
|
}
|
|
420
|
-
}, [
|
|
421
|
-
config,
|
|
422
|
-
projectId,
|
|
423
|
-
serverVersion,
|
|
424
|
-
setServerVersion,
|
|
425
|
-
addLanguage,
|
|
426
|
-
cachedLanguages,
|
|
427
|
-
setCachedLanguages
|
|
428
|
-
]);
|
|
474
|
+
}, [setStoredManifest, addLanguage]);
|
|
429
475
|
(0, react.useEffect)(() => {
|
|
430
|
-
|
|
431
|
-
|
|
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;
|
|
432
481
|
if (!hasCheckedOnMountRef.current) {
|
|
433
482
|
hasCheckedOnMountRef.current = true;
|
|
434
483
|
checkForUpdates();
|
|
@@ -440,22 +489,133 @@ function useHotUpdates(config, projectId) {
|
|
|
440
489
|
return () => {
|
|
441
490
|
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
|
442
491
|
};
|
|
443
|
-
}, [
|
|
444
|
-
config,
|
|
445
|
-
projectId,
|
|
446
|
-
checkForUpdates
|
|
447
|
-
]);
|
|
492
|
+
}, [checkForUpdates]);
|
|
448
493
|
return { checkForUpdates };
|
|
449
494
|
}
|
|
450
495
|
|
|
451
496
|
//#endregion
|
|
452
|
-
//#region src/
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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 });
|
|
456
542
|
if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
|
|
457
543
|
return response.json();
|
|
458
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
|
|
459
619
|
function detectBrowserLanguage(availableLanguages) {
|
|
460
620
|
if (typeof navigator === "undefined") return null;
|
|
461
621
|
const browserLangs = navigator.languages || [navigator.language];
|
|
@@ -467,43 +627,19 @@ function detectBrowserLanguage(availableLanguages) {
|
|
|
467
627
|
}
|
|
468
628
|
return null;
|
|
469
629
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
const manifestRef = (0, react.useRef)(manifest);
|
|
477
|
-
const loadedUrlsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
478
|
-
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);
|
|
479
636
|
const initializedRef = (0, react.useRef)(false);
|
|
480
|
-
const preloadTimeoutRef = (0, react.useRef)(null);
|
|
481
|
-
(0, react.useEffect)(() => {
|
|
482
|
-
manifestRef.current = manifest;
|
|
483
|
-
const newHash = getManifestUrlsHash(manifest);
|
|
484
|
-
const oldHash = previousManifestHashRef.current;
|
|
485
|
-
if (oldHash && newHash && oldHash !== newHash) {
|
|
486
|
-
loadedUrlsRef.current.clear();
|
|
487
|
-
useTranslationStore.setState({ translations: {} });
|
|
488
|
-
}
|
|
489
|
-
previousManifestHashRef.current = newHash;
|
|
490
|
-
}, [manifest]);
|
|
491
|
-
(0, react.useEffect)(() => {
|
|
492
|
-
if (translations) {
|
|
493
|
-
loadTranslations(translations);
|
|
494
|
-
setReady(true);
|
|
495
|
-
}
|
|
496
|
-
}, [
|
|
497
|
-
translations,
|
|
498
|
-
loadTranslations,
|
|
499
|
-
setReady
|
|
500
|
-
]);
|
|
501
637
|
(0, react.useEffect)(() => {
|
|
502
638
|
if (!isHydrated) return;
|
|
503
639
|
if (initializedRef.current) return;
|
|
504
640
|
initializedRef.current = true;
|
|
505
641
|
const store = useTranslationStore.getState();
|
|
506
|
-
const effectiveLanguages =
|
|
642
|
+
const effectiveLanguages = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages || [];
|
|
507
643
|
if (store.currentLanguage && store.currentLanguage !== "en" && effectiveLanguages.includes(store.currentLanguage)) return;
|
|
508
644
|
if (detectLanguage && effectiveLanguages.length > 0) {
|
|
509
645
|
const detected = detectBrowserLanguage(effectiveLanguages);
|
|
@@ -514,18 +650,95 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
|
|
|
514
650
|
}
|
|
515
651
|
if (defaultLanguage && defaultLanguage !== store.currentLanguage) setLanguage(defaultLanguage);
|
|
516
652
|
}, [
|
|
517
|
-
|
|
653
|
+
effectiveManifest,
|
|
518
654
|
availableLanguages,
|
|
519
655
|
defaultLanguage,
|
|
520
656
|
detectLanguage,
|
|
521
657
|
setLanguage,
|
|
522
658
|
isHydrated
|
|
523
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
|
+
]);
|
|
524
737
|
(0, react.useEffect)(() => {
|
|
525
|
-
const
|
|
526
|
-
if (
|
|
738
|
+
const languagesList = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages;
|
|
739
|
+
if (languagesList) {
|
|
527
740
|
const store = useTranslationStore.getState();
|
|
528
|
-
const merged = [...new Set([...store.availableLanguages, ...
|
|
741
|
+
const merged = [...new Set([...store.availableLanguages, ...languagesList])];
|
|
529
742
|
useTranslationStore.setState({
|
|
530
743
|
availableLanguages: merged,
|
|
531
744
|
defaultLanguage
|
|
@@ -533,97 +746,32 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
|
|
|
533
746
|
}
|
|
534
747
|
}, [
|
|
535
748
|
availableLanguages,
|
|
536
|
-
|
|
749
|
+
effectiveManifest,
|
|
537
750
|
defaultLanguage
|
|
538
751
|
]);
|
|
539
|
-
const loadLanguageFromCDN = (0, react.useCallback)(async (language, isPreload = false) => {
|
|
540
|
-
const currentManifest = manifestRef.current;
|
|
541
|
-
if (!currentManifest) {
|
|
542
|
-
if (!isPreload) setReady(true);
|
|
543
|
-
return false;
|
|
544
|
-
}
|
|
545
|
-
const cdnUrl = currentManifest.cdnUrls[language];
|
|
546
|
-
if (!cdnUrl) {
|
|
547
|
-
if (!isPreload) setReady(true);
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
if (loadedUrlsRef.current.get(language) === cdnUrl) {
|
|
551
|
-
if (!isPreload) setReady(true);
|
|
552
|
-
return true;
|
|
553
|
-
}
|
|
554
|
-
if (!isPreload) {
|
|
555
|
-
setLoading(true);
|
|
556
|
-
setReady(false);
|
|
557
|
-
}
|
|
558
|
-
try {
|
|
559
|
-
const cached = await getCachedTranslation(cdnUrl);
|
|
560
|
-
if (cached) {
|
|
561
|
-
addLanguage(language, cached);
|
|
562
|
-
loadedUrlsRef.current.set(language, cdnUrl);
|
|
563
|
-
if (!isPreload) {
|
|
564
|
-
setReady(true);
|
|
565
|
-
setLoading(false);
|
|
566
|
-
}
|
|
567
|
-
return true;
|
|
568
|
-
}
|
|
569
|
-
const translationData = await fetchTranslationsFromCDN(cdnUrl);
|
|
570
|
-
addLanguage(language, translationData);
|
|
571
|
-
loadedUrlsRef.current.set(language, cdnUrl);
|
|
572
|
-
await cacheTranslation(cdnUrl, language, currentManifest.projectId, translationData);
|
|
573
|
-
if (!isPreload) setReady(true);
|
|
574
|
-
return true;
|
|
575
|
-
} catch (error) {
|
|
576
|
-
console.error(`[ciao-tools] Failed to load translations for ${language}:`, error);
|
|
577
|
-
if (!isPreload) setReady(true);
|
|
578
|
-
return false;
|
|
579
|
-
} finally {
|
|
580
|
-
if (!isPreload) setLoading(false);
|
|
581
|
-
}
|
|
582
|
-
}, [
|
|
583
|
-
addLanguage,
|
|
584
|
-
setLoading,
|
|
585
|
-
setReady
|
|
586
|
-
]);
|
|
587
752
|
(0, react.useEffect)(() => {
|
|
588
753
|
if (!isHydrated) return;
|
|
589
|
-
if (currentLanguage ===
|
|
754
|
+
if (currentLanguage === effectiveManifest?.sourceLanguage) {
|
|
590
755
|
setReady(true);
|
|
591
756
|
return;
|
|
592
757
|
}
|
|
593
|
-
if (
|
|
594
|
-
else if (!
|
|
758
|
+
if (effectiveManifest && currentLanguage) loadLanguage(currentLanguage, false);
|
|
759
|
+
else if (!effectiveManifest) setReady(true);
|
|
595
760
|
}, [
|
|
596
|
-
|
|
761
|
+
effectiveManifest,
|
|
597
762
|
currentLanguage,
|
|
598
|
-
|
|
763
|
+
loadLanguage,
|
|
599
764
|
setReady,
|
|
600
765
|
isHydrated
|
|
601
766
|
]);
|
|
602
|
-
(0, react.useEffect)(() => {
|
|
603
|
-
if (!preloadLanguages || !manifest) return;
|
|
604
|
-
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
605
|
-
preloadTimeoutRef.current = setTimeout(async () => {
|
|
606
|
-
const languages = [...manifest.languages];
|
|
607
|
-
for (const language of languages) {
|
|
608
|
-
if (language === manifest.sourceLanguage) continue;
|
|
609
|
-
if (language === currentLanguage) continue;
|
|
610
|
-
await loadLanguageFromCDN(language, true);
|
|
611
|
-
}
|
|
612
|
-
}, preloadDelay);
|
|
613
|
-
return () => {
|
|
614
|
-
if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
|
|
615
|
-
};
|
|
616
|
-
}, [
|
|
617
|
-
manifest,
|
|
618
|
-
currentLanguage,
|
|
619
|
-
preloadLanguages,
|
|
620
|
-
preloadDelay,
|
|
621
|
-
loadLanguageFromCDN
|
|
622
|
-
]);
|
|
623
767
|
(0, react.useEffect)(() => {
|
|
624
768
|
if (onLanguageChange) onLanguageChange(currentLanguage);
|
|
625
769
|
}, [currentLanguage, onLanguageChange]);
|
|
626
|
-
|
|
770
|
+
(0, react.useEffect)(() => {
|
|
771
|
+
return () => {
|
|
772
|
+
abortControllerRef.current?.abort();
|
|
773
|
+
};
|
|
774
|
+
}, [abortControllerRef]);
|
|
627
775
|
if (blockUntilReady && (!isReady || !isHydrated)) return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, fallback);
|
|
628
776
|
return /* @__PURE__ */ react.default.createElement(react.default.Fragment, null, children);
|
|
629
777
|
}
|
|
@@ -915,6 +1063,7 @@ function useCt() {
|
|
|
915
1063
|
|
|
916
1064
|
//#endregion
|
|
917
1065
|
//#region src/components/Trans.tsx
|
|
1066
|
+
const TAG_REGEX = /<(\d+)>(.*?)<\/\1>/gs;
|
|
918
1067
|
function parseChildren(children) {
|
|
919
1068
|
const elements = [];
|
|
920
1069
|
let template = "";
|
|
@@ -940,11 +1089,10 @@ function reconstructChildren(translated, elements) {
|
|
|
940
1089
|
if (elements.length === 0) return translated;
|
|
941
1090
|
const result = [];
|
|
942
1091
|
let keyCounter = 0;
|
|
943
|
-
const tagRegex = /<(\d+)>(.*?)<\/\1>/gs;
|
|
944
1092
|
let lastIndex = 0;
|
|
945
1093
|
let match;
|
|
946
|
-
|
|
947
|
-
while ((match =
|
|
1094
|
+
TAG_REGEX.lastIndex = 0;
|
|
1095
|
+
while ((match = TAG_REGEX.exec(translated)) !== null) {
|
|
948
1096
|
if (match.index > lastIndex) {
|
|
949
1097
|
const textBefore = translated.slice(lastIndex, match.index);
|
|
950
1098
|
if (textBefore) result.push(textBefore);
|
|
@@ -1014,6 +1162,11 @@ function useLanguage() {
|
|
|
1014
1162
|
availableLanguagesInfo: availableLanguages.map(getFullLanguageInfo),
|
|
1015
1163
|
setLanguage,
|
|
1016
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;
|
|
1017
1170
|
setLanguage(availableLanguages[(availableLanguages.indexOf(currentLanguage) + 1) % availableLanguages.length]);
|
|
1018
1171
|
}, [
|
|
1019
1172
|
availableLanguages,
|
|
@@ -1027,7 +1180,8 @@ function useLanguage() {
|
|
|
1027
1180
|
|
|
1028
1181
|
//#endregion
|
|
1029
1182
|
exports.CTContextBlock = CTContextBlock;
|
|
1030
|
-
exports.CTProvider =
|
|
1183
|
+
exports.CTProvider = CiaoProvider;
|
|
1184
|
+
exports.CiaoProvider = CiaoProvider;
|
|
1031
1185
|
exports.LANGUAGE_DATA = LANGUAGE_DATA;
|
|
1032
1186
|
exports.LanguageSwitcher = LanguageSwitcher;
|
|
1033
1187
|
exports.Trans = Trans;
|