diff --git a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl index 6efc3ab2..07655403 100644 --- a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl +++ b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl @@ -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 attribute.annotations.inputHelperTextBefore??> - "${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"), - - <#if attribute.annotations.inputHelperTextAfter??> - "${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"), - - <#if attribute.annotations.inputTypePlaceholder??> - "${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"), - - - <#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}"), - - - - }; + { + var messages = { + <#list profile.attributes as attribute> + <#if attribute.displayName??> + "${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"), + + <#if attribute.annotations??> + <#if attribute.annotations.inputHelperTextBefore??> + "${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"), + + <#if attribute.annotations.inputHelperTextAfter??> + "${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"), + + <#if attribute.annotations.inputTypePlaceholder??> + "${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"), + + + <#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}"), + + + + + }; + Object.assign(kcContext["x-keycloakify"].messages, messages); + } <#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 auth?? && auth.authenticationSelections??> + { + var messages = { + <#list auth.authenticationSelections as authenticationSelection> + <#if authenticationSelection.displayName??> + "${authenticationSelection.displayName}": decodeHtmlEntities("${advancedMsg(authenticationSelection.displayName)?js_string}"), + + <#if authenticationSelection.helpText??> + "${authenticationSelection.helpText}": decodeHtmlEntities("${advancedMsg(authenticationSelection.helpText)?js_string}"), + + + }; + Object.assign(kcContext["x-keycloakify"].messages, messages); + } + +<#if themeType == "login" && pageId == "info.ftl" && requiredActions??> + { + var messages = { + <#list requiredActions as requiredAction> + "requiredAction.${requiredAction}": decodeHtmlEntities("${advancedMsg("requiredAction." + requiredAction)?js_string}"), + + }; + Object.assign(kcContext["x-keycloakify"].messages, messages); + } + +<#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}"), + + + + }; + Object.assign(kcContext["x-keycloakify"].messages, messages); + } attributes_to_attributesByName: { if( !kcContext.profile ){ diff --git a/src/login/KcContext/KcContext.ts b/src/login/KcContext/KcContext.ts index 27d0024a..fe05b0d0 100644 --- a/src/login/KcContext/KcContext.ts +++ b/src/login/KcContext/KcContext.ts @@ -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 }, @@ -155,8 +154,7 @@ export declare namespace KcContext { }; properties: {}; "x-keycloakify": { - realmMessageBundleUserProfile: Record | undefined; - realmMessageBundleTermsText: string | undefined; + messages: Record; }; }; @@ -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; }; } diff --git a/src/login/KcContext/kcContextMocks.ts b/src/login/KcContext/kcContextMocks.ts index 979b1f9b..f6cd502e 100644 --- a/src/login/KcContext/kcContextMocks.ts +++ b/src/login/KcContext/kcContextMocks.ts @@ -162,8 +162,7 @@ export const kcContextCommonMock: KcContext.Common = { isAppInitiatedAction: false, properties: {}, "x-keycloakify": { - realmMessageBundleUserProfile: undefined, - realmMessageBundleTermsText: undefined + messages: {} } }; diff --git a/src/login/i18n/i18n.tsx b/src/login/i18n/i18n.tsx index 29d85aa2..433183c0 100644 --- a/src/login/i18n/i18n.tsx +++ b/src/login/i18n/i18n.tsx @@ -11,8 +11,7 @@ export type KcContextLike = { supported: { languageTag: string; url: string; label: string }[]; }; "x-keycloakify": { - realmMessageBundleUserProfile: Record | undefined; - realmMessageBundleTermsText: string | undefined; + messages: Record; }; }; @@ -131,8 +130,7 @@ export function createGetI18n(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; messageBundle_fallbackLanguage: Record | undefined; messageBundle_currentLanguage: Partial> | undefined; - realmMessageBundleUserProfile: Record | undefined; - realmMessageBundleTermsText: string | undefined; + messageBundle_realm: Record; }) { - const { messageBundle_currentLanguage, realmMessageBundleUserProfile, realmMessageBundleTermsText } = params; + const { messageBundle_currentLanguage, messageBundle_realm } = params; const messages_fallbackLanguage = { ...params.messages_fallbackLanguage, @@ -201,12 +198,21 @@ function createI18nTranslationFunctionsFactory { - 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, 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 msgStr(`requiredAction.${requiredAction}` as const)).join(","); + html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(","); html += ""; } diff --git a/src/login/pages/SelectAuthenticator.tsx b/src/login/pages/SelectAuthenticator.tsx index 6a6a5187..8ffb8ba2 100644 --- a/src/login/pages/SelectAuthenticator.tsx +++ b/src/login/pages/SelectAuthenticator.tsx @@ -8,7 +8,7 @@ export default function SelectAuthenticator(props: PageProps
- +
-
{msg(authenticationSelection.displayName)}
-
{msg(authenticationSelection.helpText)}
+
{advancedMsg(authenticationSelection.displayName)}
+
{advancedMsg(authenticationSelection.helpText)}
diff --git a/src/login/pages/WebauthnAuthenticate.tsx b/src/login/pages/WebauthnAuthenticate.tsx index c0dbf6d0..0a185b8e 100644 --- a/src/login/pages/WebauthnAuthenticate.tsx +++ b/src/login/pages/WebauthnAuthenticate.tsx @@ -204,13 +204,13 @@ export default function WebauthnAuthenticate(props: PageProps {authenticator.transports.displayNameProperties - .map((nameProperty, i, arr) => ({ - nameProperty, + .map((displayNameProperty, i, arr) => ({ + displayNameProperty, hasNext: i !== arr.length - 1 })) - .map(({ nameProperty, hasNext }) => ( - - {msg(nameProperty)} + .map(({ displayNameProperty, hasNext }) => ( + + {advancedMsg(displayNameProperty)} {hasNext && , } ))} diff --git a/src/tools/ExtractAfterStartingWith.ts b/src/tools/ExtractAfterStartingWith.ts deleted file mode 100644 index 9674b22e..00000000 --- a/src/tools/ExtractAfterStartingWith.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type ExtractAfterStartingWith< - Prefix extends string, - StrEnum -> = StrEnum extends `${Prefix}${infer U}` ? U : never; diff --git a/stories/login/pages/Info.stories.tsx b/stories/login/pages/Info.stories.tsx index 762f7b44..195061f3 100644 --- a/stories/login/pages/Info.stories.tsx +++ b/stories/login/pages/Info.stories.tsx @@ -43,9 +43,14 @@ export const WithRequiredActions: Story = { ) diff --git a/stories/login/pages/Register.stories.tsx b/stories/login/pages/Register.stories.tsx index 14a18e5e..bf1c5370 100644 --- a/stories/login/pages/Register.stories.tsx +++ b/stories/login/pages/Register.stories.tsx @@ -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 (@mit.edu) nor a Berkeley (@berkeley.edu) 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: "Service Terms of Use" + messages: { + termsText: "Service Terms of Use" + } } }} /> diff --git a/stories/login/pages/SelectAuthenticator.stories.tsx b/stories/login/pages/SelectAuthenticator.stories.tsx index bc62822d..c952a0ff 100644 --- a/stories/login/pages/SelectAuthenticator.stories.tsx +++ b/stories/login/pages/SelectAuthenticator.stories.tsx @@ -41,3 +41,29 @@ export const WithDifferentAuthenticationMethods: Story = { /> ) }; + +export const WithRealmTranslations: Story = { + render: () => ( + + ) +}; diff --git a/stories/login/pages/Terms.stories.tsx b/stories/login/pages/Terms.stories.tsx index b0e1b02c..f5837b07 100644 --- a/stories/login/pages/Terms.stories.tsx +++ b/stories/login/pages/Terms.stories.tsx @@ -18,7 +18,9 @@ export const Default: Story = { My terms in English

" + messages: { + termsText: "

My terms in English

" + } } }} /> @@ -34,7 +36,9 @@ export const French: Story = { }, "x-keycloakify": { // cSpell: disable - realmMessageBundleTermsText: "

Mes terme en Français

" + messages: { + termsText: "

Mes terme en Français

" + } // cSpell: enable } }}