Compare commits
14 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
b73eceb535 | |||
5dc3453fc9 | |||
cef1139a4b | |||
ac96959947 | |||
4d73d877ba | |||
9f1186302e | |||
319dcc0d15 | |||
e99fdb8561 | |||
f37a342a63 | |||
09a039894d | |||
3efbb1a9fd | |||
920ee62ee3 | |||
1ace44fe31 | |||
a60f05415b |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.0-rc.83",
|
||||
"version": "10.0.0-rc.89",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -62,9 +62,6 @@
|
||||
"bluehats"
|
||||
],
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsafe": "^1.6.6"
|
||||
},
|
||||
|
@ -65,11 +65,14 @@ async function main() {
|
||||
fs
|
||||
.readFileSync(pathJoin(baseThemeDirPath, filePath))
|
||||
.toString("utf8")
|
||||
)
|
||||
).map(([key, value]: any) => [
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value.replace(/''/g, "'")
|
||||
])
|
||||
) as Record<string, string>
|
||||
)
|
||||
.map(([key, value]) => [key, value.replace(/''/g, "'")])
|
||||
.map(([key, value]) => [
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value
|
||||
])
|
||||
.map(([key, value]) => [key, key === "termsText" ? "" : value])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -28,9 +28,13 @@ export function startRebuildOnSrcChange() {
|
||||
|
||||
console.log(chalk.green("Watching for changes in src/"));
|
||||
|
||||
chokidar.watch(["src", "stories"], { ignoreInitial: true }).on("all", async () => {
|
||||
await waitForDebounce();
|
||||
chokidar
|
||||
.watch(["src", "stories"], { ignoreInitial: true })
|
||||
.on("all", async (event, path) => {
|
||||
console.log(chalk.bold(`${event}: ${path}`));
|
||||
|
||||
runYarnBuild();
|
||||
});
|
||||
await waitForDebounce();
|
||||
|
||||
runYarnBuild();
|
||||
});
|
||||
}
|
||||
|
@ -3,8 +3,7 @@ import { assert } from "tsafe/assert";
|
||||
import messages_fallbackLanguage from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import type { KcContext } from "../KcContext";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
import { fallbackLanguageTag } from "keycloakify/bin/shared/constants";
|
||||
|
||||
export type KcContextLike = {
|
||||
locale?: {
|
||||
|
@ -2,4 +2,3 @@ import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
|
||||
export type { MessageKey, KcContextLike };
|
||||
export type I18n = GenericI18n<MessageKey>;
|
||||
export { createUseI18n } from "./useI18n";
|
||||
export { fallbackLanguageTag } from "./i18n";
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
} from "./i18n";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(messageBundle: {
|
||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||
}) {
|
||||
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||
|
||||
const { getI18n } = createGetI18n(extraMessages);
|
||||
const { getI18n } = createGetI18n(messageBundle);
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||
const { kcContext } = params;
|
||||
|
@ -211,8 +211,6 @@ function decodeHtmlEntities(htmlStr){
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
|
||||
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if (
|
||||
["register.ftl", "register-user-profile.ftl", "terms.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
@ -228,6 +226,8 @@ function decodeHtmlEntities(htmlStr){
|
||||
<#local out_seq += ["/*Accessing attemptedUsername throwed an exception */"]>
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
|
||||
|
||||
<#attempt>
|
||||
<#if !object[key]??>
|
||||
|
@ -1,9 +1,7 @@
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import { type ThemeType, fallbackLanguageTag } from "../../shared/constants";
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
|
||||
import * as recast from "recast";
|
||||
import * as babelParser from "@babel/parser";
|
||||
import babelGenerate from "@babel/generator";
|
||||
@ -11,6 +9,7 @@ import * as babelTypes from "@babel/types";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export function generateMessageProperties(params: {
|
||||
themeSrcDirPath: string;
|
||||
@ -18,32 +17,92 @@ export function generateMessageProperties(params: {
|
||||
}): { languageTag: string; propertiesFileSource: string }[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
let files = crawl({
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
files = files.filter(file => {
|
||||
const regex = /\.(js|ts|tsx)$/;
|
||||
return regex.test(file);
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => {
|
||||
const regex = /\.i18n\.(ts|js|tsx)$/;
|
||||
const aIsI18nFile = regex.test(a);
|
||||
const bIsI18nFile = regex.test(b);
|
||||
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file =>
|
||||
readFileSync(file).toString("utf8").includes("createUseI18n")
|
||||
const baseMessagesDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"baseMessages"
|
||||
);
|
||||
|
||||
const extraMessages = files
|
||||
.map(file => {
|
||||
const root = recast.parse(readFileSync(file).toString("utf8"), {
|
||||
const baseMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(baseMessagesDirPath)
|
||||
.filter(baseName => baseName !== "index.ts")
|
||||
.map(basename => ({
|
||||
languageTag: basename.replace(/\.ts$/, ""),
|
||||
filePath: pathJoin(baseMessagesDirPath, basename)
|
||||
}))
|
||||
.map(({ languageTag, filePath }) => {
|
||||
const lines = fs
|
||||
.readFileSync(filePath)
|
||||
.toString("utf8")
|
||||
.split(/\r?\n/);
|
||||
|
||||
let messagesJson = "{";
|
||||
|
||||
let isInDeclaration = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!isInDeclaration) {
|
||||
if (line.startsWith("const messages")) {
|
||||
isInDeclaration = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("}")) {
|
||||
messagesJson += "}";
|
||||
break;
|
||||
}
|
||||
|
||||
messagesJson += line;
|
||||
}
|
||||
|
||||
const messages = JSON.parse(messagesJson) as Record<string, string>;
|
||||
|
||||
return [languageTag, messages];
|
||||
})
|
||||
);
|
||||
|
||||
const { i18nTsFilePath } = (() => {
|
||||
let files = crawl({
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
files = files.filter(file => {
|
||||
const regex = /\.(js|ts|tsx)$/;
|
||||
return regex.test(file);
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => {
|
||||
const regex = /\.i18n\.(ts|js|tsx)$/;
|
||||
const aIsI18nFile = regex.test(a);
|
||||
const bIsI18nFile = regex.test(b);
|
||||
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file =>
|
||||
fs.readFileSync(file).toString("utf8").includes("createUseI18n(")
|
||||
);
|
||||
|
||||
const i18nTsFilePath: string | undefined = files[0];
|
||||
|
||||
return { i18nTsFilePath };
|
||||
})();
|
||||
|
||||
const messageBundle: { [languageTag: string]: Record<string, string> } | undefined =
|
||||
(() => {
|
||||
if (i18nTsFilePath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
@ -55,7 +114,7 @@ export function generateMessageProperties(params: {
|
||||
}
|
||||
});
|
||||
|
||||
const codes: string[] = [];
|
||||
let messageBundleDeclarationTsCode: string | undefined = undefined;
|
||||
|
||||
recast.visit(root, {
|
||||
visitCallExpression: function (path) {
|
||||
@ -63,111 +122,71 @@ export function generateMessageProperties(params: {
|
||||
path.node.callee.type === "Identifier" &&
|
||||
path.node.callee.name === "createUseI18n"
|
||||
) {
|
||||
codes.push(babelGenerate(path.node.arguments[0] as any).code);
|
||||
messageBundleDeclarationTsCode = babelGenerate(
|
||||
path.node.arguments[0] as any
|
||||
).code;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
}
|
||||
});
|
||||
|
||||
return codes;
|
||||
})
|
||||
.flat()
|
||||
.map(code => {
|
||||
let extraMessages: {
|
||||
assert(messageBundleDeclarationTsCode !== undefined);
|
||||
|
||||
let messageBundle: {
|
||||
[languageTag: string]: Record<string, string>;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
eval(`${symToStr({ extraMessages })} = ${code}`);
|
||||
eval(
|
||||
`${symToStr({ messageBundle })} = ${messageBundleDeclarationTsCode}`
|
||||
);
|
||||
} catch {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: Make sure that the first argument of createUseI18n can be evaluated in a javascript",
|
||||
"runtime where only the node globals are available.",
|
||||
"WARNING: Make sure the messageBundle your provided as argument of createUseI18n can be statically evaluated.",
|
||||
"This is important because we need to put your i18n messages in messages_*.properties files",
|
||||
"or they won't be available server side.",
|
||||
"\n",
|
||||
"The following code could not be evaluated:",
|
||||
"\n",
|
||||
code
|
||||
messageBundleDeclarationTsCode
|
||||
].join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
return extraMessages;
|
||||
});
|
||||
return messageBundle;
|
||||
})();
|
||||
|
||||
const languageTags = [
|
||||
...extraMessages.map(extraMessage => Object.keys(extraMessage)).flat(),
|
||||
...fs
|
||||
.readdirSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"baseMessages"
|
||||
)
|
||||
)
|
||||
.filter(baseName => baseName !== "index.ts")
|
||||
.map(baseName => baseName.replace(/\.ts$/, ""))
|
||||
].reduce(...removeDuplicates<string>());
|
||||
|
||||
const keyValueMapByLanguageTag: Record<string, Record<string, string>> = {};
|
||||
|
||||
for (const languageTag of languageTags) {
|
||||
const keyValueMap: Record<string, string> = {
|
||||
termsText: ""
|
||||
};
|
||||
|
||||
for (const extraMessage of extraMessages) {
|
||||
const keyValueMap_i = extraMessage[languageTag];
|
||||
|
||||
if (keyValueMap_i === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(keyValueMap_i)) {
|
||||
if (key !== "termsText" && keyValueMap[key] !== undefined) {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: The following key is defined multiple times:",
|
||||
"\n",
|
||||
key,
|
||||
"\n",
|
||||
"The following value will be ignored:",
|
||||
"\n",
|
||||
value,
|
||||
"\n",
|
||||
"The following value was already defined:",
|
||||
"\n",
|
||||
keyValueMap[key]
|
||||
].join(" ")
|
||||
);
|
||||
continue;
|
||||
const mergedMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
Object.entries(baseMessageBundle).map(([languageTag, messages]) => [
|
||||
languageTag,
|
||||
{
|
||||
...messages,
|
||||
...(messageBundle === undefined
|
||||
? {}
|
||||
: messageBundle[languageTag] ??
|
||||
messageBundle[fallbackLanguageTag] ??
|
||||
messageBundle[Object.keys(messageBundle)[0]] ??
|
||||
{})
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
keyValueMap[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
keyValueMapByLanguageTag[languageTag] = keyValueMap;
|
||||
}
|
||||
|
||||
const out: { languageTag: string; propertiesFileSource: string }[] = [];
|
||||
|
||||
for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) {
|
||||
const propertiesFileSource = Object.entries(keyValueMap)
|
||||
.map(([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`)
|
||||
.join("\n");
|
||||
|
||||
out.push({
|
||||
const messageProperties: { languageTag: string; propertiesFileSource: string }[] =
|
||||
Object.entries(mergedMessageBundle).map(([languageTag, messages]) => ({
|
||||
languageTag,
|
||||
propertiesFileSource: ["", "parent=base", "", propertiesFileSource, ""].join(
|
||||
"\n"
|
||||
)
|
||||
});
|
||||
}
|
||||
propertiesFileSource: [
|
||||
"",
|
||||
...(themeType !== "account" ? ["parent=base"] : []),
|
||||
...Object.entries(messages).map(
|
||||
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
|
||||
),
|
||||
""
|
||||
].join("\n")
|
||||
}));
|
||||
|
||||
return out;
|
||||
return messageProperties;
|
||||
}
|
||||
|
@ -11,7 +11,15 @@ export function readFieldNameUsage(params: {
|
||||
}): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
const fieldNames = new Set<string>();
|
||||
// NOTE: We pre-populate with the synthetic user attributes defined in useUserProfileForm (can't be parsed automatically)
|
||||
const fieldNames = new Set<string>([
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"username",
|
||||
"password",
|
||||
"password-confirm"
|
||||
]);
|
||||
|
||||
for (const srcDirPath of [
|
||||
pathJoin(getThisCodebaseRootDirPath(), "src", themeType),
|
||||
|
@ -67,3 +67,5 @@ export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
||||
|
||||
export const containerName = "keycloak-keycloakify";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
|
@ -1,6 +1,8 @@
|
||||
{
|
||||
"id": "34c5f904-d66e-4d8f-8876-8f00d9fa9d6c",
|
||||
"realm": "myrealm",
|
||||
"displayName": "",
|
||||
"displayNameHtml": "",
|
||||
"notBefore": 0,
|
||||
"defaultSignatureAlgorithm": "RS256",
|
||||
"revokeRefreshToken": false,
|
||||
@ -1356,11 +1358,11 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
@ -1431,13 +1433,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
@ -2127,17 +2129,20 @@
|
||||
"dockerAuthenticationFlow": "docker auth",
|
||||
"attributes": {
|
||||
"cibaBackchannelTokenDeliveryMode": "poll",
|
||||
"cibaExpiresIn": "120",
|
||||
"cibaAuthRequestedUserHint": "login_hint",
|
||||
"oauth2DeviceCodeLifespan": "600",
|
||||
"clientOfflineSessionMaxLifespan": "0",
|
||||
"oauth2DevicePollingInterval": "5",
|
||||
"clientSessionIdleTimeout": "0",
|
||||
"parRequestUriLifespan": "60",
|
||||
"clientSessionMaxLifespan": "0",
|
||||
"userProfileEnabled": "true",
|
||||
"clientOfflineSessionIdleTimeout": "0",
|
||||
"cibaInterval": "5",
|
||||
"realmReusableOtpCode": "false"
|
||||
"realmReusableOtpCode": "false",
|
||||
"cibaExpiresIn": "120",
|
||||
"oauth2DeviceCodeLifespan": "600",
|
||||
"parRequestUriLifespan": "60",
|
||||
"clientSessionMaxLifespan": "0",
|
||||
"frontendUrl": "",
|
||||
"acr.loa.map": "{}"
|
||||
},
|
||||
"keycloakVersion": "23.0.7",
|
||||
"userManagedAccessAllowed": false,
|
||||
|
@ -3,8 +3,7 @@ import { assert } from "tsafe/assert";
|
||||
import messages_fallbackLanguage from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import type { KcContext } from "../KcContext";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
import { fallbackLanguageTag } from "keycloakify/bin/shared/constants";
|
||||
|
||||
export type KcContextLike = {
|
||||
locale?: {
|
||||
|
@ -2,4 +2,3 @@ import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
|
||||
export type { MessageKey, KcContextLike };
|
||||
export type I18n = GenericI18n<MessageKey>;
|
||||
export { createUseI18n } from "./useI18n";
|
||||
export { fallbackLanguageTag } from "./i18n";
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
} from "./i18n";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(messageBundle: {
|
||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||
}) {
|
||||
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||
|
||||
const { getI18n } = createGetI18n(extraMessages);
|
||||
const { getI18n } = createGetI18n(messageBundle);
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||
const { kcContext } = params;
|
||||
|
Reference in New Issue
Block a user