@scaleway/use-i18n
Version:
A small hook to handle i18n
269 lines (268 loc) • 9.02 kB
JavaScript
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 };