Correctly generate i18n messages for admin UI
This commit is contained in:
parent
c39c450e90
commit
326411ca5d
@ -40,6 +40,7 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
import propertiesParser from "properties-parser";
|
import propertiesParser from "properties-parser";
|
||||||
|
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||||
BuildContextLike_generateMessageProperties & {
|
BuildContextLike_generateMessageProperties & {
|
||||||
@ -256,15 +257,17 @@ export async function generateResources(params: {
|
|||||||
writeMessagePropertiesFiles;
|
writeMessagePropertiesFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
bring_in_spas_messages: {
|
bring_in_account_spa_messages: {
|
||||||
if (!isSpa) {
|
if (!isSpa) {
|
||||||
break bring_in_spas_messages;
|
break bring_in_account_spa_messages;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(themeType !== "login");
|
if (themeType !== "account") {
|
||||||
|
break bring_in_account_spa_messages;
|
||||||
|
}
|
||||||
|
|
||||||
const accountUiDirPath = child_process
|
const accountUiDirPath = child_process
|
||||||
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
|
.execSync(`npm list @keycloakify/keycloak-account-ui --parseable`, {
|
||||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
cwd: pathDirname(buildContext.packageJsonFilePath)
|
||||||
})
|
})
|
||||||
.toString("utf8")
|
.toString("utf8")
|
||||||
@ -279,7 +282,7 @@ export async function generateResources(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const messagesDirPath_dest = pathJoin(
|
const messagesDirPath_dest = pathJoin(
|
||||||
getThemeTypeDirPath({ themeName, themeType }),
|
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -291,7 +294,7 @@ export async function generateResources(params: {
|
|||||||
apply_theme_changes: {
|
apply_theme_changes: {
|
||||||
const messagesDirPath_theme = pathJoin(
|
const messagesDirPath_theme = pathJoin(
|
||||||
buildContext.themeSrcDirPath,
|
buildContext.themeSrcDirPath,
|
||||||
themeType,
|
"account",
|
||||||
"messages"
|
"messages"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -339,6 +342,159 @@ export async function generateResources(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bring_in_admin_messages: {
|
||||||
|
if (themeType !== "admin") {
|
||||||
|
break bring_in_admin_messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
const messagesDirPath_theme = pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"admin",
|
||||||
|
"i18n"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(
|
||||||
|
fs.existsSync(messagesDirPath_theme),
|
||||||
|
`${messagesDirPath_theme} is supposed to exist`
|
||||||
|
);
|
||||||
|
|
||||||
|
const propertiesByLang: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
base: Buffer;
|
||||||
|
override: Buffer | undefined;
|
||||||
|
overrideByThemeName: Record<string, Buffer>;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
||||||
|
type ParsedBasename = { lang: string } & (
|
||||||
|
| {
|
||||||
|
isOverride: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isOverride: true;
|
||||||
|
themeName: string | undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const parsedBasename = ((): ParsedBasename | undefined => {
|
||||||
|
const match = basename.match(/^messages_([^.]+)\.properties$/);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discriminator = match[1];
|
||||||
|
|
||||||
|
const split = discriminator.split("_override");
|
||||||
|
|
||||||
|
if (split.length === 1) {
|
||||||
|
return {
|
||||||
|
lang: discriminator,
|
||||||
|
isOverride: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(split.length === 2);
|
||||||
|
|
||||||
|
if (split[1] === "") {
|
||||||
|
return {
|
||||||
|
lang: split[0],
|
||||||
|
isOverride: true,
|
||||||
|
themeName: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const match2 = split[1].match(/^_(.+)$/);
|
||||||
|
|
||||||
|
assert(match2 !== null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
lang: split[0],
|
||||||
|
isOverride: true,
|
||||||
|
themeName: match2[1]
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (parsedBasename === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesByLang[parsedBasename.lang] ??= {
|
||||||
|
base: createObjectThatThrowsIfAccessed<Buffer>({
|
||||||
|
debugMessage: `No base ${parsedBasename.lang} translation for admin theme`
|
||||||
|
}),
|
||||||
|
override: undefined,
|
||||||
|
overrideByThemeName: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buffer = fs.readFileSync(pathJoin(messagesDirPath_theme, basename));
|
||||||
|
|
||||||
|
if (parsedBasename.isOverride === false) {
|
||||||
|
propertiesByLang[parsedBasename.lang].base = buffer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedBasename.themeName === undefined) {
|
||||||
|
propertiesByLang[parsedBasename.lang].override = buffer;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertiesByLang[parsedBasename.lang].overrideByThemeName[
|
||||||
|
parsedBasename.themeName
|
||||||
|
] = buffer;
|
||||||
|
});
|
||||||
|
|
||||||
|
writeMessagePropertiesFilesByThemeType.admin = ({
|
||||||
|
messageDirPath,
|
||||||
|
themeName
|
||||||
|
}) => {
|
||||||
|
if (!fs.existsSync(messageDirPath)) {
|
||||||
|
fs.mkdirSync(messageDirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(propertiesByLang).forEach(
|
||||||
|
([lang, { base, override, overrideByThemeName }]) => {
|
||||||
|
(languageTags ??= []).push(lang);
|
||||||
|
|
||||||
|
const messages = propertiesParser.parse(base.toString("utf8"));
|
||||||
|
|
||||||
|
if (override !== undefined) {
|
||||||
|
const overrideMessages = propertiesParser.parse(
|
||||||
|
override.toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(overrideMessages).forEach(
|
||||||
|
([key, value]) => (messages[key] = value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (themeName in overrideByThemeName) {
|
||||||
|
const overrideMessages = propertiesParser.parse(
|
||||||
|
overrideByThemeName[themeName].toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
Object.entries(overrideMessages).forEach(
|
||||||
|
([key, value]) => (messages[key] = value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const editor = propertiesParser.createEditor();
|
||||||
|
|
||||||
|
Object.entries(messages).forEach(([key, value]) => {
|
||||||
|
editor.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(messageDirPath, `messages_${lang}.properties`),
|
||||||
|
Buffer.from(editor.toString(), "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
keycloak_static_resources: {
|
keycloak_static_resources: {
|
||||||
if (isSpa) {
|
if (isSpa) {
|
||||||
break keycloak_static_resources;
|
break keycloak_static_resources;
|
||||||
|
@ -259,11 +259,12 @@ program
|
|||||||
.option({
|
.option({
|
||||||
key: "file",
|
key: "file",
|
||||||
name: (() => {
|
name: (() => {
|
||||||
const name = "file";
|
const long = "file";
|
||||||
|
const short = "f";
|
||||||
|
|
||||||
optionsKeys.push(name);
|
optionsKeys.push(long, short);
|
||||||
|
|
||||||
return name;
|
return { long, short };
|
||||||
})(),
|
})(),
|
||||||
description: [
|
description: [
|
||||||
"Relative path of the file relative to the directory of your keycloak theme source",
|
"Relative path of the file relative to the directory of your keycloak theme source",
|
||||||
|
@ -41,10 +41,18 @@ export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
|
|||||||
return [`/**`, ...lines.map(line => ` * ${line}`), ` */`].join("\n");
|
return [`/**`, ...lines.map(line => ` * ${line}`), ` */`].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
if (fileRelativePath.endsWith(".html")) {
|
||||||
return [`<!--`, ...lines.map(line => ` ${line}`), `-->`].join("\n");
|
return [`<!--`, ...lines.map(line => ` ${line}`), `-->`].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fileRelativePath.endsWith(".svg")) {
|
||||||
|
return [
|
||||||
|
`<!--`,
|
||||||
|
...lines.map(line => ` ${line.replace("--file", "-f")}`),
|
||||||
|
`-->`
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
if (fileRelativePath.endsWith(".properties")) {
|
if (fileRelativePath.endsWith(".properties")) {
|
||||||
return lines.map(line => `# ${line}`).join("\n");
|
return lines.map(line => `# ${line}`).join("\n");
|
||||||
}
|
}
|
||||||
|
90
src/bin/tools/createObjectThatThrowsIfAccessed.ts
Normal file
90
src/bin/tools/createObjectThatThrowsIfAccessed.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
const keyIsTrapped = "isTrapped_zSskDe9d";
|
||||||
|
|
||||||
|
export class AccessError extends Error {
|
||||||
|
constructor(message: string) {
|
||||||
|
super(message);
|
||||||
|
Object.setPrototypeOf(this, new.target.prototype);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createObjectThatThrowsIfAccessed<T extends object>(params?: {
|
||||||
|
debugMessage?: string;
|
||||||
|
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||||
|
}): T {
|
||||||
|
const { debugMessage = "", isPropertyWhitelisted = () => false } = params ?? {};
|
||||||
|
|
||||||
|
const get: NonNullable<ProxyHandler<T>["get"]> = (...args) => {
|
||||||
|
const [, prop] = args;
|
||||||
|
|
||||||
|
if (isPropertyWhitelisted(prop)) {
|
||||||
|
return Reflect.get(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === keyIsTrapped) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new AccessError(`Cannot access ${String(prop)} yet ${debugMessage}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const trappedObject = new Proxy<T>({} as any, {
|
||||||
|
get,
|
||||||
|
set: get
|
||||||
|
});
|
||||||
|
|
||||||
|
return trappedObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createObjectThatThrowsIfAccessedFactory(params: {
|
||||||
|
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||||
|
}) {
|
||||||
|
const { isPropertyWhitelisted } = params;
|
||||||
|
|
||||||
|
return {
|
||||||
|
createObjectThatThrowsIfAccessed: <T extends object>(params?: {
|
||||||
|
debugMessage?: string;
|
||||||
|
}) => {
|
||||||
|
const { debugMessage } = params ?? {};
|
||||||
|
|
||||||
|
return createObjectThatThrowsIfAccessed<T>({
|
||||||
|
debugMessage,
|
||||||
|
isPropertyWhitelisted
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObjectThatThrowIfAccessed(obj: object) {
|
||||||
|
return (obj as any)[keyIsTrapped] === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const THROW_IF_ACCESSED = {
|
||||||
|
__brand: "THROW_IF_ACCESSED"
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createObjectWithSomePropertiesThatThrowIfAccessed<
|
||||||
|
T extends Record<string, unknown>
|
||||||
|
>(obj: { [K in keyof T]: T[K] | typeof THROW_IF_ACCESSED }, debugMessage?: string): T {
|
||||||
|
return Object.defineProperties(
|
||||||
|
obj,
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(obj)
|
||||||
|
.filter(([, value]) => value === THROW_IF_ACCESSED)
|
||||||
|
.map(([key]) => {
|
||||||
|
const getAndSet = () => {
|
||||||
|
throw new AccessError(
|
||||||
|
`Cannot access ${key} yet ${debugMessage ?? ""}`
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pd = {
|
||||||
|
get: getAndSet,
|
||||||
|
set: getAndSet,
|
||||||
|
enumerable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
return [key, pd];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
) as any;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user