Includes in the kcContext missing realm defined translations #582

This commit is contained in:
Joseph Garrone 2024-07-10 22:18:24 +02:00
parent ca7821cfad
commit 24bb4902c2
12 changed files with 157 additions and 95 deletions

View File

@ -42,37 +42,84 @@ if( kcContext.resourceUrl && !kcContext.url ){
enumerable: false
});
}
kcContext["x-keycloakify"] = {};
kcContext["x-keycloakify"] = {
messages: {}
};
<#if profile?? && profile.attributes??>
kcContext["x-keycloakify"].realmMessageBundleUserProfile = {
<#list profile.attributes as attribute>
<#if attribute.annotations?? && attribute.displayName??>
"${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"),
</#if>
<#if attribute.annotations.inputHelperTextBefore??>
"${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"),
</#if>
<#if attribute.annotations.inputHelperTextAfter??>
"${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"),
</#if>
<#if attribute.annotations.inputTypePlaceholder??>
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
</#if>
<!-- Loop through the options that are in attribute.validators.options.options -->
<#if (
attribute.annotations.inputOptionLabelsI18nPrefix?? &&
attribute.validators?? &&
attribute.validators.options??
)>
<#list attribute.validators.options.options as option>
"${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}": decodeHtmlEntities("${msg(attribute.annotations.inputOptionLabelsI18nPrefix + "." + option)?js_string}"),
</#list>
</#if>
</#list>
};
{
var messages = {
<#list profile.attributes as attribute>
<#if attribute.displayName??>
"${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"),
</#if>
<#if attribute.annotations??>
<#if attribute.annotations.inputHelperTextBefore??>
"${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"),
</#if>
<#if attribute.annotations.inputHelperTextAfter??>
"${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"),
</#if>
<#if attribute.annotations.inputTypePlaceholder??>
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
</#if>
<!-- Loop through the options that are in attribute.validators.options.options -->
<#if (
attribute.annotations.inputOptionLabelsI18nPrefix?? &&
attribute.validators?? &&
attribute.validators.options??
)>
<#list attribute.validators.options.options as option>
"${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}": decodeHtmlEntities("${msg(attribute.annotations.inputOptionLabelsI18nPrefix + "." + option)?js_string}"),
</#list>
</#if>
</#if>
</#list>
};
Object.assign(kcContext["x-keycloakify"].messages, messages);
}
</#if>
<#if pageId == "terms.ftl" || termsAcceptanceRequired?? && termsAcceptanceRequired>
kcContext["x-keycloakify"].realmMessageBundleTermsText= decodeHtmlEntities("${msg("termsText")?js_string}");
kcContext["x-keycloakify"].messages["termsText"]= decodeHtmlEntities("${msg("termsText")?js_string}");
</#if>
<#if auth?? && auth.authenticationSelections??>
{
var messages = {
<#list auth.authenticationSelections as authenticationSelection>
<#if authenticationSelection.displayName??>
"${authenticationSelection.displayName}": decodeHtmlEntities("${advancedMsg(authenticationSelection.displayName)?js_string}"),
</#if>
<#if authenticationSelection.helpText??>
"${authenticationSelection.helpText}": decodeHtmlEntities("${advancedMsg(authenticationSelection.helpText)?js_string}"),
</#if>
</#list>
};
Object.assign(kcContext["x-keycloakify"].messages, messages);
}
</#if>
<#if themeType == "login" && pageId == "info.ftl" && requiredActions??>
{
var messages = {
<#list requiredActions as requiredAction>
"requiredAction.${requiredAction}": decodeHtmlEntities("${advancedMsg("requiredAction." + requiredAction)?js_string}"),
</#list>
};
Object.assign(kcContext["x-keycloakify"].messages, messages);
}
</#if>
<#if authenticators?? && authenticators.authenticators??>
{
var messages = {
<#list authenticators.authenticators as authenticator>
"${authenticator.label}": decodeHtmlEntities("${advancedMsg(authenticator.label)?js_string}"),
<#if authenticator.transports?? && authenticator.transports.displayNameProperties??>
<#list authenticator.transports.displayNameProperties as displayNameProperty>
"${displayNameProperty}": decodeHtmlEntities("${advancedMsg(displayNameProperty)?js_string}"),
</#list>
</#if>
</#list>
};
Object.assign(kcContext["x-keycloakify"].messages, messages);
}
</#if>
attributes_to_attributesByName: {
if( !kcContext.profile ){

View File

@ -1,9 +1,8 @@
import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constants";
import type { ExtractAfterStartingWith } from "keycloakify/tools/ExtractAfterStartingWith";
import type { ValueOf } from "keycloakify/tools/ValueOf";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";
import type { ClassKey } from "keycloakify/login/TemplateProps";
export type ExtendKcContext<
KcContextExtension extends { properties?: Record<string, string | undefined> },
@ -155,8 +154,7 @@ export declare namespace KcContext {
};
properties: {};
"x-keycloakify": {
realmMessageBundleUserProfile: Record<string, string> | undefined;
realmMessageBundleTermsText: string | undefined;
messages: Record<string, string>;
};
};
@ -221,7 +219,7 @@ export declare namespace KcContext {
export type Info = Common & {
pageId: "info.ftl";
messageHeader?: string;
requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKey>[];
requiredActions?: string[];
skipLink: boolean;
pageRedirectUri?: string;
actionUri?: string;
@ -384,7 +382,7 @@ export declare namespace KcContext {
credentialId: string;
transports: {
iconClass: string;
displayNameProperties?: MessageKey[];
displayNameProperties?: string[];
};
label: string;
createdAt: string;
@ -501,26 +499,9 @@ export declare namespace KcContext {
export namespace SelectAuthenticator {
export type AuthenticationSelection = {
authExecId: string;
displayName:
| "otp-display-name"
| "password-display-name"
| "auth-username-form-display-name"
| "auth-username-password-form-display-name"
| "webauthn-display-name"
| "webauthn-passwordless-display-name";
helpText:
| "otp-help-text"
| "password-help-text"
| "auth-username-form-help-text"
| "auth-username-password-form-help-text"
| "webauthn-help-text"
| "webauthn-passwordless-help-text";
iconCssClass?:
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass";
displayName: string;
helpText: string;
iconCssClass?: ClassKey;
};
}

View File

@ -162,8 +162,7 @@ export const kcContextCommonMock: KcContext.Common = {
isAppInitiatedAction: false,
properties: {},
"x-keycloakify": {
realmMessageBundleUserProfile: undefined,
realmMessageBundleTermsText: undefined
messages: {}
}
};

View File

@ -11,8 +11,7 @@ export type KcContextLike = {
supported: { languageTag: string; url: string; label: string }[];
};
"x-keycloakify": {
realmMessageBundleUserProfile: Record<string, string> | undefined;
realmMessageBundleTermsText: string | undefined;
messages: Record<string, string>;
};
};
@ -131,8 +130,7 @@ export function createGetI18n<ExtraMessageKey extends string = never>(messageBun
messages_fallbackLanguage,
messageBundle_fallbackLanguage: messageBundle[fallbackLanguageTag],
messageBundle_currentLanguage: messageBundle[partialI18n.currentLanguageTag],
realmMessageBundleUserProfile: kcContext["x-keycloakify"].realmMessageBundleUserProfile,
realmMessageBundleTermsText: kcContext["x-keycloakify"].realmMessageBundleTermsText
messageBundle_realm: kcContext["x-keycloakify"].messages
});
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag;
@ -179,10 +177,9 @@ function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraM
messages_fallbackLanguage: Record<MessageKey, string>;
messageBundle_fallbackLanguage: Record<ExtraMessageKey, string> | undefined;
messageBundle_currentLanguage: Partial<Record<ExtraMessageKey, string>> | undefined;
realmMessageBundleUserProfile: Record<string, string> | undefined;
realmMessageBundleTermsText: string | undefined;
messageBundle_realm: Record<string, string>;
}) {
const { messageBundle_currentLanguage, realmMessageBundleUserProfile, realmMessageBundleTermsText } = params;
const { messageBundle_currentLanguage, messageBundle_realm } = params;
const messages_fallbackLanguage = {
...params.messages_fallbackLanguage,
@ -201,12 +198,21 @@ function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraM
const { key, args, doRenderAsHtml } = props;
const messageOrUndefined: string | undefined = (() => {
const messageOrUndefined = (messages_currentLanguage as any)[key] ?? (messages_fallbackLanguage as any)[key];
terms_text: {
if (key !== "termsText") {
break terms_text;
}
const termsTextMessage = messageBundle_realm[key];
if (key === "termsText" && realmMessageBundleTermsText !== undefined) {
return realmMessageBundleTermsText;
if (termsTextMessage === undefined) {
break terms_text;
}
return termsTextMessage;
}
const messageOrUndefined = (messages_currentLanguage as any)[key] ?? (messages_fallbackLanguage as any)[key];
return messageOrUndefined;
})();
@ -259,15 +265,11 @@ function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraM
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
const { key, args, doRenderAsHtml } = props;
user_profile: {
if (realmMessageBundleUserProfile === undefined) {
break user_profile;
}
const resolvedMessage = realmMessageBundleUserProfile[key] ?? realmMessageBundleUserProfile["${" + key + "}"];
realm_messages: {
const resolvedMessage = messageBundle_realm[key] ?? messageBundle_realm["${" + key + "}"];
if (resolvedMessage === undefined) {
break user_profile;
break realm_messages;
}
return doRenderAsHtml ? (

View File

@ -5,7 +5,7 @@ import type { I18n } from "../i18n";
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { msgStr, msg } = i18n;
const { advancedMsgStr, msg } = i18n;
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
@ -34,7 +34,7 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
if (requiredActions) {
html += "<b>";
html += requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",");
html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(",");
html += "</b>";
}

View File

@ -8,7 +8,7 @@ export default function SelectAuthenticator(props: PageProps<Extract<KcContext,
const { url, auth } = kcContext;
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const { msg } = i18n;
const { msg, advancedMsg } = i18n;
return (
<Template
@ -30,11 +30,11 @@ export default function SelectAuthenticator(props: PageProps<Extract<KcContext,
value={authenticationSelection.authExecId}
>
<div className={kcClsx("kcSelectAuthListItemIconClass")}>
<i className={kcClsx(authenticationSelection.iconCssClass, "kcSelectAuthListItemIconPropertyClass")} />
<i className={kcClsx("kcSelectAuthListItemIconPropertyClass", authenticationSelection.iconCssClass)} />
</div>
<div className={kcClsx("kcSelectAuthListItemBodyClass")}>
<div className={kcClsx("kcSelectAuthListItemHeadingClass")}>{msg(authenticationSelection.displayName)}</div>
<div className={kcClsx("kcSelectAuthListItemDescriptionClass")}>{msg(authenticationSelection.helpText)}</div>
<div className={kcClsx("kcSelectAuthListItemHeadingClass")}>{advancedMsg(authenticationSelection.displayName)}</div>
<div className={kcClsx("kcSelectAuthListItemDescriptionClass")}>{advancedMsg(authenticationSelection.helpText)}</div>
</div>
<div className={kcClsx("kcSelectAuthListItemFillClass")} />
<div className={kcClsx("kcSelectAuthListItemArrowClass")}>

View File

@ -204,13 +204,13 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
className={kcClsx("kcSelectAuthListItemDescriptionClass")}
>
{authenticator.transports.displayNameProperties
.map((nameProperty, i, arr) => ({
nameProperty,
.map((displayNameProperty, i, arr) => ({
displayNameProperty,
hasNext: i !== arr.length - 1
}))
.map(({ nameProperty, hasNext }) => (
<Fragment key={nameProperty}>
<span>{msg(nameProperty)}</span>
.map(({ displayNameProperty, hasNext }) => (
<Fragment key={displayNameProperty}>
{advancedMsg(displayNameProperty)}
{hasNext && <span>, </span>}
</Fragment>
))}

View File

@ -1,4 +0,0 @@
export type ExtractAfterStartingWith<
Prefix extends string,
StrEnum
> = StrEnum extends `${Prefix}${infer U}` ? U : never;

View File

@ -43,9 +43,14 @@ export const WithRequiredActions: Story = {
<KcPageStory
kcContext={{
message: {
summary: "Server message"
summary: "Required actions: "
},
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL"]
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
"x-keycloakify": {
messages: {
"requiredAction.CUSTOM_ACTION": "Custom action"
}
}
}}
/>
)

View File

@ -69,7 +69,7 @@ export const WithRestrictedToMITStudents: Story = {
}
},
"x-keycloakify": {
realmMessageBundleUserProfile: {
messages: {
"${profile.attributes.email.inputHelperTextBefore}": "Please use your MIT or Berkeley email.",
"${profile.attributes.email.pattern.error}":
"This is not an MIT (<strong>@mit.edu</strong>) nor a Berkeley (<strong>@berkeley.edu</strong>) email."
@ -103,7 +103,7 @@ export const WithFavoritePet: Story = {
}
},
"x-keycloakify": {
realmMessageBundleUserProfile: {
messages: {
"${profile.attributes.favoritePet}": "Favorite Pet",
"${profile.attributes.favoritePet.options.cat}": "Fluffy Cat",
"${profile.attributes.favoritePet.options.dog}": "Loyal Dog",
@ -177,7 +177,9 @@ export const WithTermsAcceptance: Story = {
kcContext={{
termsAcceptanceRequired: true,
"x-keycloakify": {
realmMessageBundleTermsText: "<a href='https://example.com/terms'>Service Terms of Use</a>"
messages: {
termsText: "<a href='https://example.com/terms'>Service Terms of Use</a>"
}
}
}}
/>

View File

@ -41,3 +41,29 @@ export const WithDifferentAuthenticationMethods: Story = {
/>
)
};
export const WithRealmTranslations: Story = {
render: () => (
<KcPageStory
kcContext={{
auth: {
authenticationSelections: [
{
authExecId: "f0c22855-eda7-4092-8565-0c22f77d2ffb",
displayName: "home-idp-discovery-display-name",
helpText: "home-idp-discovery-help-text",
iconCssClass: "kcAuthenticatorDefaultClass"
}
]
},
["x-keycloakify"]: {
messages: {
"${home-idp-discovery-display-name}": "Home identity provider",
"${home-idp-discovery-help-text}":
"Sign in via your home identity provider which will be automatically determined based on your provided email address."
}
}
}}
/>
)
};

View File

@ -18,7 +18,9 @@ export const Default: Story = {
<KcPageStory
kcContext={{
"x-keycloakify": {
realmMessageBundleTermsText: "<p>My terms in <strong>English</strong></p>"
messages: {
termsText: "<p>My terms in <strong>English</strong></p>"
}
}
}}
/>
@ -34,7 +36,9 @@ export const French: Story = {
},
"x-keycloakify": {
// cSpell: disable
realmMessageBundleTermsText: "<p>Mes terme en <strong>Français</strong></p>"
messages: {
termsText: "<p>Mes terme en <strong>Français</strong></p>"
}
// cSpell: enable
}
}}