@codegouvfr/react-dsfr
Version:
French State Design System React integration library
132 lines (103 loc) • 3.93 kB
text/typescript
import type { JSX } from "./tools/JSX";
import { useMemo } from "react";
import { capitalize } from "tsafe/capitalize";
type ReactNode = string | JSX.Element | null;
function getLanguageBestApprox<Language extends string>(params: {
languages: readonly Language[];
languageLike: string;
}): Language | undefined {
const { languages, languageLike } = params;
scope: {
const lang = languages.find(lang => lang.toLowerCase() === languageLike.toLowerCase());
if (lang === undefined) {
break scope;
}
return lang;
}
scope: {
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
const lang = languages.find(lang => lang.toLowerCase().includes(iso2LanguageLike));
if (lang === undefined) {
break scope;
}
return lang;
}
return undefined;
}
type FrMessagesToTranslationFunction<
FrMessages extends Record<string, ReactNode | ((params: any) => ReactNode)>
> = {
<K extends NonFunctionMessageKey<FrMessages>>(messageKey: K): FrMessages[K] extends (
params: any
) => infer R
? R
: FrMessages[K];
} & {
<K extends FunctionMessageKey<FrMessages>>(
messageKey: K,
params: ExtractArgument<FrMessages[K]>
): FrMessages[K] extends (params: any) => infer R ? R : FrMessages[K];
};
type ExtractArgument<Message extends ReactNode | ((params: any) => ReactNode)> = Message extends (
params: any
) => any
? Parameters<Message>[0]
: never;
type NonFunctionMessageKey<
FrMessages extends Record<string, ReactNode | ((params: any) => ReactNode)>
> = {
[Key in keyof FrMessages]: FrMessages[Key] extends (params: any) => any ? never : Key;
//[Key in keyof FrMessages]: FrMessages[Key] extends "accept all" ? "accept all" : never;
}[keyof FrMessages];
type FunctionMessageKey<
FrMessages extends Record<string, ReactNode | ((params: any) => ReactNode)>
> = Exclude<keyof FrMessages, NonFunctionMessageKey<FrMessages>>;
let useLang_glob = () => "fr";
export function setUseLang(params: { useLang: () => string }) {
useLang_glob = params.useLang;
}
export function useLang() {
return useLang_glob();
}
export function createComponentI18nApi<
ComponentName extends string,
FrMessages extends Record<string, ReactNode | ((params: any) => ReactNode)>
>(params: {
componentName: ComponentName;
frMessages: FrMessages;
}): {
useTranslation: () => { t: FrMessagesToTranslationFunction<FrMessages> };
} & Record<
`add${Capitalize<ComponentName>}Translations`,
(params: { lang: string; messages: Partial<FrMessages> }) => void
> {
const { componentName, frMessages } = params;
const messagesByLang = { "fr": frMessages };
function useTranslation() {
const lang = useLang();
const bestMatchLang = useMemo(() => {
const bestApproxLang = getLanguageBestApprox({
"languages": Object.keys(messagesByLang),
"languageLike": lang
});
return bestApproxLang ?? "fr";
}, [lang]);
function t(messageKey: keyof FrMessages, params?: any): ReactNode {
const messageOrFn =
(messagesByLang as any)[bestMatchLang][messageKey] ??
(messagesByLang["fr"] as any)[messageKey];
return params === undefined ? messageOrFn : messageOrFn(params);
}
return { t };
}
function addTranslations(params: { lang: string; messages: Partial<FrMessages> }) {
const { lang, messages } = params;
Object.entries(messages)
.filter(([, value]) => value !== undefined)
.forEach(([key, value]) => (((messagesByLang as any)[lang] ??= {})[key] = value));
}
return {
useTranslation,
[`add${capitalize(componentName)}Translations`]: addTranslations
} as any;
}