Refactor i18n so that we don't have to wait for translations to be downloaded to render the page
This commit is contained in:
@ -1,8 +1,10 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useConst } from "keycloakify/tools/useConst";
|
||||
import { id } from "tsafe/id";
|
||||
import { assert } from "tsafe/assert";
|
||||
import fallbackMessages from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
@ -86,35 +88,14 @@ export type I18n = GenericI18n<MessageKey>;
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||
}) {
|
||||
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
|
||||
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n; isTranslationsDownloadOngoing: boolean } {
|
||||
const { kcContext } = params;
|
||||
|
||||
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
||||
|
||||
const refHasStartedFetching = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (refHasStartedFetching.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
refHasStartedFetching.current = true;
|
||||
|
||||
(async () => {
|
||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||
|
||||
setI18n({
|
||||
...createI18nTranslationFunctions({
|
||||
fallbackMessages: {
|
||||
...fallbackMessages,
|
||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||
} as any,
|
||||
messages: {
|
||||
...(await getMessages(currentLanguageTag)),
|
||||
...(extraMessages[currentLanguageTag] ?? {})
|
||||
} as any
|
||||
}),
|
||||
currentLanguageTag,
|
||||
const partialI18n = useMemo(
|
||||
(): Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> => ({
|
||||
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
|
||||
getChangeLocalUrl: newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
|
||||
@ -129,24 +110,94 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
|
||||
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 }));
|
||||
|
||||
useEffect(() => {
|
||||
if (refHasStartedFetching.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isActive = true;
|
||||
|
||||
refHasStartedFetching.current = true;
|
||||
|
||||
(async () => {
|
||||
const messages = await getMessages(partialI18n.currentLanguageTag);
|
||||
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
setI18n({
|
||||
...partialI18n,
|
||||
...createI18nTranslationFunctions({ messages })
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return i18n ?? null;
|
||||
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 {
|
||||
useI18n,
|
||||
ofTypeI18n: Reflect<GenericI18n<MessageKey | ExtraMessageKey>>()
|
||||
ofTypeI18n: Reflect<I18n>()
|
||||
};
|
||||
}
|
||||
|
||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
/** Note exported only for hypothetical usage in non react framework */
|
||||
export function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
|
||||
fallbackMessages: Record<MessageKey, string>;
|
||||
messages: Record<MessageKey, string>;
|
||||
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||
const { fallbackMessages, messages } = params;
|
||||
extraFallbackMessages: Record<ExtraMessageKey, string> | undefined;
|
||||
extraMessages: Partial<Record<ExtraMessageKey, string>> | undefined;
|
||||
}) {
|
||||
const { extraMessages } = params;
|
||||
|
||||
const fallbackMessages = {
|
||||
...params.fallbackMessages,
|
||||
...params.extraFallbackMessages
|
||||
};
|
||||
|
||||
function createI18nTranslationFunctions(params: {
|
||||
messages: Partial<Record<MessageKey, string>> | undefined;
|
||||
}): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||
const messages = {
|
||||
...params.messages,
|
||||
...extraMessages
|
||||
};
|
||||
|
||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
||||
const { key, args, doRenderAsHtml } = props;
|
||||
@ -242,3 +293,6 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
}) as string
|
||||
};
|
||||
}
|
||||
|
||||
return { createI18nTranslationFunctions };
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import { useConst } from "keycloakify/tools/useConst";
|
||||
import { id } from "tsafe/id";
|
||||
import { assert } from "tsafe/assert";
|
||||
import fallbackMessages from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
@ -87,36 +89,14 @@ export type I18n = GenericI18n<MessageKey>;
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||
}) {
|
||||
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
|
||||
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n; isTranslationsDownloadOngoing: boolean } {
|
||||
const { kcContext } = params;
|
||||
|
||||
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
||||
|
||||
const refHasStartedFetching = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (refHasStartedFetching.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
refHasStartedFetching.current = true;
|
||||
|
||||
(async () => {
|
||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||
|
||||
setI18n({
|
||||
...createI18nTranslationFunctions({
|
||||
fallbackMessages: {
|
||||
...fallbackMessages,
|
||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||
} as any,
|
||||
messages: {
|
||||
...(await getMessages(currentLanguageTag)),
|
||||
...(extraMessages[currentLanguageTag] ?? {})
|
||||
} as any,
|
||||
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile
|
||||
}),
|
||||
currentLanguageTag,
|
||||
const partialI18n = useMemo(
|
||||
(): Pick<I18n, "currentLanguageTag" | "getChangeLocalUrl" | "labelBySupportedLanguageTag"> => ({
|
||||
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
|
||||
getChangeLocalUrl: newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
|
||||
@ -131,25 +111,96 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
|
||||
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 }));
|
||||
|
||||
useEffect(() => {
|
||||
if (refHasStartedFetching.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
let isActive = true;
|
||||
|
||||
refHasStartedFetching.current = true;
|
||||
|
||||
(async () => {
|
||||
const messages = await getMessages(partialI18n.currentLanguageTag);
|
||||
|
||||
if (!isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
setI18n({
|
||||
...partialI18n,
|
||||
...createI18nTranslationFunctions({ messages })
|
||||
});
|
||||
})();
|
||||
|
||||
return () => {
|
||||
isActive = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return i18n ?? null;
|
||||
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 {
|
||||
useI18n,
|
||||
ofTypeI18n: Reflect<GenericI18n<MessageKey | ExtraMessageKey>>()
|
||||
ofTypeI18n: Reflect<I18n>()
|
||||
};
|
||||
}
|
||||
|
||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
/** Note exported only for hypothetical usage in non react framework */
|
||||
export function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraMessageKey extends string>(params: {
|
||||
fallbackMessages: Record<MessageKey, string>;
|
||||
messages: Record<MessageKey, string>;
|
||||
extraFallbackMessages: Record<ExtraMessageKey, string> | undefined;
|
||||
extraMessages: Partial<Record<ExtraMessageKey, string>> | undefined;
|
||||
__localizationRealmOverridesUserProfile: Record<string, string> | undefined;
|
||||
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||
const { fallbackMessages, messages, __localizationRealmOverridesUserProfile } = params;
|
||||
}) {
|
||||
const { __localizationRealmOverridesUserProfile, extraMessages } = params;
|
||||
|
||||
const fallbackMessages = {
|
||||
...params.fallbackMessages,
|
||||
...params.extraFallbackMessages
|
||||
};
|
||||
|
||||
function createI18nTranslationFunctions(params: {
|
||||
messages: Partial<Record<MessageKey, string>> | undefined;
|
||||
}): Pick<GenericI18n<MessageKey | ExtraMessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||
const messages = {
|
||||
...params.messages,
|
||||
...extraMessages
|
||||
};
|
||||
|
||||
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined {
|
||||
const { key, args, doRenderAsHtml } = props;
|
||||
@ -260,3 +311,6 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
}) as string
|
||||
};
|
||||
}
|
||||
|
||||
return { createI18nTranslationFunctions };
|
||||
}
|
||||
|
Reference in New Issue
Block a user