Most of the work done for #549

This commit is contained in:
Joseph Garrone 2024-05-27 17:18:06 +02:00
parent fbe5a1f477
commit 9f2755bc7f
7 changed files with 97 additions and 41 deletions

View File

@ -29,17 +29,7 @@ out["messagesPerField"]= {
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
return text;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
return <#if doExistErrorOnUsernameOrPassword>text<#else>undefined</#if>
<#else>
<#assign doExistMessageForField = "">
<#attempt>
@ -107,22 +97,18 @@ out["messagesPerField"]= {
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
return decodeHtmlEntities("${msg('invalidUserMessage')?js_string}");
<#recover>
return "Invalid username or password.";
</#attempt>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "";
</#attempt>
return "";
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
return decodeHtmlEntities("${messagesPerField.get('${fieldName}')?js_string}");
<#recover>
return "invalid field";
return "Invalid field";
</#attempt>
</#if>
}
@ -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>
<#if attribute.annotations.inputHelperTextBefore??>
"${attribute.annotations.inputHelperTextBefore}": "${advancedMsg(attribute.annotations.inputHelperTextBefore)?no_esc}",
"${attribute.annotations.inputHelperTextBefore}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextBefore)?js_string}"),
</#if>
<#if attribute.annotations.inputHelperTextAfter??>
"${attribute.annotations.inputHelperTextAfter}": "${advancedMsg(attribute.annotations.inputHelperTextAfter)?no_esc}",
"${attribute.annotations.inputHelperTextAfter}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputHelperTextAfter)?js_string}"),
</#if>
<#if attribute.annotations.inputTypePlaceholder??>
"${attribute.annotations.inputTypePlaceholder}": "${advancedMsg(attribute.annotations.inputTypePlaceholder)?no_esc}",
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
</#if>
</#list>
};
</#if>
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>

View File

@ -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" }';

View File

@ -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;

View File

@ -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";

View File

@ -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<string, string>;
};
assert<KcContext extends KcContextLike ? true : false>();
@ -61,7 +61,7 @@ export type GenericI18n<MessageKey extends string> = {
/**
* 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<ExtraMessageKey extends string = never>(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<ExtraMessageKey extends string = never>(extraMessa
function createI18nTranslationFunctions<MessageKey extends string>(params: {
fallbackMessages: Record<MessageKey, string>;
messages: Record<MessageKey, string>;
__localizationRealmOverridesUserProfile: Record<string, string>;
}): Pick<GenericI18n<MessageKey>, "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<MessageKey extends string>(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];

View File

@ -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<string, string>;
};
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
>();

View File

@ -221,7 +221,8 @@ export const kcContextCommonMock: KcContext.Common = {
},
scripts: [],
isAppInitiatedAction: false,
properties: {}
properties: {},
__localizationRealmOverridesUserProfile: {}
};
const loginUrl = {