UNPKG

@gulibs/react-vintl

Version:

Type-safe i18n library for React with Vite plugin and automatic type inference

471 lines (470 loc) 14.6 kB
import { n as logger, t as Logger } from "./logger-p3Rg7WdR.js"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; import { jsx } from "react/jsx-runtime"; function getNestedValue(e, m) { return m.split(".").reduce((e, m) => { if (typeof e == "object" && e && m in e) return e[m]; }, e); } function replaceParams(m, h = {}) { return m.replace(/\{\{([^}]+)\}\}/g, (m, g) => { try { return processInterpolation(g.trim(), h); } catch (h) { return logger.warn(`Failed to process interpolation "${m}":`, h), m; } }); } function processInterpolation(e, m) { let h = e.split(",").map((e) => e.trim()), g = h[0]; if (!g) throw Error("Missing parameter key"); let _ = getNestedParamValue(m, g); if (_ === void 0) throw Error(`Parameter "${g}" not found`); if (h.length === 1) return String(_); let v = h[1], y = h.slice(2); switch (v) { case "plural": return handlePlural(_, y); case "select": return handleSelect(_, y); case "number": return handleNumber(_, y); case "date": return handleDate(_, y); case "currency": return handleCurrency(_, y); default: return handleCustomFormat(_, v, y); } } function getNestedParamValue(e, m) { return m.split(".").reduce((e, m) => { if (typeof e == "object" && e && m in e) return e[m]; }, e); } function handlePlural(e, m) { let h = Number(e); if (isNaN(h)) return String(e); let g = {}; for (let e of m) { let [m, h] = e.split("=", 2).map((e) => e.trim()); m && h && (g[m] = h); } let _ = "other"; return h === 1 && "one" in g ? _ = "one" : h === 0 && "zero" in g ? _ = "zero" : h > 1 && "few" in g ? _ = "few" : h > 1 && "many" in g && (_ = "many"), (g[_] || g.other || String(e)).replace(/#/g, String(h)); } function handleSelect(e, m) { let h = String(e), g = {}; for (let e of m) { let [m, h] = e.split("=", 2).map((e) => e.trim()); m && h && (g[m] = h); } return g[h] || g.other || h; } function handleNumber(e, m) { let h = Number(e); if (isNaN(h)) return String(e); let g = {}; for (let e of m) { let [m, h] = e.split("=", 2).map((e) => e.trim()); if (m && h) { let e = Number(h); isNaN(e) || (g[m] = e); } } try { return new Intl.NumberFormat(void 0, g).format(h); } catch { return String(h); } } function handleDate(e, m) { let h; if (e instanceof Date) h = e; else if (typeof e == "string" || typeof e == "number") h = new Date(e); else return String(e); if (isNaN(h.getTime())) return String(e); let g = {}; for (let e of m) { let [m, h] = e.split("=", 2).map((e) => e.trim()); m && h && (g[m] = h); } try { return new Intl.DateTimeFormat(void 0, g).format(h); } catch { return h.toLocaleDateString(); } } function handleCurrency(e, m) { let h = Number(e); if (isNaN(h)) return String(e); let g = { style: "currency" }; for (let e of m) { let [m, h] = e.split("=", 2).map((e) => e.trim()); if (m && h) if (m === "currency") g.currency = h; else { let e = Number(h); isNaN(e) || (g[m] = e); } } try { return new Intl.NumberFormat(void 0, g).format(h); } catch { return String(h); } } function handleCustomFormat(e, m, h) { switch (m) { case "uppercase": return String(e).toUpperCase(); case "lowercase": return String(e).toLowerCase(); case "capitalize": return String(e).charAt(0).toUpperCase() + String(e).slice(1).toLowerCase(); default: return String(e); } } function validateParams(m) { if (!m || typeof m != "object") return !1; try { return JSON.stringify(m), !0; } catch { return logger.warn("Invalid params object: contains circular reference"), !1; } } function createTranslationFunction(m, h) { return (g, _) => { let v = m[h]; if (!v) return logger.warn(`Locale "${h}" not found in resources`), g; let y = getNestedValue(v, g); if (typeof y != "string") return logger.warn(`Translation key "${g}" not found or not a string in locale "${h}"`), g; if (_ && !validateParams(_)) return logger.warn(`Invalid params for key "${g}":`, _), y; try { return _ ? replaceParams(y, _) : y; } catch (m) { return logger.error(`Failed to process translation for key "${g}":`, m), y; } }; } function validateResources(m) { if (!m || typeof m != "object") return logger.error("Resources must be an object"), !1; let h = Object.keys(m); if (h.length === 0) return logger.error("Resources must contain at least one locale"), !1; let g = h[0], _ = getAllKeys(m[g]); for (let g of h.slice(1)) { let h = getAllKeys(m[g]), v = _.filter((e) => !h.includes(e)), y = h.filter((e) => !_.includes(e)); v.length > 0 && logger.warn(`Locale "${g}" is missing keys: ${v.join(", ")}`), y.length > 0 && logger.warn(`Locale "${g}" has extra keys: ${y.join(", ")}`); } return !0; } function deepMerge(e, m) { if (typeof e != "object" || !e || Array.isArray(e)) return m; if (typeof m != "object" || !m || Array.isArray(m)) return e; let h = { ...e }; for (let g in m) if (Object.prototype.hasOwnProperty.call(m, g)) { let _ = m[g], v = e[g]; typeof _ == "object" && _ && !Array.isArray(_) && typeof v == "object" && v && !Array.isArray(v) ? h[g] = deepMerge(v, _) : _ !== void 0 && (h[g] = _); } return h; } function mergeResources(m, h, g) { let _ = {}; for (let e in m) Object.prototype.hasOwnProperty.call(m, e) && (_[e] = { ...m[e] }); for (let m in h) { if (!Object.prototype.hasOwnProperty.call(h, m) || g?.locale && m !== g.locale) continue; let v = h[m]; if (!v || typeof v != "object") continue; _[m] ? _[m] = { ..._[m] } : _[m] = {}; let y = _[m]; if (g?.namespace) { y[g.namespace] || (y[g.namespace] = {}); let e; e = g.namespace in v && typeof v[g.namespace] == "object" && v[g.namespace] !== null && !Array.isArray(v[g.namespace]) ? v[g.namespace] : v; let m = deepMerge(y[g.namespace], e); y[g.namespace] = m; } else { for (let h in logger.debug("[mergeResources] Root level merge:", { locale: m, overlayKeys: Object.keys(v), baseKeys: Object.keys(y) }), v) { if (!Object.prototype.hasOwnProperty.call(v, h)) continue; y[h] || (y[h] = {}); let m = deepMerge(y[h], v[h]); y[h] = m, logger.debug("[mergeResources] Merged namespace:", h, { mergedKeys: Object.keys(m) }); } logger.debug("[mergeResources] After root level merge, baseLocale keys:", Object.keys(y)); } } return _; } function getAllKeys(e, m = "") { let h = []; if (typeof e != "object" || !e || Array.isArray(e)) return h; for (let g in e) if (Object.prototype.hasOwnProperty.call(e, g)) { let _ = m ? `${m}.${g}` : g, v = e[g]; typeof v == "object" && v && !Array.isArray(v) ? h.push(...getAllKeys(v, _)) : h.push(_); } return h; } var I18nContext = createContext(null), LOCALE_STORAGE_KEY = "@gulibs/react-vintl:locale"; function I18nProvider({ resources: m, defaultLocale: h, supportedLocales: _, children: b }) { if (!validateResources(m)) { let e = m && typeof m == "object" && Object.keys(m).length === 0 ? "Invalid i18n resources: resources object is empty. Please configure the @gulibs/react-vintl plugin in your vite.config.ts to generate locale resources from your translation files." : "Invalid i18n resources"; throw Error(e); } let S = _ || Object.keys(m), C = h || S[0]; if (!S.includes(C)) throw Error(`Default locale "${C}" is not in supported locales: ${S.join(", ")}`); let [w, T] = useState(() => { if (typeof window > "u") return C; try { let e = localStorage.getItem(LOCALE_STORAGE_KEY); if (e && S.includes(e)) return e; } catch (m) { logger.warn("Failed to read locale from localStorage:", m); } return C; }), [E, D] = useState(m), O = (m) => { if (!S.includes(m)) { logger.warn(`Locale "${m}" is not in supported locales: ${S.join(", ")}`); return; } if (T(m), typeof window < "u") try { localStorage.setItem(LOCALE_STORAGE_KEY, m); } catch (m) { logger.warn("Failed to save locale to localStorage:", m); } }; useEffect(() => { if (typeof window > "u") return; let e = (e) => { if (e.key === LOCALE_STORAGE_KEY && e.newValue) { let m = e.newValue; S.includes(m) && m !== w && T(m); } }; return window.addEventListener("storage", e), () => { window.removeEventListener("storage", e); }; }, [S, w]); let k = useCallback((e, m) => { D((h) => m?.merge === !1 ? e : mergeResources(h, e, { namespace: m?.namespace, locale: m?.locale })); }, []), A = useMemo(() => createTranslationFunction(E, w), [E, w]), j = useMemo(() => ({ locale: w, supportedLocales: S, t: A, setLocale: O, resources: E, updateResources: k }), [ w, S, A, E, k ]); return /* @__PURE__ */ jsx(I18nContext.Provider, { value: j, children: b }); } function useI18nContext() { let e = useContext(I18nContext); if (!e) throw Error("useI18nContext must be used within an I18nProvider"); return e; } function useTranslation(e) { let m = useI18nContext(); return e ? (h, g) => { let _ = `${e}.${h}`; return m.t(_, g); } : m.t; } function useI18n() { let e = useI18nContext(); return { locale: e.locale, supportedLocales: e.supportedLocales, setLocale: e.setLocale, resources: e.resources }; } function useTranslationKey(e, m) { let h = useTranslation(), _ = useCallback((g) => h(e, g || m), [ h, e, m ]); return { translation: useCallback(() => h(e, m), [ h, e, m ])(), t: _ }; } function useLocale() { let { locale: e, supportedLocales: m, setLocale: h } = useI18n(), _ = useCallback((e) => m.includes(e), [m]); return { locale: e, supportedLocales: m, setLocale: h, isSupportedLocale: _ }; } var cache = /* @__PURE__ */ new Map(), CACHE_TTL = 300 * 1e3, pendingRequests = /* @__PURE__ */ new Map(); async function loadFromUrl(e) { let m = await fetch(e); if (!m.ok) throw Error(`Failed to load translations from ${e}: ${m.statusText}`); return await m.json(); } async function loadFromFunction(e) { return await e(); } async function loadFromPromise(e) { return await e; } async function loadTranslations(e) { return typeof e == "string" ? await loadFromUrl(e) : typeof e == "function" ? await loadFromFunction(e) : await loadFromPromise(e); } function getCacheKey(e, m) { return typeof e == "string" ? `${e}:${m || "default"}` : `${String(e)}:${m || "default"}`; } function useLoadRemoteTranslations(m, h = {}) { let { locale: _, namespace: x, merge: S = !0, cache: C = !0, onSuccess: w, onError: T, retry: E = 0, retryDelay: D = 1e3 } = h, O = useI18nContext(), [k, A] = useState(!1), [j, M] = useState(null), [N, P] = useState(null), F = useRef(0), I = useRef(m), L = useRef(O.updateResources), R = useRef(O.locale); useEffect(() => { L.current = O.updateResources, R.current = O.locale; }, [O.updateResources, O.locale]); let z = useRef(/* @__PURE__ */ new Set()); useEffect(() => { I.current = m; }, [m]); let B = useMemo(() => { let e = _ || R.current; return [ typeof m == "string" ? m : String(m), e, x || "", String(S) ].join("|"); }, [ m, _, x, S ]), V = useCallback(async () => { let m = _ || R.current, h = getCacheKey(I.current, m), g = x === void 0 ? `${h}:root` : `${h}:ns:${x}`; if (z.current.has(B)) { if (logger.debug("[useLoadRemoteTranslations] Resource already loaded, skipping:", { resourceKey: B, namespace: x === void 0 ? "(root level)" : x }), C) { let e = cache.get(g); if (e) { P(e.data), M(null), A(!1); return; } } return; } if (C) { let h = cache.get(g); if (h && Date.now() - h.timestamp < CACHE_TTL) { logger.debug("[useLoadRemoteTranslations] Using cached data:", { resourceKey: B, namespace: x === void 0 ? "(root level)" : x, namespaceType: typeof x }), P(h.data), M(null), A(!1), z.current.add(B), S && (logger.debug("[useLoadRemoteTranslations] Merging cached resources:", { namespace: x === void 0 ? "(root level)" : x, locale: m }), L.current(h.data, { namespace: x, locale: m })), w && w(h.data); return; } } let v = pendingRequests.get(g); if (v) { logger.debug("[useLoadRemoteTranslations] Pending request found, waiting:", { cacheKey: g, resourceKey: B, namespace: x === void 0 ? "(root level)" : x }); try { let h = await v; P(h), M(null), A(!1), z.current.add(B), S && (logger.debug("[useLoadRemoteTranslations] Merging pending resources:", { namespace: x === void 0 ? "(root level)" : x, locale: m }), L.current(h, { namespace: x, locale: m })), w && w(h); return; } catch (m) { logger.debug("[useLoadRemoteTranslations] Pending request failed, continuing:", m); } } A(!0), M(null), logger.debug("[useLoadRemoteTranslations] Starting load:", { resourceKey: B, namespace: x === void 0 ? "(root level)" : x, namespaceType: typeof x, locale: m }); let y = async (h) => { try { let h = await loadTranslations(I.current); return C && cache.set(g, { data: h, timestamp: Date.now() }), S ? (logger.debug("[useLoadRemoteTranslations] Merging resources:", { hasResources: !!h, namespace: x === void 0 ? "(root level)" : x, namespaceType: typeof x, locale: m, resourceKeys: h[m] ? Object.keys(h[m]) : [], resourceStructure: h[m] }), L.current(h, { namespace: x, locale: m })) : logger.debug("[useLoadRemoteTranslations] Merge disabled, skipping resource merge"), z.current.add(B), P(h), M(null), A(!1), w && w(h), h; } catch (m) { let g = m instanceof Error ? m : Error(String(m)); if (h < E) return F.current = h + 1, logger.warn(`Translation load failed, retrying (${F.current}/${E})...`), await new Promise((e) => setTimeout(e, D)), await y(h + 1); throw M(g), A(!1), T ? T(g) : logger.error("Failed to load remote translations:", g), g; } }, b = y(0).finally(() => { pendingRequests.delete(g); }); pendingRequests.set(g, b); try { await b; } catch {} }, [ _, x, S, C, w, T, E, D, B ]); useEffect(() => { logger.debug("[useLoadRemoteTranslations] useEffect triggered:", { resourceKey: B, namespace: x === void 0 ? "(root level)" : x, namespaceType: typeof x }), F.current = 0, V(); }, [ V, B, x ]); let H = useCallback(() => { F.current = 0, z.current.delete(B), V(); }, [V, B]); return { loading: k, error: j, data: N, retry: H }; } export { I18nProvider, Logger, createTranslationFunction, deepMerge, getNestedValue, logger, mergeResources, replaceParams, useI18n, useI18nContext, useLoadRemoteTranslations, useLocale, useTranslation, useTranslationKey, validateResources };