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.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,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
- serverVersion: null,
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
- setServerVersion: (version) => {
228
+ setStoredManifest: (manifest) => {
325
229
  set({
326
- serverVersion: version,
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
- serverVersion: state.serverVersion,
338
- cachedLanguages: state.cachedLanguages
238
+ storedManifest: state.storedManifest
339
239
  }),
340
240
  onRehydrateStorage: () => (_, error) => {
341
- if (!error && hydrationResolver) hydrationResolver();
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 { serverVersion, setServerVersion, addLanguage, cachedLanguages, setCachedLanguages } = 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();
378
422
  const checkForUpdates = (0, react.useCallback)(async () => {
379
- if (!config || !projectId || isCheckingRef.current) return;
380
- 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;
381
428
  isCheckingRef.current = true;
382
429
  try {
383
430
  console.log("[ciao-tools] Checking for hot updates...");
384
- const manifest = await fetchLatestManifest(projectId);
385
- if (!manifest) {
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:", manifest.version);
390
- const isFirstCheck = serverVersion === null;
391
- const hasNewVersion = serverVersion !== null && manifest.version > serverVersion;
392
- const hasNoCache = cachedLanguages.length === 0;
393
- if (isFirstCheck || hasNewVersion || hasNoCache) {
394
- if (Object.keys(manifest.urls).length > 0) {
395
- const reason = hasNewVersion ? "new version" : hasNoCache ? "no cached translations" : "first check";
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(projectId);
445
+ if (hasNewVersion) await clearCache(currentProjectId);
398
446
  const updatedLanguages = [];
399
- for (const [langCode, url] of Object.entries(manifest.urls)) try {
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, projectId, translations);
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 && config.onTranslationsUpdated) config.onTranslationsUpdated(updatedLanguages);
457
+ if (hasNewVersion && currentConfig.onTranslationsUpdated) currentConfig.onTranslationsUpdated(updatedLanguages);
411
458
  }
412
459
  }
413
- } else console.log("[ciao-tools] Already up to date (version " + manifest.version + ")");
414
- 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 + ")");
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
- if (!config || !projectId) return;
431
- 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;
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/components/CTProvider.tsx
453
- const PRELOAD_DELAY_MS = 5e3;
454
- async function fetchTranslationsFromCDN(url) {
455
- 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 });
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
- function getManifestUrlsHash(manifest) {
471
- if (!manifest) return "";
472
- return JSON.stringify(manifest.cdnUrls);
473
- }
474
- function CTProvider({ children, translations, manifest, defaultLanguage = "en", availableLanguages, onLanguageChange, detectLanguage = true, blockUntilReady = true, fallback = null, preloadLanguages = true, preloadDelay = PRELOAD_DELAY_MS, hotUpdates }) {
475
- const { loadTranslations, setLanguage, addLanguage, setLoading, setReady, currentLanguage, translations: storeTranslations, isReady, isHydrated } = useTranslationStore();
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 = manifest ? [...manifest.languages] : availableLanguages || [];
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
- manifest,
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 effectiveLanguages = manifest ? [...manifest.languages] : availableLanguages;
526
- if (effectiveLanguages) {
738
+ const languagesList = effectiveManifest ? [...effectiveManifest.languages] : availableLanguages;
739
+ if (languagesList) {
527
740
  const store = useTranslationStore.getState();
528
- const merged = [...new Set([...store.availableLanguages, ...effectiveLanguages])];
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
- manifest,
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 === manifest?.sourceLanguage) {
754
+ if (currentLanguage === effectiveManifest?.sourceLanguage) {
590
755
  setReady(true);
591
756
  return;
592
757
  }
593
- if (manifest && currentLanguage) loadLanguageFromCDN(currentLanguage, false);
594
- else if (!manifest) setReady(true);
758
+ if (effectiveManifest && currentLanguage) loadLanguage(currentLanguage, false);
759
+ else if (!effectiveManifest) setReady(true);
595
760
  }, [
596
- manifest,
761
+ effectiveManifest,
597
762
  currentLanguage,
598
- loadLanguageFromCDN,
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
- useHotUpdates(hotUpdates, manifest?.projectId);
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
- tagRegex.lastIndex = 0;
947
- while ((match = tagRegex.exec(translated)) !== null) {
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 = 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;