@gulibs/react-vintl
Version:
Type-safe i18n library for React with Vite plugin and automatic type inference
471 lines (470 loc) • 14.6 kB
JavaScript
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 };