diff --git a/.gitignore b/.gitignore index 5fd2cc8f..687df547 100644 --- a/.gitignore +++ b/.gitignore @@ -48,8 +48,8 @@ jspm_packages .idea -/src/login/i18n/baseMessages/ -/src/account/i18n/baseMessages/ +/src/login/i18n/messages_defaultSet/ +/src/account/i18n/messages_defaultSet/ # VS Code devcontainers .devcontainer diff --git a/.prettierignore b/.prettierignore index fadee0cc..6cc2f1d3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -6,8 +6,8 @@ node_modules/ /src/tools/types/ /build_keycloak/ /.vscode/ -/src/login/i18n/baseMessages/ -/src/account/i18n/baseMessages/ +/src/login/i18n/messages_defaultSet/ +/src/account/i18n/messages_defaultSet/ /dist_test /sample_react_project/ /sample_custom_react_project/ diff --git a/scripts/generate-i18n-messages.ts b/scripts/generate-i18n-messages.ts index b13712cd..89affa4f 100644 --- a/scripts/generate-i18n-messages.ts +++ b/scripts/generate-i18n-messages.ts @@ -110,12 +110,12 @@ async function main() { source: keycloakifyExtraMessages }); - const baseMessagesDirPath = pathJoin( + const messagesDirPath = pathJoin( thisCodebaseRootDirPath, "src", themeType, "i18n", - "baseMessages" + "messages_defaultSet" ); const generatedFileHeader = [ @@ -127,7 +127,7 @@ async function main() { ].join("\n"); languages.forEach(language => { - const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`); + const filePath = pathJoin(messagesDirPath, `${language}.ts`); fs.mkdirSync(pathDirname(filePath), { recursive: true }); @@ -155,14 +155,14 @@ async function main() { }); fs.writeFileSync( - pathJoin(baseMessagesDirPath, "index.ts"), + pathJoin(messagesDirPath, "index.ts"), Buffer.from( [ generatedFileHeader, `import * as en from "./en";`, "", - "export async function getMessages(currentLanguageTag: string) {", - " const { default: messages } = await (() => {", + "export async function fetchMessages_defaultSet(currentLanguageTag: string) {", + " const { default: messages_defaultSet } = await (() => {", " switch (currentLanguageTag) {", ` case "en": return en;`, ...languages @@ -174,7 +174,7 @@ async function main() { ' default: return { "default": {} };', " }", " })();", - " return messages;", + " return messages_defaultSet;", "}" ].join("\n"), "utf8" diff --git a/src/account/KcContext/KcContext.ts b/src/account/KcContext/KcContext.ts index 4dafce0c..1b3a064d 100644 --- a/src/account/KcContext/KcContext.ts +++ b/src/account/KcContext/KcContext.ts @@ -118,7 +118,10 @@ export declare namespace KcContext { lastName?: string; username?: string; }; - properties: Record; + properties: {}; + "x-keycloakify": { + messages: Record; + }; }; export type Password = Common & { diff --git a/src/account/KcContext/kcContextMocks.ts b/src/account/KcContext/kcContextMocks.ts index 47abcb7f..7564c0cf 100644 --- a/src/account/KcContext/kcContextMocks.ts +++ b/src/account/KcContext/kcContextMocks.ts @@ -82,7 +82,10 @@ export const kcContextCommonMock: KcContext.Common = { email: "john.doe@code.gouv.fr", username: "doe_j" }, - properties: {} + properties: {}, + "x-keycloakify": { + messages: {} + } }; export const kcContextMocks: KcContext[] = [ diff --git a/src/account/i18n/GenericI18n.tsx b/src/account/i18n/GenericI18n.tsx new file mode 100644 index 00000000..a640ece0 --- /dev/null +++ b/src/account/i18n/GenericI18n.tsx @@ -0,0 +1,6 @@ +import type { GenericI18n_noJsx } from "./i18n"; + +export type GenericI18n = GenericI18n_noJsx & { + msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element; + advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element; +}; diff --git a/src/account/i18n/i18n.tsx b/src/account/i18n/i18n.tsx index 5d477801..3411c3f4 100644 --- a/src/account/i18n/i18n.tsx +++ b/src/account/i18n/i18n.tsx @@ -1,22 +1,24 @@ import "keycloakify/tools/Object.fromEntries"; import { assert } from "tsafe/assert"; -import messages_fallbackLanguage from "./baseMessages/en"; -import { getMessages } from "./baseMessages"; +import messages_defaultSet_fallbackLanguage from "./messages_defaultSet/en"; +import { fetchMessages_defaultSet } from "./messages_defaultSet"; import type { KcContext } from "../KcContext"; import { fallbackLanguageTag } from "keycloakify/bin/shared/constants"; +import { id } from "tsafe/id"; export type KcContextLike = { locale?: { currentLanguageTag: string; supported: { languageTag: string; url: string; label: string }[]; }; + "x-keycloakify": { + messages: Record; + }; }; assert(); -export type MessageKey = keyof typeof messages_fallbackLanguage; - -export type GenericI18n = { +export type GenericI18n_noJsx = { /** * e.g: "en", "fr", "zh-CN" * @@ -36,16 +38,21 @@ export type GenericI18n = { * */ labelBySupportedLanguageTag: Record; /** - * Examples assuming currentLanguageTag === "en" * - * msg("access-denied") === Access denied - * msg("impersonateTitleHtml", "Foo") === Foo Impersonate User - */ - msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element; - /** - * It's the same thing as msg() but instead of returning a JSX.Element it returns a string. - * It can be more convenient to manipulate strings but if there are HTML tags it wont render. + * Examples assuming currentLanguageTag === "en" + * { + * en: { + * "access-denied": "Access denied", + * "impersonateTitleHtml": "{0} Impersonate User", + * "bar": "Bar {0}" + * } + * } + * + * msgStr("access-denied") === "Access denied" + * msgStr("not-a-message-key") Throws an error * msgStr("impersonateTitleHtml", "Foo") === "Foo Impersonate User" + * msgStr("${bar}", "c") === "Bar <strong>XXX</strong>" + * The html in the arg is partially escaped for security reasons, it might come from an untrusted source, it's not safe to render it as html. */ msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string; /** @@ -56,24 +63,11 @@ export type GenericI18n = { * { * en: { * "access-denied": "Access denied", - * "foo": "Foo {0} {1}", - * "bar": "Bar {0}" * } * } * - * advancedMsg("${access-denied} foo bar") === {msgStr("access-denied")} foo bar === Access denied foo bar - * advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === Access denied - * advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === not-a-message-key - * advancedMsg("${bar}", "c") - * === {msgStr("bar", "XXX")} - * === Bar <strong>XXX</strong> (The html in the arg is partially escaped for security reasons, it might be untrusted) - * advancedMsg("${foo} xx ${bar}", "a", "b", "c") - * === {msgStr("foo", "a", "b")} xx {msgStr("bar")} - * === Foo a b xx Bar {0} (The substitution are only applied in the first message) - */ - advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element; - /** - * See advancedMsg() but instead of returning a JSX.Element it returns a string. + * advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied" + * advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key" */ advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string; @@ -85,10 +79,12 @@ export type GenericI18n = { isFetchingTranslations: boolean; }; -export function createGetI18n(messageBundle: { - [languageTag: string]: { [key in ExtraMessageKey]: string }; +export type MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage; + +export function createGetI18n(messagesByLanguageTag_themeDefined: { + [languageTag: string]: { [key in MessageKey_themeDefined]: string }; }) { - type I18n = GenericI18n; + type I18n = GenericI18n_noJsx; type Result = { i18n: I18n; prI18n_currentLanguage: Promise | undefined }; @@ -123,10 +119,18 @@ export function createGetI18n(messageBun labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])) }; - const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory({ - messages_fallbackLanguage, - messageBundle_fallbackLanguage: messageBundle[fallbackLanguageTag], - messageBundle_currentLanguage: messageBundle[partialI18n.currentLanguageTag] + const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory({ + messages_themeDefined: + messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ?? + messagesByLanguageTag_themeDefined[fallbackLanguageTag] ?? + (() => { + const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0]; + if (firstLanguageTag === undefined) { + return undefined; + } + return messagesByLanguageTag_themeDefined[firstLanguageTag]; + })(), + messages_fromKcServer: kcContext["x-keycloakify"].messages }); const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag; @@ -135,18 +139,18 @@ export function createGetI18n(messageBun i18n: { ...partialI18n, ...createI18nTranslationFunctions({ - messages_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_fallbackLanguage : undefined + messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined }), isFetchingTranslations: !isCurrentLanguageFallbackLanguage }, prI18n_currentLanguage: isCurrentLanguageFallbackLanguage ? undefined : (async () => { - const messages_currentLanguage = await getMessages(partialI18n.currentLanguageTag); + const messages_defaultSet_currentLanguage = await fetchMessages_defaultSet(partialI18n.currentLanguageTag); const i18n_currentLanguage: I18n = { ...partialI18n, - ...createI18nTranslationFunctions({ messages_currentLanguage }), + ...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }), isFetchingTranslations: false }; @@ -169,118 +173,76 @@ export function createGetI18n(messageBun return { getI18n }; } -function createI18nTranslationFunctionsFactory(params: { - messages_fallbackLanguage: Record; - messageBundle_fallbackLanguage: Record | undefined; - messageBundle_currentLanguage: Partial> | undefined; +function createI18nTranslationFunctionsFactory(params: { + messages_themeDefined: Record | undefined; + messages_fromKcServer: Record; }) { - const { messageBundle_currentLanguage } = params; - - const messages_fallbackLanguage = { - ...params.messages_fallbackLanguage, - ...params.messageBundle_fallbackLanguage - }; + const { messages_themeDefined, messages_fromKcServer } = params; function createI18nTranslationFunctions(params: { - messages_currentLanguage: Partial> | undefined; - }): Pick, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> { - const messages_currentLanguage = { - ...params.messages_currentLanguage, - ...messageBundle_currentLanguage - }; + messages_defaultSet_currentLanguage: Partial> | undefined; + }): Pick, "msgStr" | "advancedMsgStr"> { + const { messages_defaultSet_currentLanguage } = params; - function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined { - const { key, args, doRenderAsHtml } = props; + function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined { + const { key, args } = props; - const messageOrUndefined: string | undefined = (messages_currentLanguage as any)[key] ?? (messages_fallbackLanguage as any)[key]; + const message = + id>(messages_fromKcServer)[key] ?? + id | undefined>(messages_themeDefined)?.[key] ?? + id | undefined>(messages_defaultSet_currentLanguage)?.[key] ?? + id>(messages_defaultSet_fallbackLanguage)[key]; - if (messageOrUndefined === undefined) { + if (message === undefined) { return undefined; } - const message = messageOrUndefined; + const startIndex = message + .match(/{[0-9]+}/g) + ?.map(g => g.match(/{([0-9]+)}/)![1]) + .map(indexStr => parseInt(indexStr)) + .sort((a, b) => a - b)[0]; - const messageWithArgsInjectedIfAny = (() => { - const startIndex = message - .match(/{[0-9]+}/g) - ?.map(g => g.match(/{([0-9]+)}/)![1]) - .map(indexStr => parseInt(indexStr)) - .sort((a, b) => a - b)[0]; - - if (startIndex === undefined) { - // No {0} in message (no arguments expected) - return message; - } - - let messageWithArgsInjected = message; - - args.forEach((arg, i) => { - if (arg === undefined) { - return; - } - - messageWithArgsInjected = messageWithArgsInjected.replace( - new RegExp(`\\{${i + startIndex}\\}`, "g"), - arg.replace(//g, ">") - ); - }); - - return messageWithArgsInjected; - })(); - - return doRenderAsHtml ? ( - - ) : ( - messageWithArgsInjectedIfAny - ); - } - - function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string { - const { key, args, doRenderAsHtml } = props; - - if (!/\$\{[^}]+\}/.test(key)) { - const resolvedMessage = resolveMsg({ key, args, doRenderAsHtml }); - - if (resolvedMessage === undefined) { - return doRenderAsHtml ? : key; - } - - return resolvedMessage; + if (startIndex === undefined) { + // No {0} in message (no arguments expected) + return message; } - let isFirstMatch = true; + let messageWithArgsInjected = message; - const resolvedComplexMessage = key.replace(/\$\{([^}]+)\}/g, (...[, key_i]) => { - const replaceBy = resolveMsg({ key: key_i, args: isFirstMatch ? args : [], doRenderAsHtml: false }) ?? key_i; + args.forEach((arg, i) => { + if (arg === undefined) { + return; + } - isFirstMatch = false; - - return replaceBy; + messageWithArgsInjected = messageWithArgsInjected.replace( + new RegExp(`\\{${i + startIndex}\\}`, "g"), + arg.replace(//g, ">") + ); }); - return doRenderAsHtml ? : resolvedComplexMessage; + return messageWithArgsInjected; + } + + function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[] }): string { + const { key, args } = props; + + const match = key.match(/^\$\{(.+)\}$/); + + if (match === null) { + return key; + } + + return resolveMsg({ key: match[1], args }) ?? key; } return { - msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string, - msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element, - advancedMsg: (key, ...args) => - resolveMsgAdvanced({ - key, - args, - doRenderAsHtml: true - }) as JSX.Element, - advancedMsgStr: (key, ...args) => - resolveMsgAdvanced({ - key, - args, - doRenderAsHtml: false - }) as string + msgStr: (key, ...args) => { + const resolvedMessage = resolveMsg({ key, args }); + assert(resolvedMessage !== undefined, `Message with key "${key}" not found`); + return resolvedMessage; + }, + advancedMsgStr: (key, ...args) => resolveMsgAdvanced({ key, args }) }; } diff --git a/src/account/i18n/index.ts b/src/account/i18n/index.ts index 5e58b25b..f492a960 100644 --- a/src/account/i18n/index.ts +++ b/src/account/i18n/index.ts @@ -1,4 +1,5 @@ -import type { GenericI18n, MessageKey, KcContextLike } from "./i18n"; -export type { MessageKey, KcContextLike }; -export type I18n = GenericI18n; +import type { GenericI18n } from "./GenericI18n"; +import type { MessageKey_defaultSet, KcContextLike } from "./i18n"; +export type { MessageKey_defaultSet, KcContextLike }; +export type I18n = GenericI18n; export { createUseI18n } from "./useI18n"; diff --git a/src/account/i18n/useI18n.ts b/src/account/i18n/useI18n.ts deleted file mode 100644 index 023ade06..00000000 --- a/src/account/i18n/useI18n.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useEffect, useState } from "react"; -import { - createGetI18n, - type GenericI18n, - type MessageKey, - type KcContextLike -} from "./i18n"; -import { Reflect } from "tsafe/Reflect"; - -export function createUseI18n(messageBundle: { - [languageTag: string]: { [key in ExtraMessageKey]: string }; -}) { - type I18n = GenericI18n; - - const { getI18n } = createGetI18n(messageBundle); - - function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } { - const { kcContext } = params; - - const { i18n, prI18n_currentLanguage } = getI18n({ kcContext }); - - const [i18n_toReturn, setI18n_toReturn] = useState(i18n); - - useEffect(() => { - let isActive = true; - - prI18n_currentLanguage?.then(i18n => { - if (!isActive) { - return; - } - - setI18n_toReturn(i18n); - }); - - return () => { - isActive = false; - }; - }, []); - - return { i18n: i18n_toReturn }; - } - - return { useI18n, ofTypeI18n: Reflect() }; -} diff --git a/src/account/i18n/useI18n.tsx b/src/account/i18n/useI18n.tsx new file mode 100644 index 00000000..55545392 --- /dev/null +++ b/src/account/i18n/useI18n.tsx @@ -0,0 +1,81 @@ +import { useEffect, useState } from "react"; +import { createGetI18n, type GenericI18n_noJsx, type KcContextLike, type MessageKey_defaultSet } from "./i18n"; +import { GenericI18n } from "./GenericI18n"; +import { Reflect } from "tsafe/Reflect"; + +export function createUseI18n(messageBundle: { + [languageTag: string]: { [key in MessageKey_themeDefined]: string }; +}) { + type MessageKey = MessageKey_defaultSet | MessageKey_themeDefined; + + type I18n = GenericI18n; + + const { withJsx } = (() => { + const cache = new WeakMap, GenericI18n>(); + + function renderHtmlString(htmlString: string): JSX.Element { + return ( +
+ ); + } + + function withJsx(i18n_noJsx: GenericI18n_noJsx): I18n { + use_cache: { + const i18n = cache.get(i18n_noJsx); + + if (i18n === undefined) { + break use_cache; + } + + return i18n; + } + + const i18n: I18n = { + ...i18n_noJsx, + msg: (...args) => renderHtmlString(i18n_noJsx.msgStr(...args)), + advancedMsg: (...args) => renderHtmlString(i18n_noJsx.advancedMsgStr(...args)) + }; + + cache.set(i18n_noJsx, i18n); + + return i18n; + } + + return { withJsx }; + })(); + + const { getI18n } = createGetI18n(messageBundle); + + function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } { + const { kcContext } = params; + + const { i18n, prI18n_currentLanguage } = getI18n({ kcContext }); + + const [i18n_toReturn, setI18n_toReturn] = useState(withJsx(i18n)); + + useEffect(() => { + let isActive = true; + + prI18n_currentLanguage?.then(i18n => { + if (!isActive) { + return; + } + + setI18n_toReturn(withJsx(i18n)); + }); + + return () => { + isActive = false; + }; + }, []); + + return { i18n: i18n_toReturn }; + } + + return { useI18n, ofTypeI18n: Reflect() }; +} diff --git a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl index b01ae08a..79c332e0 100644 --- a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl +++ b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl @@ -1,6 +1,24 @@ <#assign pageId="PAGE_ID_xIgLsPgGId9D8e"> <#assign themeType="KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr"> +<#assign xKeycloakifyMessages = {}> +<#assign debugMessage=""> + const kcContext = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc}; + +<@addNonAutomaticallyGatherableMessagesToXKeycloakifyMessages /> + +console.log(`${debugMessage}`); + +kcContext["x-keycloakify"] = {}; + +{ + var messages = {}; + <#list xKeycloakifyMessages as key, resolvedMsg> + messages["${key}"] = decodeHtmlEntities("${resolvedMsg?js_string}"); + + kcContext["x-keycloakify"].messages = messages; +} + if( kcContext.messagesPerField ){ var existsError_singleFieldName = kcContext.messagesPerField.existsError; kcContext.messagesPerField.existsError = function (){ @@ -42,93 +60,7 @@ if( kcContext.resourceUrl && !kcContext.url ){ enumerable: false }); } -kcContext["x-keycloakify"] = { - messages: {} -}; -<#if profile?? && profile.attributes??> - { - var messages = { - <#list profile.attributes as attribute> - <#if attribute.displayName??> - "${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"), - - <#if attribute.group??> - <#if attribute.group.displayDescription??> - "${attribute.group.displayDescription}": decodeHtmlEntities("${advancedMsg(attribute.group.displayDescription)?js_string}"), - - <#if attribute.group.displayHeader??> - "${attribute.group.displayHeader}": decodeHtmlEntities("${advancedMsg(attribute.group.displayHeader)?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"].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 ){ break attributes_to_attributesByName; @@ -579,6 +511,17 @@ function decodeHtmlEntities(htmlStr){ <#return object?c> + <#local isString = ""> + <#attempt> + <#local isString = object?is_string> + <#recover> + <#return "ABORT: Can't test if it's a string"> + + + <#if isString> + <@addToXKeycloakifyMessagesIfMessageKey str=object /> + + <#attempt> <#return '"' + object?js_string + '"'>; <#recover> @@ -629,4 +572,73 @@ function decodeHtmlEntities(htmlStr){ <#function are_same_path path searchedPath> <#return path?size == searchedPath?size && is_subpath(path, searchedPath)> - \ No newline at end of file + + +<#macro addToXKeycloakifyMessagesIfMessageKey str> + <#if !msg?? || !msg?is_method> + <#return> + + <#if (str?length > 200)> + <#return> + + <#local key=removeBrackets(str)> + <#if key?length==0> + <#return> + + <#if !(key?matches(r"^[a-zA-Z0-9-_.]*$"))> + <#return> + + <#local resolvedMsg=msg(key)> + <#if resolvedMsg==key> + <#return> + + <#assign xKeycloakifyMessages = xKeycloakifyMessages + { "${key}": resolvedMsg }> + + +<#function removeBrackets str> + <#if str?starts_with("${") && str?ends_with("}")> + <#return str[2..(str?length-2)]> + <#else> + <#return str> + + + +<#macro addNonAutomaticallyGatherableMessagesToXKeycloakifyMessages> + <#if profile?? && profile?is_hash && profile.attributes?? && profile.attributes?is_enumerable> + <#list profile.attributes as attribute> + <#if !( + attribute.annotations?? && attribute.annotations?is_hash && + attribute.annotations.inputOptionLabelsI18nPrefix?? && attribute.annotations.inputOptionLabelsI18nPrefix?is_string + )> + <#continue> + + <#local prefix=attribute.annotations.inputOptionLabelsI18nPrefix> + <#if !( + attribute.validators?? && attribute.validators?is_hash && + attribute.validators.options?? && attribute.validators.options?is_hash && + attribute.validators.options.options?? && attribute.validators.options.options?is_enumerable + )> + <#continue> + + <#list attribute.validators.options.options as option> + <#if !option?is_string> + <#continue> + + <@addToXKeycloakifyMessagesIfMessageKey str="${prefix}.${option}" /> + + + + <#if pageId == "terms.ftl" || termsAcceptanceRequired?? && termsAcceptanceRequired> + <@addToXKeycloakifyMessagesIfMessageKey str="termsText" /> + + <#if requiredActions?? && requiredActions?is_enumerable> + <#list requiredActions as requiredAction> + <#if !requiredAction?is_string> + <#continue> + + <@addToXKeycloakifyMessagesIfMessageKey str="requiredAction.${requiredAction}" /> + + + + + diff --git a/src/bin/keycloakify/generateResources/generateMessageProperties.ts b/src/bin/keycloakify/generateResources/generateMessageProperties.ts index b81df673..4f8e2339 100644 --- a/src/bin/keycloakify/generateResources/generateMessageProperties.ts +++ b/src/bin/keycloakify/generateResources/generateMessageProperties.ts @@ -22,7 +22,7 @@ export function generateMessageProperties(params: { "src", themeType, "i18n", - "baseMessages" + "messages_defaultSet" ); const baseMessageBundle: { [languageTag: string]: Record } = diff --git a/src/login/i18n/GenericI18n.tsx b/src/login/i18n/GenericI18n.tsx new file mode 100644 index 00000000..a640ece0 --- /dev/null +++ b/src/login/i18n/GenericI18n.tsx @@ -0,0 +1,6 @@ +import type { GenericI18n_noJsx } from "./i18n"; + +export type GenericI18n = GenericI18n_noJsx & { + msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element; + advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element; +}; diff --git a/src/login/i18n/i18n.tsx b/src/login/i18n/i18n.tsx index 433183c0..3bd56e49 100644 --- a/src/login/i18n/i18n.tsx +++ b/src/login/i18n/i18n.tsx @@ -1,9 +1,10 @@ import "keycloakify/tools/Object.fromEntries"; import { assert } from "tsafe/assert"; -import messages_fallbackLanguage from "./baseMessages/en"; -import { getMessages } from "./baseMessages"; +import messages_defaultSet_fallbackLanguage from "./messages_defaultSet/en"; +import { fetchMessages_defaultSet } from "./messages_defaultSet"; import type { KcContext } from "../KcContext"; import { fallbackLanguageTag } from "keycloakify/bin/shared/constants"; +import { id } from "tsafe/id"; export type KcContextLike = { locale?: { @@ -17,9 +18,7 @@ export type KcContextLike = { assert(); -export type MessageKey = keyof typeof messages_fallbackLanguage; - -export type GenericI18n = { +export type GenericI18n_noJsx = { /** * e.g: "en", "fr", "zh-CN" * @@ -39,16 +38,21 @@ export type GenericI18n = { * */ labelBySupportedLanguageTag: Record; /** - * Examples assuming currentLanguageTag === "en" * - * msg("access-denied") === Access denied - * msg("impersonateTitleHtml", "Foo") === Foo Impersonate User - */ - msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element; - /** - * It's the same thing as msg() but instead of returning a JSX.Element it returns a string. - * It can be more convenient to manipulate strings but if there are HTML tags it wont render. + * Examples assuming currentLanguageTag === "en" + * { + * en: { + * "access-denied": "Access denied", + * "impersonateTitleHtml": "{0} Impersonate User", + * "bar": "Bar {0}" + * } + * } + * + * msgStr("access-denied") === "Access denied" + * msgStr("not-a-message-key") Throws an error * msgStr("impersonateTitleHtml", "Foo") === "Foo Impersonate User" + * msgStr("${bar}", "c") === "Bar <strong>XXX</strong>" + * The html in the arg is partially escaped for security reasons, it might come from an untrusted source, it's not safe to render it as html. */ msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string; /** @@ -59,24 +63,11 @@ export type GenericI18n = { * { * en: { * "access-denied": "Access denied", - * "foo": "Foo {0} {1}", - * "bar": "Bar {0}" * } * } * - * advancedMsg("${access-denied} foo bar") === {msgStr("access-denied")} foo bar === Access denied foo bar - * advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === Access denied - * advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === not-a-message-key - * advancedMsg("${bar}", "c") - * === {msgStr("bar", "XXX")} - * === Bar <strong>XXX</strong> (The html in the arg is partially escaped for security reasons, it might be untrusted) - * advancedMsg("${foo} xx ${bar}", "a", "b", "c") - * === {msgStr("foo", "a", "b")} xx {msgStr("bar")} - * === Foo a b xx Bar {0} (The substitution are only applied in the first message) - */ - advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element; - /** - * See advancedMsg() but instead of returning a JSX.Element it returns a string. + * advancedMsgStr("${access-denied}") === advancedMsgStr("access-denied") === msgStr("access-denied") === "Access denied" + * advancedMsgStr("${not-a-message-key}") === advancedMsgStr("not-a-message-key") === "not-a-message-key" */ advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string; @@ -88,10 +79,12 @@ export type GenericI18n = { isFetchingTranslations: boolean; }; -export function createGetI18n(messageBundle: { - [languageTag: string]: { [key in ExtraMessageKey]: string }; +export type MessageKey_defaultSet = keyof typeof messages_defaultSet_fallbackLanguage; + +export function createGetI18n(messagesByLanguageTag_themeDefined: { + [languageTag: string]: { [key in MessageKey_themeDefined]: string }; }) { - type I18n = GenericI18n; + type I18n = GenericI18n_noJsx; type Result = { i18n: I18n; prI18n_currentLanguage: Promise | undefined }; @@ -126,11 +119,18 @@ export function createGetI18n(messageBun labelBySupportedLanguageTag: Object.fromEntries((kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])) }; - const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory({ - messages_fallbackLanguage, - messageBundle_fallbackLanguage: messageBundle[fallbackLanguageTag], - messageBundle_currentLanguage: messageBundle[partialI18n.currentLanguageTag], - messageBundle_realm: kcContext["x-keycloakify"].messages + const { createI18nTranslationFunctions } = createI18nTranslationFunctionsFactory({ + messages_themeDefined: + messagesByLanguageTag_themeDefined[partialI18n.currentLanguageTag] ?? + messagesByLanguageTag_themeDefined[fallbackLanguageTag] ?? + (() => { + const firstLanguageTag = Object.keys(messagesByLanguageTag_themeDefined)[0]; + if (firstLanguageTag === undefined) { + return undefined; + } + return messagesByLanguageTag_themeDefined[firstLanguageTag]; + })(), + messages_fromKcServer: kcContext["x-keycloakify"].messages }); const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag; @@ -139,18 +139,18 @@ export function createGetI18n(messageBun i18n: { ...partialI18n, ...createI18nTranslationFunctions({ - messages_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_fallbackLanguage : undefined + messages_defaultSet_currentLanguage: isCurrentLanguageFallbackLanguage ? messages_defaultSet_fallbackLanguage : undefined }), isFetchingTranslations: !isCurrentLanguageFallbackLanguage }, prI18n_currentLanguage: isCurrentLanguageFallbackLanguage ? undefined : (async () => { - const messages_currentLanguage = await getMessages(partialI18n.currentLanguageTag); + const messages_defaultSet_currentLanguage = await fetchMessages_defaultSet(partialI18n.currentLanguageTag); const i18n_currentLanguage: I18n = { ...partialI18n, - ...createI18nTranslationFunctions({ messages_currentLanguage }), + ...createI18nTranslationFunctions({ messages_defaultSet_currentLanguage }), isFetchingTranslations: false }; @@ -173,155 +173,72 @@ export function createGetI18n(messageBun return { getI18n }; } -function createI18nTranslationFunctionsFactory(params: { - messages_fallbackLanguage: Record; - messageBundle_fallbackLanguage: Record | undefined; - messageBundle_currentLanguage: Partial> | undefined; - messageBundle_realm: Record; +function createI18nTranslationFunctionsFactory(params: { + messages_themeDefined: Record | undefined; + messages_fromKcServer: Record; }) { - const { messageBundle_currentLanguage, messageBundle_realm } = params; - - const messages_fallbackLanguage = { - ...params.messages_fallbackLanguage, - ...params.messageBundle_fallbackLanguage - }; + const { messages_themeDefined, messages_fromKcServer } = params; function createI18nTranslationFunctions(params: { - messages_currentLanguage: Partial> | undefined; - }): Pick, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> { - const messages_currentLanguage = { - ...params.messages_currentLanguage, - ...messageBundle_currentLanguage - }; + messages_defaultSet_currentLanguage: Partial> | undefined; + }): Pick, "msgStr" | "advancedMsgStr"> { + const { messages_defaultSet_currentLanguage } = params; - function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): string | JSX.Element | undefined { - const { key, args, doRenderAsHtml } = props; + function resolveMsg(props: { key: string; args: (string | undefined)[] }): string | undefined { + const { key, args } = props; - const messageOrUndefined: string | undefined = (() => { - terms_text: { - if (key !== "termsText") { - break terms_text; - } - const termsTextMessage = messageBundle_realm[key]; + const message = + id>(messages_fromKcServer)[key] ?? + id | undefined>(messages_themeDefined)?.[key] ?? + id | undefined>(messages_defaultSet_currentLanguage)?.[key] ?? + id>(messages_defaultSet_fallbackLanguage)[key]; - if (termsTextMessage === undefined) { - break terms_text; - } - - return termsTextMessage; - } - - const messageOrUndefined = (messages_currentLanguage as any)[key] ?? (messages_fallbackLanguage as any)[key]; - - return messageOrUndefined; - })(); - - if (messageOrUndefined === undefined) { + if (message === undefined) { return undefined; } - const message = messageOrUndefined; + const startIndex = message + .match(/{[0-9]+}/g) + ?.map(g => g.match(/{([0-9]+)}/)![1]) + .map(indexStr => parseInt(indexStr)) + .sort((a, b) => a - b)[0]; - const messageWithArgsInjectedIfAny = (() => { - const startIndex = message - .match(/{[0-9]+}/g) - ?.map(g => g.match(/{([0-9]+)}/)![1]) - .map(indexStr => parseInt(indexStr)) - .sort((a, b) => a - b)[0]; + if (startIndex === undefined) { + // No {0} in message (no arguments expected) + return message; + } - if (startIndex === undefined) { - // No {0} in message (no arguments expected) - return message; + let messageWithArgsInjected = message; + + args.forEach((arg, i) => { + if (arg === undefined) { + return; } - let messageWithArgsInjected = message; - - args.forEach((arg, i) => { - if (arg === undefined) { - return; - } - - messageWithArgsInjected = messageWithArgsInjected.replace( - new RegExp(`\\{${i + startIndex}\\}`, "g"), - arg.replace(//g, ">") - ); - }); - - return messageWithArgsInjected; - })(); - - return doRenderAsHtml ? ( - - ) : ( - messageWithArgsInjectedIfAny - ); - } - - function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string { - const { key, args, doRenderAsHtml } = props; - - realm_messages: { - const resolvedMessage = messageBundle_realm[key] ?? messageBundle_realm["${" + key + "}"]; - - if (resolvedMessage === undefined) { - break realm_messages; - } - - return doRenderAsHtml ? ( - - ) : ( - resolvedMessage + messageWithArgsInjected = messageWithArgsInjected.replace( + new RegExp(`\\{${i + startIndex}\\}`, "g"), + arg.replace(//g, ">") ); - } - - if (!/\$\{[^}]+\}/.test(key)) { - const resolvedMessage = resolveMsg({ key, args, doRenderAsHtml }); - - if (resolvedMessage === undefined) { - return doRenderAsHtml ? : key; - } - - return resolvedMessage; - } - - let isFirstMatch = true; - - const resolvedComplexMessage = key.replace(/\$\{([^}]+)\}/g, (...[, key_i]) => { - const replaceBy = resolveMsg({ key: key_i, args: isFirstMatch ? args : [], doRenderAsHtml: false }) ?? key_i; - - isFirstMatch = false; - - return replaceBy; }); - return doRenderAsHtml ? : resolvedComplexMessage; + return messageWithArgsInjected; + } + + function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[] }): string { + const { key, args } = props; + + const match = key.match(/^\$\{(.+)\}$/); + + return resolveMsg({ key: match !== null ? match[1] : key, args }) ?? key; } return { - msgStr: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: false }) as string, - msg: (key, ...args) => resolveMsg({ key, args, doRenderAsHtml: true }) as JSX.Element, - advancedMsg: (key, ...args) => - resolveMsgAdvanced({ - key, - args, - doRenderAsHtml: true - }) as JSX.Element, - advancedMsgStr: (key, ...args) => - resolveMsgAdvanced({ - key, - args, - doRenderAsHtml: false - }) as string + msgStr: (key, ...args) => { + const resolvedMessage = resolveMsg({ key, args }); + assert(resolvedMessage !== undefined, `Message with key "${key}" not found`); + return resolvedMessage; + }, + advancedMsgStr: (key, ...args) => resolveMsgAdvanced({ key, args }) }; } diff --git a/src/login/i18n/index.ts b/src/login/i18n/index.ts index 5e58b25b..f492a960 100644 --- a/src/login/i18n/index.ts +++ b/src/login/i18n/index.ts @@ -1,4 +1,5 @@ -import type { GenericI18n, MessageKey, KcContextLike } from "./i18n"; -export type { MessageKey, KcContextLike }; -export type I18n = GenericI18n; +import type { GenericI18n } from "./GenericI18n"; +import type { MessageKey_defaultSet, KcContextLike } from "./i18n"; +export type { MessageKey_defaultSet, KcContextLike }; +export type I18n = GenericI18n; export { createUseI18n } from "./useI18n"; diff --git a/src/login/i18n/useI18n.ts b/src/login/i18n/useI18n.ts deleted file mode 100644 index 023ade06..00000000 --- a/src/login/i18n/useI18n.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { useEffect, useState } from "react"; -import { - createGetI18n, - type GenericI18n, - type MessageKey, - type KcContextLike -} from "./i18n"; -import { Reflect } from "tsafe/Reflect"; - -export function createUseI18n(messageBundle: { - [languageTag: string]: { [key in ExtraMessageKey]: string }; -}) { - type I18n = GenericI18n; - - const { getI18n } = createGetI18n(messageBundle); - - function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } { - const { kcContext } = params; - - const { i18n, prI18n_currentLanguage } = getI18n({ kcContext }); - - const [i18n_toReturn, setI18n_toReturn] = useState(i18n); - - useEffect(() => { - let isActive = true; - - prI18n_currentLanguage?.then(i18n => { - if (!isActive) { - return; - } - - setI18n_toReturn(i18n); - }); - - return () => { - isActive = false; - }; - }, []); - - return { i18n: i18n_toReturn }; - } - - return { useI18n, ofTypeI18n: Reflect() }; -} diff --git a/src/login/i18n/useI18n.tsx b/src/login/i18n/useI18n.tsx new file mode 100644 index 00000000..c153f4ea --- /dev/null +++ b/src/login/i18n/useI18n.tsx @@ -0,0 +1,90 @@ +import { useEffect, useState } from "react"; +import { createGetI18n, type GenericI18n_noJsx, type KcContextLike, type MessageKey_defaultSet } from "./i18n"; +import { GenericI18n } from "./GenericI18n"; +import { Reflect } from "tsafe/Reflect"; + +export function createUseI18n(messageBundle: { + [languageTag: string]: { [key in MessageKey_themeDefined]: string }; +}) { + type MessageKey = MessageKey_defaultSet | MessageKey_themeDefined; + + type I18n = GenericI18n; + + const { withJsx } = (() => { + const cache = new WeakMap, GenericI18n>(); + + function renderHtmlString(htmlString: string): JSX.Element { + return ( +
+ ); + /* + return ( + + ); + */ + } + + function withJsx(i18n_noJsx: GenericI18n_noJsx): I18n { + use_cache: { + const i18n = cache.get(i18n_noJsx); + + if (i18n === undefined) { + break use_cache; + } + + return i18n; + } + + const i18n: I18n = { + ...i18n_noJsx, + msg: (...args) => renderHtmlString(i18n_noJsx.msgStr(...args)), + advancedMsg: (...args) => renderHtmlString(i18n_noJsx.advancedMsgStr(...args)) + }; + + cache.set(i18n_noJsx, i18n); + + return i18n; + } + + return { withJsx }; + })(); + + const { getI18n } = createGetI18n(messageBundle); + + function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } { + const { kcContext } = params; + + const { i18n, prI18n_currentLanguage } = getI18n({ kcContext }); + + const [i18n_toReturn, setI18n_toReturn] = useState(withJsx(i18n)); + + useEffect(() => { + let isActive = true; + + prI18n_currentLanguage?.then(i18n => { + if (!isActive) { + return; + } + + setI18n_toReturn(withJsx(i18n)); + }); + + return () => { + isActive = false; + }; + }, []); + + return { i18n: i18n_toReturn }; + } + + return { useI18n, ofTypeI18n: Reflect() }; +} diff --git a/src/login/lib/useUserProfileForm.tsx b/src/login/lib/useUserProfileForm.tsx index 51983cc4..e960cb9d 100644 --- a/src/login/lib/useUserProfileForm.tsx +++ b/src/login/lib/useUserProfileForm.tsx @@ -9,7 +9,7 @@ import { formatNumber } from "keycloakify/tools/formatNumber"; import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext"; import type { KcContext } from "../KcContext"; -import type { MessageKey } from "keycloakify/login/i18n"; +import type { MessageKey_defaultSet } from "keycloakify/login/i18n"; import { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n"; import type { I18n } from "../i18n"; @@ -148,7 +148,7 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType .map(name => id({ name: name, - displayName: id<`\${${MessageKey}}`>(`\${${name}}`), + displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`), required: true, value: (kcContext.register as any).formData[name] ?? "", html5DataAnnotations: {}, @@ -176,7 +176,7 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType .map(name => id({ name: name, - displayName: id<`\${${MessageKey}}`>(`\${${name}}`), + displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`), required: true, value: (kcContext as any).user[name] ?? "", html5DataAnnotations: {}, @@ -202,7 +202,7 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType return [ id({ name: "email", - displayName: id<`\${${MessageKey}}`>(`\${email}`), + displayName: id<`\${${MessageKey_defaultSet}}`>(`\${email}`), required: true, value: (kcContext.email as any).value ?? "", html5DataAnnotations: {}, @@ -293,7 +293,7 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType 0, { name: "password", - displayName: id<`\${${MessageKey}}`>("${password}"), + displayName: id<`\${${MessageKey_defaultSet}}`>("${password}"), required: true, readOnly: false, validators: {}, @@ -303,7 +303,7 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType }, { name: "password-confirm", - displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"), + displayName: id<`\${${MessageKey_defaultSet}}`>("${passwordConfirm}"), required: true, readOnly: false, validators: {}, @@ -1134,7 +1134,7 @@ function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18 break validator_x; } - const msgArgs = [errorMessageKey ?? id("shouldMatchPattern"), pattern] as const; + const msgArgs = [errorMessageKey ?? id("shouldMatchPattern"), pattern] as const; errors.push({ errorMessage: {advancedMsg(...msgArgs)}, @@ -1173,7 +1173,7 @@ function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18 break validator_x; } - const msgArgs = [id("invalidEmailMessage")] as const; + const msgArgs = [id("invalidEmailMessage")] as const; errors.push({ errorMessage: {msg(...msgArgs)}, @@ -1265,11 +1265,11 @@ function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18 break validator_x; } - const msgArgs = [id("notAValidOption")] as const; + const msgArgs = [id("notAValidOption")] as const; errors.push({ - errorMessage: {advancedMsg(...msgArgs)}, - errorMessageStr: advancedMsgStr(...msgArgs), + errorMessage: {msg(...msgArgs)}, + errorMessageStr: msgStr(...msgArgs), fieldIndex: undefined, source: { type: "validator", diff --git a/src/login/pages/Info.tsx b/src/login/pages/Info.tsx index e904d823..8573d634 100644 --- a/src/login/pages/Info.tsx +++ b/src/login/pages/Info.tsx @@ -34,7 +34,7 @@ export default function Info(props: PageProps advancedMsgStr(`requiredAction.${requiredAction}`)).join(","); + html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(", "); html += ""; } diff --git a/stories/login/pages/Register.stories.tsx b/stories/login/pages/Register.stories.tsx index bf1c5370..0a8c395f 100644 --- a/stories/login/pages/Register.stories.tsx +++ b/stories/login/pages/Register.stories.tsx @@ -70,8 +70,8 @@ export const WithRestrictedToMITStudents: Story = { }, "x-keycloakify": { messages: { - "${profile.attributes.email.inputHelperTextBefore}": "Please use your MIT or Berkeley email.", - "${profile.attributes.email.pattern.error}": + "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." } } @@ -104,10 +104,10 @@ export const WithFavoritePet: Story = { }, "x-keycloakify": { messages: { - "${profile.attributes.favoritePet}": "Favorite Pet", - "${profile.attributes.favoritePet.options.cat}": "Fluffy Cat", - "${profile.attributes.favoritePet.options.dog}": "Loyal Dog", - "${profile.attributes.favoritePet.options.fish}": "Peaceful Fish" + "profile.attributes.favoritePet": "Favorite Pet", + "profile.attributes.favoritePet.options.cat": "Fluffy Cat", + "profile.attributes.favoritePet.options.dog": "Loyal Dog", + "profile.attributes.favoritePet.options.fish": "Peaceful Fish" } } }}