UNPKG

@gulibs/react-vintl

Version:

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

294 lines (293 loc) 8.75 kB
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 };