2024-07-13 19:33:59 +02:00
|
|
|
import { type ThemeType, FALLBACK_LANGUAGE_TAG } from "../../shared/constants";
|
2023-07-24 00:49:12 +02:00
|
|
|
import { crawl } from "../../tools/crawl";
|
|
|
|
import { join as pathJoin } from "path";
|
|
|
|
import { symToStr } from "tsafe/symToStr";
|
|
|
|
import * as recast from "recast";
|
|
|
|
import * as babelParser from "@babel/parser";
|
|
|
|
import babelGenerate from "@babel/generator";
|
|
|
|
import * as babelTypes from "@babel/types";
|
2024-06-08 14:02:07 +02:00
|
|
|
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
2024-06-21 02:13:31 +02:00
|
|
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
|
|
|
import * as fs from "fs";
|
2024-06-22 20:12:02 +02:00
|
|
|
import { assert } from "tsafe/assert";
|
2023-07-24 00:49:12 +02:00
|
|
|
|
|
|
|
export function generateMessageProperties(params: {
|
|
|
|
themeSrcDirPath: string;
|
|
|
|
themeType: ThemeType;
|
|
|
|
}): { languageTag: string; propertiesFileSource: string }[] {
|
|
|
|
const { themeSrcDirPath, themeType } = params;
|
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
const baseMessagesDirPath = pathJoin(
|
|
|
|
getThisCodebaseRootDirPath(),
|
|
|
|
"src",
|
|
|
|
themeType,
|
|
|
|
"i18n",
|
2024-07-13 09:07:11 +02:00
|
|
|
"messages_defaultSet"
|
2024-06-22 20:12:02 +02:00
|
|
|
);
|
2023-07-24 00:49:12 +02:00
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
const baseMessageBundle: { [languageTag: string]: Record<string, string> } =
|
|
|
|
Object.fromEntries(
|
|
|
|
fs
|
|
|
|
.readdirSync(baseMessagesDirPath)
|
2024-09-21 04:36:48 +02:00
|
|
|
.filter(
|
|
|
|
basename => basename !== "index.ts" && basename !== "LanguageTag.ts"
|
|
|
|
)
|
2024-06-22 20:12:02 +02:00
|
|
|
.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;
|
|
|
|
}
|
2023-07-24 00:49:12 +02:00
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
const messages = JSON.parse(messagesJson) as Record<string, string>;
|
2023-07-24 00:49:12 +02:00
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
return [languageTag, messages];
|
|
|
|
})
|
|
|
|
);
|
2023-07-24 00:49:12 +02:00
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
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;
|
|
|
|
});
|
2023-07-24 00:49:12 +02:00
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
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"), {
|
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
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
let messageBundleDeclarationTsCode: string | undefined = undefined;
|
2023-07-24 00:49:12 +02:00
|
|
|
|
|
|
|
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"
|
|
|
|
) {
|
2024-06-22 20:12:02 +02:00
|
|
|
messageBundleDeclarationTsCode = babelGenerate(
|
|
|
|
path.node.arguments[0] as any
|
|
|
|
).code;
|
|
|
|
return false;
|
2023-07-24 00:49:12 +02:00
|
|
|
}
|
2024-06-22 20:12:02 +02:00
|
|
|
|
2023-07-24 00:49:12 +02:00
|
|
|
this.traverse(path);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
assert(messageBundleDeclarationTsCode !== undefined);
|
|
|
|
|
2024-06-22 17:05:14 +02:00
|
|
|
let messageBundle: {
|
2024-05-20 15:48:51 +02:00
|
|
|
[languageTag: string]: Record<string, string>;
|
|
|
|
} = {};
|
2023-07-24 00:49:12 +02:00
|
|
|
|
|
|
|
try {
|
2024-06-22 20:12:02 +02:00
|
|
|
eval(
|
|
|
|
`${symToStr({ messageBundle })} = ${messageBundleDeclarationTsCode}`
|
|
|
|
);
|
2023-07-24 00:49:12 +02:00
|
|
|
} catch {
|
|
|
|
console.warn(
|
|
|
|
[
|
2024-06-22 20:12:02 +02:00
|
|
|
"WARNING: Make sure the messageBundle your provided as argument of createUseI18n can be statically evaluated.",
|
2023-07-24 00:49:12 +02:00
|
|
|
"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",
|
2024-06-22 20:12:02 +02:00
|
|
|
messageBundleDeclarationTsCode
|
2023-07-24 00:49:12 +02:00
|
|
|
].join(" ")
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-06-22 17:05:14 +02:00
|
|
|
return messageBundle;
|
2024-06-22 20:12:02 +02:00
|
|
|
})();
|
|
|
|
|
|
|
|
const mergedMessageBundle: { [languageTag: string]: Record<string, string> } =
|
|
|
|
Object.fromEntries(
|
|
|
|
Object.entries(baseMessageBundle).map(([languageTag, messages]) => [
|
|
|
|
languageTag,
|
|
|
|
{
|
|
|
|
...messages,
|
|
|
|
...(messageBundle === undefined
|
|
|
|
? {}
|
|
|
|
: messageBundle[languageTag] ??
|
2024-07-13 19:33:59 +02:00
|
|
|
messageBundle[FALLBACK_LANGUAGE_TAG] ??
|
2024-06-22 20:12:02 +02:00
|
|
|
messageBundle[Object.keys(messageBundle)[0]] ??
|
|
|
|
{})
|
2023-07-24 00:49:12 +02:00
|
|
|
}
|
2024-06-22 20:12:02 +02:00
|
|
|
])
|
2024-06-22 17:05:14 +02:00
|
|
|
);
|
2023-07-24 00:49:12 +02:00
|
|
|
|
2024-06-22 20:12:02 +02:00
|
|
|
const messageProperties: { languageTag: string; propertiesFileSource: string }[] =
|
|
|
|
Object.entries(mergedMessageBundle).map(([languageTag, messages]) => ({
|
2023-07-24 00:49:12 +02:00
|
|
|
languageTag,
|
2024-06-22 20:12:02 +02:00
|
|
|
propertiesFileSource: [
|
|
|
|
"",
|
|
|
|
...(themeType !== "account" ? ["parent=base"] : []),
|
|
|
|
...Object.entries(messages).map(
|
|
|
|
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
|
|
|
|
),
|
|
|
|
""
|
|
|
|
].join("\n")
|
|
|
|
}));
|
|
|
|
|
|
|
|
return messageProperties;
|
2023-07-24 00:49:12 +02:00
|
|
|
}
|