Refactor i18n, make component use the hook directly
This commit is contained in:
@ -1,9 +1,7 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useConst } from "keycloakify/tools/useConst";
|
||||
import { id } from "tsafe/id";
|
||||
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";
|
||||
@ -20,7 +18,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> = {
|
||||
/**
|
||||
@ -82,116 +80,143 @@ export type GenericI18n<MessageKey extends string> = {
|
||||
* See advancedMsg() but instead of returning a JSX.Element it returns a string.
|
||||
*/
|
||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||
|
||||
/**
|
||||
* Initially the messages are in english (fallback language).
|
||||
* The translations in the current language are being fetched dynamically.
|
||||
* This property is true while the translations are being fetched.
|
||||
*/
|
||||
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],
|
||||
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile
|
||||
});
|
||||
|
||||
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>;
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n; isTranslationsDownloadOngoing: boolean } {
|
||||
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],
|
||||
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
const [i18n, setI18n] = useState<I18n | undefined>(undefined);
|
||||
|
||||
const refHasStartedFetching = useConst(() => ({ current: false }));
|
||||
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(i18n);
|
||||
|
||||
useEffect(() => {
|
||||
if (refHasStartedFetching.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isActive = true;
|
||||
|
||||
refHasStartedFetching.current = true;
|
||||
|
||||
(async () => {
|
||||
const messages = await getMessages(partialI18n.currentLanguageTag);
|
||||
|
||||
prI18n_currentLanguage?.then(i18n => {
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
setI18n({
|
||||
...partialI18n,
|
||||
...createI18nTranslationFunctions({ messages })
|
||||
});
|
||||
})();
|
||||
setI18n_toReturn(i18n);
|
||||
});
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const pendingI18n = useMemo(() => {
|
||||
if (i18n !== undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return id<I18n>({
|
||||
...partialI18n,
|
||||
...createI18nTranslationFunctions({ messages: undefined })
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
i18n: i18n ?? (assert(pendingI18n !== undefined), pendingI18n),
|
||||
isTranslationsDownloadOngoing: i18n === undefined
|
||||
};
|
||||
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;
|
||||
__localizationRealmOverridesUserProfile: Record<string, string> | undefined;
|
||||
}) {
|
||||
const { __localizationRealmOverridesUserProfile, extraMessages } = params;
|
||||
|
||||
const fallbackMessages = {
|
||||
...params.fallbackMessages,
|
||||
...params.extraFallbackMessages
|
||||
const messages_fallbackLanguage = {
|
||||
...params.messages_fallbackLanguage,
|
||||
...params.extraMessages_fallbackLanguage
|
||||
};
|
||||
|
||||
function createI18nTranslationFunctions(params: {
|
||||
@ -205,7 +230,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;
|
||||
|
@ -1,2 +1,10 @@
|
||||
export type { I18n, MessageKey } from "./i18n";
|
||||
export { createUseI18n, fallbackLanguageTag } from "./i18n";
|
||||
export type { MessageKey } from "./i18n";
|
||||
import { createUseI18n } from "./i18n";
|
||||
export { createUseI18n };
|
||||
export { fallbackLanguageTag } from "./i18n";
|
||||
|
||||
const { useI18n, ofTypeI18n } = createUseI18n({});
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
||||
|
||||
export { useI18n };
|
||||
|
Reference in New Issue
Block a user