Files
Loïc GuitautandLoïc Guitaut 4e432c19d2 DEV: Upgrade the MessageFormat library (JS)
This patch upgrades the MessageFormat library to version 3.3.0 from
0.1.5.

Our `I18n.messageFormat` method signature is unchanged, and now uses the
new API under the hood.

We don’t need dedicated locale files for handling pluralization rules
anymore as everything is now included by the library itself.

The compilation of the messages now happens through our
`messageformat-wrapper` gem. It then outputs an ES module that includes
all its needed dependencies.

Most of the changes happen in `JsLocaleHelper` and in the `ExtraLocales`
controller.

A new method called `.output_MF` has been introduced in
`JsLocaleHelper`. It handles all the fetching, compiling and
transpiling to generate the proper MF messages in JS. Overrides and
fallbacks are also handled directly in this method.

The other main change is that now the MF translations are served through
the `ExtraLocales` controller instead of being statically compiled in a
JS file, then having to patch the messages using overrides and
fallbacks. Now the MF translations are just another bundle that is
created on the fly and cached by the client.
2024-07-10 09:51:25 +02:00

138 lines
3.7 KiB
JavaScript

const Plugin = require("broccoli-plugin");
const Yaml = require("js-yaml");
const fs = require("fs");
const concat = require("broccoli-concat");
const mergeTrees = require("broccoli-merge-trees");
const MessageFormat = require("@messageformat/core");
const deepmerge = require("deepmerge");
const glob = require("glob");
const { shouldLoadPlugins } = require("discourse-plugins");
let built = false;
class TranslationPlugin extends Plugin {
constructor(inputNodes, inputFile, options) {
super(inputNodes, {
...options,
persistentOutput: true,
});
this.inputFile = inputFile;
}
replaceMF(formats, input, path = []) {
if (!input) {
return;
}
Object.keys(input).forEach((key) => {
let value = input[key];
let subpath = path.concat(key);
if (typeof value === "object") {
this.replaceMF(formats, value, subpath);
} else if (key.endsWith("_MF")) {
// omit locale.js
let mfPath = subpath.slice(2).join(".");
formats[mfPath] = this.mf.compile(value);
}
});
}
build() {
// We could get fancy eventually and do this based on whether the yaml
// or vendor files change but in practice we shouldn't need exact up to date
// translations in admin.
if (built) {
return;
}
let parsed = {};
this.inputPaths.forEach((path) => {
let file = path + "/" + this.inputFile;
let yaml = fs.readFileSync(file, { encoding: "UTF-8" });
let loaded = Yaml.load(yaml, { json: true });
parsed = deepmerge(parsed, loaded);
});
let extras = {
en: {
admin: parsed.en.admin_js.admin,
wizard: parsed.en.wizard_js.wizard,
},
};
delete parsed.en.admin_js;
delete parsed.en.wizard_js;
let formats = {};
this.mf = new MessageFormat("en");
this.replaceMF(formats, parsed);
this.replaceMF(formats, extras);
formats = Object.entries(formats).map(([k, v]) => `"${k}": ${v}`);
let contents = `
(function() {
I18n.locale = 'en';
I18n.translations = ${JSON.stringify(parsed)};
I18n.extras = ${JSON.stringify(extras)};
const Messages = require("@messageformat/runtime/messages").default;
const { number, plural, select } = require("@messageformat/runtime");
const { en } = require("@messageformat/runtime/lib/cardinals");
const msgData = { en: { ${formats.join(",\n")} } };
const messages = new Messages(msgData, "en");
messages.defaultLocale = "en";
I18n._mfMessages = messages;
})()
`;
fs.writeFileSync(
`${this.outputPath}/` + this.inputFile.replace(".yml", ".js"),
contents
);
built = true;
}
}
module.exports = function translatePlugin(...params) {
return new TranslationPlugin(...params);
};
module.exports.createI18nTree = function (discourseRoot, vendorJs) {
let translations = [discourseRoot + "/config/locales"];
if (shouldLoadPlugins()) {
translations = translations.concat(
glob
.sync(discourseRoot + "/plugins/*/config/locales/client.en.yml")
.map((f) => f.replace(/\/client\.en\.yml$/, ""))
);
}
let en = new TranslationPlugin(translations, "client.en.yml");
return concat(
mergeTrees([
vendorJs,
discourseRoot + "/app/assets/javascripts/locales",
en,
]),
{
inputFiles: [
"i18n.js",
"moment.js",
"moment-timezone-with-data.js",
"client.en.js",
],
headerFiles: ["i18n.js", "moment.js", "moment-timezone-with-data.js"],
footerFiles: ["client.en.js"],
outputFile: `assets/test-i18n.js`,
}
);
};
module.exports.TranslationPlugin = TranslationPlugin;