Almost done, left to extract the extra language resources

This commit is contained in:
Joseph Garrone
2024-09-22 04:39:24 +02:00
parent bb163132fe
commit b0b6b994ed
6 changed files with 391 additions and 154 deletions

View File

@ -10,12 +10,26 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext";
export type BuildContextLike = {
themeNames: string[];
themeSrcDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function generateMessageProperties(params: {
themeSrcDirPath: string;
buildContext: BuildContextLike;
themeType: ThemeType;
}): { languageTag: string; propertiesFileSource: string }[] {
const { themeSrcDirPath, themeType } = params;
}): {
languageTags: string[];
writeMessagePropertiesFiles: (params: {
messageDirPath: string;
themeName: string;
}) => void;
} {
const { buildContext, themeType } = params;
const baseMessagesDirPath = pathJoin(
getThisCodebaseRootDirPath(),
@ -25,51 +39,49 @@ export function generateMessageProperties(params: {
"messages_defaultSet"
);
const baseMessageBundle: { [languageTag: string]: Record<string, string> } =
Object.fromEntries(
fs
.readdirSync(baseMessagesDirPath)
.filter(basename => basename !== "index.ts" && basename !== "types.ts")
.map(basename => ({
languageTag: basename.replace(/\.ts$/, ""),
filePath: pathJoin(baseMessagesDirPath, basename)
}))
.map(({ languageTag, filePath }) => {
const lines = fs
.readFileSync(filePath)
.toString("utf8")
.split(/\r?\n/);
const messages_defaultSet_by_languageTag_defaultSet: {
[languageTag_defaultSet: string]: Record<string, string>;
} = Object.fromEntries(
fs
.readdirSync(baseMessagesDirPath)
.filter(basename => basename !== "index.ts" && basename !== "types.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 messagesJson = "{";
let isInDeclaration = false;
let isInDeclaration = false;
for (const line of lines) {
if (!isInDeclaration) {
if (line.startsWith("const messages")) {
isInDeclaration = true;
}
continue;
for (const line of lines) {
if (!isInDeclaration) {
if (line.startsWith("const messages")) {
isInDeclaration = true;
}
if (line.startsWith("}")) {
messagesJson += "}";
break;
}
messagesJson += line;
continue;
}
const messages = JSON.parse(messagesJson) as Record<string, string>;
if (line.startsWith("}")) {
messagesJson += "}";
break;
}
return [languageTag, messages];
})
);
messagesJson += line;
}
const messages = JSON.parse(messagesJson) as Record<string, string>;
return [languageTag, messages];
})
);
const { i18nTsFilePath } = (() => {
let files = crawl({
dirPath: pathJoin(themeSrcDirPath, themeType),
dirPath: pathJoin(buildContext.themeSrcDirPath, themeType),
returnedPathsType: "absolute"
});
@ -88,7 +100,7 @@ export function generateMessageProperties(params: {
files = files.sort((a, b) => a.length - b.length);
files = files.filter(file =>
fs.readFileSync(file).toString("utf8").includes("createUseI18n(")
fs.readFileSync(file).toString("utf8").includes("i18nBuilder")
);
const i18nTsFilePath: string | undefined = files[0];
@ -96,13 +108,13 @@ export function generateMessageProperties(params: {
return { i18nTsFilePath };
})();
const messageBundle: { [languageTag: string]: Record<string, string> } | undefined =
(() => {
if (i18nTsFilePath === undefined) {
return undefined;
}
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
const i18nTsRoot = (() => {
if (i18nTsFilePath === undefined) {
return undefined;
}
const root: recast.types.ASTNode = recast.parse(
fs.readFileSync(i18nTsFilePath).toString("utf8"),
{
parser: {
parse: (code: string) =>
babelParser.parse(code, {
@ -112,81 +124,176 @@ export function generateMessageProperties(params: {
generator: babelGenerate,
types: babelTypes
}
});
}
);
return root;
})();
let messageBundleDeclarationTsCode: string | undefined = undefined;
const messages_defaultSet_by_languageTag_notInDefaultSet:
| { [languageTag_notInDefaultSet: string]: Record<string, string> }
| undefined = (() => {
if (i18nTsRoot === undefined) {
return undefined;
}
recast.visit(root, {
visitCallExpression: function (path) {
if (
path.node.callee.type === "Identifier" &&
path.node.callee.name === "createUseI18n"
) {
messageBundleDeclarationTsCode = babelGenerate(
path.node.arguments[0] as any
).code;
return false;
let firstArgumentCode: string | undefined = undefined;
recast.visit(i18nTsRoot, {
visitCallExpression: function (path) {
const node = path.node;
if (
node.callee.type === "MemberExpression" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "withExtraLanguages"
) {
firstArgumentCode = babelGenerate(node.arguments[0] as any).code;
return false;
}
this.traverse(path);
}
});
if (firstArgumentCode === undefined) {
return undefined;
}
//todo
//TODO
return {};
})();
const messages_defaultSet_by_languageTag = {
...messages_defaultSet_by_languageTag_defaultSet,
...messages_defaultSet_by_languageTag_notInDefaultSet
};
const messages_themeDefined_by_languageTag:
| {
[languageTag: string]:
| Record<string, string | Record<string, string>>
| undefined;
}
| undefined = (() => {
if (i18nTsRoot === undefined) {
return undefined;
}
let firstArgumentCode: string | undefined = undefined;
recast.visit(i18nTsRoot, {
visitCallExpression: function (path) {
const node = path.node;
if (
node.callee.type === "MemberExpression" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "withCustomTranslations"
) {
firstArgumentCode = babelGenerate(node.arguments[0] as any).code;
return false;
}
this.traverse(path);
}
});
if (firstArgumentCode === undefined) {
return undefined;
}
let messages_themeDefined_by_languageTag: {
[languageTag: string]: Record<string, string | Record<string, string>>;
} = {};
try {
eval(
`${symToStr({ messages_themeDefined_by_languageTag })} = ${firstArgumentCode}`
);
} catch {
console.warn(
[
"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",
firstArgumentCode
].join(" ")
);
return undefined;
}
return messages_themeDefined_by_languageTag;
})();
const languageTags = Object.keys(messages_defaultSet_by_languageTag);
return {
languageTags,
writeMessagePropertiesFiles: ({ messageDirPath, themeName }) => {
for (const languageTag of languageTags) {
const messages = {
...messages_defaultSet_by_languageTag[languageTag]
};
add_theme_defined_messages: {
if (messages_themeDefined_by_languageTag === undefined) {
break add_theme_defined_messages;
}
this.traverse(path);
let messages_themeDefined =
messages_themeDefined_by_languageTag[languageTag];
if (messages_themeDefined === undefined) {
messages_themeDefined =
messages_themeDefined_by_languageTag[FALLBACK_LANGUAGE_TAG];
}
if (messages_themeDefined === undefined) {
messages_themeDefined =
messages_themeDefined_by_languageTag[
Object.keys(messages_themeDefined_by_languageTag)[0]
];
}
if (messages_themeDefined === undefined) {
break add_theme_defined_messages;
}
for (const [key, messageOrMessageByThemeName] of Object.entries(
messages_themeDefined
)) {
const message = (() => {
if (typeof messageOrMessageByThemeName === "string") {
return messageOrMessageByThemeName;
}
const message = messageOrMessageByThemeName[themeName];
assert(message !== undefined);
return message;
})();
messages[key] = message;
}
}
});
assert(messageBundleDeclarationTsCode !== undefined);
const propertiesFileSource = [
"",
...Object.entries(messages).map(
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
),
""
].join("\n");
let messageBundle: {
[languageTag: string]: Record<string, string>;
} = {};
try {
eval(
`${symToStr({ messageBundle })} = ${messageBundleDeclarationTsCode}`
);
} catch {
console.warn(
[
"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",
messageBundleDeclarationTsCode
].join(" ")
fs.writeFileSync(
pathJoin(messageDirPath, `messages_${languageTag}.properties`),
Buffer.from(propertiesFileSource, "utf8")
);
}
return messageBundle;
})();
const mergedMessageBundle: { [languageTag: string]: Record<string, string> } =
Object.fromEntries(
Object.entries(baseMessageBundle).map(([languageTag, messages]) => [
languageTag,
{
...messages,
...(messageBundle === undefined
? {}
: messageBundle[languageTag] ??
messageBundle[FALLBACK_LANGUAGE_TAG] ??
messageBundle[Object.keys(messageBundle)[0]] ??
{})
}
])
);
const messageProperties: { languageTag: string; propertiesFileSource: string }[] =
Object.entries(mergedMessageBundle).map(([languageTag, messages]) => ({
languageTag,
propertiesFileSource: [
"",
...(themeType !== "account" ? ["parent=base"] : []),
...Object.entries(messages).map(
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
),
""
].join("\n")
}));
return messageProperties;
}
};
}

View File

@ -26,7 +26,7 @@ export async function generateResources(params: {
rmSync(resourcesDirPath, { recursive: true });
}
await generateResourcesForMainTheme({
const { writeMessagePropertiesFiles } = await generateResourcesForMainTheme({
resourcesDirPath,
themeName,
buildContext
@ -36,7 +36,8 @@ export async function generateResources(params: {
generateResourcesForThemeVariant({
resourcesDirPath,
themeName,
themeVariantName
themeVariantName,
writeMessagePropertiesFiles
});
}
}

View File

@ -17,7 +17,10 @@ import type { BuildContext } from "../../shared/buildContext";
import { assert, type Equals } from "tsafe/assert";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
import {
generateMessageProperties,
type BuildContextLike as BuildContextLike_generateMessageProperties
} from "./generateMessageProperties";
import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import {
@ -29,24 +32,30 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
import * as child_process from "child_process";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & {
extraThemeProperties: string[] | undefined;
projectDirPath: string;
projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
themeSrcDirPath: string;
bundler: "vite" | "webpack";
packageJsonFilePath: string;
};
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_generateMessageProperties & {
extraThemeProperties: string[] | undefined;
projectDirPath: string;
projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
themeSrcDirPath: string;
bundler: "vite" | "webpack";
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateResourcesForMainTheme(params: {
buildContext: BuildContextLike;
themeName: string;
resourcesDirPath: string;
buildContext: BuildContextLike;
}): Promise<void> {
}): Promise<{
writeMessagePropertiesFiles: (params: {
getMessageDirPath: (params: { themeType: ThemeType }) => string;
themeName: string;
}) => void;
}> {
const { themeName, resourcesDirPath, buildContext } = params;
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
@ -54,6 +63,10 @@ export async function generateResourcesForMainTheme(params: {
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
};
const writeMessagePropertiesFilesByThemeType: Partial<
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
> = {};
for (const themeType of ["login", "account"] as const) {
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
continue;
@ -187,30 +200,27 @@ export async function generateResourcesForMainTheme(params: {
);
});
let languageTags: string[] | undefined = undefined;
i18n_messages_generation: {
if (isForAccountSpa) {
break i18n_messages_generation;
}
generateMessageProperties({
themeSrcDirPath: buildContext.themeSrcDirPath,
const wrap = generateMessageProperties({
buildContext,
themeType
}).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
});
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
recursive: true
});
languageTags = wrap.languageTags;
const { writeMessagePropertiesFiles } = wrap;
const propertiesFilePath = pathJoin(
messagesDirPath,
`messages_${languageTag}.properties`
);
writeMessagePropertiesFilesByThemeType[themeType] =
writeMessagePropertiesFiles;
fs.writeFileSync(
propertiesFilePath,
Buffer.from(propertiesFileSource, "utf8")
);
writeMessagePropertiesFiles({
messageDirPath: pathJoin(themeTypeDirPath, "messages"),
themeName
});
}
@ -280,7 +290,10 @@ export async function generateResourcesForMainTheme(params: {
...(buildContext.extraThemeProperties ?? []),
...buildContext.environmentVariables.map(
({ name, default: defaultValue }) =>
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`,
...(languageTags === undefined
? []
: `locales=${languageTags.join(",")}`)
)
].join("\n\n"),
"utf8"
@ -338,4 +351,20 @@ export async function generateResourcesForMainTheme(params: {
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
});
}
return {
writeMessagePropertiesFiles: ({ getMessageDirPath, themeName }) => {
objectEntries(writeMessagePropertiesFilesByThemeType).forEach(
([themeType, writeMessagePropertiesFiles]) => {
if (writeMessagePropertiesFiles === undefined) {
return;
}
writeMessagePropertiesFiles({
messageDirPath: getMessageDirPath({ themeType }),
themeName
});
}
);
}
};
}

View File

@ -1,27 +1,27 @@
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildContext } from "../../shared/buildContext";
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert";
export type BuildContextLike = {
keycloakifyBuildDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
import type { ThemeType } from "../../shared/constants";
export function generateResourcesForThemeVariant(params: {
resourcesDirPath: string;
themeName: string;
themeVariantName: string;
writeMessagePropertiesFiles: (params: {
getMessageDirPath: (params: { themeType: ThemeType }) => string;
themeName: string;
}) => void;
}) {
const { resourcesDirPath, themeName, themeVariantName } = params;
const { resourcesDirPath, themeName, themeVariantName, writeMessagePropertiesFiles } =
params;
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
const themeVariantDirPath = pathJoin(mainThemeDirPath, "..", themeVariantName);
transformCodebase({
srcDirPath: mainThemeDirPath,
destDirPath: pathJoin(mainThemeDirPath, "..", themeVariantName),
destDirPath: themeVariantDirPath,
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (
pathExtname(fileRelativePath) === ".ftl" &&
@ -67,4 +67,10 @@ export function generateResourcesForThemeVariant(params: {
return newMetaInfKeycloakTheme;
}
});
writeMessagePropertiesFiles({
getMessageDirPath: ({ themeType }) =>
pathJoin(themeVariantDirPath, themeType, "messages"),
themeName: themeVariantName
});
}