diff --git a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl index 58d8ca09..87deb552 100644 --- a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl +++ b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl @@ -29,17 +29,7 @@ out["messagesPerField"]= { <#recover> <#assign doExistErrorOnUsernameOrPassword = true> - <#if doExistErrorOnUsernameOrPassword> - return text; - <#else> - <#assign doExistMessageForField = ""> - <#attempt> - <#assign doExistMessageForField = messagesPerField.exists('${fieldName}')> - <#recover> - <#assign doExistMessageForField = true> - - return <#if doExistMessageForField>text<#else>undefined; - + return <#if doExistErrorOnUsernameOrPassword>text<#else>undefined <#else> <#assign doExistMessageForField = ""> <#attempt> @@ -107,22 +97,18 @@ out["messagesPerField"]= { <#if doExistErrorOnUsernameOrPassword> <#attempt> - return "${kcSanitize(msg('invalidUserMessage'))?no_esc}"; + return decodeHtmlEntities("${msg('invalidUserMessage')?js_string}"); <#recover> return "Invalid username or password."; <#else> - <#attempt> - return "${messagesPerField.get('${fieldName}')?no_esc}"; - <#recover> - return ""; - + return ""; <#else> <#attempt> - return "${messagesPerField.get('${fieldName}')?no_esc}"; + return decodeHtmlEntities("${messagesPerField.get('${fieldName}')?js_string}"); <#recover> - return "invalid field"; + return "Invalid field"; } @@ -180,27 +166,36 @@ try { } catch(error) { } <#if profile?? && profile.attributes??> - out["__localizationReamlOverrides_userProfile"] = { + out["lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX"] = { <#list profile.attributes as attribute> <#if attribute.annotations?? && attribute.displayName??> - "${attribute.displayName}xx": "${advancedMsg(attribute.displayName)?no_esc}", + "${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"), <#if attribute.annotations.inputHelperTextBefore??> - "${attribute.annotations.inputHelperTextBefore}": "${advancedMsg(attribute.annotations.inputHelperTextBefore)?no_esc}", + "${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"), <#if attribute.annotations.inputHelperTextAfter??> - "${attribute.annotations.inputHelperTextAfter}": "${advancedMsg(attribute.annotations.inputHelperTextAfter)?no_esc}", + "${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"), <#if attribute.annotations.inputTypePlaceholder??> - "${attribute.annotations.inputTypePlaceholder}": "${advancedMsg(attribute.annotations.inputTypePlaceholder)?no_esc}", + "${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"), }; - return out; +function decodeHtmlEntities(htmlStr){ + var element = decodeHtmlEntities.element; + if (!element) { + element = document.createElement("textarea"); + decodeHtmlEntities.element = element; + } + element.innerHTML = htmlStr; + return textarea.value; +} + })(); <#function ftl_object_to_js_code_declaring_an_object object path> diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts index bca110bd..5de7fa87 100644 --- a/src/bin/keycloakify/generateFtl/generateFtl.ts +++ b/src/bin/keycloakify/generateFtl/generateFtl.ts @@ -10,7 +10,8 @@ import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, - resources_common + resources_common, + nameOfTheLocalizationRealmOverridesUserProfileProperty } from "../../shared/constants"; import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; @@ -135,8 +136,11 @@ export function generateFtlFilesCodeFactory(params: { .replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion) .replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType) .replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", themeName) - .replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common); - + .replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common) + .replace( + "lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX", + nameOfTheLocalizationRealmOverridesUserProfileProperty + ); const ftlObjectToJsCodeDeclaringAnObjectPlaceholder = '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }'; diff --git a/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts b/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts index 3d5f2ccc..473731c1 100644 --- a/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts +++ b/src/bin/keycloakify/generateSrcMainResources/generateMessageProperties.ts @@ -184,18 +184,47 @@ function toUTF16(codePoint: number): string { } } -// Escapes special characters and converts unicode to UTF-16 encoding +// Escapes special characters for use in a .properties file function escapeString(str: string): string { let escapedStr = ""; for (const char of [...str]) { const codePoint = char.codePointAt(0); if (!codePoint) continue; - if (char === "'") { - escapedStr += "''"; // double single quotes - } else if (codePoint > 0x7f) { - escapedStr += toUTF16(codePoint); // non-ascii characters - } else { - escapedStr += char; + + switch (char) { + case "\n": + escapedStr += "\\n"; + break; + case "\r": + escapedStr += "\\r"; + break; + case "\t": + escapedStr += "\\t"; + break; + case "\\": + escapedStr += "\\\\"; + break; + case ":": + escapedStr += "\\:"; + break; + case "=": + escapedStr += "\\="; + break; + case "#": + escapedStr += "\\#"; + break; + case "!": + escapedStr += "\\!"; + break; + case "'": + escapedStr += "''"; + break; + default: + if (codePoint > 0x7f) { + escapedStr += toUTF16(codePoint); // Non-ASCII characters + } else { + escapedStr += char; // ASCII character needs no escape + } } } return escapedStr; diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index ebd7bf3d..2600ea2d 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -1,4 +1,6 @@ export const nameOfTheGlobal = "kcContext"; +export const nameOfTheLocalizationRealmOverridesUserProfileProperty = + "__localizationRealmOverridesUserProfile"; export const keycloak_resources = "keycloak-resources"; export const resources_common = "resources-common"; export const lastKeycloakVersionWithAccountV1 = "21.1.2"; diff --git a/src/login/i18n/i18n.tsx b/src/login/i18n/i18n.tsx index 37dd41bd..81760663 100644 --- a/src/login/i18n/i18n.tsx +++ b/src/login/i18n/i18n.tsx @@ -1,5 +1,4 @@ import "minimal-polyfills/Object.fromEntries"; -//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35 import { useEffect, useState, useRef } from "react"; import fallbackMessages from "./baseMessages/en"; import { getMessages } from "./baseMessages"; @@ -13,6 +12,7 @@ export type KcContextLike = { currentLanguageTag: string; supported: { languageTag: string; url: string; label: string }[]; }; + __localizationRealmOverridesUserProfile: Record; }; assert(); @@ -61,7 +61,7 @@ export type GenericI18n = { /** * Examples assuming currentLanguageTag === "en" * advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar" - * advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key" + * advancedMsg("${not-a-message-key}") === advancedMsg("not-a-message-key") === "not-a-message-key" */ advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string; }; @@ -99,7 +99,8 @@ export function createUseI18n(extraMessa ...(await getMessages(currentLanguageTag)), ...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}), ...(extraMessages[currentLanguageTag] ?? {}) - } as any + } as any, + __localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile }), currentLanguageTag, getChangeLocalUrl: newLanguageTag => { @@ -129,8 +130,9 @@ export function createUseI18n(extraMessa function createI18nTranslationFunctions(params: { fallbackMessages: Record; messages: Record; + __localizationRealmOverridesUserProfile: Record; }): Pick, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> { - const { fallbackMessages, messages } = params; + const { fallbackMessages, messages /*__localizationRealmOverridesUserProfile*/ } = params; function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined { const { key, args, doRenderAsHtml } = props; @@ -186,6 +188,15 @@ function createI18nTranslationFunctions(params: { function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string { const { key, args, doRenderAsHtml } = props; + // TODO: + /* + if( key in __localizationRealmOverridesUserProfile ){ + + + + } + */ + const match = key.match(/^\$\{([^{]+)\}$/); const keyUnwrappedFromCurlyBraces = match === null ? key : match[1]; diff --git a/src/login/kcContext/KcContext.ts b/src/login/kcContext/KcContext.ts index f72a8a13..53963577 100644 --- a/src/login/kcContext/KcContext.ts +++ b/src/login/kcContext/KcContext.ts @@ -1,4 +1,8 @@ -import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constants"; +import type { + ThemeType, + LoginThemePageId, + nameOfTheLocalizationRealmOverridesUserProfileProperty +} from "keycloakify/bin/shared/constants"; import { assert } from "tsafe/assert"; import type { Equals } from "tsafe"; import type { MessageKey } from "../i18n/i18n"; @@ -140,6 +144,7 @@ export declare namespace KcContext { tabId: string; ssoLoginInOtherTabsUrl: string; }; + __localizationRealmOverridesUserProfile: Record; }; export type SamlPostForm = Common & { @@ -750,3 +755,12 @@ export type PasswordPolicies = { /** Whether the password can be the email address */ notEmail?: boolean; }; + +assert< + KcContext.Common extends Record< + typeof nameOfTheLocalizationRealmOverridesUserProfileProperty, + unknown + > + ? true + : false +>(); diff --git a/src/login/kcContext/kcContextMocks.ts b/src/login/kcContext/kcContextMocks.ts index b4b07878..8802a346 100644 --- a/src/login/kcContext/kcContextMocks.ts +++ b/src/login/kcContext/kcContextMocks.ts @@ -221,7 +221,8 @@ export const kcContextCommonMock: KcContext.Common = { }, scripts: [], isAppInitiatedAction: false, - properties: {} + properties: {}, + __localizationRealmOverridesUserProfile: {} }; const loginUrl = {