// // Copyright 2022 DXOS.org // import { enUS as dtLocaleEnUs, type Locale } from 'date-fns/locale'; import i18Next, { type TFunction, type Resource } from 'i18next'; import React, { type ReactNode, useEffect, createContext, useState, Suspense, useContext } from 'react'; import { initReactI18next, useTranslation as useI18NextTranslation } from 'react-i18next'; const initialLng = 'en-US'; const initialNs = 'dxos-common'; const initialDtLocale = dtLocaleEnUs; // TODO(thure): `Parameters` causes typechecking issues because `TFunction` has so many signatures. export type Label = string | [string, { ns: string; count?: number; defaultValue?: string }]; export const isLabel = (o: any): o is Label => typeof o === 'string' || (Array.isArray(o) && o.length === 2 && typeof o[0] === 'string' && !!o[1] && typeof o[1] === 'object' && 'ns' in o[1] && typeof o[1].ns === 'string'); export const toLocalizedString = (label: Label, t: TFunction) => (Array.isArray(label) ? t(...label) : label); export const resources = { [initialLng]: { [initialNs]: { 'loading translations': 'Loading translations…', }, }, } as const; void i18Next.use(initReactI18next).init({ resources, lng: initialLng, defaultNS: initialNs, interpolation: { escapeValue: false, }, }); export interface TranslationsProviderProps { children?: ReactNode; // TODO(wittjosiah): Rename to `placeholder` to match ClientProvider? // Placeholder => loading, fallback => error. fallback?: ReactNode; resourceExtensions?: Resource[]; appNs?: string; dtLocale?: Locale; } export const TranslationsContext = createContext({ appNs: initialNs, dtLocale: initialDtLocale, }); export const useTranslation = (...args: Parameters) => { const result = useI18NextTranslation(...args); const { dtLocale } = useContext(TranslationsContext); return { ...result, dtLocale }; }; export const TranslationsProvider = ({ fallback, resourceExtensions, children, appNs, dtLocale, }: TranslationsProviderProps) => { const [loaded, setLoaded] = useState(false); useEffect(() => { setLoaded(false); if (resourceExtensions && resourceExtensions.length) { resourceExtensions.forEach((resource) => { Object.keys(resource).forEach((language) => { Object.keys(resource[language]).forEach((ns) => { i18Next.addResourceBundle(language, ns, resource[language][ns]); }); }); }); } setLoaded(true); }, [resourceExtensions]); // TODO(thure): This is not ideal, but i18next was causing `Suspense` to not render the fallback even when the child was asking for namespaces yet to be added. // TODO(burdon): Fallbacks should only appear after a short delay, and if the displayed then be visible for 500mx to avoid startup flickering. return ( {loaded ? children : fallback} ); };