experimental-ciao-react 1.0.0

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 ADDED
@@ -0,0 +1,1000 @@
1
+ let react_jsx_runtime = require("react/jsx-runtime");
2
+ let react = require("react");
3
+ let zustand = require("zustand");
4
+ let zustand_middleware = require("zustand/middleware");
5
+
6
+ //#region src/components/CTContextBlock.tsx
7
+ function CTContextBlock({ children }) {
8
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
9
+ }
10
+
11
+ //#endregion
12
+ //#region src/cache.ts
13
+ const DB_NAME = "ciao-tools-translations";
14
+ const DB_VERSION = 1;
15
+ const STORE_NAME = "translations";
16
+ let dbPromise = null;
17
+ function openDB() {
18
+ if (dbPromise) return dbPromise;
19
+ dbPromise = new Promise((resolve, reject) => {
20
+ if (typeof indexedDB === "undefined") {
21
+ reject(/* @__PURE__ */ new Error("IndexedDB not available"));
22
+ return;
23
+ }
24
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
25
+ request.onerror = () => reject(request.error);
26
+ request.onsuccess = () => resolve(request.result);
27
+ request.onupgradeneeded = (event) => {
28
+ const db = event.target.result;
29
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
30
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "url" });
31
+ store.createIndex("language", "language", { unique: false });
32
+ store.createIndex("projectId", "projectId", { unique: false });
33
+ }
34
+ };
35
+ });
36
+ return dbPromise;
37
+ }
38
+ async function getCachedTranslation(url) {
39
+ try {
40
+ const db = await openDB();
41
+ return new Promise((resolve, reject) => {
42
+ const request = db.transaction(STORE_NAME, "readonly").objectStore(STORE_NAME).get(url);
43
+ request.onerror = () => reject(request.error);
44
+ request.onsuccess = () => {
45
+ const entry = request.result;
46
+ resolve(entry?.data ?? null);
47
+ };
48
+ });
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+ async function cacheTranslation(url, language, projectId, data) {
54
+ try {
55
+ const db = await openDB();
56
+ return new Promise((resolve, reject) => {
57
+ const store = db.transaction(STORE_NAME, "readwrite").objectStore(STORE_NAME);
58
+ const entry = {
59
+ url,
60
+ language,
61
+ projectId,
62
+ data,
63
+ cachedAt: Date.now()
64
+ };
65
+ const request = store.put(entry);
66
+ request.onerror = () => reject(request.error);
67
+ request.onsuccess = () => resolve();
68
+ });
69
+ } catch {}
70
+ }
71
+ async function clearCache(projectId) {
72
+ try {
73
+ const db = await openDB();
74
+ return new Promise((resolve, reject) => {
75
+ const transaction = db.transaction(STORE_NAME, "readwrite");
76
+ const store = transaction.objectStore(STORE_NAME);
77
+ if (projectId) {
78
+ const request = store.index("projectId").openCursor(IDBKeyRange.only(projectId));
79
+ request.onsuccess = (event) => {
80
+ const cursor = event.target.result;
81
+ if (cursor) {
82
+ cursor.delete();
83
+ cursor.continue();
84
+ }
85
+ };
86
+ transaction.oncomplete = () => resolve();
87
+ transaction.onerror = () => reject(transaction.error);
88
+ } else {
89
+ const request = store.clear();
90
+ request.onerror = () => reject(request.error);
91
+ request.onsuccess = () => resolve();
92
+ }
93
+ });
94
+ } catch {}
95
+ }
96
+ async function getCacheStats() {
97
+ try {
98
+ const db = await openDB();
99
+ return new Promise((resolve, reject) => {
100
+ const transaction = db.transaction(STORE_NAME, "readonly");
101
+ const store = transaction.objectStore(STORE_NAME);
102
+ const countRequest = store.count();
103
+ const languages = /* @__PURE__ */ new Set();
104
+ const cursorRequest = store.openCursor();
105
+ cursorRequest.onsuccess = (event) => {
106
+ const cursor = event.target.result;
107
+ if (cursor) {
108
+ languages.add(cursor.value.language);
109
+ cursor.continue();
110
+ }
111
+ };
112
+ transaction.oncomplete = () => {
113
+ resolve({
114
+ count: countRequest.result,
115
+ languages: Array.from(languages)
116
+ });
117
+ };
118
+ transaction.onerror = () => reject(transaction.error);
119
+ });
120
+ } catch {
121
+ return {
122
+ count: 0,
123
+ languages: []
124
+ };
125
+ }
126
+ }
127
+
128
+ //#endregion
129
+ //#region src/interpolate.ts
130
+ const numberFormatCache = /* @__PURE__ */ new Map();
131
+ const dateFormatCache = /* @__PURE__ */ new Map();
132
+ const pluralRulesCache = /* @__PURE__ */ new Map();
133
+ function clearFormatterCache() {
134
+ numberFormatCache.clear();
135
+ dateFormatCache.clear();
136
+ pluralRulesCache.clear();
137
+ }
138
+ function getNumberFormatter(locale) {
139
+ const key = `number:${locale}`;
140
+ if (!numberFormatCache.has(key)) numberFormatCache.set(key, new Intl.NumberFormat(locale, { maximumFractionDigits: 20 }));
141
+ return numberFormatCache.get(key);
142
+ }
143
+ function getCurrencyFormatter(locale, currency) {
144
+ const key = `currency:${locale}:${currency}`;
145
+ if (!numberFormatCache.has(key)) numberFormatCache.set(key, new Intl.NumberFormat(locale, {
146
+ style: "currency",
147
+ currency
148
+ }));
149
+ return numberFormatCache.get(key);
150
+ }
151
+ function getPercentFormatter(locale) {
152
+ const key = `percent:${locale}`;
153
+ if (!numberFormatCache.has(key)) numberFormatCache.set(key, new Intl.NumberFormat(locale, {
154
+ style: "percent",
155
+ minimumFractionDigits: 0,
156
+ maximumFractionDigits: 2
157
+ }));
158
+ return numberFormatCache.get(key);
159
+ }
160
+ function getDateFormatter(locale, style) {
161
+ const key = `date:${locale}:${style}`;
162
+ if (!dateFormatCache.has(key)) {
163
+ const options = { dateStyle: style };
164
+ dateFormatCache.set(key, new Intl.DateTimeFormat(locale, options));
165
+ }
166
+ return dateFormatCache.get(key);
167
+ }
168
+ function getTimeFormatter(locale, style) {
169
+ const key = `time:${locale}:${style}`;
170
+ if (!dateFormatCache.has(key)) {
171
+ const options = { timeStyle: style };
172
+ dateFormatCache.set(key, new Intl.DateTimeFormat(locale, options));
173
+ }
174
+ return dateFormatCache.get(key);
175
+ }
176
+ function getPluralRules(locale) {
177
+ if (!pluralRulesCache.has(locale)) pluralRulesCache.set(locale, new Intl.PluralRules(locale));
178
+ return pluralRulesCache.get(locale);
179
+ }
180
+ function formatValue(value, format, locale) {
181
+ if (value === null || value === void 0) return "";
182
+ if (!format) return String(value);
183
+ const parts = format.split(":");
184
+ switch (parts[0]) {
185
+ case "number": {
186
+ const num = typeof value === "number" ? value : Number(value);
187
+ if (Number.isNaN(num)) return String(value);
188
+ return getNumberFormatter(locale).format(num);
189
+ }
190
+ case "currency": {
191
+ const currency = parts[1];
192
+ if (!currency) return String(value);
193
+ const num = typeof value === "number" ? value : Number(value);
194
+ if (Number.isNaN(num)) return String(value);
195
+ return getCurrencyFormatter(locale, currency).format(num);
196
+ }
197
+ case "percent": {
198
+ const num = typeof value === "number" ? value : Number(value);
199
+ if (Number.isNaN(num)) return String(value);
200
+ return getPercentFormatter(locale).format(num);
201
+ }
202
+ case "date":
203
+ if (!(value instanceof Date)) return String(value);
204
+ return getDateFormatter(locale, parts[1] || "medium").format(value);
205
+ case "time":
206
+ if (!(value instanceof Date)) return String(value);
207
+ return getTimeFormatter(locale, parts[1] || "medium").format(value);
208
+ case "plural": {
209
+ const singular = parts[1];
210
+ const plural = parts[2];
211
+ if (!singular || !plural) return String(value);
212
+ const num = typeof value === "number" ? value : Number(value);
213
+ return getPluralRules(locale).select(num) === "one" ? singular : plural;
214
+ }
215
+ default: return String(value);
216
+ }
217
+ }
218
+ function interpolate(text, values, locale) {
219
+ const escaped = text.replace(/\{\{/g, "\0").replace(/\}\}/g, "");
220
+ let result;
221
+ if (!values || Object.keys(values).length === 0) result = escaped;
222
+ else result = escaped.replace(/\{([^}]+)\}/g, (match, placeholder) => {
223
+ const trimmed = placeholder.trim();
224
+ const colonIndex = trimmed.indexOf(":");
225
+ let key;
226
+ let format;
227
+ if (colonIndex === -1) {
228
+ key = trimmed;
229
+ format = void 0;
230
+ } else {
231
+ key = trimmed.substring(0, colonIndex);
232
+ format = trimmed.substring(colonIndex + 1);
233
+ }
234
+ if (!(key in values)) return match;
235
+ const value = values[key];
236
+ return formatValue(value, format, locale);
237
+ });
238
+ return result.replace(/\x00/g, "{").replace(/\x01/g, "}");
239
+ }
240
+
241
+ //#endregion
242
+ //#region src/store.ts
243
+ const useTranslationStore = (0, zustand.create)()((0, zustand_middleware.persist)((set) => ({
244
+ currentLanguage: "en",
245
+ defaultLanguage: "en",
246
+ availableLanguages: ["en"],
247
+ translations: {},
248
+ isLoading: false,
249
+ isReady: false,
250
+ serverVersion: null,
251
+ lastVersionCheck: null,
252
+ setLanguage: (language) => {
253
+ set({
254
+ currentLanguage: language,
255
+ isReady: false
256
+ });
257
+ },
258
+ loadTranslations: (translations) => {
259
+ set((state) => {
260
+ const languages = Object.keys(translations);
261
+ const availableLanguages = [...new Set([...state.availableLanguages, ...languages])];
262
+ const merged = { ...state.translations };
263
+ for (const lang of languages) merged[lang] = {
264
+ ...merged[lang],
265
+ ...translations[lang]
266
+ };
267
+ return {
268
+ translations: merged,
269
+ availableLanguages
270
+ };
271
+ });
272
+ },
273
+ addLanguage: (language, translations) => {
274
+ set((state) => ({
275
+ translations: {
276
+ ...state.translations,
277
+ [language]: {
278
+ ...state.translations[language],
279
+ ...translations
280
+ }
281
+ },
282
+ availableLanguages: state.availableLanguages.includes(language) ? state.availableLanguages : [...state.availableLanguages, language]
283
+ }));
284
+ },
285
+ setLoading: (loading) => {
286
+ set({ isLoading: loading });
287
+ },
288
+ setReady: (ready) => {
289
+ set({ isReady: ready });
290
+ },
291
+ setServerVersion: (version) => {
292
+ set({
293
+ serverVersion: version,
294
+ lastVersionCheck: Date.now()
295
+ });
296
+ }
297
+ }), {
298
+ name: "ciao-tools-language",
299
+ partialize: (state) => ({ currentLanguage: state.currentLanguage })
300
+ }));
301
+ function getTranslation(translations, language, text, values) {
302
+ const translated = translations[language]?.[text] ?? text;
303
+ if (!values || Object.keys(values).length === 0) return translated;
304
+ return interpolate(translated, values, language);
305
+ }
306
+
307
+ //#endregion
308
+ //#region src/hooks/useHotUpdates.ts
309
+ const CDN_BASE_URL = "https://t1.ciao-tools.com";
310
+ async function fetchLatestManifest(projectId) {
311
+ const url = `${CDN_BASE_URL}/translations/${projectId}/latest.json`;
312
+ try {
313
+ const response = await fetch(url, { cache: "no-cache" });
314
+ if (!response.ok) {
315
+ if (response.status === 404) return null;
316
+ throw new Error(`Failed to fetch latest manifest: ${response.statusText}`);
317
+ }
318
+ return response.json();
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+ async function fetchTranslations(url) {
324
+ const response = await fetch(url);
325
+ if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
326
+ return response.json();
327
+ }
328
+ function useHotUpdates(config, projectId) {
329
+ const isCheckingRef = (0, react.useRef)(false);
330
+ const hasCheckedOnMountRef = (0, react.useRef)(false);
331
+ const { serverVersion, setServerVersion, addLanguage } = useTranslationStore();
332
+ const checkForUpdates = (0, react.useCallback)(async () => {
333
+ if (!config || !projectId || isCheckingRef.current) return;
334
+ if (config.enabled === false) return;
335
+ isCheckingRef.current = true;
336
+ try {
337
+ console.log("[ciao-tools] Checking for hot updates...");
338
+ const manifest = await fetchLatestManifest(projectId);
339
+ if (!manifest) {
340
+ console.log("[ciao-tools] No latest.json found (project may not have hot updates yet)");
341
+ return;
342
+ }
343
+ console.log("[ciao-tools] Found latest.json, version:", manifest.version);
344
+ const isFirstCheck = serverVersion === null;
345
+ const hasNewVersion = serverVersion !== null && manifest.version > serverVersion;
346
+ if (isFirstCheck || hasNewVersion) {
347
+ if (Object.keys(manifest.urls).length > 0) {
348
+ console.log("[ciao-tools] Fetching updated translations...");
349
+ await clearCache(projectId);
350
+ const updatedLanguages = [];
351
+ for (const [langCode, url] of Object.entries(manifest.urls)) try {
352
+ const translations = await fetchTranslations(url);
353
+ addLanguage(langCode, translations);
354
+ await cacheTranslation(url, langCode, projectId, translations);
355
+ updatedLanguages.push(langCode);
356
+ } catch (err) {
357
+ console.error(`[ciao-tools] Failed to fetch ${langCode} translations:`, err);
358
+ }
359
+ if (updatedLanguages.length > 0) {
360
+ console.log("[ciao-tools] Updated translations for:", updatedLanguages);
361
+ if (hasNewVersion && config.onTranslationsUpdated) config.onTranslationsUpdated(updatedLanguages);
362
+ }
363
+ }
364
+ } else console.log("[ciao-tools] Already up to date (version " + manifest.version + ")");
365
+ setServerVersion(manifest.version);
366
+ } catch (error) {
367
+ console.error("[ciao-tools] Hot update check failed:", error);
368
+ } finally {
369
+ isCheckingRef.current = false;
370
+ }
371
+ }, [
372
+ config,
373
+ projectId,
374
+ serverVersion,
375
+ setServerVersion,
376
+ addLanguage
377
+ ]);
378
+ (0, react.useEffect)(() => {
379
+ if (!config || !projectId) return;
380
+ if (config.enabled === false) return;
381
+ if (!hasCheckedOnMountRef.current) {
382
+ hasCheckedOnMountRef.current = true;
383
+ checkForUpdates();
384
+ }
385
+ const handleVisibilityChange = () => {
386
+ if (document.visibilityState === "visible") checkForUpdates();
387
+ };
388
+ document.addEventListener("visibilitychange", handleVisibilityChange);
389
+ return () => {
390
+ document.removeEventListener("visibilitychange", handleVisibilityChange);
391
+ };
392
+ }, [
393
+ config,
394
+ projectId,
395
+ checkForUpdates
396
+ ]);
397
+ return { checkForUpdates };
398
+ }
399
+
400
+ //#endregion
401
+ //#region src/components/CTProvider.tsx
402
+ const PRELOAD_DELAY_MS = 5e3;
403
+ async function fetchTranslationsFromCDN(url) {
404
+ const response = await fetch(url);
405
+ if (!response.ok) throw new Error(`Failed to fetch translations: ${response.statusText}`);
406
+ return response.json();
407
+ }
408
+ function detectBrowserLanguage(availableLanguages) {
409
+ if (typeof navigator === "undefined") return null;
410
+ const browserLangs = navigator.languages || [navigator.language];
411
+ for (const browserLang of browserLangs) {
412
+ const normalized = browserLang.toLowerCase();
413
+ if (availableLanguages.includes(normalized)) return normalized;
414
+ const langCode = normalized.split("-")[0];
415
+ if (availableLanguages.includes(langCode)) return langCode;
416
+ }
417
+ return null;
418
+ }
419
+ function getManifestUrlsHash(manifest) {
420
+ if (!manifest) return "";
421
+ return JSON.stringify(manifest.cdnUrls);
422
+ }
423
+ function CTProvider({ children, translations, manifest, defaultLanguage = "en", availableLanguages, onLanguageChange, detectLanguage = true, blockUntilReady = false, fallback = null, preloadLanguages = true, preloadDelay = PRELOAD_DELAY_MS, hotUpdates }) {
424
+ const { loadTranslations, setLanguage, addLanguage, setLoading, setReady, currentLanguage, translations: storeTranslations, isReady } = useTranslationStore();
425
+ const manifestRef = (0, react.useRef)(manifest);
426
+ const loadedUrlsRef = (0, react.useRef)(/* @__PURE__ */ new Map());
427
+ const previousManifestHashRef = (0, react.useRef)("");
428
+ const initializedRef = (0, react.useRef)(false);
429
+ const preloadTimeoutRef = (0, react.useRef)(null);
430
+ (0, react.useEffect)(() => {
431
+ manifestRef.current = manifest;
432
+ const newHash = getManifestUrlsHash(manifest);
433
+ const oldHash = previousManifestHashRef.current;
434
+ if (oldHash && newHash && oldHash !== newHash) {
435
+ loadedUrlsRef.current.clear();
436
+ useTranslationStore.setState({ translations: {} });
437
+ }
438
+ previousManifestHashRef.current = newHash;
439
+ }, [manifest]);
440
+ (0, react.useEffect)(() => {
441
+ if (translations) {
442
+ loadTranslations(translations);
443
+ setReady(true);
444
+ }
445
+ }, [
446
+ translations,
447
+ loadTranslations,
448
+ setReady
449
+ ]);
450
+ (0, react.useEffect)(() => {
451
+ if (initializedRef.current) return;
452
+ initializedRef.current = true;
453
+ const store = useTranslationStore.getState();
454
+ const effectiveLanguages = manifest ? [...manifest.languages] : availableLanguages || [];
455
+ if (store.currentLanguage && store.currentLanguage !== "en" && effectiveLanguages.includes(store.currentLanguage)) return;
456
+ if (detectLanguage && effectiveLanguages.length > 0) {
457
+ const detected = detectBrowserLanguage(effectiveLanguages);
458
+ if (detected) {
459
+ setLanguage(detected);
460
+ return;
461
+ }
462
+ }
463
+ if (defaultLanguage && defaultLanguage !== store.currentLanguage) setLanguage(defaultLanguage);
464
+ }, [
465
+ manifest,
466
+ availableLanguages,
467
+ defaultLanguage,
468
+ detectLanguage,
469
+ setLanguage
470
+ ]);
471
+ (0, react.useEffect)(() => {
472
+ const effectiveLanguages = manifest ? [...manifest.languages] : availableLanguages;
473
+ if (effectiveLanguages) {
474
+ const store = useTranslationStore.getState();
475
+ const merged = [...new Set([...store.availableLanguages, ...effectiveLanguages])];
476
+ useTranslationStore.setState({
477
+ availableLanguages: merged,
478
+ defaultLanguage
479
+ });
480
+ }
481
+ }, [
482
+ availableLanguages,
483
+ manifest,
484
+ defaultLanguage
485
+ ]);
486
+ const loadLanguageFromCDN = (0, react.useCallback)(async (language, isPreload = false) => {
487
+ const currentManifest = manifestRef.current;
488
+ if (!currentManifest) {
489
+ if (!isPreload) setReady(true);
490
+ return false;
491
+ }
492
+ const cdnUrl = currentManifest.cdnUrls[language];
493
+ if (!cdnUrl) {
494
+ if (!isPreload) setReady(true);
495
+ return false;
496
+ }
497
+ if (loadedUrlsRef.current.get(language) === cdnUrl) {
498
+ if (!isPreload) setReady(true);
499
+ return true;
500
+ }
501
+ if (!isPreload) {
502
+ setLoading(true);
503
+ setReady(false);
504
+ }
505
+ try {
506
+ const cached = await getCachedTranslation(cdnUrl);
507
+ if (cached) {
508
+ addLanguage(language, cached);
509
+ loadedUrlsRef.current.set(language, cdnUrl);
510
+ if (!isPreload) {
511
+ setReady(true);
512
+ setLoading(false);
513
+ }
514
+ return true;
515
+ }
516
+ const translationData = await fetchTranslationsFromCDN(cdnUrl);
517
+ addLanguage(language, translationData);
518
+ loadedUrlsRef.current.set(language, cdnUrl);
519
+ await cacheTranslation(cdnUrl, language, currentManifest.projectId, translationData);
520
+ if (!isPreload) setReady(true);
521
+ return true;
522
+ } catch (error) {
523
+ console.error(`[ciao-tools] Failed to load translations for ${language}:`, error);
524
+ if (!isPreload) setReady(true);
525
+ return false;
526
+ } finally {
527
+ if (!isPreload) setLoading(false);
528
+ }
529
+ }, [
530
+ addLanguage,
531
+ setLoading,
532
+ setReady
533
+ ]);
534
+ (0, react.useEffect)(() => {
535
+ if (currentLanguage === manifest?.sourceLanguage) {
536
+ setReady(true);
537
+ return;
538
+ }
539
+ if (manifest && currentLanguage) loadLanguageFromCDN(currentLanguage, false);
540
+ else if (!manifest) setReady(true);
541
+ }, [
542
+ manifest,
543
+ currentLanguage,
544
+ loadLanguageFromCDN,
545
+ setReady
546
+ ]);
547
+ (0, react.useEffect)(() => {
548
+ if (!preloadLanguages || !manifest) return;
549
+ if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
550
+ preloadTimeoutRef.current = setTimeout(async () => {
551
+ const languages = [...manifest.languages];
552
+ for (const language of languages) {
553
+ if (language === manifest.sourceLanguage) continue;
554
+ if (language === currentLanguage) continue;
555
+ await loadLanguageFromCDN(language, true);
556
+ }
557
+ }, preloadDelay);
558
+ return () => {
559
+ if (preloadTimeoutRef.current) clearTimeout(preloadTimeoutRef.current);
560
+ };
561
+ }, [
562
+ manifest,
563
+ currentLanguage,
564
+ preloadLanguages,
565
+ preloadDelay,
566
+ loadLanguageFromCDN
567
+ ]);
568
+ (0, react.useEffect)(() => {
569
+ if (onLanguageChange) onLanguageChange(currentLanguage);
570
+ }, [currentLanguage, onLanguageChange]);
571
+ useHotUpdates(hotUpdates, manifest?.projectId);
572
+ if (blockUntilReady && !isReady) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: fallback });
573
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children });
574
+ }
575
+
576
+ //#endregion
577
+ //#region src/components/LanguageSwitcher.tsx
578
+ const LANGUAGE_DATA = {
579
+ en: {
580
+ name: "English",
581
+ nativeName: "English",
582
+ flag: "🇺🇸"
583
+ },
584
+ es: {
585
+ name: "Spanish",
586
+ nativeName: "Español",
587
+ flag: "🇪🇸"
588
+ },
589
+ fr: {
590
+ name: "French",
591
+ nativeName: "Français",
592
+ flag: "🇫🇷"
593
+ },
594
+ de: {
595
+ name: "German",
596
+ nativeName: "Deutsch",
597
+ flag: "🇩🇪"
598
+ },
599
+ it: {
600
+ name: "Italian",
601
+ nativeName: "Italiano",
602
+ flag: "🇮🇹"
603
+ },
604
+ pt: {
605
+ name: "Portuguese",
606
+ nativeName: "Português",
607
+ flag: "🇵🇹"
608
+ },
609
+ ja: {
610
+ name: "Japanese",
611
+ nativeName: "日本語",
612
+ flag: "🇯🇵"
613
+ },
614
+ ko: {
615
+ name: "Korean",
616
+ nativeName: "한국어",
617
+ flag: "🇰🇷"
618
+ },
619
+ zh: {
620
+ name: "Chinese",
621
+ nativeName: "中文",
622
+ flag: "🇨🇳"
623
+ },
624
+ ar: {
625
+ name: "Arabic",
626
+ nativeName: "العربية",
627
+ flag: "🇸🇦"
628
+ },
629
+ ru: {
630
+ name: "Russian",
631
+ nativeName: "Русский",
632
+ flag: "🇷🇺"
633
+ },
634
+ nl: {
635
+ name: "Dutch",
636
+ nativeName: "Nederlands",
637
+ flag: "🇳🇱"
638
+ },
639
+ pl: {
640
+ name: "Polish",
641
+ nativeName: "Polski",
642
+ flag: "🇵🇱"
643
+ },
644
+ sv: {
645
+ name: "Swedish",
646
+ nativeName: "Svenska",
647
+ flag: "🇸🇪"
648
+ },
649
+ da: {
650
+ name: "Danish",
651
+ nativeName: "Dansk",
652
+ flag: "🇩🇰"
653
+ },
654
+ fi: {
655
+ name: "Finnish",
656
+ nativeName: "Suomi",
657
+ flag: "🇫🇮"
658
+ },
659
+ no: {
660
+ name: "Norwegian",
661
+ nativeName: "Norsk",
662
+ flag: "🇳🇴"
663
+ },
664
+ tr: {
665
+ name: "Turkish",
666
+ nativeName: "Türkçe",
667
+ flag: "🇹🇷"
668
+ },
669
+ cs: {
670
+ name: "Czech",
671
+ nativeName: "Čeština",
672
+ flag: "🇨🇿"
673
+ },
674
+ el: {
675
+ name: "Greek",
676
+ nativeName: "Ελληνικά",
677
+ flag: "🇬🇷"
678
+ },
679
+ he: {
680
+ name: "Hebrew",
681
+ nativeName: "עברית",
682
+ flag: "🇮🇱"
683
+ },
684
+ hu: {
685
+ name: "Hungarian",
686
+ nativeName: "Magyar",
687
+ flag: "🇭🇺"
688
+ },
689
+ id: {
690
+ name: "Indonesian",
691
+ nativeName: "Bahasa Indonesia",
692
+ flag: "🇮🇩"
693
+ },
694
+ th: {
695
+ name: "Thai",
696
+ nativeName: "ไทย",
697
+ flag: "🇹🇭"
698
+ },
699
+ vi: {
700
+ name: "Vietnamese",
701
+ nativeName: "Tiếng Việt",
702
+ flag: "🇻🇳"
703
+ },
704
+ uk: {
705
+ name: "Ukrainian",
706
+ nativeName: "Українська",
707
+ flag: "🇺🇦"
708
+ },
709
+ ro: {
710
+ name: "Romanian",
711
+ nativeName: "Română",
712
+ flag: "🇷🇴"
713
+ },
714
+ bg: {
715
+ name: "Bulgarian",
716
+ nativeName: "Български",
717
+ flag: "🇧🇬"
718
+ },
719
+ sk: {
720
+ name: "Slovak",
721
+ nativeName: "Slovenčina",
722
+ flag: "🇸🇰"
723
+ },
724
+ lt: {
725
+ name: "Lithuanian",
726
+ nativeName: "Lietuvių",
727
+ flag: "🇱🇹"
728
+ },
729
+ lv: {
730
+ name: "Latvian",
731
+ nativeName: "Latviešu",
732
+ flag: "🇱🇻"
733
+ },
734
+ et: {
735
+ name: "Estonian",
736
+ nativeName: "Eesti",
737
+ flag: "🇪🇪"
738
+ },
739
+ sl: {
740
+ name: "Slovenian",
741
+ nativeName: "Slovenščina",
742
+ flag: "🇸🇮"
743
+ },
744
+ bs: {
745
+ name: "Bosnian",
746
+ nativeName: "Bosanski",
747
+ flag: "🇧🇦"
748
+ },
749
+ hr: {
750
+ name: "Croatian",
751
+ nativeName: "Hrvatski",
752
+ flag: "🇭🇷"
753
+ },
754
+ sr: {
755
+ name: "Serbian",
756
+ nativeName: "Српски",
757
+ flag: "🇷🇸"
758
+ },
759
+ kmr: {
760
+ name: "Kurdish",
761
+ nativeName: "Kurdî",
762
+ flag: "🇮🇶"
763
+ },
764
+ fa: {
765
+ name: "Persian",
766
+ nativeName: "فارسی",
767
+ flag: "🇮🇷"
768
+ },
769
+ hi: {
770
+ name: "Hindi",
771
+ nativeName: "हिन्दी",
772
+ flag: "🇮🇳"
773
+ },
774
+ bn: {
775
+ name: "Bengali",
776
+ nativeName: "বাংলা",
777
+ flag: "🇧🇩"
778
+ },
779
+ ms: {
780
+ name: "Malay",
781
+ nativeName: "Bahasa Melayu",
782
+ flag: "🇲🇾"
783
+ }
784
+ };
785
+ function getLanguageInfo(code) {
786
+ return LANGUAGE_DATA[code.toLowerCase()] ?? {
787
+ name: code.toUpperCase(),
788
+ nativeName: code.toUpperCase(),
789
+ flag: "🌐"
790
+ };
791
+ }
792
+ function getFullLanguageInfo(code) {
793
+ return {
794
+ code,
795
+ ...getLanguageInfo(code)
796
+ };
797
+ }
798
+ function formatLanguageDisplay(code, display = "flag-native") {
799
+ const info = getLanguageInfo(code);
800
+ switch (display) {
801
+ case "flag": return info.flag;
802
+ case "name": return info.name;
803
+ case "native": return info.nativeName;
804
+ case "flag-name": return `${info.flag} ${info.name}`;
805
+ case "flag-native": return `${info.flag} ${info.nativeName}`;
806
+ case "code": return code.toUpperCase();
807
+ default: return `${info.flag} ${info.nativeName}`;
808
+ }
809
+ }
810
+ function LanguageSwitcher({ className, variant = "dropdown", display = "flag-native", onChange }) {
811
+ const { currentLanguage, availableLanguages, setLanguage } = useTranslationStore();
812
+ const handleChange = (newLanguage) => {
813
+ setLanguage(newLanguage);
814
+ onChange?.(newLanguage);
815
+ };
816
+ if (variant === "buttons") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
817
+ className,
818
+ role: "group",
819
+ "aria-label": "Language selection",
820
+ children: availableLanguages.map((lang) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
821
+ type: "button",
822
+ onClick: () => handleChange(lang),
823
+ "aria-pressed": currentLanguage === lang,
824
+ "data-active": currentLanguage === lang,
825
+ children: formatLanguageDisplay(lang, display)
826
+ }, lang))
827
+ });
828
+ if (variant === "minimal") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
829
+ type: "button",
830
+ className,
831
+ onClick: () => {
832
+ handleChange(availableLanguages[(availableLanguages.indexOf(currentLanguage) + 1) % availableLanguages.length]);
833
+ },
834
+ "aria-label": `Current language: ${getLanguageInfo(currentLanguage).name}. Click to change.`,
835
+ children: formatLanguageDisplay(currentLanguage, display)
836
+ });
837
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
838
+ value: currentLanguage,
839
+ onChange: (e) => handleChange(e.target.value),
840
+ className,
841
+ "aria-label": "Select language",
842
+ children: availableLanguages.map((lang) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
843
+ value: lang,
844
+ children: formatLanguageDisplay(lang, display)
845
+ }, lang))
846
+ });
847
+ }
848
+
849
+ //#endregion
850
+ //#region src/hooks/useCt.ts
851
+ function isInterpolationValues(arg) {
852
+ return typeof arg === "object" && arg !== null && !Array.isArray(arg) && !(arg instanceof Date);
853
+ }
854
+ function useCt() {
855
+ const { translations, currentLanguage } = useTranslationStore();
856
+ return (0, react.useCallback)((text, contextOrValues, maybeValues) => {
857
+ let values;
858
+ if (typeof contextOrValues === "string") values = maybeValues;
859
+ else if (isInterpolationValues(contextOrValues)) values = contextOrValues;
860
+ return getTranslation(translations, currentLanguage, text, values);
861
+ }, [translations, currentLanguage]);
862
+ }
863
+
864
+ //#endregion
865
+ //#region src/components/Trans.tsx
866
+ function parseChildren(children) {
867
+ const elements = [];
868
+ let template = "";
869
+ const processNode = (node) => {
870
+ if (node === null || node === void 0) return "";
871
+ if (typeof node === "string") return node;
872
+ if (typeof node === "number") return String(node);
873
+ if ((0, react.isValidElement)(node)) {
874
+ const index = elements.length;
875
+ elements.push(node);
876
+ return `<${index}>${processNode(node.props.children)}</${index}>`;
877
+ }
878
+ if (Array.isArray(node)) return node.map(processNode).join("");
879
+ return "";
880
+ };
881
+ template = processNode(children);
882
+ return {
883
+ template,
884
+ elements
885
+ };
886
+ }
887
+ function reconstructChildren(translated, elements) {
888
+ if (elements.length === 0) return translated;
889
+ const result = [];
890
+ let keyCounter = 0;
891
+ const tagRegex = /<(\d+)>(.*?)<\/\1>/gs;
892
+ let lastIndex = 0;
893
+ let match;
894
+ tagRegex.lastIndex = 0;
895
+ while ((match = tagRegex.exec(translated)) !== null) {
896
+ if (match.index > lastIndex) {
897
+ const textBefore = translated.slice(lastIndex, match.index);
898
+ if (textBefore) result.push(textBefore);
899
+ }
900
+ const elementIndex = parseInt(match[1], 10);
901
+ const innerContent = match[2];
902
+ const originalElement = elements[elementIndex];
903
+ if (originalElement) {
904
+ const processedInner = reconstructChildren(innerContent, elements);
905
+ const cloned = (0, react.cloneElement)(originalElement, {
906
+ key: `trans-${keyCounter++}`,
907
+ children: processedInner
908
+ });
909
+ result.push(cloned);
910
+ }
911
+ lastIndex = match.index + match[0].length;
912
+ }
913
+ if (lastIndex < translated.length) result.push(translated.slice(lastIndex));
914
+ if (result.length === 0) return translated;
915
+ return result.length === 1 ? result[0] : result;
916
+ }
917
+ function Trans({ children, context, values }) {
918
+ const ct = useCt();
919
+ const { template, elements } = parseChildren(children);
920
+ let translated;
921
+ if (context && values) translated = ct(template, context, values);
922
+ else if (context) translated = ct(template, context);
923
+ else if (values) translated = ct(template, values);
924
+ else translated = ct(template);
925
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: reconstructChildren(translated, elements) });
926
+ }
927
+
928
+ //#endregion
929
+ //#region src/hooks/useLanguage.ts
930
+ function useCurrentLanguage() {
931
+ return useTranslationStore((state) => state.currentLanguage);
932
+ }
933
+ function useSetLanguage() {
934
+ return useTranslationStore((state) => state.setLanguage);
935
+ }
936
+ function useAvailableLanguages() {
937
+ return useTranslationStore((state) => state.availableLanguages);
938
+ }
939
+ function useIsLoading() {
940
+ return useTranslationStore((state) => state.isLoading);
941
+ }
942
+ function useIsReady() {
943
+ return useTranslationStore((state) => state.isReady);
944
+ }
945
+ function useLanguageInfo(code) {
946
+ const currentLanguage = useTranslationStore((state) => state.currentLanguage);
947
+ return getFullLanguageInfo(code ?? currentLanguage);
948
+ }
949
+ function useAvailableLanguagesInfo() {
950
+ return useTranslationStore((state) => state.availableLanguages).map(getFullLanguageInfo);
951
+ }
952
+ function useLanguage() {
953
+ const currentLanguage = useTranslationStore((state) => state.currentLanguage);
954
+ const availableLanguages = useTranslationStore((state) => state.availableLanguages);
955
+ const setLanguage = useTranslationStore((state) => state.setLanguage);
956
+ const isLoading = useTranslationStore((state) => state.isLoading);
957
+ const isReady = useTranslationStore((state) => state.isReady);
958
+ return {
959
+ currentLanguage,
960
+ currentLanguageInfo: getFullLanguageInfo(currentLanguage),
961
+ availableLanguages,
962
+ availableLanguagesInfo: availableLanguages.map(getFullLanguageInfo),
963
+ setLanguage,
964
+ cycleLanguage: (0, react.useCallback)(() => {
965
+ setLanguage(availableLanguages[(availableLanguages.indexOf(currentLanguage) + 1) % availableLanguages.length]);
966
+ }, [
967
+ availableLanguages,
968
+ currentLanguage,
969
+ setLanguage
970
+ ]),
971
+ isLoading,
972
+ isReady
973
+ };
974
+ }
975
+
976
+ //#endregion
977
+ exports.CTContextBlock = CTContextBlock;
978
+ exports.CTProvider = CTProvider;
979
+ exports.LANGUAGE_DATA = LANGUAGE_DATA;
980
+ exports.LanguageSwitcher = LanguageSwitcher;
981
+ exports.Trans = Trans;
982
+ exports.clearCache = clearCache;
983
+ exports.clearFormatterCache = clearFormatterCache;
984
+ exports.formatLanguageDisplay = formatLanguageDisplay;
985
+ exports.getCacheStats = getCacheStats;
986
+ exports.getFullLanguageInfo = getFullLanguageInfo;
987
+ exports.getLanguageInfo = getLanguageInfo;
988
+ exports.interpolate = interpolate;
989
+ exports.useAvailableLanguages = useAvailableLanguages;
990
+ exports.useAvailableLanguagesInfo = useAvailableLanguagesInfo;
991
+ exports.useCt = useCt;
992
+ exports.useCurrentLanguage = useCurrentLanguage;
993
+ exports.useHotUpdates = useHotUpdates;
994
+ exports.useIsLoading = useIsLoading;
995
+ exports.useIsReady = useIsReady;
996
+ exports.useLanguage = useLanguage;
997
+ exports.useLanguageInfo = useLanguageInfo;
998
+ exports.useSetLanguage = useSetLanguage;
999
+ exports.useTranslationStore = useTranslationStore;
1000
+ //# sourceMappingURL=index.cjs.map