Refactor: Use hook instead of Context for i18n

This commit is contained in:
garronej
2022-07-31 22:30:32 +02:00
parent 0641151ca1
commit 7a0a046596
22 changed files with 238 additions and 222 deletions

View File

@ -2,16 +2,16 @@ import React, { 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 { useI18n } from "../i18n"; import type { I18n } from "../i18n";
const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Error } & KcProps) => {
const { msg } = useI18n();
const Error = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Error; i18n: I18n } & KcProps) => {
const { message, client } = kcContext; const { message, client } = kcContext;
const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("errorTitle")} headerNode={msg("errorTitle")}

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 { useI18n } from "../i18n"; import type { I18n } from "../i18n";
const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info } & KcProps) => { const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Info; i18n: I18n } & KcProps) => {
const { msg, msgStr } = useI18n(); const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined); assert(kcContext.message !== undefined);
@ -14,7 +14,7 @@ const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info } &
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>} headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}

View File

@ -1,7 +1,8 @@
import React, { lazy, memo, Suspense } from "react"; import React, { lazy, memo, Suspense } from "react";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import { I18nProvider } from "../i18n"; import { __unsafe_useI18n as useI18n } from "../i18n";
import type { I18n } from "../i18n";
const Login = lazy(() => import("./Login")); const Login = lazy(() => import("./Login"));
const Register = lazy(() => import("./Register")); const Register = lazy(() => import("./Register"));
@ -20,48 +21,60 @@ const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp")); const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm")); const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => { const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...props }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
const i18n = (function useClosure() {
const i18n = useI18n({
kcContext,
"extraMessages": {},
"doSkip": userProvidedI18n !== undefined,
});
return userProvidedI18n ?? i18n;
})();
if (i18n === undefined) {
return null;
}
return ( return (
<I18nProvider kcContext={kcContext}> <Suspense>
<Suspense> {(() => {
{(() => { switch (kcContext.pageId) {
switch (kcContext.pageId) { case "login.ftl":
case "login.ftl": return <Login {...{ kcContext, i18n, ...props }} />;
return <Login {...{ kcContext, ...props }} />; case "register.ftl":
case "register.ftl": return <Register {...{ kcContext, i18n, ...props }} />;
return <Register {...{ kcContext, ...props }} />; case "register-user-profile.ftl":
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, i18n, ...props }} />;
return <RegisterUserProfile {...{ kcContext, ...props }} />; case "info.ftl":
case "info.ftl": return <Info {...{ kcContext, i18n, ...props }} />;
return <Info {...{ kcContext, ...props }} />; case "error.ftl":
case "error.ftl": return <Error {...{ kcContext, i18n, ...props }} />;
return <Error {...{ kcContext, ...props }} />; case "login-reset-password.ftl":
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, i18n, ...props }} />;
return <LoginResetPassword {...{ kcContext, ...props }} />; case "login-verify-email.ftl":
case "login-verify-email.ftl": return <LoginVerifyEmail {...{ kcContext, i18n, ...props }} />;
return <LoginVerifyEmail {...{ kcContext, ...props }} />; case "terms.ftl":
case "terms.ftl": return <Terms {...{ kcContext, i18n, ...props }} />;
return <Terms {...{ kcContext, ...props }} />; case "login-otp.ftl":
case "login-otp.ftl": return <LoginOtp {...{ kcContext, i18n, ...props }} />;
return <LoginOtp {...{ kcContext, ...props }} />; case "login-update-password.ftl":
case "login-update-password.ftl": return <LoginUpdatePassword {...{ kcContext, i18n, ...props }} />;
return <LoginUpdatePassword {...{ kcContext, ...props }} />; case "login-update-profile.ftl":
case "login-update-profile.ftl": return <LoginUpdateProfile {...{ kcContext, i18n, ...props }} />;
return <LoginUpdateProfile {...{ kcContext, ...props }} />; case "login-idp-link-confirm.ftl":
case "login-idp-link-confirm.ftl": return <LoginIdpLinkConfirm {...{ kcContext, i18n, ...props }} />;
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />; case "login-idp-link-email.ftl":
case "login-idp-link-email.ftl": return <LoginIdpLinkEmail {...{ kcContext, i18n, ...props }} />;
return <LoginIdpLinkEmail {...{ kcContext, ...props }} />; case "login-page-expired.ftl":
case "login-page-expired.ftl": return <LoginPageExpired {...{ kcContext, i18n, ...props }} />;
return <LoginPageExpired {...{ kcContext, ...props }} />; case "login-config-totp.ftl":
case "login-config-totp.ftl": return <LoginConfigTotp {...{ kcContext, i18n, ...props }} />;
return <LoginConfigTotp {...{ kcContext, ...props }} />; case "logout-confirm.ftl":
case "logout-confirm.ftl": return <LogoutConfirm {...{ kcContext, i18n, ...props }} />;
return <LogoutConfirm {...{ kcContext, ...props }} />; }
} })()}
})()} </Suspense>
</Suspense>
</I18nProvider>
); );
}); });

View File

@ -5,12 +5,12 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
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";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => { const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Login; i18n: I18n } & KcProps) => {
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext; const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
@ -32,7 +32,7 @@ const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login }
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayInfo={social.displayInfo} displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined} displayWide={realm.password && social.providers !== undefined}

View File

@ -2,15 +2,16 @@ import React, { 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 { useI18n } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { I18n } from "../i18n";
const LoginConfigTotp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginConfigTotp } & KcProps) => { const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n } & KcProps) => {
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext; const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = { const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
HmacSHA1: "SHA1", HmacSHA1: "SHA1",
HmacSHA256: "SHA256", HmacSHA256: "SHA256",
@ -19,7 +20,7 @@ const LoginConfigTotp = memo(({ kcContext, ...props }: { kcContext: KcContextBas
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("loginTotpTitle")} headerNode={msg("loginTotpTitle")}
formNode={ formNode={

View File

@ -2,19 +2,19 @@ import React, { 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 { useI18n } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { I18n } from "../i18n";
const LoginIdpLinkConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm } & KcProps) => { const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm; i18n: I18n } & KcProps) => {
const { url, idpAlias } = kcContext; const { url, idpAlias } = kcContext;
const { msg } = useI18n(); const { msg } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("confirmLinkIdpTitle")} headerNode={msg("confirmLinkIdpTitle")}
formNode={ formNode={

View File

@ -2,16 +2,16 @@ import React, { 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 { useI18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginIdpLinkEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail } & KcProps) => { const LoginIdpLinkEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail; i18n: I18n } & KcProps) => {
const { url, realm, brokerContext, idpAlias } = kcContext; const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = useI18n(); const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("emailLinkIdpTitle", idpAlias)} headerNode={msg("emailLinkIdpTitle", idpAlias)}
formNode={ formNode={

View File

@ -2,17 +2,17 @@ import React, { 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 { useI18n } from "../i18n";
import { headInsert } from "../tools/headInsert"; import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../../bin/tools/pathJoin"; import { pathJoin } from "../../bin/tools/pathJoin";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { I18n } from "../i18n";
const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginOtp } & KcProps) => { const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginOtp; i18n: I18n } & KcProps) => {
const { otpLogin, url } = kcContext; const { otpLogin, url } = kcContext;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
useEffect(() => { useEffect(() => {
let isCleanedUp = false; let isCleanedUp = false;
@ -33,7 +33,7 @@ const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("doLogIn")} headerNode={msg("doLogIn")}
formNode={ formNode={

View File

@ -2,16 +2,16 @@ import React, { 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 { useI18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginPageExpired = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginPageExpired } & KcProps) => { const LoginPageExpired = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginPageExpired; i18n: I18n } & KcProps) => {
const { url } = kcContext; const { url } = kcContext;
const { msg } = useI18n(); const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("pageExpiredTitle")} headerNode={msg("pageExpiredTitle")}

View File

@ -2,19 +2,19 @@ import React, { 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 { useI18n } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { I18n } from "../i18n";
const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginResetPassword } & KcProps) => { const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginResetPassword; i18n: I18n } & KcProps) => {
const { url, realm, auth } = kcContext; const { url, realm, auth } = kcContext;
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("emailForgotTitle")} headerNode={msg("emailForgotTitle")}

View File

@ -2,19 +2,19 @@ import React, { 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 { useI18n } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { I18n } from "../i18n";
const LoginUpdatePassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdatePassword } & KcProps) => { const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n } & KcProps) => {
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext; const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("updatePasswordTitle")} headerNode={msg("updatePasswordTitle")}
formNode={ formNode={

View File

@ -2,19 +2,19 @@ import React, { 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 { useI18n } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { I18n } from "../i18n";
const LoginUpdateProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdateProfile } & KcProps) => { const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdateProfile; i18n: I18n } & KcProps) => {
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
const { url, user, messagesPerField, isAppInitiatedAction } = kcContext; const { url, user, messagesPerField, isAppInitiatedAction } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("loginProfileTitle")} headerNode={msg("loginProfileTitle")}
formNode={ formNode={

View File

@ -2,16 +2,16 @@ import React, { 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 { useI18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail } & KcProps) => { const LoginVerifyEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; i18n: I18n } & KcProps) => {
const { msg } = useI18n(); const { msg } = i18n;
const { url, user } = kcContext; const { url, user } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("emailVerifyTitle")} headerNode={msg("emailVerifyTitle")}

View File

@ -1,21 +1,20 @@
import React, { memo } from "react"; import React, { memo } from "react";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-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 { useI18n } from "../i18n"; import type { I18n } from "../i18n";
const LogoutConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LogoutConfirm } & KcProps) => { const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LogoutConfirm; i18n: I18n } & KcProps) => {
const { url, client, logoutConfirm } = kcContext; const { url, client, logoutConfirm } = kcContext;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("logoutConfirmTitle")} headerNode={msg("logoutConfirmTitle")}

View File

@ -2,19 +2,19 @@ import React, { 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 { useI18n } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { I18n } from "../i18n";
const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register } & KcProps) => { const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Register; i18n: I18n } & KcProps) => {
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext; const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("registerTitle")} headerNode={msg("registerTitle")}
formNode={ formNode={

View File

@ -2,16 +2,16 @@ import React, { 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 { useI18n } 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";
import { useFormValidationSlice } from "../useFormValidationSlice"; import { useFormValidationSlice } from "../useFormValidationSlice";
import type { I18n } from "../i18n";
const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => { const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext; const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
const { cx, css } = useCssAndCx(); const { cx, css } = useCssAndCx();
@ -27,14 +27,14 @@ const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcConte
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
displayMessage={messagesPerField.exists("global")} displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true} displayRequiredFields={true}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
headerNode={msg("registerTitle")} headerNode={msg("registerTitle")}
formNode={ formNode={
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post"> <form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} {...props} /> <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
{recaptchaRequired && ( {recaptchaRequired && (
<div className="form-group"> <div className="form-group">
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
@ -66,15 +66,15 @@ const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcConte
); );
}); });
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile } & KcProps & type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps &
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & { Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void; onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
}; };
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => { const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, i18n, ...props }: UserProfileFormFieldsProps) => {
const { cx, css } = useCssAndCx(); const { cx, css } = useCssAndCx();
const { advancedMsg } = useI18n(); const { advancedMsg } = i18n;
const { const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable }, formValidationState: { fieldStateByAttributeName, isFormSubmittable },
@ -82,6 +82,7 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
attributesWithPassword, attributesWithPassword,
} = useFormValidationSlice({ } = useFormValidationSlice({
kcContext, kcContext,
i18n,
}); });
useEffect(() => { useEffect(() => {

View File

@ -8,7 +8,7 @@ import { pathJoin } from "../../bin/tools/pathJoin";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import type { KcTemplateProps } from "./KcProps"; import type { KcTemplateProps } from "./KcProps";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export type TemplateProps = { export type TemplateProps = {
displayInfo?: boolean; displayInfo?: boolean;
@ -24,7 +24,7 @@ export type TemplateProps = {
* to avoid pulling the default theme assets. * to avoid pulling the default theme assets.
*/ */
doFetchDefaultThemeResources: boolean; doFetchDefaultThemeResources: boolean;
} & { kcContext: KcContextBase } & KcTemplateProps; } & { kcContext: KcContextBase; i18n: I18n } & KcTemplateProps;
const Template = memo((props: TemplateProps) => { const Template = memo((props: TemplateProps) => {
const { const {
@ -38,6 +38,7 @@ const Template = memo((props: TemplateProps) => {
formNode, formNode,
infoNode = null, infoNode = null,
kcContext, kcContext,
i18n,
doFetchDefaultThemeResources, doFetchDefaultThemeResources,
} = props; } = props;
@ -47,7 +48,7 @@ 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, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = useI18n(); const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [string]) => changeLocale(kcLanguageTag)); const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [string]) => changeLocale(kcLanguageTag));

View File

@ -2,23 +2,36 @@ import React, { 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 { useI18n } from "../i18n";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import { Evt } from "evt"; import { Evt } from "evt";
import { useRerenderOnStateChange } from "evt/hooks"; import { useRerenderOnStateChange } from "evt/hooks";
import { assert } from "tsafe/assert";
import { fallbackLanguageTag } from "../i18n";
import type { I18n } from "../i18n";
export const evtTermMarkdown = Evt.create<string | undefined>(undefined); export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
/** Allow to avoid bundling the terms and download it on demand*/ export type KcContextLike = {
export function useDownloadTerms(params: { downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string> }) { locale?: {
const { downloadTermMarkdown } = params; currentLanguageTag: string;
};
};
const { currentLanguageTag } = useI18n(); assert<KcContextBase extends KcContextLike ? true : false>();
/** Allow to avoid bundling the terms and download it on demand*/
export function useDownloadTerms(params: {
kcContext: KcContextLike;
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
}) {
const { kcContext, downloadTermMarkdown } = params;
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
downloadTermMarkdown({ currentLanguageTag }).then(thermMarkdown => { downloadTermMarkdown({
"currentLanguageTag": kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag,
}).then(thermMarkdown => {
if (!isMounted) { if (!isMounted) {
return; return;
} }
@ -32,8 +45,8 @@ export function useDownloadTerms(params: { downloadTermMarkdown: (params: { curr
}, []); }, []);
} }
const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms } & KcProps) => { const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Terms; i18n: I18n } & KcProps) => {
const { msg, msgStr } = useI18n(); const { msg, msgStr } = i18n;
useRerenderOnStateChange(evtTermMarkdown); useRerenderOnStateChange(evtTermMarkdown);
@ -47,7 +60,7 @@ const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms }
return ( return (
<Template <Template
{...{ kcContext, ...props }} {...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true} doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("termsTitle")} headerNode={msg("termsTitle")}

View File

@ -1,7 +1,7 @@
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl"; import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import type { BaseMessageKey } from "../i18n/createI18nApi"; import type { MessageKeyBase } from "../i18n";
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;
@ -153,7 +153,7 @@ export declare namespace KcContextBase {
export type Info = Common & { export type Info = Common & {
pageId: "info.ftl"; pageId: "info.ftl";
messageHeader?: string; messageHeader?: string;
requiredActions?: ExtractAfterStartingWith<"requiredAction.", BaseMessageKey>[]; requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKeyBase>[];
skipLink: boolean; skipLink: boolean;
pageRedirectUri?: string; pageRedirectUri?: string;
actionUri?: string; actionUri?: string;

View File

@ -1,8 +0,0 @@
import { createI18nApi } from "./createI18nApi";
import type { I18n } from "./createI18nApi";
export const { I18nProvider, useI18n } = createI18nApi({
"extraMessages": {},
});
export type MessageKey = ReturnType<typeof useI18n> extends I18n<infer U> ? U : never;

View File

@ -1,15 +1,25 @@
import "minimal-polyfills/Object.fromEntries"; import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35 //NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import React, { createContext, useContext, useEffect, useState, memo } from "react"; import React, { useEffect, useState } from "react";
import type { ReactNode } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import type baseMessages from "./generated_messages/18.0.1/login/en"; import type baseMessages from "./generated_messages/18.0.1/login/en";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
const fallbackLanguageTag = "en"; export const fallbackLanguageTag = "en";
export type I18n<MessageKey extends string> = { export type KcContextLike = {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
};
};
assert<KcContextBase extends KcContextLike ? true : false>();
export type MessageKeyBase = keyof typeof baseMessages | keyof typeof keycloakifyExtraMessages[typeof fallbackLanguageTag];
export type I18n<MessageKey extends string = MessageKeyBase> = {
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string; msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element; msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
/** advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") */ /** advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") */
@ -22,102 +32,85 @@ export type I18n<MessageKey extends string> = {
labelBySupportedLanguageTag: Record<string, string>; labelBySupportedLanguageTag: Record<string, string>;
}; };
export type KcContextLike = { export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params: {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
};
};
assert<KcContextBase extends KcContextLike ? true : false>();
export type I18nProviderProps = {
children: ReactNode;
fallback?: ReactNode;
kcContext: KcContextLike; kcContext: KcContextLike;
};
const allExtraMessages: { [languageTag: string]: { [key: string]: string } } = {};
export function createI18nApi<ExtraMessageKey extends string = never>(params: {
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } }; extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
}) { doSkip: boolean;
Object.assign(allExtraMessages, params.extraMessages); }): I18n<MessageKeyBase | ExtraMessageKey> | undefined {
const { kcContext, extraMessages, doSkip } = params;
type MessageKey = ExtraMessageKey | keyof typeof baseMessages | keyof typeof keycloakifyExtraMessages[typeof fallbackLanguageTag]; const [i18n, setI18n] = useState<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined);
const context = createContext<I18n<MessageKey> | undefined>(undefined); useEffect(() => {
if (doSkip) {
return;
}
function useI18n(): I18n<MessageKey> { let isMounted = true;
const i18n = useContext(context);
assert(i18n !== undefined, "Now Wrapped in <I18nProvider>"); (async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
return i18n; const [fallbackMessages, messages] = await Promise.all([
} import("./generated_messages/18.0.1/login/en"),
import(`./generated_kcMessages/18.0.1/login/${currentLanguageTag}`),
]);
const I18nProvider = memo((props: I18nProviderProps) => { if (!isMounted) {
const { children, fallback, kcContext } = props; return;
}
const [i18n, setI18n] = useState<I18n<MessageKey> | undefined>(undefined); setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {}),
} as any,
"messages": {
...messages,
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {}),
} as any,
}),
currentLanguageTag,
"changeLocale": newLanguageTag => {
const { locale } = kcContext;
useEffect(() => { assert(locale !== undefined, "Internationalization not enabled");
let isMounted = true;
(async () => { const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
const [fallbackMessages, messages] = await Promise.all([ assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
import("./generated_messages/18.0.1/login/en"),
import(`./generated_kcMessages/18.0.1/login/${currentLanguageTag}`),
]);
if (!isMounted) { window.location.href = targetSupportedLocale.url;
return;
}
setI18n({ assert(false, "never");
...createI18nTranslationFunctions({ },
"fallbackMessages": { "labelBySupportedLanguageTag": Object.fromEntries(
...fallbackMessages, (kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]),
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}), ),
...(allExtraMessages[fallbackLanguageTag] ?? {}), });
} as any, })();
"messages": {
...messages,
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(allExtraMessages[currentLanguageTag] ?? {}),
} as any,
}),
currentLanguageTag,
"changeLocale": newLanguageTag => {
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled"); return () => {
isMounted = false;
};
}, []);
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag); return i18n;
}
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`); const useI18n_private = __unsafe_useI18n;
window.location.href = targetSupportedLocale.url; export function useI18n<ExtraMessageKey extends string = never>(params: {
kcContext: KcContextLike;
assert(false, "never"); extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
}, }): I18n<MessageKeyBase | ExtraMessageKey> | undefined {
"labelBySupportedLanguageTag": Object.fromEntries( return useI18n_private({
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]), ...params,
), "doSkip": false,
});
})();
return () => {
isMounted = false;
};
}, []);
return <context.Provider value={i18n}>{i18n === undefined ? fallback ?? null : children}</context.Provider>;
}); });
return { useI18n, I18nProvider };
} }
function createI18nTranslationFunctions<MessageKey extends string>(params: { function createI18nTranslationFunctions<MessageKey extends string>(params: {

View File

@ -1,8 +1,7 @@
import "./tools/Array.prototype.every"; import "./tools/Array.prototype.every";
import React, { useMemo, useReducer, Fragment } from "react"; import React, { useMemo, useReducer, Fragment } from "react";
import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase"; import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
import { useI18n } from "./i18n"; import type { I18n, MessageKeyBase } from "./i18n";
import type { MessageKey } from "./i18n";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { emailRegexp } from "./tools/emailRegExp"; import { emailRegexp } from "./tools/emailRegExp";
@ -15,15 +14,16 @@ export function useGetErrors(params: {
attributes: { name: string; value?: string; validators: Validators }[]; attributes: { name: string; value?: string; validators: Validators }[];
}; };
}; };
i18n: I18n;
}) { }) {
const { kcContext } = params; const { kcContext, i18n } = params;
const { const {
messagesPerField, messagesPerField,
profile: { attributes }, profile: { attributes },
} = kcContext; } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = useI18n(); const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
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;
@ -134,7 +134,7 @@ export function useGetErrors(params: {
const msgArg = [ const msgArg = [
errorMessageKey ?? errorMessageKey ??
id<MessageKey>( id<MessageKeyBase>(
(() => { (() => {
switch (shouldBe) { switch (shouldBe) {
case "equal": case "equal":
@ -175,7 +175,7 @@ export function useGetErrors(params: {
break scope; break scope;
} }
const msgArgs = [errorMessageKey ?? id<MessageKey>("shouldMatchPattern"), pattern] as const; const msgArgs = [errorMessageKey ?? id<MessageKeyBase>("shouldMatchPattern"), pattern] as const;
errors.push({ errors.push({
validatorName, validatorName,
@ -207,7 +207,7 @@ export function useGetErrors(params: {
break scope; break scope;
} }
const msgArgs = [id<MessageKey>("invalidEmailMessage")] as const; const msgArgs = [id<MessageKeyBase>("invalidEmailMessage")] as const;
errors.push({ errors.push({
validatorName, validatorName,
@ -287,7 +287,7 @@ export function useGetErrors(params: {
break scope; break scope;
} }
const msgArgs = [id<MessageKey>("notAValidOption")] as const; const msgArgs = [id<MessageKeyBase>("notAValidOption")] as const;
errors.push({ errors.push({
validatorName, validatorName,
@ -315,6 +315,7 @@ export function useFormValidationSlice(params: {
}; };
/** NOTE: Try to avoid passing a new ref every render for better performances. */ /** NOTE: Try to avoid passing a new ref every render for better performances. */
passwordValidators?: Validators; passwordValidators?: Validators;
i18n: I18n;
}) { }) {
const { const {
kcContext, kcContext,
@ -324,6 +325,7 @@ export function useFormValidationSlice(params: {
"min": "4", "min": "4",
}, },
}, },
i18n,
} = params; } = params;
const attributesWithPassword = useMemo( const attributesWithPassword = useMemo(
@ -342,7 +344,7 @@ export function useFormValidationSlice(params: {
curr, curr,
id<Attribute>({ id<Attribute>({
"name": "password", "name": "password",
"displayName": id<`\${${MessageKey}}`>("${password}"), "displayName": id<`\${${MessageKeyBase}}`>("${password}"),
"required": true, "required": true,
"readOnly": false, "readOnly": false,
"validators": passwordValidators, "validators": passwordValidators,
@ -352,7 +354,7 @@ export function useFormValidationSlice(params: {
}), }),
id<Attribute>({ id<Attribute>({
"name": "password-confirm", "name": "password-confirm",
"displayName": id<`\${${MessageKey}}`>("${passwordConfirm}"), "displayName": id<`\${${MessageKeyBase}}`>("${passwordConfirm}"),
"required": true, "required": true,
"readOnly": false, "readOnly": false,
"validators": { "validators": {
@ -360,7 +362,7 @@ export function useFormValidationSlice(params: {
"name": "password", "name": "password",
"ignore.empty.value": true, "ignore.empty.value": true,
"shouldBe": "equal", "shouldBe": "equal",
"error-message": id<`\${${MessageKey}}`>("${invalidPasswordConfirmMessage}"), "error-message": id<`\${${MessageKeyBase}}`>("${invalidPasswordConfirmMessage}"),
}, },
}, },
"annotations": {}, "annotations": {},
@ -382,6 +384,7 @@ export function useFormValidationSlice(params: {
"attributes": attributesWithPassword, "attributes": attributesWithPassword,
}, },
}, },
i18n,
}); });
const initialInternalState = useMemo( const initialInternalState = useMemo(