@gulibs/react-vintl
Version:
Type-safe i18n library for React with Vite plugin and automatic type inference
294 lines (293 loc) • 8.75 kB
JavaScript
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { jsx } from "react/jsx-runtime";
function isProduction() {
return typeof process < "u" && process.env.NODE_ENV ? process.env.NODE_ENV === "production" : typeof window < "u" ? !window.location.hostname.includes("localhost") && !window.location.hostname.includes("127.0.0.1") && !window.location.hostname.includes("0.0.0.0") : !1;
}
var Logger = class {
constructor(e = {}) {
let v = isProduction();
this.config = {
debug: e.debug ?? !v,
warn: e.warn ?? !v,
error: e.error ?? !0
};
}
debug(...e) {
this.config.debug && console.log("[react-vintl]", ...e);
}
warn(...e) {
this.config.warn && console.warn("[react-vintl]", ...e);
}
error(...e) {
this.config.error && console.error("[react-vintl]", ...e);
}
setConfig(e) {
this.config = {
...this.config,
...e
};
}
getConfig() {
return { ...this.config };
}
};
const logger = new Logger();
function getNestedValue(e, v) {
return v.split(".").reduce((e, v) => {
if (typeof e == "object" && e && v in e) return e[v];
}, e);
}
function replaceParams(e, v = {}) {
return e.replace(/\{\{([^}]+)\}\}/g, (e, y) => {
try {
return processInterpolation(y.trim(), v);
} catch (v) {
return logger.warn(`Failed to process interpolation "${e}":`, v), e;
}
});
}
function processInterpolation(e, v) {
let y = e.split(",").map((e) => e.trim()), b = y[0];
if (!b) throw Error("Missing parameter key");
let x = getNestedParamValue(v, b);
if (x === void 0) throw Error(`Parameter "${b}" not found`);
if (y.length === 1) return String(x);
let S = y[1], C = y.slice(2);
switch (S) {
case "plural": return handlePlural(x, C);
case "select": return handleSelect(x, C);
case "number": return handleNumber(x, C);
case "date": return handleDate(x, C);
case "currency": return handleCurrency(x, C);
default: return handleCustomFormat(x, S, C);
}
}
function getNestedParamValue(e, v) {
return v.split(".").reduce((e, v) => {
if (typeof e == "object" && e && v in e) return e[v];
}, e);
}
function handlePlural(e, v) {
let y = Number(e);
if (isNaN(y)) return String(e);
let b = {};
for (let e of v) {
let [v, y] = e.split("=", 2).map((e) => e.trim());
v && y && (b[v] = y);
}
let x = "other";
return y === 1 && "one" in b ? x = "one" : y === 0 && "zero" in b ? x = "zero" : y > 1 && "few" in b ? x = "few" : y > 1 && "many" in b && (x = "many"), (b[x] || b.other || String(e)).replace(/#/g, String(y));
}
function handleSelect(e, v) {
let y = String(e), b = {};
for (let e of v) {
let [v, y] = e.split("=", 2).map((e) => e.trim());
v && y && (b[v] = y);
}
return b[y] || b.other || y;
}
function handleNumber(e, v) {
let y = Number(e);
if (isNaN(y)) return String(e);
let b = {};
for (let e of v) {
let [v, y] = e.split("=", 2).map((e) => e.trim());
if (v && y) {
let e = Number(y);
isNaN(e) || (b[v] = e);
}
}
try {
return new Intl.NumberFormat(void 0, b).format(y);
} catch {
return String(y);
}
}
function handleDate(e, v) {
let y;
if (e instanceof Date) y = e;
else if (typeof e == "string" || typeof e == "number") y = new Date(e);
else return String(e);
if (isNaN(y.getTime())) return String(e);
let b = {};
for (let e of v) {
let [v, y] = e.split("=", 2).map((e) => e.trim());
v && y && (b[v] = y);
}
try {
return new Intl.DateTimeFormat(void 0, b).format(y);
} catch {
return y.toLocaleDateString();
}
}
function handleCurrency(e, v) {
let y = Number(e);
if (isNaN(y)) return String(e);
let b = { style: "currency" };
for (let e of v) {
let [v, y] = e.split("=", 2).map((e) => e.trim());
if (v && y) if (v === "currency") b.currency = y;
else {
let e = Number(y);
isNaN(e) || (b[v] = e);
}
}
try {
return new Intl.NumberFormat(void 0, b).format(y);
} catch {
return String(y);
}
}
function handleCustomFormat(e, v, y) {
switch (v) {
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(e) {
if (!e || typeof e != "object") return !1;
try {
return JSON.stringify(e), !0;
} catch {
return logger.warn("Invalid params object: contains circular reference"), !1;
}
}
function createTranslationFunction(e, v) {
return (y, b) => {
let x = e[v];
if (!x) return logger.warn(`Locale "${v}" not found in resources`), y;
let S = getNestedValue(x, y);
if (typeof S != "string") return logger.warn(`Translation key "${y}" not found or not a string in locale "${v}"`), y;
if (b && !validateParams(b)) return logger.warn(`Invalid params for key "${y}":`, b), S;
try {
return b ? replaceParams(S, b) : S;
} catch (e) {
return logger.error(`Failed to process translation for key "${y}":`, e), S;
}
};
}
function validateResources(e) {
if (!e || typeof e != "object") return logger.error("Resources must be an object"), !1;
let v = Object.keys(e);
if (v.length === 0) return logger.error("Resources must contain at least one locale"), !1;
let y = v[0], b = getAllKeys(e[y]);
for (let y of v.slice(1)) {
let v = getAllKeys(e[y]), x = b.filter((e) => !v.includes(e)), S = v.filter((e) => !b.includes(e));
x.length > 0 && logger.warn(`Locale "${y}" is missing keys: ${x.join(", ")}`), S.length > 0 && logger.warn(`Locale "${y}" has extra keys: ${S.join(", ")}`);
}
return !0;
}
function getAllKeys(e, v = "") {
let y = [];
if (typeof e != "object" || !e || Array.isArray(e)) return y;
for (let b in e) if (Object.prototype.hasOwnProperty.call(e, b)) {
let x = v ? `${v}.${b}` : b, S = e[b];
typeof S == "object" && S && !Array.isArray(S) ? y.push(...getAllKeys(S, x)) : y.push(x);
}
return y;
}
var I18nContext = createContext(null), LOCALE_STORAGE_KEY = "@gulibs/react-vintl:locale";
function I18nProvider({ resources: e, defaultLocale: v, supportedLocales: y, children: w }) {
if (!validateResources(e)) {
let v = e && typeof e == "object" && Object.keys(e).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(v);
}
let T = y || Object.keys(e), E = v || T[0];
if (!T.includes(E)) throw Error(`Default locale "${E}" is not in supported locales: ${T.join(", ")}`);
let [D, O] = useState(() => {
if (typeof window > "u") return E;
try {
let e = localStorage.getItem(LOCALE_STORAGE_KEY);
if (e && T.includes(e)) return e;
} catch (e) {
logger.warn("Failed to read locale from localStorage:", e);
}
return E;
}), k = (e) => {
if (!T.includes(e)) {
logger.warn(`Locale "${e}" is not in supported locales: ${T.join(", ")}`);
return;
}
if (O(e), typeof window < "u") try {
localStorage.setItem(LOCALE_STORAGE_KEY, e);
} catch (e) {
logger.warn("Failed to save locale to localStorage:", e);
}
};
useEffect(() => {
if (typeof window > "u") return;
let e = (e) => {
if (e.key === LOCALE_STORAGE_KEY && e.newValue) {
let v = e.newValue;
T.includes(v) && v !== D && O(v);
}
};
return window.addEventListener("storage", e), () => {
window.removeEventListener("storage", e);
};
}, [T, D]);
let A = useMemo(() => createTranslationFunction(e, D), [e, D]), j = useMemo(() => ({
locale: D,
supportedLocales: T,
t: A,
setLocale: k,
resources: e
}), [
D,
T,
A,
e
]);
return /* @__PURE__ */ jsx(I18nContext.Provider, {
value: j,
children: w
});
}
function useI18nContext() {
let e = useContext(I18nContext);
if (!e) throw Error("useI18nContext must be used within an I18nProvider");
return e;
}
function useTranslation(e) {
let v = useI18nContext();
return e ? (y, b) => {
let x = `${e}.${y}`;
return v.t(x, b);
} : v.t;
}
function useI18n() {
let e = useI18nContext();
return {
locale: e.locale,
supportedLocales: e.supportedLocales,
setLocale: e.setLocale,
resources: e.resources
};
}
function useTranslationKey(e, y) {
let b = useTranslation(), x = useCallback((v) => b(e, v || y), [
b,
e,
y
]);
return {
translation: useCallback(() => b(e, y), [
b,
e,
y
])(),
t: x
};
}
function useLocale() {
let { locale: e, supportedLocales: y, setLocale: b } = useI18n(), x = useCallback((e) => y.includes(e), [y]);
return {
locale: e,
supportedLocales: y,
setLocale: b,
isSupportedLocale: x
};
}
export { I18nProvider, Logger, createTranslationFunction, getNestedValue, logger, replaceParams, useI18n, useI18nContext, useLocale, useTranslation, useTranslationKey, validateResources };