UNPKG

@scaleway/use-i18n

Version:
269 lines (268 loc) 9.02 kB
import formatDate from "./formatDate.js"; import formatters from "./formatters.js"; import formatUnit from "./formatUnit.js"; import { formatDistanceToNow, formatDistanceToNowStrict, formatDuration, intervalToDuration } from "date-fns"; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { jsx } from "react/jsx-runtime"; //#region src/usei18n.tsx const LOCALE_ITEM_STORAGE = "locale"; const areNamespacesLoaded = (namespaces, loadedNamespaces = []) => namespaces.every((n) => loadedNamespaces.includes(n)); const getCurrentLocale = ({ defaultLocale, isLocaleSupported, localeItemStorage }) => { if (typeof window !== "undefined") { const { languages: browserLocales } = navigator; const currentLocalFromlocalStorage = localStorage.getItem(localeItemStorage); if (currentLocalFromlocalStorage && isLocaleSupported(currentLocalFromlocalStorage)) return currentLocalFromlocalStorage; localStorage.removeItem(localeItemStorage); const findedBrowserLocal = browserLocales.find((locale) => isLocaleSupported(locale)); if (findedBrowserLocal) { localStorage.setItem(localeItemStorage, findedBrowserLocal); return findedBrowserLocal; } if (defaultLocale && isLocaleSupported(defaultLocale)) { localStorage.setItem(localeItemStorage, defaultLocale); return defaultLocale; } } return defaultLocale; }; const I18nContext = createContext(void 0); function useI18n() { const context = useContext(I18nContext); if (context === void 0) throw new Error("useI18n must be used within a I18nProvider"); return context; } const padNumberWithZeros = (num, targetLength = 2) => String(num).padStart(targetLength, "0"); function useTranslation(namespaces, load) { const context = useContext(I18nContext); if (context === void 0) throw new Error("useTranslation must be used within a I18nProvider"); const { loadTranslations, namespaces: loadedNamespaces } = context; const key = namespaces.join(","); useEffect(() => { key.split(",").map(async (namespace) => loadTranslations(namespace, load)); }, [ loadTranslations, key, load ]); const isLoaded = useMemo(() => areNamespacesLoaded(namespaces, loadedNamespaces), [loadedNamespaces, namespaces]); return { ...context, isLoaded }; } const initialDefaultTranslations = {}; const I18nContextProvider = ({ children, defaultLoad, defaultLocale, defaultTranslations = initialDefaultTranslations, enableDebugKey = false, enableDefaultLocale = false, loadDateLocale, loadDateLocaleAsync, localeItemStorage = LOCALE_ITEM_STORAGE, onLoadDateLocaleError, onTranslateError, onLoadTranslationError, isLocaleSupported }) => { const [currentLocale, setCurrentLocale] = useState(getCurrentLocale({ defaultLocale, isLocaleSupported, localeItemStorage })); const [translations, setTranslations] = useState(defaultTranslations); const [namespaces, setNamespaces] = useState([]); const [dateFnsLocale, setDateFnsLocale] = useState(loadDateLocale?.(currentLocale) ?? void 0); const loadDateFNS = loadDateLocale ?? loadDateLocaleAsync; const setDateFns = useCallback(async (locale) => { try { const dateFns = await loadDateFNS(locale); setDateFnsLocale(dateFns); } catch (error) { if (error instanceof Error && onLoadDateLocaleError) onLoadDateLocaleError(error); setDateFnsLocale(dateFnsLocale); } }, [ loadDateFNS, setDateFnsLocale, onLoadDateLocaleError, dateFnsLocale ]); /** * At first render when we find a local on the localStorage which is not the same as the default, * we should switch also the date-fns local related to the current local. * As the method is async, we obviously need a useEffect to apply this change... */ useEffect(() => { if (!dateFnsLocale) setDateFns(currentLocale).then().catch(() => null); }, [ currentLocale, dateFnsLocale, setDateFns, setDateFnsLocale ]); const loadTranslations = useCallback(async (namespace, load = defaultLoad) => { const result = { [currentLocale]: { default: {} }, defaultLocale: { default: {} } }; if (enableDefaultLocale && currentLocale !== defaultLocale) try { result.defaultLocale = await load({ locale: defaultLocale, namespace }); } catch (error) { onLoadTranslationError?.(error); } try { const defaultCurrentLocaleLoad = await load({ locale: currentLocale, namespace }); result[currentLocale] = defaultCurrentLocaleLoad; } catch (error) { onLoadTranslationError?.(error); } const trad = { ...result.defaultLocale.default, ...result[currentLocale]?.default }; setTranslations((prevState) => ({ ...prevState, [defaultLocale]: { ...prevState[defaultLocale], ...result.defaultLocale.default }, [currentLocale]: { ...prevState[currentLocale], ...trad } })); setNamespaces((prevState) => [.../* @__PURE__ */ new Set([...prevState, namespace])]); return namespace; }, [ defaultLoad, currentLocale, enableDefaultLocale, defaultLocale, onLoadTranslationError ]); const switchLocale = useCallback(async (locale) => { if (isLocaleSupported(locale)) { localStorage.setItem(localeItemStorage, locale); setCurrentLocale(locale); await setDateFns(locale); } }, [ setDateFns, localeItemStorage, setCurrentLocale, isLocaleSupported ]); const formatNumber = useCallback((numb, options) => formatters.getNumberFormat(currentLocale, options).format(numb), [currentLocale]); const formatList = useCallback((listFormat, options) => formatters.getListFormat(currentLocale, options).format(listFormat), [currentLocale]); const formatUnit$1 = useCallback((value, options) => formatUnit(currentLocale, value, options), [currentLocale]); const formatDate$1 = useCallback((value, options = "short") => formatDate(currentLocale, value, options), [currentLocale]); const datetime = useCallback((date, options) => formatters.getDateTimeFormat(currentLocale, options).format(date), [currentLocale]); const relativeTimeStrict = useCallback((date, options = { addSuffix: true, unit: "day" }) => { const finalDate = new Date(date); return formatDistanceToNowStrict(finalDate, { locale: dateFnsLocale, ...options ?? { addSuffix: true, unit: "day" } }); }, [dateFnsLocale]); const formatDuration$1 = useCallback((durationInSeconds, format) => { const duration = { years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0, ...intervalToDuration({ end: durationInSeconds * 1e3, start: 0 }) }; if (format === "clock-milliseconds") { const parts = durationInSeconds.toString().split("."); const milliseconds = (parts[1] ? parseFloat("0." + parts[1]) : 0) * 1e3; return `${padNumberWithZeros(duration.hours ?? 0)}:${padNumberWithZeros(duration.minutes ?? 0)}:${padNumberWithZeros(duration.seconds ?? 0)}:${padNumberWithZeros(milliseconds ?? 0, 3)}`; } if (format === "clock") return `${padNumberWithZeros(duration.hours ?? 0)}:${padNumberWithZeros(duration.minutes ?? 0)}:${padNumberWithZeros(duration.seconds ?? 0)}`; return formatDuration(duration, { locale: dateFnsLocale, ...format }); }, [dateFnsLocale]); const relativeTime = useCallback((date, options = { addSuffix: true }) => { const finalDate = new Date(date); return formatDistanceToNow(finalDate, { locale: dateFnsLocale, ...options }); }, [dateFnsLocale]); const translate = useCallback((key, context) => { const value = translations[currentLocale]?.[key]; if (enableDebugKey) return key; if (!value) return ""; if (context) try { return formatters.getTranslationFormat(value, currentLocale).format(context); } catch (error) { onTranslateError?.({ currentLocale, defaultLocale, error, key, value }); const defaultValue = translations[defaultLocale]?.[key]; return formatters.getTranslationFormat(defaultValue, defaultLocale).format(context); } return value; }, [ currentLocale, translations, enableDebugKey, defaultLocale, onTranslateError ]); const namespaceTranslation = useCallback((scope, t = translate) => (key, context) => t(`${scope}.${key}`, context) || t(key, context), [translate]); const value = useMemo(() => ({ currentLocale, dateFnsLocale, datetime, formatDate: formatDate$1, formatList, formatNumber, formatUnit: formatUnit$1, formatDuration: formatDuration$1, loadTranslations, namespaces, namespaceTranslation, relativeTime, relativeTimeStrict, setTranslations, switchLocale, t: translate, translations }), [ currentLocale, dateFnsLocale, datetime, formatDate$1, formatList, formatNumber, formatUnit$1, formatDuration$1, loadTranslations, namespaceTranslation, namespaces, relativeTime, relativeTimeStrict, setTranslations, switchLocale, translate, translations ]); return /* @__PURE__ */ jsx(I18nContext.Provider, { value, children }); }; //#endregion export { I18nContextProvider as default, useI18n, useTranslation };