Rewrite copy-res i18n build as a webpack plugin (#32664)
* Replace copy-res with a webpack plugin Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Specify writeToDisk=true for webpack-dev-server Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Use async fs methods Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import webpack from "webpack";
|
||||
import * as fs from "node:fs/promises";
|
||||
import * as path from "node:path";
|
||||
import _ from "lodash";
|
||||
import { type Translations } from "matrix-web-i18n";
|
||||
|
||||
interface Options {
|
||||
// Path to the strings for the application, will be used to deduce what languages are supported
|
||||
stringsPath: string;
|
||||
// Additional paths to strings which will be merged into the application's own strings for supported languages
|
||||
additionalStringsPaths?: string[];
|
||||
}
|
||||
|
||||
async function exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class I18nWebpackPlugin {
|
||||
private readonly options: Options;
|
||||
|
||||
public constructor(options: Options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public apply(compiler: webpack.Compiler): void {
|
||||
const { RawSource } = compiler.webpack.sources;
|
||||
|
||||
compiler.hooks.thisCompilation.tap("I18nWebpackPlugin", (compilation) => {
|
||||
compilation.hooks.processAssets.tapPromise(
|
||||
{
|
||||
name: "I18nWebpackPlugin",
|
||||
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS,
|
||||
},
|
||||
async () => {
|
||||
const paths = [this.options.stringsPath, ...(this.options.additionalStringsPaths ?? [])].map((p) =>
|
||||
path.resolve(compiler.context, p),
|
||||
);
|
||||
|
||||
const logger = compilation.getLogger("I18nWebpackPlugin");
|
||||
|
||||
for (const p of paths) {
|
||||
compilation.contextDependencies.add(p);
|
||||
|
||||
if (!(await exists(p))) {
|
||||
compilation.errors.push(
|
||||
new webpack.WebpackError(`I18nWebpackPlugin: strings path not found: ${p}`),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const primaryPath = paths[0];
|
||||
const includeLangs = [...new Set([...(await fs.readdir(primaryPath))])]
|
||||
.filter((fn) => fn.endsWith(".json"))
|
||||
.map((f) => f.slice(0, -5));
|
||||
|
||||
const langFileMap: Record<string, string> = {};
|
||||
|
||||
for (const lang of includeLangs) {
|
||||
let translations: Translations = {};
|
||||
for (const p of paths) {
|
||||
const f = path.join(p, lang + ".json");
|
||||
|
||||
if (await exists(f)) {
|
||||
try {
|
||||
const content = await fs.readFile(f, "utf-8");
|
||||
translations = _.merge(translations, JSON.parse(content));
|
||||
compilation.fileDependencies.add(f);
|
||||
} catch (e) {
|
||||
compilation.errors.push(
|
||||
new webpack.WebpackError(
|
||||
`I18nWebpackPlugin: Failed to read or parse ${f}: ${e}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const json = JSON.stringify(translations, null, 4);
|
||||
const jsonBuffer = Buffer.from(json);
|
||||
const digest = compiler.webpack.util
|
||||
.createHash("xxhash64")
|
||||
.update(jsonBuffer)
|
||||
.digest("hex")
|
||||
.slice(0, 7);
|
||||
const filename = `${lang}.${digest}.json`;
|
||||
|
||||
compilation.emitAsset(`i18n/${filename}`, new RawSource(jsonBuffer));
|
||||
langFileMap[lang] = filename;
|
||||
logger.debug(`Generated language file: ${filename}`);
|
||||
}
|
||||
|
||||
// Generate languages.json
|
||||
const languages: Record<string, string> = {};
|
||||
includeLangs.forEach((lang) => {
|
||||
const normalizedLanguage = lang.toLowerCase().replace("_", "-");
|
||||
const languageParts = normalizedLanguage.split("-");
|
||||
if (languageParts.length == 2 && languageParts[0] == languageParts[1]) {
|
||||
languages[languageParts[0]] = langFileMap[lang];
|
||||
} else {
|
||||
languages[normalizedLanguage] = langFileMap[lang];
|
||||
}
|
||||
});
|
||||
|
||||
compilation.emitAsset("i18n/languages.json", new RawSource(JSON.stringify(languages, null, 4)));
|
||||
logger.info("Generated languages.json and language files");
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user