Apply the new way i18n is implemented to every pages

This commit is contained in:
Joseph Garrone
2024-06-09 04:43:18 +02:00
parent 77e32aad2a
commit 8f006f0009
45 changed files with 308 additions and 312 deletions

View File

@ -1,8 +1,7 @@
import "keycloakify/tools/Object.fromEntries";
import { useEffect, useState, useMemo } from "react";
import { useConst } from "keycloakify/tools/useConst";
import { useEffect, useState } from "react";
import { assert } from "tsafe/assert";
import fallbackMessages from "./baseMessages/en";
import messages_fallbackLanguage from "./baseMessages/en";
import { getMessages } from "./baseMessages";
import type { KcContext } from "../KcContext";
import { Reflect } from "tsafe/Reflect";
@ -18,7 +17,7 @@ export type KcContextLike = {
assert<KcContext extends KcContextLike ? true : false>();
export type MessageKey = keyof typeof fallbackMessages;
export type MessageKey = keyof typeof messages_fallbackLanguage;
export type GenericI18n<MessageKey extends string> = {
/**
@ -89,74 +88,109 @@ export type GenericI18n<MessageKey extends string> = {
isFetchingTranslations: boolean;
};
export type I18n = GenericI18n<MessageKey>;
function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } }) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
type Result = { i18n: I18n; prI18n_currentLanguage: Promise<I18n> | undefined };
const cachedResultByKcContext = new WeakMap<KcContextLike, Result>();
function getI18n(params: { kcContext: KcContextLike }): Result {
const { kcContext } = params;
use_cache: {
const cachedResult = cachedResultByKcContext.get(kcContext);
if (cachedResult === undefined) {
break use_cache;
}
return cachedResult;
}
const partialI18n: Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> = {
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
getChangeLocalUrl: newLanguageTag => {
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
return targetSupportedLocale.url;
},
labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]))
};
const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({
messages_fallbackLanguage,
extraMessages_fallbackLanguage: extraMessages[fallbackLanguageTag],
extraMessages: extraMessages[partialI18n.currentLanguageTag]
});
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag !== fallbackLanguageTag;
const result: Result = {
i18n: {
...partialI18n,
...createI18nTranslationFunctions({ messages: undefined }),
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
},
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
? undefined
: (async () => {
const messages = await getMessages(partialI18n.currentLanguageTag);
const i18n_currentLanguage: I18n = {
...partialI18n,
...createI18nTranslationFunctions({ messages }),
isFetchingTranslations: false
};
// NOTE: This promise.resolve is just because without it we TypeScript
// gives a Variable 'result' is used before being assigned. error
await Promise.resolve().then(() => {
result.i18n = i18n_currentLanguage;
result.prI18n_currentLanguage = undefined;
});
return i18n_currentLanguage;
})()
};
cachedResultByKcContext.set(kcContext, result);
return result;
}
return { getI18n };
}
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
const { getI18n } = createGetI18n(extraMessages);
function useI18n(params: { kcContext: KcContextLike }): I18n {
const { kcContext } = params;
const partialI18n = useMemo(
(): Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> => ({
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
getChangeLocalUrl: newLanguageTag => {
const { locale } = kcContext;
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
return targetSupportedLocale.url;
},
labelBySupportedLanguageTag: Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
}),
[]
);
const { createI18nTranslationFunctions } = useMemo(
() =>
createI18nTranslationFunctionsFactory<MessageKey, ExtraMessageKey>({
fallbackMessages,
extraFallbackMessages: extraMessages[fallbackLanguageTag],
extraMessages: extraMessages[partialI18n.currentLanguageTag]
}),
[]
);
const [i18n, setI18n] = useState<I18n | undefined>(undefined);
const refHasStartedFetching = useConst(() => ({ current: false }));
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
useEffect(() => {
if (partialI18n.currentLanguageTag === fallbackLanguageTag) {
return;
}
if (refHasStartedFetching.current) {
return;
}
let isActive = true;
refHasStartedFetching.current = true;
getMessages(partialI18n.currentLanguageTag).then(messages => {
prI18n_currentLanguage?.then(i18n => {
if (!isActive) {
return;
}
setI18n({
...partialI18n,
...createI18nTranslationFunctions({ messages }),
isFetchingTranslations: false
});
setI18n_toReturn(i18n);
});
return () => {
@ -164,35 +198,22 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
};
}, []);
const fallbackI18n = useMemo(
(): I18n => ({
...partialI18n,
...createI18nTranslationFunctions({ messages: undefined }),
isFetchingTranslations: partialI18n.currentLanguageTag !== fallbackLanguageTag
}),
[]
);
return i18n ?? fallbackI18n;
return i18n_toReturn;
}
return {
useI18n,
ofTypeI18n: Reflect<I18n>()
};
return { useI18n, ofTypeI18n: Reflect<I18n>() };
}
/** Note exported only for hypothetical usage in non react framework */
export function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
fallbackMessages: Record<MessageKey, string>;
extraFallbackMessages: Record<ExtraMessageKey, string> | undefined;
function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
messages_fallbackLanguage: Record<MessageKey, string>;
extraMessages_fallbackLanguage: Record<ExtraMessageKey, string> | undefined;
extraMessages: Partial<Record<ExtraMessageKey, string>> | undefined;
}) {
const { extraMessages } = params;
const fallbackMessages = {
...params.fallbackMessages,
...params.extraFallbackMessages
const messages_fallbackLanguage = {
...params.messages_fallbackLanguage,
...params.extraMessages_fallbackLanguage
};
function createI18nTranslationFunctions(params: {
@ -206,7 +227,7 @@ export function createI18nTranslationFunctionsFactory<MessageKey extends string,
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
const { key, args, doRenderAsHtml } = props;
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (messages_fallbackLanguage as any)[key];
if (messageOrUndefined === undefined) {
return undefined;