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