Untrack account i18n, code will be generated automatically
This commit is contained in:
parent
801a5cce17
commit
cc700f0ba0
@ -1,6 +0,0 @@
|
|||||||
import type { GenericI18n_noJsx } from "./i18n";
|
|
||||||
|
|
||||||
export type GenericI18n<MessageKey extends string> = GenericI18n_noJsx<MessageKey> & {
|
|
||||||
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
|
||||||
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
|
||||||
};
|
|
@ -1,250 +0,0 @@
|
|||||||
import "keycloakify/tools/Object.fromEntries";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import messages_defaultSet_fallbackLanguage from "./messages_defaultSet/en";
|
|
||||||
import { fetchMessages_defaultSet } from "./messages_defaultSet";
|
|
||||||
import type { KcContext } from "../KcContext";
|
|
||||||
import { FALLBACK_LANGUAGE_TAG } from "keycloakify/bin/shared/constants";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
|
|
||||||
export type KcContextLike = {
|
|
||||||
locale?: {
|
|
||||||
currentLanguageTag: string;
|
|
||||||
supported: { languageTag: string; url: string; label: string }[];
|
|
||||||
};
|
|
||||||
"x-keycloakify": {
|
|
||||||
messages: Record<string, string>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
assert<KcContext extends KcContextLike ? true : false>();
|
|
||||||
|
|
||||||
export type GenericI18n_noJsx<MessageKey extends string> = {
|
|
||||||
/**
|
|
||||||
* e.g: "en", "fr", "zh-CN"
|
|
||||||
*
|
|
||||||
* The current language
|
|
||||||
*/
|
|
||||||
currentLanguageTag: string;
|
|
||||||
/**
|
|
||||||
* Redirect to this url to change the language.
|
|
||||||
* After reload currentLanguageTag === newLanguageTag
|
|
||||||
*/
|
|
||||||
getChangeLocaleUrl: (newLanguageTag: string) => string;
|
|
||||||
/**
|
|
||||||
* e.g. "en" => "English", "fr" => "Français", ...
|
|
||||||
*
|
|
||||||
* Used to render a select that enable user to switch language.
|
|
||||||
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
|
||||||
* */
|
|
||||||
labelBySupportedLanguageTag: Record<string, string>;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Examples assuming currentLanguageTag === "en"
|
|
||||||
* {
|
|
||||||
* en: {
|
|
||||||
* "access-denied": "Access denied",
|
|
||||||
* "impersonateTitleHtml": "<strong>{0}</strong> Impersonate User",
|
|
||||||
* "bar": "Bar {0}"
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* msgStr("access-denied") === "Access denied"
|
|
||||||
* msgStr("not-a-message-key") Throws an error
|
|
||||||
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
|
||||||
* msgStr("${bar}", "<strong>c</strong>") === "Bar <strong>XXX</strong>"
|
|
||||||
* The html in the arg is partially escaped for security reasons, it might come from an untrusted source, it's not safe to render it as html.
|
|
||||||
*/
|
|
||||||
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
|
||||||
/**
|
|
||||||
* This is meant to be used when the key argument is variable, something that might have been configured by the user
|
|
||||||
* in the Keycloak admin for example.
|
|
||||||
*
|
|
||||||
* Examples assuming currentLanguageTag === "en"
|
|
||||||
* {
|
|
||||||
* en: {
|
|
||||||
* "access-denied": "Access denied",
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
*
|
|
||||||
* advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied"
|
|
||||||
* advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key"
|
|
||||||
*/
|
|
||||||
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 MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage;
|
|
||||||
|
|
||||||
export function createGetI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag_themeDefined: {
|
|
||||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
|
||||||
}) {
|
|
||||||
type I18n = GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>;
|
|
||||||
|
|
||||||
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" | "getChangeLocaleUrl" | "labelBySupportedLanguageTag"> = {
|
|
||||||
currentLanguageTag: kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG,
|
|
||||||
getChangeLocaleUrl: 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_themeDefined>({
|
|
||||||
messages_themeDefined:
|
|
||||||
messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ??
|
|
||||||
messagesByLanguageTag_themeDefined[FALLBACK_LANGUAGE_TAG] ??
|
|
||||||
(() => {
|
|
||||||
const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0];
|
|
||||||
if (firstLanguageTag === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return messagesByLanguageTag_themeDefined[firstLanguageTag];
|
|
||||||
})(),
|
|
||||||
messages_fromKcServer: kcContext["x-keycloakify"].messages
|
|
||||||
});
|
|
||||||
|
|
||||||
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === FALLBACK_LANGUAGE_TAG;
|
|
||||||
|
|
||||||
const result: Result = {
|
|
||||||
i18n: {
|
|
||||||
...partialI18n,
|
|
||||||
...createI18nTranslationFunctions({
|
|
||||||
messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined
|
|
||||||
}),
|
|
||||||
isFetchingTranslations: !isCurrentLanguageFallbackLanguage
|
|
||||||
},
|
|
||||||
prI18n_currentLanguage: isCurrentLanguageFallbackLanguage
|
|
||||||
? undefined
|
|
||||||
: (async () => {
|
|
||||||
const messages_defaultSet_currentLanguage = await fetchMessages_defaultSet(partialI18n.currentLanguageTag);
|
|
||||||
|
|
||||||
const i18n_currentLanguage: I18n = {
|
|
||||||
...partialI18n,
|
|
||||||
...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }),
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends string>(params: {
|
|
||||||
messages_themeDefined: Record<MessageKey_themeDefined, string> | undefined;
|
|
||||||
messages_fromKcServer: Record<string, string>;
|
|
||||||
}) {
|
|
||||||
const { messages_themeDefined, messages_fromKcServer } = params;
|
|
||||||
|
|
||||||
function createI18nTranslationFunctions(params: {
|
|
||||||
messages_defaultSet_currentLanguage: Partial<Record<MessageKey_defaultSet, string>> | undefined;
|
|
||||||
}): Pick<GenericI18n_noJsx<MessageKey_defaultSet | MessageKey_themeDefined>, "msgStr" | "advancedMsgStr"> {
|
|
||||||
const { messages_defaultSet_currentLanguage } = params;
|
|
||||||
|
|
||||||
function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined {
|
|
||||||
const { key, args } = props;
|
|
||||||
|
|
||||||
const message =
|
|
||||||
id<Record<string, string | undefined>>(messages_fromKcServer)[key] ??
|
|
||||||
id<Record<string, string | undefined> | undefined>(messages_themeDefined)?.[key] ??
|
|
||||||
id<Record<string, string | undefined> | undefined>(messages_defaultSet_currentLanguage)?.[key] ??
|
|
||||||
id<Record<string, string | undefined>>(messages_defaultSet_fallbackLanguage)[key];
|
|
||||||
|
|
||||||
if (message === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startIndex = message
|
|
||||||
.match(/{[0-9]+}/g)
|
|
||||||
?.map(g => g.match(/{([0-9]+)}/)![1])
|
|
||||||
.map(indexStr => parseInt(indexStr))
|
|
||||||
.sort((a, b) => a - b)[0];
|
|
||||||
|
|
||||||
if (startIndex === undefined) {
|
|
||||||
// No {0} in message (no arguments expected)
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
let messageWithArgsInjected = message;
|
|
||||||
|
|
||||||
args.forEach((arg, i) => {
|
|
||||||
if (arg === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
|
||||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
|
||||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return messageWithArgsInjected;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[] }): string {
|
|
||||||
const { key, args } = props;
|
|
||||||
|
|
||||||
const match = key.match(/^\$\{(.+)\}$/);
|
|
||||||
|
|
||||||
if (match === null) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resolveMsg({ key: match[1], args }) ?? key;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
msgStr: (key, ...args) => {
|
|
||||||
const resolvedMessage = resolveMsg({ key, args });
|
|
||||||
assert(resolvedMessage !== undefined, `Message with key "${key}" not found`);
|
|
||||||
return resolvedMessage;
|
|
||||||
},
|
|
||||||
advancedMsgStr: (key, ...args) => resolveMsgAdvanced({ key, args })
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { createI18nTranslationFunctions };
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
import type { GenericI18n } from "./GenericI18n";
|
|
||||||
import type { MessageKey_defaultSet, KcContextLike } from "./i18n";
|
|
||||||
export type { MessageKey_defaultSet, KcContextLike };
|
|
||||||
export type I18n = GenericI18n<MessageKey_defaultSet>;
|
|
||||||
export { createUseI18n } from "./useI18n";
|
|
@ -1,95 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { createGetI18n, type GenericI18n_noJsx, type KcContextLike, type MessageKey_defaultSet } from "./i18n";
|
|
||||||
import { GenericI18n } from "./GenericI18n";
|
|
||||||
import { Reflect } from "tsafe/Reflect";
|
|
||||||
|
|
||||||
export function createUseI18n<MessageKey_themeDefined extends string = never>(messagesByLanguageTag: {
|
|
||||||
[languageTag: string]: { [key in MessageKey_themeDefined]: string };
|
|
||||||
}) {
|
|
||||||
type MessageKey = MessageKey_defaultSet | MessageKey_themeDefined;
|
|
||||||
|
|
||||||
type I18n = GenericI18n<MessageKey>;
|
|
||||||
|
|
||||||
const { withJsx } = (() => {
|
|
||||||
const cache = new WeakMap<GenericI18n_noJsx<MessageKey>, GenericI18n<MessageKey>>();
|
|
||||||
|
|
||||||
function renderHtmlString(params: { htmlString: string; msgKey: string }): JSX.Element {
|
|
||||||
const { htmlString, msgKey } = params;
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
data-kc-msg={msgKey}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: htmlString
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function withJsx(i18n_noJsx: GenericI18n_noJsx<MessageKey>): I18n {
|
|
||||||
use_cache: {
|
|
||||||
const i18n = cache.get(i18n_noJsx);
|
|
||||||
|
|
||||||
if (i18n === undefined) {
|
|
||||||
break use_cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
return i18n;
|
|
||||||
}
|
|
||||||
|
|
||||||
const i18n: I18n = {
|
|
||||||
...i18n_noJsx,
|
|
||||||
msg: (msgKey, ...args) => renderHtmlString({ htmlString: i18n_noJsx.msgStr(msgKey, ...args), msgKey }),
|
|
||||||
advancedMsg: (msgKey, ...args) => renderHtmlString({ htmlString: i18n_noJsx.advancedMsgStr(msgKey, ...args), msgKey })
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.set(i18n_noJsx, i18n);
|
|
||||||
|
|
||||||
return i18n;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { withJsx };
|
|
||||||
})();
|
|
||||||
|
|
||||||
add_style: {
|
|
||||||
const attributeName = "data-kc-i18n";
|
|
||||||
|
|
||||||
// Check if already exists in head
|
|
||||||
if (document.querySelector(`style[${attributeName}]`) !== null) {
|
|
||||||
break add_style;
|
|
||||||
}
|
|
||||||
|
|
||||||
const styleElement = document.createElement("style");
|
|
||||||
styleElement.attributes.setNamedItem(document.createAttribute(attributeName));
|
|
||||||
(styleElement.textContent = `[data-kc-msg] { display: inline-block; }`), document.head.prepend(styleElement);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { getI18n } = createGetI18n(messagesByLanguageTag);
|
|
||||||
|
|
||||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
|
||||||
const { kcContext } = params;
|
|
||||||
|
|
||||||
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
|
|
||||||
|
|
||||||
const [i18n_toReturn, setI18n_toReturn] = useState<I18n>(withJsx(i18n));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isActive = true;
|
|
||||||
|
|
||||||
prI18n_currentLanguage?.then(i18n => {
|
|
||||||
if (!isActive) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setI18n_toReturn(withJsx(i18n));
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isActive = false;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { i18n: i18n_toReturn };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { useI18n, ofTypeI18n: Reflect<I18n>() };
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user