Files
ThreadNet-Web/apps/web/I18nWebpackPlugin.ts
T
Michael TelatynskiandGitHub 4aa1a3549e Enable oxlint restriction ruleset (#34307)
* Remove stale max-len disablements

* Remove stale camelCase & naming-convention disablements

* Remove stale ban-ts-comment disablements

* Remove stale no-var disablements

* Remove stale no-empty-property disablements

* Remove stale react rule disablements

* Remove stale no-constant-condition disablements

* Remove stale no-unused-vars disablements

* Remove stale disablements for disabled rules

* fixup camelcase

* Remove dead code

* Tidy code

* Tweak oxlint config

* Use oxlint to apply jsx/tsx extension consistently

* Fix import

* Fix imports

* Rename affected snapshots

* Update more imports

* Enable restriction ruleset

* Make code comply with new rules

* Make code comply with react/button-has-type

* Make code comply with typescript/non-nullable-type-assertion-style

* Comply with node/no-process-env

* Comply with unicorn/prefer-node-protocol

* Comply with unicorn/import-style

* Comply with unicorn/no-process-exit

* Comply with no-proto

* Comply with node/handle-callback-err

* Comply with import/no-commonjs

* Comply with node/no-path-concat

* Comply with unicorn/no-length-as-slice-end

* Comply with unicorn/no-document-cookie

* Comply with unicorn/prefer-module

* Comply with typescript/prefer-literal-enum-member

* Comply with jsx-a11y/anchor-ambiguous-text

* Tweak oxlint config

* Fix resolves

* Iterate

* Iterate

* Iterate

* Iterate

* Iterate

* Iterate

* Iterate
2026-08-04 09:15:07 +00:00

124 lines
5.0 KiB
TypeScript

/*
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 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");
},
);
});
}
}