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