i18n rebuild from the ground up

This commit is contained in:
garronej 2022-04-27 21:02:10 +02:00
parent cc8b2e72c1
commit 7e7071305f
26 changed files with 288 additions and 361 deletions

View File

@ -61,6 +61,7 @@
}, },
"devDependencies": { "devDependencies": {
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@types/node": "^17.0.25", "@types/node": "^17.0.25",
"@types/react": "^17.0.0", "@types/react": "^17.0.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
@ -77,6 +78,7 @@
"cheerio": "^1.0.0-rc.5", "cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2", "cli-select": "^1.1.2",
"evt": "2.0.0-beta.39", "evt": "2.0.0-beta.39",
"memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.1", "minimal-polyfills": "^2.2.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"powerhooks": "^0.14.0", "powerhooks": "^0.14.0",

View File

@ -2,10 +2,10 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Error } & KcProps) => { export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Error } & KcProps) => {
const { msg } = useKcMessage(); const { msg } = getMsg(kcContext);
const { message, client } = kcContext; const { message, client } = kcContext;

View File

@ -3,10 +3,10 @@ import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert"; import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
export const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info } & KcProps) => { export const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info } & KcProps) => {
const { msg } = useKcMessage(); const { msg } = getMsg(kcContext);
assert(kcContext.message !== undefined); assert(kcContext.message !== undefined);

View File

@ -2,7 +2,7 @@ import { useState, memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react"; import type { FormEventHandler } from "react";
@ -10,7 +10,7 @@ import type { FormEventHandler } from "react";
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => { export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => {
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext; const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginIdpLinkConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm } & KcProps) => { export const LoginIdpLinkConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm } & KcProps) => {
const { url, idpAlias } = kcContext; const { url, idpAlias } = kcContext;
const { msg } = useKcMessage(); const { msg } = getMsg(kcContext);
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();

View File

@ -2,12 +2,12 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
export const LoginIdpLinkEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail } & KcProps) => { export const LoginIdpLinkEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail } & KcProps) => {
const { url, realm, brokerContext, idpAlias } = kcContext; const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = useKcMessage(); const { msg } = getMsg(kcContext);
return ( return (
<Template <Template

View File

@ -2,7 +2,7 @@ import { useEffect, memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { headInsert } from "../tools/headInsert"; import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../tools/pathJoin"; import { pathJoin } from "../tools/pathJoin";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
@ -12,7 +12,7 @@ export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBas
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
useEffect(() => { useEffect(() => {
let isCleanedUp = false; let isCleanedUp = false;

View File

@ -2,12 +2,12 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
export const LoginPageExpired = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginPageExpired } & KcProps) => { export const LoginPageExpired = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginPageExpired } & KcProps) => {
const { url } = kcContext; const { url } = kcContext;
const { msg } = useKcMessage(); const { msg } = getMsg(kcContext);
return ( return (
<Template <Template

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginResetPassword } & KcProps) => { export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginResetPassword } & KcProps) => {
const { url, realm, auth } = kcContext; const { url, realm, auth } = kcContext;
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginUpdatePassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdatePassword } & KcProps) => { export const LoginUpdatePassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdatePassword } & KcProps) => {
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext; const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginUpdateProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdateProfile } & KcProps) => { export const LoginUpdateProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdateProfile } & KcProps) => {
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
const { url, user, messagesPerField, isAppInitiatedAction } = kcContext; const { url, user, messagesPerField, isAppInitiatedAction } = kcContext;

View File

@ -2,10 +2,10 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail } & KcProps) => { export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail } & KcProps) => {
const { msg } = useKcMessage(); const { msg } = getMsg(kcContext);
const { url } = kcContext; const { url } = kcContext;

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register } & KcProps) => { export const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register } & KcProps) => {
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext; const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();

View File

@ -2,7 +2,7 @@ import { useMemo, memo, useEffect, useState, Fragment } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase"; import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { ReactComponent } from "../tools/ReactComponent"; import type { ReactComponent } from "../tools/ReactComponent";
import { useCallbackFactory } from "powerhooks/useCallbackFactory"; import { useCallbackFactory } from "powerhooks/useCallbackFactory";
@ -11,7 +11,7 @@ import { useFormValidationSlice } from "../useFormValidationSlice";
export const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => { export const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext; const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
const { cx, css } = useCssAndCx(); const { cx, css } = useCssAndCx();
@ -74,7 +74,7 @@ type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => { const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => {
const { cx, css } = useCssAndCx(); const { cx, css } = useCssAndCx();
const { advancedMsg } = useKcMessage(); const { advancedMsg } = getMsg(kcContext);
const { const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable }, formValidationState: { fieldStateByAttributeName, isFormSubmittable },

View File

@ -1,12 +1,9 @@
import { useReducer, useEffect, memo } from "react"; import { useReducer, useEffect, memo } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg, getCurrentKcLanguageTag, changeLocale, getTagLabel } from "../i18n";
import { useKcLanguageTag } from "../i18n/useKcLanguageTag"; import type { KcLanguageTag } from "../i18n";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { assert } from "../tools/assert"; import { assert } from "../tools/assert";
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { getBestMatchAmongKcLanguageTag } from "../i18n/KcLanguageTag";
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
import { useCallbackFactory } from "powerhooks/useCallbackFactory"; import { useCallbackFactory } from "powerhooks/useCallbackFactory";
import { headInsert } from "../tools/headInsert"; import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../tools/pathJoin"; import { pathJoin } from "../tools/pathJoin";
@ -51,36 +48,19 @@ export const Template = memo((props: TemplateProps) => {
console.log("Rendering this page with react using keycloakify"); console.log("Rendering this page with react using keycloakify");
}, []); }, []);
const { msg } = useKcMessage(); const { msg } = getMsg(kcContext);
const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag(); const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [KcLanguageTag]) =>
changeLocale({
const onChangeLanguageClickFactory = useCallbackFactory(([languageTag]: [KcLanguageTag]) => setKcLanguageTag(languageTag)); kcContext,
kcLanguageTag,
}),
);
const onTryAnotherWayClick = useConstCallback(() => (document.forms["kc-select-try-another-way-form" as never].submit(), false)); const onTryAnotherWayClick = useConstCallback(() => (document.forms["kc-select-try-another-way-form" as never].submit(), false));
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext; const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
useEffect(() => {
if (!realm.internationalizationEnabled) {
return;
}
assert(locale !== undefined);
const kcContext_kcLanguageTag = getBestMatchAmongKcLanguageTag(locale.current);
if (["error.ftl", "info.ftl", "login-page-expired.ftl"].indexOf(kcContext.pageId) >= 0) {
setKcLanguageTag(kcContext_kcLanguageTag);
return;
}
if (kcLanguageTag !== kcContext_kcLanguageTag) {
window.location.href = locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag)!.url;
}
}, [kcLanguageTag]);
const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false); const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
useEffect(() => { useEffect(() => {
@ -158,13 +138,13 @@ export const Template = memo((props: TemplateProps) => {
<div id="kc-locale-wrapper" className={cx(props.kcLocaleWrapperClass)}> <div id="kc-locale-wrapper" className={cx(props.kcLocaleWrapperClass)}>
<div className="kc-dropdown" id="kc-locale-dropdown"> <div className="kc-dropdown" id="kc-locale-dropdown">
<a href="#" id="kc-current-locale-link"> <a href="#" id="kc-current-locale-link">
{getKcLanguageTagLabel(kcLanguageTag)} {getTagLabel({ "kcLanguageTag": getCurrentKcLanguageTag(kcContext), kcContext })}
</a> </a>
<ul> <ul>
{locale.supported.map(({ languageTag }) => ( {locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item"> <li key={languageTag} className="kc-dropdown-item">
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}> <a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
{getKcLanguageTagLabel(languageTag)} {getTagLabel({ "kcLanguageTag": languageTag, kcContext })}
</a> </a>
</li> </li>
))} ))}

View File

@ -1,12 +1,33 @@
import { memo } from "react"; import { useReducer, useEffect, memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { getMsg } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import { kcMessages, getCurrentKcLanguageTag } from "../i18n";
import type { KcLanguageTag } from "../i18n";
/** Allow to avoid bundling the terms and download it on demand*/
export function useDownloadTerms(params: {
kcContext: KcContextBase;
downloadTermMarkdown: (params: { currentKcLanguageTag: KcLanguageTag }) => Promise<string>;
}) {
const { kcContext, downloadTermMarkdown } = params;
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
const currentKcLanguageTag = getCurrentKcLanguageTag(kcContext);
downloadTermMarkdown({ currentKcLanguageTag }).then(thermMarkdown => {
kcMessages[currentKcLanguageTag].termsText = thermMarkdown;
forceUpdate();
});
}, []);
}
export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms } & KcProps) => { export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms } & KcProps) => {
const { msg, msgStr } = useKcMessage(); const { msg, msgStr } = getMsg(kcContext);
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();

View File

@ -1,9 +1,8 @@
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl"; import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
import type { KcLanguageTag } from "../i18n/KcLanguageTag"; import type { KcLanguageTag } from "../i18n";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/useKcMessage"; import type { MessageKey } from "../i18n";
import type { LanguageLabel } from "../i18n/KcLanguageTag";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never; type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
@ -47,13 +46,10 @@ export declare namespace KcContextBase {
locale?: { locale?: {
supported: { supported: {
url: string; url: string;
label: string;
languageTag: KcLanguageTag; languageTag: KcLanguageTag;
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
* or getLanguageLabel(languageTag) === label
*/
//label: LanguageLabel;
}[]; }[];
current: LanguageLabel; currentLanguageTag: KcLanguageTag;
}; };
auth?: { auth?: {
showUsername: boolean; showUsername: boolean;

View File

@ -1,7 +1,5 @@
import "minimal-polyfills/Object.fromEntries"; import "minimal-polyfills/Object.fromEntries";
import type { KcContextBase, Attribute } from "../KcContextBase"; import type { KcContextBase, Attribute } from "../KcContextBase";
import { getEvtKcLanguage } from "../../i18n/useKcLanguageTag";
import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
//NOTE: Aside because we want to be able to import them from node //NOTE: Aside because we want to be able to import them from node
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath"; import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
@ -32,81 +30,100 @@ export const kcContextCommonMock: KcContextBase.Common = {
}, },
"locale": { "locale": {
"supported": [ "supported": [
/* spell-checker: disable */
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
"label": "Deutsch",
"languageTag": "de", "languageTag": "de",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
"label": "Norsk",
"languageTag": "no", "languageTag": "no",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
"label": "Русский",
"languageTag": "ru", "languageTag": "ru",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
"label": "Svenska",
"languageTag": "sv", "languageTag": "sv",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
"label": "Português (Brasil)",
"languageTag": "pt-BR", "languageTag": "pt-BR",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
"label": "Lietuvių",
"languageTag": "lt", "languageTag": "lt",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
"label": "English",
"languageTag": "en", "languageTag": "en",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
"label": "Italiano",
"languageTag": "it", "languageTag": "it",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
"label": "Français",
"languageTag": "fr", "languageTag": "fr",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
"label": "中文简体",
"languageTag": "zh-CN", "languageTag": "zh-CN",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
"label": "Español",
"languageTag": "es", "languageTag": "es",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
"label": "Čeština",
"languageTag": "cs", "languageTag": "cs",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
"label": "日本語",
"languageTag": "ja", "languageTag": "ja",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
"label": "Slovenčina",
"languageTag": "sk", "languageTag": "sk",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
"label": "Polski",
"languageTag": "pl", "languageTag": "pl",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
"label": "Català",
"languageTag": "ca", "languageTag": "ca",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
"label": "Nederlands",
"languageTag": "nl", "languageTag": "nl",
}, },
{ {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr", "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
"label": "Türkçe",
"languageTag": "tr", "languageTag": "tr",
}, },
/* spell-checker: enable */
], ],
//"current": null as any "currentLanguageTag": "en",
"current": "English",
}, },
"auth": { "auth": {
"showUsername": false, "showUsername": false,
@ -124,11 +141,6 @@ export const kcContextCommonMock: KcContextBase.Common = {
"isAppInitiatedAction": false, "isAppInitiatedAction": false,
}; };
Object.defineProperty(kcContextCommonMock.locale!, "current", {
"get": () => getKcLanguageTagLabel(getEvtKcLanguage().state),
"enumerable": true,
});
const loginUrl = { const loginUrl = {
...kcContextCommonMock.url, ...kcContextCommonMock.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg", "loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",

View File

@ -1,63 +0,0 @@
import { objectKeys } from "tsafe/objectKeys";
import { kcMessages } from "./kcMessages/login";
export type KcLanguageTag = keyof typeof kcMessages;
const kcLanguageByTagLabel = {
/* spell-checker: disable */
"es": "Español",
"it": "Italiano",
"fr": "Français",
"ca": "Català",
"en": "English",
"de": "Deutsch",
"no": "Norsk",
"pt-BR": "Português (Brasil)",
"ru": "Русский",
"sk": "Slovenčina",
"ja": "日本語",
"pl": "Polski",
"zh-CN": "中文简体",
"sv": "Svenska",
"lt": "Lietuvių",
"cs": "Čeština",
"nl": "Nederlands",
"tr": "Türkçe",
"da": "Dansk",
"hu": "Magyar",
/* spell-checker: enable */
} as const;
export type LanguageLabel = typeof kcLanguageByTagLabel[keyof typeof kcLanguageByTagLabel];
export function getKcLanguageTagLabel(language: KcLanguageTag): LanguageLabel {
return kcLanguageByTagLabel[language] ?? language;
}
export const kcLanguageTags = objectKeys(kcMessages);
/**
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
* it corresponds to: "fr".
* If there is no reasonable match it's guessed from navigator.language.
* If still no matches "en" is returned.
*/
export function getBestMatchAmongKcLanguageTag(languageLike: string): KcLanguageTag {
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
const kcLanguageTag = kcLanguageTags.find(
language =>
language.toLowerCase().includes(iso2LanguageLike) ||
getKcLanguageTagLabel(language).toLocaleLowerCase() === languageLike.toLocaleLowerCase(),
);
if (kcLanguageTag !== undefined) {
return kcLanguageTag;
}
if (languageLike !== navigator.language) {
return getBestMatchAmongKcLanguageTag(navigator.language);
}
return "en";
}

180
src/lib/i18n/index.tsx Normal file
View File

@ -0,0 +1,180 @@
import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import ReactMarkdown from "react-markdown";
import memoize from "memoizee";
import { kcMessages as kcMessagesBase } from "./generated_kcMessages/15.0.2/login";
import { assert } from "tsafe/assert";
export const kcMessages = {
...kcMessagesBase,
"en": {
...kcMessagesBase["en"],
"termsText": "⏳",
"shouldBeEqual": "{0} should be equal to {1}",
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option",
},
"fr": {
...kcMessagesBase["fr"],
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être egale à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entiers",
"notAValidOption": "N'est pas une option valide",
/* spell-checker: enable */
},
};
export type KcLanguageTag = keyof typeof kcMessages;
type KcContextLike = { locale?: { currentLanguageTag: KcLanguageTag } };
export function getCurrentKcLanguageTag(kcContext: KcContextLike) {
return kcContext.locale?.currentLanguageTag ?? "en";
}
export function getTagLabel(params: {
kcContext: {
locale?: {
supported: { languageTag: KcLanguageTag; label: string }[];
};
};
kcLanguageTag: KcLanguageTag;
}): string {
const { kcContext, kcLanguageTag } = params;
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag);
assert(targetSupportedLocale !== undefined, `${kcLanguageTag} need to be enabled in Keycloak admin`);
return targetSupportedLocale.label;
}
export function changeLocale(params: {
kcContext: {
locale?: {
supported: { languageTag: KcLanguageTag; url: string }[];
};
};
kcLanguageTag: KcLanguageTag;
}): never {
const { kcContext, kcLanguageTag } = params;
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag);
assert(targetSupportedLocale !== undefined, `${kcLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url;
assert(false, "never");
}
export type MessageKey = keyof typeof kcMessages["en"];
function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): Key extends MessageKey ? (DoRenderMarkdown extends true ? JSX.Element : string) : undefined {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
let str = kcMessages[kcLanguageTag as any as "en"][key as MessageKey] ?? kcMessages["en"][key as MessageKey];
if (str === undefined) {
return undefined as any;
}
str = (() => {
const startIndex = str
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {
return str;
}
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
str = str.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
});
return str;
})();
return (
doRenderMarkdown ? (
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
{str}
</ReactMarkdown>
) : (
str
)
) as any;
}
function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): DoRenderMarkdown extends true ? JSX.Element : string {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
const match = key.match(/^\$\{([^{]+)\}$/);
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
args,
kcLanguageTag,
doRenderMarkdown,
});
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
}
/**
* When the language is switched the page is reloaded, this may appear
* as a bug as you might notice that the language successfully switch before
* reload.
* However we need to tell Keycloak that the user have changed the language
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
* https://user-images.githubusercontent.com/6702424/138096682-351bb61f-f24e-4caf-91b7-cca8cfa2cb58.mov
*
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied")
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
*
*
* NOTE: This function is memoized, it always returns the same object for a given kcContext)
*
*/
export const getMsg = memoize((kcContext: KcContextLike) => {
const kcLanguageTag = getCurrentKcLanguageTag(kcContext);
return {
"msgStr": (key: MessageKey, ...args: (string | undefined)[]): string => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
"msg": (key: MessageKey, ...args: (string | undefined)[]): JSX.Element => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsg": <Key extends string>(key: Key, ...args: (string | undefined)[]): JSX.Element =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsgStr": <Key extends string>(key: Key, ...args: (string | undefined)[]): string =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
};
});

View File

@ -1,50 +0,0 @@
import { kcMessages as kcMessagesBase } from "../generated_kcMessages/15.0.2/login";
import { Evt } from "evt";
import { objectKeys } from "tsafe/objectKeys";
const kcMessages = {
...kcMessagesBase,
"en": {
...kcMessagesBase["en"],
"shouldBeEqual": "{0} should be equal to {1}",
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option",
},
"fr": {
...kcMessagesBase["fr"],
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être egale à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entiers",
"notAValidOption": "N'est pas une option valide",
/* spell-checker: enable */
},
};
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
(["termsText", "doAccept", "doDecline", "termsTitle"] as const).forEach(key =>
objectKeys(kcMessages).forEach(kcLanguage =>
Object.defineProperty(
kcMessages[kcLanguage],
key,
(() => {
let value = key === "termsText" ? "⏳" : kcMessages[kcLanguage][key];
return {
"enumerable": true,
"get": () => value,
"set": (newValue: string) => {
value = newValue;
Evt.asPostable(evtTermsUpdated).post();
},
};
})(),
),
),
);
export { kcMessages };

View File

@ -1,28 +0,0 @@
import { createUseGlobalState } from "powerhooks/useGlobalState";
import { getKcContextFromWindow } from "../getKcContext/getKcContextFromWindow";
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
import type { StatefulEvt } from "powerhooks";
import { KcLanguageTag } from "./KcLanguageTag";
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
const wrap = createUseGlobalState(
"kcLanguageTag",
() => {
const kcContext = getKcContextFromWindow();
const languageLike = kcContext?.locale?.current ?? (typeof navigator === "undefined" ? undefined : navigator.language);
if (languageLike === undefined) {
return "en";
}
return getBestMatchAmongKcLanguageTag(languageLike);
},
{ "persistance": "localStorage" },
);
export const { useKcLanguageTag } = wrap;
export function getEvtKcLanguage(): StatefulEvt<KcLanguageTag> {
return wrap.evtKcLanguageTag;
}

View File

@ -1,115 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
import { useReducer } from "react";
import { useKcLanguageTag } from "./useKcLanguageTag";
import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
import { useEvt } from "evt/hooks";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import ReactMarkdown from "react-markdown";
import { useGuaranteedMemo } from "powerhooks/useGuaranteedMemo";
export { kcMessages };
export type MessageKey = keyof typeof kcMessages["en"];
function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): Key extends MessageKey ? (DoRenderMarkdown extends true ? JSX.Element : string) : undefined {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
let str = kcMessages[kcLanguageTag as any as "en"][key as MessageKey] ?? kcMessages["en"][key as MessageKey];
if (str === undefined) {
return undefined as any;
}
str = (() => {
const startIndex = str
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {
return str;
}
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
str = str.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
});
return str;
})();
return (
doRenderMarkdown ? (
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
{str}
</ReactMarkdown>
) : (
str
)
) as any;
}
function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): DoRenderMarkdown extends true ? JSX.Element : string {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
const match = key.match(/^\$\{([^{]+)\}$/);
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
args,
kcLanguageTag,
doRenderMarkdown,
});
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
}
/**
* When the language is switched the page is reloaded, this may appear
* as a bug as you might notice that the language successfully switch before
* reload.
* However we need to tell Keycloak that the user have changed the language
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
* https://user-images.githubusercontent.com/6702424/138096682-351bb61f-f24e-4caf-91b7-cca8cfa2cb58.mov
*
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied")
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
*
*/
export function useKcMessage() {
const { kcLanguageTag } = useKcLanguageTag();
const [trigger, forceUpdate] = useReducer((counter: number) => counter + 1, 0);
useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []);
return useGuaranteedMemo(
() => ({
"msgStr": (key: MessageKey, ...args: (string | undefined)[]): string =>
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
"msg": (key: MessageKey, ...args: (string | undefined)[]): JSX.Element =>
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsg": <Key extends string>(key: Key, ...args: (string | undefined)[]): JSX.Element =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsgStr": <Key extends string>(key: Key, ...args: (string | undefined)[]): string =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
}),
[kcLanguageTag, trigger],
);
}

View File

@ -1,18 +1,10 @@
export * from "./getKcContext"; export * from "./getKcContext";
export * from "./i18n/KcLanguageTag"; export * from "./i18n";
export * from "./i18n/useKcLanguageTag";
export * from "./i18n/useKcMessage"; export { useDownloadTerms } from "./components/Terms";
export * from "./i18n/kcMessages/login";
export * from "./components/KcProps"; export * from "./components/KcProps";
export * from "./components/Login";
export * from "./components/Template";
export * from "./components/KcApp";
export * from "./components/Info";
export * from "./components/Error";
export * from "./components/LoginResetPassword";
export * from "./components/LoginVerifyEmail";
export * from "./keycloakJsAdapter"; export * from "./keycloakJsAdapter";
export * from "./useFormValidationSlice"; export * from "./useFormValidationSlice";

View File

@ -1,35 +1,30 @@
import "./tools/Array.prototype.every"; import "./tools/Array.prototype.every";
import { useMemo, useReducer, Fragment } from "react"; import { useMemo, useReducer, Fragment } from "react";
import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase"; import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
import { useKcMessage } from "./i18n/useKcMessage"; import { getMsg } from "./i18n";
import type { KcLanguageTag } from "./i18n";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import type { MessageKey } from "./i18n/useKcMessage"; import type { MessageKey } from "./i18n";
import { emailRegexp } from "./tools/emailRegExp"; import { emailRegexp } from "./tools/emailRegExp";
export type KcContextLike = {
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
attributes: { name: string; value?: string; validators: Validators }[];
passwordRequired: boolean;
realm: { registrationEmailAsUsername: boolean };
};
export function useGetErrors(params: { export function useGetErrors(params: {
kcContext: { kcContext: {
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">; messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
profile: { profile: {
attributes: { name: string; value?: string; validators: Validators }[]; attributes: { name: string; value?: string; validators: Validators }[];
}; };
locale?: { currentLanguageTag: KcLanguageTag };
}; };
}) { }) {
const { const { kcContext } = params;
kcContext: {
messagesPerField,
profile: { attributes },
},
} = params;
const { msg, msgStr, advancedMsg, advancedMsgStr } = useKcMessage(); const {
messagesPerField,
profile: { attributes },
} = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = getMsg(kcContext);
const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => { const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => {
const { name, fieldValueByAttributeName } = params; const { name, fieldValueByAttributeName } = params;

View File

@ -219,6 +219,11 @@
dependencies: dependencies:
"@types/unist" "*" "@types/unist" "*"
"@types/memoizee@^0.4.7":
version "0.4.7"
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.7.tgz#fa0025b5ea1c8acb85fd52a76982c5a096c2c91e"
integrity sha512-EMtvWnRC/W0KYmXxbPJAMg/M1OXkFboSpd/IzkNMDXfoFPE4uom1RHFl8q7m/4R0y9eG1yaoaIPiMHk0vMA0eA==
"@types/node@^17.0.25": "@types/node@^17.0.25":
version "17.0.25" version "17.0.25"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448"