2024-05-15 05:14:01 +02:00
|
|
|
import type { ThemeType } from "../../shared/constants";
|
2023-07-24 00:49:12 +02:00
|
|
|
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";
|
|
|
|
import * as babelTypes from "@babel/types";
|
|
|
|
|
|
|
|
export function generateMessageProperties(params: {
|
|
|
|
themeSrcDirPath: string;
|
|
|
|
themeType: ThemeType;
|
|
|
|
}): { languageTag: string; propertiesFileSource: string }[] {
|
|
|
|
const { themeSrcDirPath, themeType } = params;
|
|
|
|
|
|
|
|
let files = crawl({
|
2024-05-20 15:48:51 +02:00
|
|
|
dirPath: pathJoin(themeSrcDirPath, themeType),
|
|
|
|
returnedPathsType: "absolute"
|
2023-07-24 00:49:12 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
2024-05-20 15:48:51 +02:00
|
|
|
files = files.filter(file =>
|
|
|
|
readFileSync(file).toString("utf8").includes("createUseI18n")
|
|
|
|
);
|
2023-07-24 00:49:12 +02:00
|
|
|
|
|
|
|
if (files.length === 0) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const extraMessages = files
|
|
|
|
.map(file => {
|
|
|
|
const root = recast.parse(readFileSync(file).toString("utf8"), {
|
2024-05-20 15:48:51 +02:00
|
|
|
parser: {
|
|
|
|
parse: (code: string) =>
|
|
|
|
babelParser.parse(code, {
|
|
|
|
sourceType: "module",
|
|
|
|
plugins: ["typescript"]
|
|
|
|
}),
|
|
|
|
generator: babelGenerate,
|
|
|
|
types: babelTypes
|
2023-07-24 00:49:12 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const codes: string[] = [];
|
|
|
|
|
|
|
|
recast.visit(root, {
|
2024-05-20 15:48:51 +02:00
|
|
|
visitCallExpression: function (path) {
|
|
|
|
if (
|
|
|
|
path.node.callee.type === "Identifier" &&
|
|
|
|
path.node.callee.name === "createUseI18n"
|
|
|
|
) {
|
2023-07-24 00:49:12 +02:00
|
|
|
codes.push(babelGenerate(path.node.arguments[0] as any).code);
|
|
|
|
}
|
|
|
|
this.traverse(path);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return codes;
|
|
|
|
})
|
|
|
|
.flat()
|
|
|
|
.map(code => {
|
2024-05-20 15:48:51 +02:00
|
|
|
let extraMessages: {
|
|
|
|
[languageTag: string]: Record<string, string>;
|
|
|
|
} = {};
|
2023-07-24 00:49:12 +02:00
|
|
|
|
|
|
|
try {
|
|
|
|
eval(`${symToStr({ extraMessages })} = ${code}`);
|
|
|
|
} 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.",
|
|
|
|
"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
|
|
|
|
].join(" ")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return extraMessages;
|
|
|
|
});
|
|
|
|
|
|
|
|
const languageTags = extraMessages
|
|
|
|
.map(extraMessage => Object.keys(extraMessage))
|
|
|
|
.flat()
|
|
|
|
.reduce(...removeDuplicates<string>());
|
|
|
|
|
|
|
|
const keyValueMapByLanguageTag: Record<string, Record<string, string>> = {};
|
|
|
|
|
|
|
|
for (const languageTag of languageTags) {
|
|
|
|
const keyValueMap: Record<string, string> = {};
|
|
|
|
|
|
|
|
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 (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;
|
|
|
|
}
|
|
|
|
|
|
|
|
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}=${escapeString(value)}`)
|
|
|
|
.join("\n");
|
|
|
|
|
|
|
|
out.push({
|
|
|
|
languageTag,
|
2024-05-20 15:48:51 +02:00
|
|
|
propertiesFileSource: [
|
|
|
|
"# This file was generated by keycloakify",
|
|
|
|
"",
|
|
|
|
"parent=base",
|
|
|
|
"",
|
|
|
|
propertiesFileSource,
|
|
|
|
""
|
|
|
|
].join("\n")
|
2023-07-24 00:49:12 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return out;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Convert a JavaScript string to UTF-16 encoding
|
|
|
|
function toUTF16(codePoint: number): string {
|
|
|
|
if (codePoint <= 0xffff) {
|
|
|
|
// BMP character
|
|
|
|
return "\\u" + codePoint.toString(16).padStart(4, "0");
|
|
|
|
} else {
|
|
|
|
// Non-BMP character
|
|
|
|
codePoint -= 0x10000;
|
|
|
|
let highSurrogate = (codePoint >> 10) + 0xd800;
|
|
|
|
let lowSurrogate = (codePoint % 0x400) + 0xdc00;
|
2024-05-20 15:48:51 +02:00
|
|
|
return (
|
|
|
|
"\\u" +
|
|
|
|
highSurrogate.toString(16).padStart(4, "0") +
|
|
|
|
"\\u" +
|
|
|
|
lowSurrogate.toString(16).padStart(4, "0")
|
|
|
|
);
|
2023-07-24 00:49:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-05-27 17:18:06 +02:00
|
|
|
// Escapes special characters for use in a .properties file
|
2023-07-24 00:49:12 +02:00
|
|
|
function escapeString(str: string): string {
|
|
|
|
let escapedStr = "";
|
|
|
|
for (const char of [...str]) {
|
|
|
|
const codePoint = char.codePointAt(0);
|
|
|
|
if (!codePoint) continue;
|
2024-05-27 17:18:06 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
}
|
2023-07-24 00:49:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return escapedStr;
|
|
|
|
}
|