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/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/cache.ts
12
- const DB_NAME = "ciao-tools-translations";
13
- const DB_VERSION = 1;
14
- const STORE_NAME = "translations";
15
- let dbPromise = null;
16
- function openDB() {
17
- if (dbPromise) return dbPromise;
18
- dbPromise = new Promise((resolve, reject) => {
19
- if (typeof indexedDB === "undefined") {
20
- reject(/* @__PURE__ */ new Error("IndexedDB not available"));
21
- return;
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
- } catch {}
94
- }
95
- async function getCacheStats() {
96
- try {
97
- const db = await openDB();
98
- return new Promise((resolve, reject) => {
99
- const transaction = db.transaction(STORE_NAME, "readonly");
100
- const store = transaction.objectStore(STORE_NAME);
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
- //#endregion
128
- //#region src/interpolate.ts
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,9 +156,8 @@ const useTranslationStore = create()(persist((set) => ({
251
156
  isLoading: false,
252
157
  isReady: false,
253
158
  isHydrated: false,
254
- serverVersion: null,
159
+ storedManifest: null,
255
160
  lastVersionCheck: null,
256
- cachedLanguages: [],
257
161
  setLanguage: (language) => {
258
162
  set({
259
163
  currentLanguage: language,
@@ -293,24 +197,24 @@ const useTranslationStore = create()(persist((set) => ({
293
197
  setReady: (ready) => {
294
198
  set({ isReady: ready });
295
199
  },
296
- setServerVersion: (version) => {
200
+ setStoredManifest: (manifest) => {
297
201
  set({
298
- serverVersion: version,
202
+ storedManifest: manifest,
299
203
  lastVersionCheck: Date.now()
300
204
  });
301
- },
302
- setCachedLanguages: (languages) => {
303
- set({ cachedLanguages: languages });
304
205
  }
305
206
  }), {
306
207
  name: "ciao-tools-language",
307
208
  partialize: (state) => ({
308
209
  currentLanguage: state.currentLanguage,
309
- serverVersion: state.serverVersion,
310
- cachedLanguages: state.cachedLanguages
210
+ storedManifest: state.storedManifest
311
211
  }),
312
212
  onRehydrateStorage: () => (_, error) => {
313
- if (!error && hydrationResolver) hydrationResolver();
213
+ if (error) console.error("[ciao-tools] Storage hydration failed:", error);
214
+ if (hydrationResolver) {
215
+ hydrationResolver();
216
+ hydrationResolver = null;
217
+ }
314
218
  }
315
219
  }));
316
220
  hydrationPromise.then(() => {
@@ -322,6 +226,133 @@ function getTranslation(translations, language, text, values) {
322
226
  return interpolate(translated, values, language);
323
227
  }
324
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
+
325
356
  //#endregion
326
357
  //#region src/hooks/useHotUpdates.ts
327
358
  const CDN_BASE_URL = "https://t1.ciao-tools.com";
@@ -334,7 +365,8 @@ async function fetchLatestManifest(projectId) {
334
365
  throw new Error(`Failed to fetch latest manifest: ${response.statusText}`);
335
366
  }
336
367
  return response.json();
337
- } catch {
368
+ } catch (error) {
369
+ console.warn("[ciao-tools] Failed to fetch latest manifest:", error);
338
370
  return null;
339
371
  }
340
372
  }
@@ -343,64 +375,81 @@ async function fetchTranslations(url) {
343
375
  if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
344
376
  return response.json();
345
377
  }
346
- function useHotUpdates(config, projectId) {
378
+ function useHotUpdates(config, projectId, sourceLanguage) {
347
379
  const isCheckingRef = useRef(false);
348
380
  const hasCheckedOnMountRef = useRef(false);
349
- const { serverVersion, setServerVersion, addLanguage, cachedLanguages, setCachedLanguages } = useTranslationStore();
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();
350
394
  const checkForUpdates = useCallback(async () => {
351
- if (!config || !projectId || isCheckingRef.current) return;
352
- if (config.enabled !== true) return;
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;
353
400
  isCheckingRef.current = true;
354
401
  try {
355
402
  console.log("[ciao-tools] Checking for hot updates...");
356
- const manifest = await fetchLatestManifest(projectId);
357
- if (!manifest) {
403
+ const latestManifest = await fetchLatestManifest(currentProjectId);
404
+ if (!latestManifest) {
358
405
  console.log("[ciao-tools] No latest.json found (project may not have hot updates yet)");
359
406
  return;
360
407
  }
361
- console.log("[ciao-tools] Found latest.json, version:", manifest.version);
362
- const isFirstCheck = serverVersion === null;
363
- const hasNewVersion = serverVersion !== null && manifest.version > serverVersion;
364
- const hasNoCache = cachedLanguages.length === 0;
365
- if (isFirstCheck || hasNewVersion || hasNoCache) {
366
- if (Object.keys(manifest.urls).length > 0) {
367
- const reason = hasNewVersion ? "new version" : hasNoCache ? "no cached translations" : "first check";
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;
413
+ if (isFirstCheck || hasNewVersion) {
414
+ if (Object.keys(latestManifest.urls).length > 0) {
415
+ const reason = hasNewVersion ? "new version" : "first check";
368
416
  console.log(`[ciao-tools] Fetching translations (${reason})...`);
369
- await clearCache(projectId);
417
+ if (hasNewVersion) await clearCache(currentProjectId);
370
418
  const updatedLanguages = [];
371
- for (const [langCode, url] of Object.entries(manifest.urls)) try {
419
+ for (const [langCode, url] of Object.entries(latestManifest.urls)) try {
372
420
  const translations = await fetchTranslations(url);
373
421
  addLanguage(langCode, translations);
374
- await cacheTranslation(url, langCode, projectId, translations);
422
+ await cacheTranslation(url, langCode, currentProjectId, translations);
375
423
  updatedLanguages.push(langCode);
376
424
  } catch (err) {
377
425
  console.error(`[ciao-tools] Failed to fetch ${langCode} translations:`, err);
378
426
  }
379
427
  if (updatedLanguages.length > 0) {
380
- setCachedLanguages(updatedLanguages);
381
428
  console.log("[ciao-tools] Updated translations for:", updatedLanguages);
382
- if (hasNewVersion && config.onTranslationsUpdated) config.onTranslationsUpdated(updatedLanguages);
429
+ if (hasNewVersion && currentConfig.onTranslationsUpdated) currentConfig.onTranslationsUpdated(updatedLanguages);
383
430
  }
384
431
  }
385
- } else console.log("[ciao-tools] Already up to date (version " + manifest.version + ")");
386
- setServerVersion(manifest.version);
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 + ")");
387
441
  } catch (error) {
388
442
  console.error("[ciao-tools] Hot update check failed:", error);
389
443
  } finally {
390
444
  isCheckingRef.current = false;
391
445
  }
392
- }, [
393
- config,
394
- projectId,
395
- serverVersion,
396
- setServerVersion,
397
- addLanguage,
398
- cachedLanguages,
399
- setCachedLanguages
400
- ]);
446
+ }, [setStoredManifest, addLanguage]);
401
447
  useEffect(() => {
402
- if (!config || !projectId) return;
403
- if (config.enabled !== true) return;
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;
404
453
  if (!hasCheckedOnMountRef.current) {
405
454
  hasCheckedOnMountRef.current = true;
406
455
  checkForUpdates();
@@ -412,22 +461,133 @@ function useHotUpdates(config, projectId) {
412
461
  return () => {
413
462
  document.removeEventListener("visibilitychange", handleVisibilityChange);
414
463
  };
415
- }, [
416
- config,
417
- projectId,
418
- checkForUpdates
419
- ]);
464
+ }, [checkForUpdates]);
420
465
  return { checkForUpdates };
421
466
  }
422
467
 
423
468
  //#endregion
424
- //#region src/components/CTProvider.tsx
425
- const PRELOAD_DELAY_MS = 5e3;
426
- async function fetchTranslationsFromCDN(url) {
427
- const response = await fetch(url);
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 });
428
514
  if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
429
515
  return response.json();
430
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
431
591
  function detectBrowserLanguage(availableLanguages) {
432
592
  if (typeof navigator === "undefined") return null;
433
593
  const browserLangs = navigator.languages || [navigator.language];
@@ -439,43 +599,19 @@ function detectBrowserLanguage(availableLanguages) {
439
599
  }
440
600
  return null;
441
601
  }
442
- function getManifestUrlsHash(manifest) {
443
- if (!manifest) return "";
444
- return JSON.stringify(manifest.cdnUrls);
445
- }
446
- function CTProvider({ children, translations, manifest, defaultLanguage = "en", availableLanguages, onLanguageChange, detectLanguage = true, blockUntilReady = true, fallback = null, preloadLanguages = true, preloadDelay = PRELOAD_DELAY_MS, hotUpdates }) {
447
- const { loadTranslations, setLanguage, addLanguage, setLoading, setReady, currentLanguage, translations: storeTranslations, isReady, isHydrated } = useTranslationStore();
448
- const manifestRef = useRef(manifest);
449
- const loadedUrlsRef = useRef(/* @__PURE__ */ new Map());
450
- 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);
451
608
  const initializedRef = useRef(false);
452
- const preloadTimeoutRef = useRef(null);
453
- useEffect(() => {
454
- manifestRef.current = manifest;
455
- const newHash = getManifestUrlsHash(manifest);
456
- const oldHash = previousManifestHashRef.current;
457
- if (oldHash && newHash && oldHash !== newHash) {
458
- loadedUrlsRef.current.clear();
459
- useTranslationStore.setState({ translations: {} });
460
- }
461
- previousManifestHashRef.current = newHash;
462
- }, [manifest]);
463
- useEffect(() => {
464
- if (translations) {
465
- loadTranslations(translations);
466
- setReady(true);
467
- }
468
- }, [
469
- translations,
470
- loadTranslations,
471
- setReady
472
- ]);
473
609
  useEffect(() => {
474
610
  if (!isHydrated) return;
475
611
  if (initializedRef.current) return;
476
612
  initializedRef.current = true;
477
613
  const store = useTranslationStore.getState();
478
- const effectiveLanguages = manifest ? [...manifest.languages] : availableLanguages || [];
614
+ const effectiveLanguages = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages || [];
479
615
  if (store.currentLanguage && store.currentLanguage !== "en" && effectiveLanguages.includes(store.currentLanguage)) return;
480
616
  if (detectLanguage && effectiveLanguages.length > 0) {
481
617
  const detected = detectBrowserLanguage(effectiveLanguages);
@@ -486,18 +622,95 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
486
622
  }
487
623
  if (defaultLanguage && defaultLanguage !== store.currentLanguage) setLanguage(defaultLanguage);
488
624
  }, [
489
- manifest,
625
+ effectiveManifest,
490
626
  availableLanguages,
491
627
  defaultLanguage,
492
628
  detectLanguage,
493
629
  setLanguage,
494
630
  isHydrated
495
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
+ ]);
496
709
  useEffect(() => {
497
- const effectiveLanguages = manifest ? [...manifest.languages] : availableLanguages;
498
- if (effectiveLanguages) {
710
+ const languagesList = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages;
711
+ if (languagesList) {
499
712
  const store = useTranslationStore.getState();
500
- const merged = [...new Set([...store.availableLanguages, ...effectiveLanguages])];
713
+ const merged = [...new Set([...store.availableLanguages, ...languagesList])];
501
714
  useTranslationStore.setState({
502
715
  availableLanguages: merged,
503
716
  defaultLanguage
@@ -505,97 +718,32 @@ function CTProvider({ children, translations, manifest, defaultLanguage = "en",
505
718
  }
506
719
  }, [
507
720
  availableLanguages,
508
- manifest,
721
+ effectiveManifest,
509
722
  defaultLanguage
510
723
  ]);
511
- const loadLanguageFromCDN = useCallback(async (language, isPreload = false) => {
512
- const currentManifest = manifestRef.current;
513
- if (!currentManifest) {
514
- if (!isPreload) setReady(true);
515
- return false;
516
- }
517
- const cdnUrl = currentManifest.cdnUrls[language];
518
- if (!cdnUrl) {
519
- if (!isPreload) setReady(true);
520
- return false;
521
- }
522
- if (loadedUrlsRef.current.get(language) === cdnUrl) {
523
- if (!isPreload) setReady(true);
524
- return true;
525
- }
526
- if (!isPreload) {
527
- setLoading(true);
528
- setReady(false);
529
- }
530
- try {
531
- const cached = await getCachedTranslation(cdnUrl);
532
- if (cached) {
533
- addLanguage(language, cached);
534
- loadedUrlsRef.current.set(language, cdnUrl);
535
- if (!isPreload) {
536
- setReady(true);
537
- setLoading(false);
538
- }
539
- return true;
540
- }
541
- const translationData = await fetchTranslationsFromCDN(cdnUrl);
542
- addLanguage(language, translationData);
543
- loadedUrlsRef.current.set(language, cdnUrl);
544
- await cacheTranslation(cdnUrl, language, currentManifest.projectId, translationData);
545
- if (!isPreload) setReady(true);
546
- return true;
547
- } catch (error) {
548
- console.error(`[ciao-tools] Failed to load translations for ${language}:`, error);
549
- if (!isPreload) setReady(true);
550
- return false;
551
- } finally {
552
- if (!isPreload) setLoading(false);
553
- }
554
- }, [
555
- addLanguage,
556
- setLoading,
557
- setReady
558
- ]);
559
724
  useEffect(() => {
560
725
  if (!isHydrated) return;
561
- if (currentLanguage === manifest?.sourceLanguage) {
726
+ if (currentLanguage === effectiveManifest?.sourceLanguage) {
562
727
  setReady(true);
563
728
  return;
564
729
  }
565
- if (manifest && currentLanguage) loadLanguageFromCDN(currentLanguage, false);
566
- else if (!manifest) setReady(true);
730
+ if (effectiveManifest && currentLanguage) loadLanguage(currentLanguage, false);
731
+ else if (!effectiveManifest) setReady(true);
567
732
  }, [
568
- manifest,
733
+ effectiveManifest,
569
734
  currentLanguage,
570
- loadLanguageFromCDN,
735
+ loadLanguage,
571
736
  setReady,
572
737
  isHydrated
573
738
  ]);
574
- useEffect(() => {
575
- if (!preloadLanguages || !manifest) return;
576
- if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
577
- preloadTimeoutRef.current = setTimeout(async () => {
578
- const languages = [...manifest.languages];
579
- for (const language of languages) {
580
- if (language === manifest.sourceLanguage) continue;
581
- if (language === currentLanguage) continue;
582
- await loadLanguageFromCDN(language, true);
583
- }
584
- }, preloadDelay);
585
- return () => {
586
- if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
587
- };
588
- }, [
589
- manifest,
590
- currentLanguage,
591
- preloadLanguages,
592
- preloadDelay,
593
- loadLanguageFromCDN
594
- ]);
595
739
  useEffect(() => {
596
740
  if (onLanguageChange) onLanguageChange(currentLanguage);
597
741
  }, [currentLanguage, onLanguageChange]);
598
- useHotUpdates(hotUpdates, manifest?.projectId);
742
+ useEffect(() => {
743
+ return () => {
744
+ abortControllerRef.current?.abort();
745
+ };
746
+ }, [abortControllerRef]);
599
747
  if (blockUntilReady && (!isReady || !isHydrated)) return /* @__PURE__ */ React.createElement(React.Fragment, null, fallback);
600
748
  return /* @__PURE__ */ React.createElement(React.Fragment, null, children);
601
749
  }
@@ -887,6 +1035,7 @@ function useCt() {
887
1035
 
888
1036
  //#endregion
889
1037
  //#region src/components/Trans.tsx
1038
+ const TAG_REGEX = /<(\d+)>(.*?)<\/\1>/gs;
890
1039
  function parseChildren(children) {
891
1040
  const elements = [];
892
1041
  let template = "";
@@ -912,11 +1061,10 @@ function reconstructChildren(translated, elements) {
912
1061
  if (elements.length === 0) return translated;
913
1062
  const result = [];
914
1063
  let keyCounter = 0;
915
- const tagRegex = /<(\d+)>(.*?)<\/\1>/gs;
916
1064
  let lastIndex = 0;
917
1065
  let match;
918
- tagRegex.lastIndex = 0;
919
- while ((match = tagRegex.exec(translated)) !== null) {
1066
+ TAG_REGEX.lastIndex = 0;
1067
+ while ((match = TAG_REGEX.exec(translated)) !== null) {
920
1068
  if (match.index > lastIndex) {
921
1069
  const textBefore = translated.slice(lastIndex, match.index);
922
1070
  if (textBefore) result.push(textBefore);
@@ -986,6 +1134,11 @@ function useLanguage() {
986
1134
  availableLanguagesInfo: availableLanguages.map(getFullLanguageInfo),
987
1135
  setLanguage,
988
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;
989
1142
  setLanguage(availableLanguages[(availableLanguages.indexOf(currentLanguage) + 1) % availableLanguages.length]);
990
1143
  }, [
991
1144
  availableLanguages,
@@ -998,5 +1151,5 @@ function useLanguage() {
998
1151
  }
999
1152
 
1000
1153
  //#endregion
1001
- 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 };
1002
1155
  //# sourceMappingURL=index.js.map