From 301713ef9631e52975781b60b8b4c4a0d75530cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Guitaut?= Date: Mon, 17 Jun 2024 18:21:04 +0200 Subject: [PATCH] DEV: Upgrade the MessageFormat library (JS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Gemfile | 1 + Gemfile.lock | 5 +- .../javascripts/discourse-i18n/package.json | 3 +- .../javascripts/discourse-i18n/src/index.js | 38 ++- .../app/instance-initializers/localization.js | 5 - .../javascripts/discourse/app/loader-shims.js | 9 + .../discourse/lib/translation-plugin.js | 34 +-- app/assets/javascripts/discourse/package.json | 8 +- .../discourse/tests/unit/lib/i18n-test.js | 57 +++- .../discourse/tests/unit/localization-test.js | 23 -- app/controllers/extra_locales_controller.rb | 107 ++++---- app/models/translation_override.rb | 12 +- app/views/layouts/application.html.erb | 5 +- app/views/qunit/theme.html.erb | 3 +- lib/discourse.rb | 1 - lib/javascripts/locale/af.js | 6 - lib/javascripts/locale/am.js | 6 - lib/javascripts/locale/ar.js | 18 -- lib/javascripts/locale/be.js | 14 - lib/javascripts/locale/bg.js | 6 - lib/javascripts/locale/bn.js | 6 - lib/javascripts/locale/br.js | 18 -- lib/javascripts/locale/bs.js | 10 - lib/javascripts/locale/ca.js | 6 - lib/javascripts/locale/cs.js | 9 - lib/javascripts/locale/cy.js | 18 -- lib/javascripts/locale/da.js | 6 - lib/javascripts/locale/de.js | 6 - lib/javascripts/locale/el.js | 6 - lib/javascripts/locale/en.js | 6 - lib/javascripts/locale/es.js | 6 - lib/javascripts/locale/et.js | 6 - lib/javascripts/locale/eu.js | 6 - lib/javascripts/locale/fa.js | 3 - lib/javascripts/locale/fa_IR.js | 3 - lib/javascripts/locale/fi.js | 6 - lib/javascripts/locale/fil.js | 6 - lib/javascripts/locale/fr.js | 6 - lib/javascripts/locale/ga.js | 9 - lib/javascripts/locale/gl.js | 6 - lib/javascripts/locale/gsw.js | 6 - lib/javascripts/locale/gu.js | 6 - lib/javascripts/locale/he.js | 6 - lib/javascripts/locale/hi.js | 6 - lib/javascripts/locale/hr.js | 10 - lib/javascripts/locale/hu.js | 3 - lib/javascripts/locale/hy.js | 6 - lib/javascripts/locale/id.js | 3 - lib/javascripts/locale/in.js | 3 - lib/javascripts/locale/is.js | 6 - lib/javascripts/locale/it.js | 6 - lib/javascripts/locale/iw.js | 6 - lib/javascripts/locale/ja.js | 3 - lib/javascripts/locale/kn.js | 3 - lib/javascripts/locale/ko.js | 3 - lib/javascripts/locale/lag.js | 9 - lib/javascripts/locale/ln.js | 6 - lib/javascripts/locale/lt.js | 10 - lib/javascripts/locale/lv.js | 9 - lib/javascripts/locale/mk.js | 6 - lib/javascripts/locale/ml.js | 6 - lib/javascripts/locale/mo.js | 10 - lib/javascripts/locale/mr.js | 6 - lib/javascripts/locale/ms.js | 3 - lib/javascripts/locale/mt.js | 12 - lib/javascripts/locale/nl.js | 6 - lib/javascripts/locale/no.js | 6 - lib/javascripts/locale/or.js | 6 - lib/javascripts/locale/pl_PL.js | 15 - lib/javascripts/locale/pt.js | 6 - lib/javascripts/locale/pt_BR.js | 6 - lib/javascripts/locale/ro.js | 10 - lib/javascripts/locale/ru.js | 16 -- lib/javascripts/locale/shi.js | 9 - lib/javascripts/locale/sk.js | 9 - lib/javascripts/locale/sl.js | 12 - lib/javascripts/locale/sq.js | 6 - lib/javascripts/locale/sr.js | 11 - lib/javascripts/locale/sv.js | 6 - lib/javascripts/locale/sw.js | 6 - lib/javascripts/locale/ta.js | 6 - lib/javascripts/locale/te.js | 6 - lib/javascripts/locale/th.js | 3 - lib/javascripts/locale/tl.js | 6 - lib/javascripts/locale/tr.js | 3 - lib/javascripts/locale/tr_TR.js | 3 - lib/javascripts/locale/ug.js | 6 - lib/javascripts/locale/uk.js | 15 - lib/javascripts/locale/ur.js | 6 - lib/javascripts/locale/vi.js | 3 - lib/javascripts/locale/zh.js | 3 - lib/javascripts/locale/zh_CN.js | 3 - lib/javascripts/locale/zh_TW.js | 3 - lib/javascripts/messageformat-lookup.js | 14 - lib/js_locale_helper.rb | 144 ++++------ lib/plugin/instance.rb | 10 +- lib/tasks/qunit.rake | 1 - package.json | 3 +- spec/lib/js_locale_helper_spec.rb | 258 ++++++------------ spec/lib/plugin/instance_spec.rb | 19 -- spec/models/translation_override_spec.rb | 14 - .../requests/extra_locales_controller_spec.rb | 31 ++- yarn.lock | 127 ++++----- 103 files changed, 400 insertions(+), 1079 deletions(-) delete mode 100644 lib/javascripts/locale/af.js delete mode 100644 lib/javascripts/locale/am.js delete mode 100644 lib/javascripts/locale/ar.js delete mode 100644 lib/javascripts/locale/be.js delete mode 100644 lib/javascripts/locale/bg.js delete mode 100644 lib/javascripts/locale/bn.js delete mode 100644 lib/javascripts/locale/br.js delete mode 100644 lib/javascripts/locale/bs.js delete mode 100644 lib/javascripts/locale/ca.js delete mode 100644 lib/javascripts/locale/cs.js delete mode 100644 lib/javascripts/locale/cy.js delete mode 100644 lib/javascripts/locale/da.js delete mode 100644 lib/javascripts/locale/de.js delete mode 100644 lib/javascripts/locale/el.js delete mode 100644 lib/javascripts/locale/en.js delete mode 100644 lib/javascripts/locale/es.js delete mode 100644 lib/javascripts/locale/et.js delete mode 100644 lib/javascripts/locale/eu.js delete mode 100644 lib/javascripts/locale/fa.js delete mode 100644 lib/javascripts/locale/fa_IR.js delete mode 100644 lib/javascripts/locale/fi.js delete mode 100644 lib/javascripts/locale/fil.js delete mode 100644 lib/javascripts/locale/fr.js delete mode 100644 lib/javascripts/locale/ga.js delete mode 100644 lib/javascripts/locale/gl.js delete mode 100644 lib/javascripts/locale/gsw.js delete mode 100644 lib/javascripts/locale/gu.js delete mode 100644 lib/javascripts/locale/he.js delete mode 100644 lib/javascripts/locale/hi.js delete mode 100644 lib/javascripts/locale/hr.js delete mode 100644 lib/javascripts/locale/hu.js delete mode 100644 lib/javascripts/locale/hy.js delete mode 100644 lib/javascripts/locale/id.js delete mode 100644 lib/javascripts/locale/in.js delete mode 100644 lib/javascripts/locale/is.js delete mode 100644 lib/javascripts/locale/it.js delete mode 100644 lib/javascripts/locale/iw.js delete mode 100644 lib/javascripts/locale/ja.js delete mode 100644 lib/javascripts/locale/kn.js delete mode 100644 lib/javascripts/locale/ko.js delete mode 100644 lib/javascripts/locale/lag.js delete mode 100644 lib/javascripts/locale/ln.js delete mode 100644 lib/javascripts/locale/lt.js delete mode 100644 lib/javascripts/locale/lv.js delete mode 100644 lib/javascripts/locale/mk.js delete mode 100644 lib/javascripts/locale/ml.js delete mode 100644 lib/javascripts/locale/mo.js delete mode 100644 lib/javascripts/locale/mr.js delete mode 100644 lib/javascripts/locale/ms.js delete mode 100644 lib/javascripts/locale/mt.js delete mode 100644 lib/javascripts/locale/nl.js delete mode 100644 lib/javascripts/locale/no.js delete mode 100644 lib/javascripts/locale/or.js delete mode 100644 lib/javascripts/locale/pl_PL.js delete mode 100644 lib/javascripts/locale/pt.js delete mode 100644 lib/javascripts/locale/pt_BR.js delete mode 100644 lib/javascripts/locale/ro.js delete mode 100644 lib/javascripts/locale/ru.js delete mode 100644 lib/javascripts/locale/shi.js delete mode 100644 lib/javascripts/locale/sk.js delete mode 100644 lib/javascripts/locale/sl.js delete mode 100644 lib/javascripts/locale/sq.js delete mode 100644 lib/javascripts/locale/sr.js delete mode 100644 lib/javascripts/locale/sv.js delete mode 100644 lib/javascripts/locale/sw.js delete mode 100644 lib/javascripts/locale/ta.js delete mode 100644 lib/javascripts/locale/te.js delete mode 100644 lib/javascripts/locale/th.js delete mode 100644 lib/javascripts/locale/tl.js delete mode 100644 lib/javascripts/locale/tr.js delete mode 100644 lib/javascripts/locale/tr_TR.js delete mode 100644 lib/javascripts/locale/ug.js delete mode 100644 lib/javascripts/locale/uk.js delete mode 100644 lib/javascripts/locale/ur.js delete mode 100644 lib/javascripts/locale/vi.js delete mode 100644 lib/javascripts/locale/zh.js delete mode 100644 lib/javascripts/locale/zh_CN.js delete mode 100644 lib/javascripts/locale/zh_TW.js delete mode 100644 lib/javascripts/messageformat-lookup.js diff --git a/Gemfile b/Gemfile index 04df2b0b7ca..4aa1e315536 100644 --- a/Gemfile +++ b/Gemfile @@ -89,6 +89,7 @@ gem "mini_sql" gem "pry-rails", require: false gem "pry-byebug", require: false gem "rtlcss", require: false +gem "messageformat-wrapper", require: false gem "rake" gem "thor", require: false diff --git a/Gemfile.lock b/Gemfile.lock index 0c6c51fe87a..9cee28dcb07 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -234,6 +234,8 @@ GEM memory_profiler (1.0.2) message_bus (4.3.8) rack (>= 1.1.3) + messageformat-wrapper (1.0.0) + mini_racer (>= 0.6.3) method_source (1.1.0) mini_mime (1.1.5) mini_racer (0.9.0) @@ -646,6 +648,7 @@ DEPENDENCIES maxminddb memory_profiler message_bus + messageformat-wrapper mini_mime mini_racer mini_scheduler @@ -728,4 +731,4 @@ DEPENDENCIES yard BUNDLED WITH - 2.5.3 + 2.5.9 diff --git a/app/assets/javascripts/discourse-i18n/package.json b/app/assets/javascripts/discourse-i18n/package.json index d447e64e3d8..7456b248699 100644 --- a/app/assets/javascripts/discourse-i18n/package.json +++ b/app/assets/javascripts/discourse-i18n/package.json @@ -17,7 +17,8 @@ "src" ], "dependencies": { - "@embroider/addon-shim": "^1.8.9" + "@embroider/addon-shim": "^1.8.9", + "make-plural": "^7.4.0" }, "engines": { "node": ">= 18", diff --git a/app/assets/javascripts/discourse-i18n/src/index.js b/app/assets/javascripts/discourse-i18n/src/index.js index 650836ff59d..a9db4df2ff1 100644 --- a/app/assets/javascripts/discourse-i18n/src/index.js +++ b/app/assets/javascripts/discourse-i18n/src/index.js @@ -4,6 +4,8 @@ if (window.I18n) { ); } +import * as Cardinals from "make-plural/cardinals"; + // The placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`. const PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm; const SEPARATOR = "."; @@ -13,19 +15,14 @@ export class I18n { defaultLocale = "en"; // Set current locale to null - local = null; + locale = null; fallbackLocale = null; translations = null; extras = null; noFallbacks = false; testing = false; - // Set default pluralization rule - pluralizationRules = { - en(n) { - return n === 0 ? ["zero", "none", "other"] : n === 1 ? "one" : "other"; - }, - }; + pluralizationRules = Cardinals; translate = (scope, options) => this._translate(scope, options); @@ -36,6 +33,13 @@ export class I18n { return this.locale || this.defaultLocale; } + get pluralizationNormalizedLocale() { + if (this.currentLocale() === "pt") { + return "pt_PT"; + } + return this.currentLocale().replace(/[_-].*/, ""); + } + enableVerboseLocalization() { let counter = 0; let keys = {}; @@ -192,7 +196,9 @@ export class I18n { options = this.prepareOptions(options); let count = options.count.toString(); - let pluralizer = this.pluralizer(options.locale || this.currentLocale()); + let pluralizer = this.pluralizer( + options.locale || this.pluralizationNormalizedLocale + ); let key = pluralizer(Math.abs(count)); let keys = typeof key === "object" && key instanceof Array ? key : [key]; let message = this.findAndTranslateValidNode(keys, translation); @@ -371,6 +377,22 @@ export class I18n { isValidNode(obj, node) { return obj[node] !== null && obj[node] !== undefined; } + + messageFormat(key, options) { + const message = this._mfMessages.hasMessage( + key, + this._mfMessages.locale, + this._mfMessages.defaultLocale + ); + if (!message) { + return "Missing Key: " + key; + } + try { + return this._mfMessages.get(key, options); + } catch (err) { + return err.message; + } + } } export class I18nMissingInterpolationArgument extends Error { diff --git a/app/assets/javascripts/discourse/app/instance-initializers/localization.js b/app/assets/javascripts/discourse/app/instance-initializers/localization.js index 8d07056e2bb..c482d1dca14 100644 --- a/app/assets/javascripts/discourse/app/instance-initializers/localization.js +++ b/app/assets/javascripts/discourse/app/instance-initializers/localization.js @@ -39,10 +39,5 @@ export default { } } } - - for (let [key, value] of Object.entries(I18n._mfOverrides || {})) { - key = key.replace(/^[a-z_]*js\./, ""); - I18n._compiledMFs[key] = value; - } }, }; diff --git a/app/assets/javascripts/discourse/app/loader-shims.js b/app/assets/javascripts/discourse/app/loader-shims.js index f32488d69e9..035527ff118 100644 --- a/app/assets/javascripts/discourse/app/loader-shims.js +++ b/app/assets/javascripts/discourse/app/loader-shims.js @@ -64,3 +64,12 @@ loaderShim("truth-helpers/helpers/not", () => loaderShim("truth-helpers/helpers/or", () => importSync("truth-helpers/helpers/or") ); +loaderShim("@messageformat/runtime/messages", () => + importSync("@messageformat/runtime/messages") +); +loaderShim("@messageformat/runtime", () => + importSync("@messageformat/runtime") +); +loaderShim("@messageformat/runtime/lib/cardinals", () => + importSync("@messageformat/runtime/lib/cardinals") +); diff --git a/app/assets/javascripts/discourse/lib/translation-plugin.js b/app/assets/javascripts/discourse/lib/translation-plugin.js index 0adbf39613c..ef626bdf1be 100644 --- a/app/assets/javascripts/discourse/lib/translation-plugin.js +++ b/app/assets/javascripts/discourse/lib/translation-plugin.js @@ -3,7 +3,7 @@ const Yaml = require("js-yaml"); const fs = require("fs"); const concat = require("broccoli-concat"); const mergeTrees = require("broccoli-merge-trees"); -const MessageFormat = require("messageformat"); +const MessageFormat = require("@messageformat/core"); const deepmerge = require("deepmerge"); const glob = require("glob"); const { shouldLoadPlugins } = require("discourse-plugins"); @@ -34,7 +34,7 @@ class TranslationPlugin extends Plugin { } else if (key.endsWith("_MF")) { // omit locale.js let mfPath = subpath.slice(2).join("."); - formats[mfPath] = this.mf.precompile(this.mf.parse(value)); + formats[mfPath] = this.mf.compile(value); } }); } @@ -74,11 +74,19 @@ class TranslationPlugin extends Plugin { formats = Object.entries(formats).map(([k, v]) => `"${k}": ${v}`); let contents = ` - I18n.locale = 'en'; - I18n.translations = ${JSON.stringify(parsed)}; - I18n.extras = ${JSON.stringify(extras)}; - MessageFormat = { locale: {} }; - I18n._compiledMFs = { ${formats.join(",\n")} }; + (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( @@ -110,7 +118,6 @@ module.exports.createI18nTree = function (discourseRoot, vendorJs) { mergeTrees([ vendorJs, discourseRoot + "/app/assets/javascripts/locales", - discourseRoot + "/lib/javascripts", en, ]), { @@ -118,17 +125,10 @@ module.exports.createI18nTree = function (discourseRoot, vendorJs) { "i18n.js", "moment.js", "moment-timezone-with-data.js", - "messageformat-lookup.js", - "locale/en.js", "client.en.js", ], - headerFiles: [ - "i18n.js", - "moment.js", - "moment-timezone-with-data.js", - "messageformat-lookup.js", - ], - footerFiles: ["client.en.js", "locale/en.js"], + headerFiles: ["i18n.js", "moment.js", "moment-timezone-with-data.js"], + footerFiles: ["client.en.js"], outputFile: `assets/test-i18n.js`, } ); diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 924cc6dc419..5d47bdedc86 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -20,7 +20,9 @@ "@faker-js/faker": "^8.4.1", "@glimmer/syntax": "^0.92.0", "@highlightjs/cdn-assets": "^11.10.0", + "@messageformat/runtime": "^3.0.1", "ace-builds": "^1.35.2", + "decorator-transforms": "^2.0.0", "discourse-hbr": "1.0.0", "discourse-widget-hbs": "1.0.0", "ember-route-template": "^1.0.3", @@ -29,8 +31,7 @@ "highlight.js": "^11.10.0", "jspreadsheet-ce": "^4.13.4", "morphlex": "^0.0.16", - "pretty-text": "1.0.0", - "decorator-transforms": "^2.0.0" + "pretty-text": "1.0.0" }, "devDependencies": { "@babel/core": "^7.24.7", @@ -105,7 +106,6 @@ "js-yaml": "^4.1.0", "loader.js": "^4.7.0", "message-bus-client": "^4.3.8", - "messageformat": "0.1.5", "pretender": "^3.4.7", "qunit": "^2.21.0", "qunit-dom": "^3.2.0", @@ -129,4 +129,4 @@ "ember": { "edition": "octane" } -} \ No newline at end of file +} diff --git a/app/assets/javascripts/discourse/tests/unit/lib/i18n-test.js b/app/assets/javascripts/discourse/tests/unit/lib/i18n-test.js index 588fe01644c..a7314664fe7 100644 --- a/app/assets/javascripts/discourse/tests/unit/lib/i18n-test.js +++ b/app/assets/javascripts/discourse/tests/unit/lib/i18n-test.js @@ -72,6 +72,13 @@ module("Unit | Utility | i18n", function (hooks) { with_multiple_interpolate_arguments: "Hi %{username}, %{username2}", }, }, + ja: { + js: { + topic_stat_sentence_week: { + other: "先週、新しいトピックが %{count} 件投稿されました。", + }, + }, + }, }; // fake pluralization rules @@ -172,18 +179,6 @@ module("Unit | Utility | i18n", function (hooks) { }, }, }; - I18n.pluralizationRules.pl_PL = function (n) { - if (n === 1) { - return "one"; - } - if (n % 10 >= 2 && n % 10 <= 4) { - return "few"; - } - if (n % 10 === 0) { - return "many"; - } - return "other"; - }; assert.strictEqual( I18n.t("admin.dashboard.title"), @@ -218,6 +213,20 @@ module("Unit | Utility | i18n", function (hooks) { assert.strictEqual(I18n.t("word_count", { count: 3 }), "3 words"); assert.strictEqual(I18n.t("word_count", { count: 10 }), "10 words"); assert.strictEqual(I18n.t("word_count", { count: 100 }), "100 words"); + + I18n.locale = "ja"; + assert.strictEqual( + I18n.t("topic_stat_sentence_week", { count: 0 }), + "先週、新しいトピックが 0 件投稿されました。" + ); + assert.strictEqual( + I18n.t("topic_stat_sentence_week", { count: 1 }), + "先週、新しいトピックが 1 件投稿されました。" + ); + assert.strictEqual( + I18n.t("topic_stat_sentence_week", { count: 2 }), + "先週、新しいトピックが 2 件投稿されました。" + ); }); test("adds the count to the missing translation strings", function (assert) { @@ -323,4 +332,28 @@ module("Unit | Utility | i18n", function (hooks) { I18n.testing = false; } }); + + test("pluralizationNormalizedLocale", function (assert) { + I18n.locale = "pt"; + + assert.strictEqual( + I18n.pluralizationNormalizedLocale, + "pt_PT", + "returns 'pt_PT' for the 'pt' locale, this is a special case of the 'make-plural' lib." + ); + + Object.entries({ + pt_BR: "pt", + en_GB: "en", + bs_BA: "bs", + "fr-BE": "fr", + }).forEach(([raw, normalized]) => { + I18n.locale = raw; + assert.strictEqual( + I18n.pluralizationNormalizedLocale, + normalized, + `returns '${normalized}' for '${raw}'` + ); + }); + }); }); diff --git a/app/assets/javascripts/discourse/tests/unit/localization-test.js b/app/assets/javascripts/discourse/tests/unit/localization-test.js index 153fcb62ac9..566f6c1a6b8 100644 --- a/app/assets/javascripts/discourse/tests/unit/localization-test.js +++ b/app/assets/javascripts/discourse/tests/unit/localization-test.js @@ -10,9 +10,7 @@ module("initializer:localization", function (hooks) { this._locale = I18n.locale; this._translations = I18n.translations; this._extras = I18n.extras; - this._compiledMFs = I18n._compiledMFs; this._overrides = I18n._overrides; - this._mfOverrides = I18n._mfOverrides; I18n.locale = "fr"; @@ -37,10 +35,6 @@ module("initializer:localization", function (hooks) { }, }; - I18n._compiledMFs = { - "user.messages.some_key_MF": () => "user.messages.some_key_MF (FR)", - }; - I18n.extras = { fr: { admin: { @@ -67,9 +61,7 @@ module("initializer:localization", function (hooks) { I18n.locale = this._locale; I18n.translations = this._translations; I18n.extras = this._extras; - I18n._compiledMFs = this._compiledMFs; I18n._overrides = this._overrides; - I18n._mfOverrides = this._mfOverrides; }); test("translation overrides", function (assert) { @@ -159,21 +151,6 @@ module("initializer:localization", function (hooks) { ); }); - test("translation overrides for MessageFormat strings", function (assert) { - I18n._mfOverrides = { - "js.user.messages.some_key_MF": () => - "user.messages.some_key_MF (FR override)", - }; - - LocalizationInitializer.initialize(this.owner); - - assert.strictEqual( - I18n.messageFormat("user.messages.some_key_MF", {}), - "user.messages.some_key_MF (FR override)", - "overrides existing MessageFormat string" - ); - }); - test("skip translation override if parent node is not an object", function (assert) { I18n._overrides = { fr: { diff --git a/app/controllers/extra_locales_controller.rb b/app/controllers/extra_locales_controller.rb index 58c00f8a47d..7fb4fab9f64 100644 --- a/app/controllers/extra_locales_controller.rb +++ b/app/controllers/extra_locales_controller.rb @@ -11,6 +11,61 @@ class ExtraLocalesController < ApplicationController OVERRIDES_BUNDLE ||= "overrides" MD5_HASH_LENGTH ||= 32 + MF_BUNDLE = "mf" + BUNDLES = [OVERRIDES_BUNDLE, MF_BUNDLE] + + class << self + def js_digests + @js_digests ||= {} + end + + def bundle_js_hash(bundle) + bundle_key = "#{bundle}_#{I18n.locale}" + if bundle.in?(BUNDLES) + site = RailsMultisite::ConnectionManagement.current_db + + js_digests[site] ||= {} + js_digests[site][bundle_key] ||= begin + js = bundle_js(bundle) + js.present? ? Digest::MD5.hexdigest(js) : nil + end + else + js_digests[bundle_key] ||= Digest::MD5.hexdigest(bundle_js(bundle)) + end + end + + def url(bundle) + "#{Discourse.base_path}/extra-locales/#{bundle}?v=#{bundle_js_hash(bundle)}" + end + + def client_overrides_exist? + bundle_js_hash(OVERRIDES_BUNDLE).present? + end + + def bundle_js(bundle) + locale_str = I18n.locale.to_s + bundle_str = "#{bundle}_js" + + case bundle + when OVERRIDES_BUNDLE + JsLocaleHelper.output_client_overrides(locale_str) + when MF_BUNDLE + JsLocaleHelper.output_MF(locale_str) + else + JsLocaleHelper.output_extra_locales(bundle_str, locale_str) + end + end + + def bundle_js_with_hash(bundle) + js = bundle_js(bundle) + [js, Digest::MD5.hexdigest(js)] + end + + def clear_cache! + site = RailsMultisite::ConnectionManagement.current_db + js_digests.delete(site) + end + end def show bundle = params[:bundle] @@ -18,60 +73,18 @@ class ExtraLocalesController < ApplicationController version = params[:v] if version.present? - if version.kind_of?(String) && version.length == MD5_HASH_LENGTH - hash = ExtraLocalesController.bundle_js_hash(bundle) - immutable_for(1.year) if hash == version - else - raise Discourse::InvalidParameters.new(:v) - end + raise Discourse::InvalidParameters.new(:v) unless version.to_s.size == MD5_HASH_LENGTH end - render plain: ExtraLocalesController.bundle_js(bundle), content_type: "application/javascript" - end + content, hash = ExtraLocalesController.bundle_js_with_hash(bundle) + immutable_for(1.year) if hash == version - def self.bundle_js_hash(bundle) - if bundle == OVERRIDES_BUNDLE - site = RailsMultisite::ConnectionManagement.current_db - - @by_site ||= {} - @by_site[site] ||= {} - @by_site[site][I18n.locale] ||= begin - js = bundle_js(bundle) - js.present? ? Digest::MD5.hexdigest(js) : nil - end - else - @bundle_js_hash ||= {} - @bundle_js_hash["#{bundle}_#{I18n.locale}"] ||= Digest::MD5.hexdigest(bundle_js(bundle)) - end - end - - def self.url(bundle) - "#{Discourse.base_path}/extra-locales/#{bundle}?v=#{bundle_js_hash(bundle)}" - end - - def self.client_overrides_exist? - bundle_js_hash(OVERRIDES_BUNDLE).present? - end - - def self.bundle_js(bundle) - locale_str = I18n.locale.to_s - bundle_str = "#{bundle}_js" - - if bundle == OVERRIDES_BUNDLE - JsLocaleHelper.output_client_overrides(locale_str) - else - JsLocaleHelper.output_extra_locales(bundle_str, locale_str) - end - end - - def self.clear_cache! - site = RailsMultisite::ConnectionManagement.current_db - @by_site&.delete(site) + render plain: content, content_type: "application/javascript" end private def valid_bundle?(bundle) - bundle == OVERRIDES_BUNDLE || (bundle =~ /\A(admin|wizard)\z/ && current_user&.staff?) + bundle.in?(BUNDLES) || (bundle =~ /\A(admin|wizard)\z/ && current_user&.staff?) end end diff --git a/app/models/translation_override.rb b/app/models/translation_override.rb index d37e7301588..5862dc5dc7c 100644 --- a/app/models/translation_override.rb +++ b/app/models/translation_override.rb @@ -48,6 +48,14 @@ class TranslationOverride < ActiveRecord::Base attribute :status, :integer enum status: { up_to_date: 0, outdated: 1, invalid_interpolation_keys: 2, deprecated: 3 } + scope :mf_locales, ->(locale) { where(locale: locale).where("translation_key LIKE '%_MF'") } + scope :client_locales, + ->(locale) do + where(locale: locale) + .where("translation_key LIKE 'js.%' OR translation_key LIKE 'admin_js.%'") + .where.not("translation_key LIKE '%_MF'") + end + def self.upsert!(locale, key, value) params = { locale: locale, translation_key: key } @@ -58,10 +66,6 @@ class TranslationOverride < ActiveRecord::Base I18n.overrides_disabled { I18n.t(transform_pluralized_key(key), locale: :en) } data = { value: sanitized_value, original_translation: original_translation } - if key.end_with?("_MF") - _, filename = JsLocaleHelper.find_message_format_locale([locale], fallback_to_english: false) - data[:compiled_js] = JsLocaleHelper.compile_message_format(filename, locale, sanitized_value) - end params.merge!(data) if translation_override.new_record? i18n_changed(locale, [key]) if translation_override.update(data) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 7051b794911..29465b6de57 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -27,7 +27,7 @@ <% add_resource_preload_list(script_asset_path("browser-update"), "script") %> " as="script" nonce="<%= csp_nonce_placeholder %>"> " as="script" nonce="<%= csp_nonce_placeholder %>"> - + <%= preload_script 'browser-detect' %> <%= preload_script "vendor" %> @@ -37,8 +37,9 @@ <%- end %> <%= preload_script "locales/#{I18n.locale}" %> + <%= preload_script_url ExtraLocalesController.url("mf") %> <%- if ExtraLocalesController.client_overrides_exist? %> - <%= preload_script_url ExtraLocalesController.url('overrides') %> + <%= preload_script_url ExtraLocalesController.url("overrides") %> <%- end %> <%- if staff? %> diff --git a/app/views/qunit/theme.html.erb b/app/views/qunit/theme.html.erb index 6e48425e4cb..cf6e86fdd5b 100644 --- a/app/views/qunit/theme.html.erb +++ b/app/views/qunit/theme.html.erb @@ -11,6 +11,7 @@ <%= preload_script "discourse" %> <%= preload_script "test" %> <%= preload_script "locales/#{I18n.locale}" %> + <%= preload_script_url ExtraLocalesController.url("mf") %> <%= preload_script "admin" %> <%- Discourse.find_plugin_js_assets(include_disabled: true).each do |file| %> <%= preload_script file %> @@ -50,7 +51,7 @@ <% elsif @suggested_themes %>

Theme QUnit Test Runner

- <%- if @suggested_themes.size == 0 %> + <%- if @suggested_themes.empty? %>

Cannot find any theme tests.

<%- else %>

Select a theme/component:

diff --git a/lib/discourse.rb b/lib/discourse.rb index 774e065b298..c893f6ad9da 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -916,7 +916,6 @@ module Discourse PrettyText.reset_context DiscourseJsProcessor::Transpiler.reset_context if defined?(DiscourseJsProcessor::Transpiler) - JsLocaleHelper.reset_context if defined?(JsLocaleHelper) # warm up v8 after fork, that way we do not fork a v8 context # it may cause issues if bg threads in a v8 isolate randomly stop diff --git a/lib/javascripts/locale/af.js b/lib/javascripts/locale/af.js deleted file mode 100644 index b03849f2d3a..00000000000 --- a/lib/javascripts/locale/af.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.af = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/am.js b/lib/javascripts/locale/am.js deleted file mode 100644 index aa8af1d1a4f..00000000000 --- a/lib/javascripts/locale/am.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.am = function(n) { - if (n === 0 || n == 1) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/ar.js b/lib/javascripts/locale/ar.js deleted file mode 100644 index d33d95ddba8..00000000000 --- a/lib/javascripts/locale/ar.js +++ /dev/null @@ -1,18 +0,0 @@ -MessageFormat.locale.ar = function(n) { - if (n === 0) { - return 'zero'; - } - if (n == 1) { - return 'one'; - } - if (n == 2) { - return 'two'; - } - if ((n % 100) >= 3 && (n % 100) <= 10 && n == Math.floor(n)) { - return 'few'; - } - if ((n % 100) >= 11 && (n % 100) <= 99 && n == Math.floor(n)) { - return 'many'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/be.js b/lib/javascripts/locale/be.js deleted file mode 100644 index 5525d2d4339..00000000000 --- a/lib/javascripts/locale/be.js +++ /dev/null @@ -1,14 +0,0 @@ -MessageFormat.locale.be = function (n) { - var r10 = n % 10, r100 = n % 100; - - if (r10 == 1 && r100 != 11) - return 'one'; - - if (r10 >= 2 && r10 <= 4 && (r100 < 12 || r100 > 14) && n == Math.floor(n)) - return 'few'; - - if ((r10 == 0 || (r10 >= 5 && r10 <= 9) || (r100 >= 11 && r100 <= 14)) && n == Math.floor(n)) - return 'many'; - - return 'other'; -}; diff --git a/lib/javascripts/locale/bg.js b/lib/javascripts/locale/bg.js deleted file mode 100644 index 868baea07d5..00000000000 --- a/lib/javascripts/locale/bg.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.bg = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/bn.js b/lib/javascripts/locale/bn.js deleted file mode 100644 index 1641ff32d6a..00000000000 --- a/lib/javascripts/locale/bn.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.bn = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/br.js b/lib/javascripts/locale/br.js deleted file mode 100644 index 2e0d43fee1d..00000000000 --- a/lib/javascripts/locale/br.js +++ /dev/null @@ -1,18 +0,0 @@ -MessageFormat.locale.br = function (n) { - if (n === 0) { - return 'zero'; - } - if (n == 1) { - return 'one'; - } - if (n == 2) { - return 'two'; - } - if (n == 3) { - return 'few'; - } - if (n == 6) { - return 'many'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/bs.js b/lib/javascripts/locale/bs.js deleted file mode 100644 index e2c139ddf96..00000000000 --- a/lib/javascripts/locale/bs.js +++ /dev/null @@ -1,10 +0,0 @@ -MessageFormat.locale.bs = function (n) { - if ((n % 10) == 1 && (n % 100) != 11) { - return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 4 && - ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/ca.js b/lib/javascripts/locale/ca.js deleted file mode 100644 index e2a685c674f..00000000000 --- a/lib/javascripts/locale/ca.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.ca = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/cs.js b/lib/javascripts/locale/cs.js deleted file mode 100644 index 6a7f67ee230..00000000000 --- a/lib/javascripts/locale/cs.js +++ /dev/null @@ -1,9 +0,0 @@ -MessageFormat.locale.cs = function (n) { - if (n == 1) { - return 'one'; - } - if (n == 2 || n == 3 || n == 4) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/cy.js b/lib/javascripts/locale/cy.js deleted file mode 100644 index d98b1f49fa0..00000000000 --- a/lib/javascripts/locale/cy.js +++ /dev/null @@ -1,18 +0,0 @@ -MessageFormat.locale.cy = function (n) { - if (n === 0) { - return 'zero'; - } - if (n == 1) { - return 'one'; - } - if (n == 2) { - return 'two'; - } - if (n == 3) { - return 'few'; - } - if (n == 6) { - return 'many'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/da.js b/lib/javascripts/locale/da.js deleted file mode 100644 index 7ea5765b295..00000000000 --- a/lib/javascripts/locale/da.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.da = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/de.js b/lib/javascripts/locale/de.js deleted file mode 100644 index edca71c5400..00000000000 --- a/lib/javascripts/locale/de.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.de = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/el.js b/lib/javascripts/locale/el.js deleted file mode 100644 index 8c5215a4442..00000000000 --- a/lib/javascripts/locale/el.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.el = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/en.js b/lib/javascripts/locale/en.js deleted file mode 100644 index c2380b9bfe6..00000000000 --- a/lib/javascripts/locale/en.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.en = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/es.js b/lib/javascripts/locale/es.js deleted file mode 100644 index 4397d10b8cc..00000000000 --- a/lib/javascripts/locale/es.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.es = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/et.js b/lib/javascripts/locale/et.js deleted file mode 100644 index d4b7f5a3139..00000000000 --- a/lib/javascripts/locale/et.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.et = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/eu.js b/lib/javascripts/locale/eu.js deleted file mode 100644 index 6da55df13e7..00000000000 --- a/lib/javascripts/locale/eu.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.eu = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/fa.js b/lib/javascripts/locale/fa.js deleted file mode 100644 index 4280d1dabba..00000000000 --- a/lib/javascripts/locale/fa.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.fa = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/fa_IR.js b/lib/javascripts/locale/fa_IR.js deleted file mode 100644 index a830b2cdc16..00000000000 --- a/lib/javascripts/locale/fa_IR.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.fa_IR = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/fi.js b/lib/javascripts/locale/fi.js deleted file mode 100644 index 3315a840453..00000000000 --- a/lib/javascripts/locale/fi.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.fi = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/fil.js b/lib/javascripts/locale/fil.js deleted file mode 100644 index af882daf477..00000000000 --- a/lib/javascripts/locale/fil.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.fil = function(n) { - if (n === 0 || n == 1) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/fr.js b/lib/javascripts/locale/fr.js deleted file mode 100644 index e562c3f8cb2..00000000000 --- a/lib/javascripts/locale/fr.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.fr = function (n) { - if (n >= 0 && n < 2) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/ga.js b/lib/javascripts/locale/ga.js deleted file mode 100644 index c29aaadb66b..00000000000 --- a/lib/javascripts/locale/ga.js +++ /dev/null @@ -1,9 +0,0 @@ -MessageFormat.locale.ga = function (n) { - if (n == 1) { - return 'one'; - } - if (n == 2) { - return 'two'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/gl.js b/lib/javascripts/locale/gl.js deleted file mode 100644 index 0d2a1b448c4..00000000000 --- a/lib/javascripts/locale/gl.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.gl = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/gsw.js b/lib/javascripts/locale/gsw.js deleted file mode 100644 index 9aae2bcc8b6..00000000000 --- a/lib/javascripts/locale/gsw.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.gsw = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/gu.js b/lib/javascripts/locale/gu.js deleted file mode 100644 index 70820dd9e0b..00000000000 --- a/lib/javascripts/locale/gu.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.gu = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/he.js b/lib/javascripts/locale/he.js deleted file mode 100644 index bf828a5a7b3..00000000000 --- a/lib/javascripts/locale/he.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.he = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/hi.js b/lib/javascripts/locale/hi.js deleted file mode 100644 index 68fac22a36d..00000000000 --- a/lib/javascripts/locale/hi.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.hi = function(n) { - if (n === 0 || n == 1) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/hr.js b/lib/javascripts/locale/hr.js deleted file mode 100644 index 815bf174ab9..00000000000 --- a/lib/javascripts/locale/hr.js +++ /dev/null @@ -1,10 +0,0 @@ -MessageFormat.locale.hr = function (n) { - if ((n % 10) == 1 && (n % 100) != 11) { - return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 4 && - ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/hu.js b/lib/javascripts/locale/hu.js deleted file mode 100644 index 1fa3c21b26a..00000000000 --- a/lib/javascripts/locale/hu.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.hu = function(n) { - return 'other'; -}; diff --git a/lib/javascripts/locale/hy.js b/lib/javascripts/locale/hy.js deleted file mode 100644 index 7e05bb23718..00000000000 --- a/lib/javascripts/locale/hy.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.hy = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/id.js b/lib/javascripts/locale/id.js deleted file mode 100644 index fb4b62bdee4..00000000000 --- a/lib/javascripts/locale/id.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.id = function(n) { - return 'other'; -}; diff --git a/lib/javascripts/locale/in.js b/lib/javascripts/locale/in.js deleted file mode 100644 index 95abe006a9e..00000000000 --- a/lib/javascripts/locale/in.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale["in"] = function(n) { - return 'other'; -}; diff --git a/lib/javascripts/locale/is.js b/lib/javascripts/locale/is.js deleted file mode 100644 index 48efd8f4636..00000000000 --- a/lib/javascripts/locale/is.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.is = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/it.js b/lib/javascripts/locale/it.js deleted file mode 100644 index be964ccbe14..00000000000 --- a/lib/javascripts/locale/it.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.it = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/iw.js b/lib/javascripts/locale/iw.js deleted file mode 100644 index a25fb2b170d..00000000000 --- a/lib/javascripts/locale/iw.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.iw = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/ja.js b/lib/javascripts/locale/ja.js deleted file mode 100644 index a02267fa0a7..00000000000 --- a/lib/javascripts/locale/ja.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.ja = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/kn.js b/lib/javascripts/locale/kn.js deleted file mode 100644 index 44c782db72f..00000000000 --- a/lib/javascripts/locale/kn.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.kn = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/ko.js b/lib/javascripts/locale/ko.js deleted file mode 100644 index 899ffeae93c..00000000000 --- a/lib/javascripts/locale/ko.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.ko = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/lag.js b/lib/javascripts/locale/lag.js deleted file mode 100644 index d4990b96c43..00000000000 --- a/lib/javascripts/locale/lag.js +++ /dev/null @@ -1,9 +0,0 @@ -MessageFormat.locale.lag = function (n) { - if (n === 0) { - return 'zero'; - } - if (n > 0 && n < 2) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/ln.js b/lib/javascripts/locale/ln.js deleted file mode 100644 index 562e220b8cf..00000000000 --- a/lib/javascripts/locale/ln.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.ln = function(n) { - if (n === 0 || n == 1) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/lt.js b/lib/javascripts/locale/lt.js deleted file mode 100644 index 82878cfef9b..00000000000 --- a/lib/javascripts/locale/lt.js +++ /dev/null @@ -1,10 +0,0 @@ -MessageFormat.locale.lt = function (n) { - if ((n % 10) == 1 && ((n % 100) < 11 || (n % 100) > 19)) { - return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 9 && - ((n % 100) < 11 || (n % 100) > 19) && n == Math.floor(n)) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/lv.js b/lib/javascripts/locale/lv.js deleted file mode 100644 index 75beb340148..00000000000 --- a/lib/javascripts/locale/lv.js +++ /dev/null @@ -1,9 +0,0 @@ -MessageFormat.locale.lv = function (n) { - if (n === 0) { - return 'zero'; - } - if ((n % 10) == 1 && (n % 100) != 11) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/mk.js b/lib/javascripts/locale/mk.js deleted file mode 100644 index c17aa2e3ad2..00000000000 --- a/lib/javascripts/locale/mk.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.mk = function (n) { - if ((n % 10) == 1 && n != 11) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/ml.js b/lib/javascripts/locale/ml.js deleted file mode 100644 index f400a5f10cc..00000000000 --- a/lib/javascripts/locale/ml.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.ml = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/mo.js b/lib/javascripts/locale/mo.js deleted file mode 100644 index 16d84d98f3e..00000000000 --- a/lib/javascripts/locale/mo.js +++ /dev/null @@ -1,10 +0,0 @@ -MessageFormat.locale.mo = function (n) { - if (n == 1) { - return 'one'; - } - if (n === 0 || n != 1 && (n % 100) >= 1 && - (n % 100) <= 19 && n == Math.floor(n)) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/mr.js b/lib/javascripts/locale/mr.js deleted file mode 100644 index da4d494ac19..00000000000 --- a/lib/javascripts/locale/mr.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.mr = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/ms.js b/lib/javascripts/locale/ms.js deleted file mode 100644 index e635ae79aa4..00000000000 --- a/lib/javascripts/locale/ms.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.ms = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/mt.js b/lib/javascripts/locale/mt.js deleted file mode 100644 index 6a071a7345d..00000000000 --- a/lib/javascripts/locale/mt.js +++ /dev/null @@ -1,12 +0,0 @@ -MessageFormat.locale.mt = function (n) { - if (n == 1) { - return 'one'; - } - if (n === 0 || ((n % 100) >= 2 && (n % 100) <= 4 && n == Math.floor(n))) { - return 'few'; - } - if ((n % 100) >= 11 && (n % 100) <= 19 && n == Math.floor(n)) { - return 'many'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/nl.js b/lib/javascripts/locale/nl.js deleted file mode 100644 index 617e9071b04..00000000000 --- a/lib/javascripts/locale/nl.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.nl = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/no.js b/lib/javascripts/locale/no.js deleted file mode 100644 index 025d3489d9b..00000000000 --- a/lib/javascripts/locale/no.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.no = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/or.js b/lib/javascripts/locale/or.js deleted file mode 100644 index 04240a1859e..00000000000 --- a/lib/javascripts/locale/or.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.or = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/pl_PL.js b/lib/javascripts/locale/pl_PL.js deleted file mode 100644 index 05437893163..00000000000 --- a/lib/javascripts/locale/pl_PL.js +++ /dev/null @@ -1,15 +0,0 @@ -MessageFormat.locale.pl_PL = function (n) { - if (n == 1) { - return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 4 && - ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { - return 'few'; - } - if ((n % 10) === 0 || n != 1 && (n % 10) == 1 || - ((n % 10) >= 5 && (n % 10) <= 9 || (n % 100) >= 12 && (n % 100) <= 14) && - n == Math.floor(n)) { - return 'many'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/pt.js b/lib/javascripts/locale/pt.js deleted file mode 100644 index a4b65eb906d..00000000000 --- a/lib/javascripts/locale/pt.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.pt = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/pt_BR.js b/lib/javascripts/locale/pt_BR.js deleted file mode 100644 index c2c797c490c..00000000000 --- a/lib/javascripts/locale/pt_BR.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.pt_BR = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/ro.js b/lib/javascripts/locale/ro.js deleted file mode 100644 index 26453eadd6e..00000000000 --- a/lib/javascripts/locale/ro.js +++ /dev/null @@ -1,10 +0,0 @@ -MessageFormat.locale.ro = function (n) { - if (n == 1) { - return 'one'; - } - if (n === 0 || n != 1 && (n % 100) >= 1 && - (n % 100) <= 19 && n == Math.floor(n)) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/ru.js b/lib/javascripts/locale/ru.js deleted file mode 100644 index 09d2ed9cb68..00000000000 --- a/lib/javascripts/locale/ru.js +++ /dev/null @@ -1,16 +0,0 @@ -MessageFormat.locale.ru = function (n) { - var r10 = n % 10, r100 = n % 100; - - if (r10 == 1 && r100 != 11) - return 'one'; - - if (r10 >= 2 && r10 <= 4 && (r100 < 12 || r100 > 14) && n == Math.floor(n)) - return 'few'; - - if (r10 === 0 || (r10 >= 5 && r10 <= 9) || - (r100 >= 11 && r100 <= 14) && n == Math.floor(n)) { - return 'many'; - } - - return 'other'; -}; diff --git a/lib/javascripts/locale/shi.js b/lib/javascripts/locale/shi.js deleted file mode 100644 index 9e86dca14a6..00000000000 --- a/lib/javascripts/locale/shi.js +++ /dev/null @@ -1,9 +0,0 @@ -MessageFormat.locale.shi = function(n) { - if (n >= 0 && n <= 1) { - return 'one'; - } - if (n >= 2 && n <= 10 && n == Math.floor(n)) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/sk.js b/lib/javascripts/locale/sk.js deleted file mode 100644 index ef041ddb236..00000000000 --- a/lib/javascripts/locale/sk.js +++ /dev/null @@ -1,9 +0,0 @@ -MessageFormat.locale.sk = function (n) { - if (n == 1) { - return 'one'; - } - if (n == 2 || n == 3 || n == 4) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/sl.js b/lib/javascripts/locale/sl.js deleted file mode 100644 index 7dd591b01e9..00000000000 --- a/lib/javascripts/locale/sl.js +++ /dev/null @@ -1,12 +0,0 @@ -MessageFormat.locale.sl = function (n) { - if ((n % 100) == 1) { - return 'one'; - } - if ((n % 100) == 2) { - return 'two'; - } - if ((n % 100) == 3 || (n % 100) == 4) { - return 'few'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/sq.js b/lib/javascripts/locale/sq.js deleted file mode 100644 index 1e683894f21..00000000000 --- a/lib/javascripts/locale/sq.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.sq = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/sr.js b/lib/javascripts/locale/sr.js deleted file mode 100644 index 9bd86634529..00000000000 --- a/lib/javascripts/locale/sr.js +++ /dev/null @@ -1,11 +0,0 @@ -MessageFormat.locale.sr = function (n) { - var r10 = n % 10, r100 = n % 100; - - if (r10 == 1 && r100 != 11) - return 'one'; - - if (r10 >= 2 && r10 <= 4 && (r100 < 12 || r100 > 14) && n == Math.floor(n)) - return 'few'; - - return 'other'; -}; \ No newline at end of file diff --git a/lib/javascripts/locale/sv.js b/lib/javascripts/locale/sv.js deleted file mode 100644 index e566a339d12..00000000000 --- a/lib/javascripts/locale/sv.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.sv = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/sw.js b/lib/javascripts/locale/sw.js deleted file mode 100644 index 7dd56c146fe..00000000000 --- a/lib/javascripts/locale/sw.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.sw = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/ta.js b/lib/javascripts/locale/ta.js deleted file mode 100644 index 08a4ae01fdf..00000000000 --- a/lib/javascripts/locale/ta.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.ta = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/te.js b/lib/javascripts/locale/te.js deleted file mode 100644 index 61ccb27f6c5..00000000000 --- a/lib/javascripts/locale/te.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.te = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/th.js b/lib/javascripts/locale/th.js deleted file mode 100644 index 9ef170824d8..00000000000 --- a/lib/javascripts/locale/th.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.th = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/tl.js b/lib/javascripts/locale/tl.js deleted file mode 100644 index bc68843df27..00000000000 --- a/lib/javascripts/locale/tl.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.tl = function(n) { - if (n === 0 || n == 1) { - return 'one'; - } - return 'other'; -}; diff --git a/lib/javascripts/locale/tr.js b/lib/javascripts/locale/tr.js deleted file mode 100644 index 438941ad9b4..00000000000 --- a/lib/javascripts/locale/tr.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.tr = function(n) { - return 'other'; -}; diff --git a/lib/javascripts/locale/tr_TR.js b/lib/javascripts/locale/tr_TR.js deleted file mode 100644 index 83d9c02c960..00000000000 --- a/lib/javascripts/locale/tr_TR.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.tr_TR = function(n) { - return 'other'; -}; diff --git a/lib/javascripts/locale/ug.js b/lib/javascripts/locale/ug.js deleted file mode 100644 index bcf429ee076..00000000000 --- a/lib/javascripts/locale/ug.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.ug = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/uk.js b/lib/javascripts/locale/uk.js deleted file mode 100644 index edb43885ab4..00000000000 --- a/lib/javascripts/locale/uk.js +++ /dev/null @@ -1,15 +0,0 @@ -MessageFormat.locale.uk = function (n) { - if ((n % 10) == 1 && (n % 100) != 11) { - return 'one'; - } - if ((n % 10) >= 2 && (n % 10) <= 4 && - ((n % 100) < 12 || (n % 100) > 14) && n == Math.floor(n)) { - return 'few'; - } - if ((n % 10) === 0 || ((n % 10) >= 5 && (n % 10) <= 9) || - ((n % 100) >= 11 && (n % 100) <= 14) && n == Math.floor(n)) { - // return 'many'; - return 'other'; // TODO should be "many" but is not defined in translations - } - return 'other'; -}; diff --git a/lib/javascripts/locale/ur.js b/lib/javascripts/locale/ur.js deleted file mode 100644 index 5a875c9e372..00000000000 --- a/lib/javascripts/locale/ur.js +++ /dev/null @@ -1,6 +0,0 @@ -MessageFormat.locale.ur = function ( n ) { - if ( n === 1 ) { - return "one"; - } - return "other"; -}; diff --git a/lib/javascripts/locale/vi.js b/lib/javascripts/locale/vi.js deleted file mode 100644 index 8a5b74698ca..00000000000 --- a/lib/javascripts/locale/vi.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.vi = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/zh.js b/lib/javascripts/locale/zh.js deleted file mode 100644 index 6ae270cf660..00000000000 --- a/lib/javascripts/locale/zh.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.zh = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/zh_CN.js b/lib/javascripts/locale/zh_CN.js deleted file mode 100644 index ef595cb377d..00000000000 --- a/lib/javascripts/locale/zh_CN.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.zh_CN = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/locale/zh_TW.js b/lib/javascripts/locale/zh_TW.js deleted file mode 100644 index 3f4bf033904..00000000000 --- a/lib/javascripts/locale/zh_TW.js +++ /dev/null @@ -1,3 +0,0 @@ -MessageFormat.locale.zh_TW = function ( n ) { - return "other"; -}; diff --git a/lib/javascripts/messageformat-lookup.js b/lib/javascripts/messageformat-lookup.js deleted file mode 100644 index eecb5e22bc3..00000000000 --- a/lib/javascripts/messageformat-lookup.js +++ /dev/null @@ -1,14 +0,0 @@ -(function () { - I18n.messageFormat = function (key, options) { - var fn = I18n._compiledMFs[key]; - if (fn) { - try { - return fn(options); - } catch (err) { - return err.message; - } - } else { - return "Missing Key: " + key; - } - }; -})(); diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index b3ac0f695b4..771984e318d 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -117,14 +117,14 @@ module JsLocaleHelper @loaded_merges = nil end - def self.translations_for(locale_str) + def self.translations_for(locale_str, no_fallback: false) clear_cache! if Rails.env.development? locale_sym = locale_str.to_sym translations = I18n.with_locale(locale_sym) do - if locale_sym == :en + if locale_sym == :en || no_fallback load_translations(locale_sym) else load_translations_merged(*I18n.fallbacks[locale_sym]) @@ -134,14 +134,43 @@ module JsLocaleHelper Marshal.load(Marshal.dump(translations)) end + def self.output_MF(locale) + require "messageformat" + + message_formats = + I18n.fallbacks[locale] + .each_with_object({}) do |l, hash| + translations = translations_for(l, no_fallback: true) + hash[l.to_s.dasherize] = remove_message_formats!(translations, l).merge( + TranslationOverride + .mf_locales(l) + .pluck(:translation_key, :value) + .to_h + .transform_keys { _1.sub(/^[a-z_]*js\./, "") }, + ) + end + .compact_blank + compiled = MessageFormat.compile(message_formats.keys, message_formats) + transpiled = DiscourseJsProcessor.transpile(<<~JS, "", "discourse-mf") + import Messages from '@messageformat/runtime/messages'; + #{compiled.sub("export default", "const msgData =")}; + const messages = new Messages(msgData, "#{locale.to_s.dasherize}"); + messages.defaultLocale = "en"; + globalThis.I18n._mfMessages = messages; + JS + <<~JS + #{transpiled} + require("discourse-mf"); + JS + end + def self.output_locale(locale) locale_str = locale.to_s fallback_locale_str = LocaleSiteSetting.fallback_locale(locale_str)&.to_s translations = translations_for(locale_str) - message_formats = remove_message_formats!(translations, locale) - mf_locale, mf_filename = find_message_format_locale([locale_str], fallback_to_english: true) - result = generate_message_format(message_formats, mf_locale, mf_filename) + remove_message_formats!(translations, locale) + result = +"" translations.keys.each do |l| translations[l].keys.each { |k| translations[l].delete(k) unless k == "js" } @@ -153,9 +182,6 @@ module JsLocaleHelper if fallback_locale_str && fallback_locale_str != "en" result << "I18n.fallbackLocale = '#{fallback_locale_str}';\n" end - if mf_locale != "en" - result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{mf_locale};\n" - end # moment result << File.read("#{Rails.root}/vendor/assets/javascripts/moment.js") @@ -168,44 +194,24 @@ module JsLocaleHelper end def self.output_client_overrides(main_locale) - all_overrides = {} - has_overrides = false - - I18n.fallbacks[main_locale].each do |locale| - overrides = - all_overrides[locale] = TranslationOverride - .where(locale: locale) - .where("translation_key LIKE 'js.%' OR translation_key LIKE 'admin_js.%'") - .pluck(:translation_key, :value, :compiled_js) - - has_overrides ||= overrides.present? - end - - return "" if !has_overrides - - result = +"I18n._overrides = {};" - existing_keys = Set.new - message_formats = [] - - all_overrides.each do |locale, overrides| - translations = {} - - overrides.each do |key, value, compiled_js| - next if existing_keys.include?(key) - existing_keys << key - - if key.end_with?("_MF") - message_formats << "#{key.inspect}: #{compiled_js}" - else - translations[key] = value + locales = I18n.fallbacks[main_locale] + all_overrides = + locales + .each_with_object({}) do |locale, overrides| + overrides[locale] = TranslationOverride + .client_locales(locale) + .pluck(:translation_key, :value) + .to_h end - end + .compact_blank - result << "I18n._overrides['#{locale}'] = #{translations.to_json};" if translations.present? + return "" if all_overrides.blank? + + all_overrides.reduce do |(_, main_overrides), (_, fallback_overrides)| + fallback_overrides.slice!(*fallback_overrides.keys - main_overrides.keys) end - result << "I18n._mfOverrides = {#{message_formats.join(", ")}};" - result + "I18n._overrides = #{all_overrides.compact_blank.to_json};" end def self.output_extra_locales(bundle, locale) @@ -251,11 +257,6 @@ module JsLocaleHelper end end - def self.find_message_format_locale(locale_chain, fallback_to_english:) - path = "#{Rails.root}/lib/javascripts/locale" - find_locale(locale_chain, path, :message_format, fallback_to_english: fallback_to_english) - end - def self.find_locale(locale_chain, path, type, fallback_to_english:) locale_chain.map!(&:to_s) @@ -301,55 +302,6 @@ module JsLocaleHelper filename && File.exist?(filename) ? File.read(filename) << "\n" : "" end - def self.generate_message_format(message_formats, locale, filename) - formats = - message_formats - .map { |k, v| k.inspect << " : " << compile_message_format(filename, locale, v) } - .join(", ") - - result = +"MessageFormat = {locale: {}};\n" - result << "I18n._compiledMFs = {#{formats}};\n" - result << File.read(filename) << "\n" - - if locale != "en" - # Include "en" pluralization rules for use in fallbacks - _, en_filename = find_message_format_locale(["en"], fallback_to_english: false) - result << File.read(en_filename) << "\n" - end - - result << File.read("#{Rails.root}/lib/javascripts/messageformat-lookup.js") << "\n" - end - - def self.reset_context - @ctx&.dispose - @ctx = nil - end - - @mutex = Mutex.new - def self.with_context - @mutex.synchronize do - yield( - @ctx ||= - begin - ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000) - ctx.load("#{Rails.root}/node_modules/messageformat/messageformat.js") - ctx - end - ) - end - end - - def self.compile_message_format(path, locale, format) - with_context do |ctx| - ctx.load(path) if File.exist?(path) - ctx.eval("mf = new MessageFormat('#{locale}');") - ctx.eval("mf.precompile(mf.parse(#{format.inspect}))") - end - rescue MiniRacer::EvalError => e - message = +"Invalid Format: " << e.message - "function(){ return #{message.inspect};}" - end - def self.remove_message_formats!(translations, locale) message_formats = {} I18n.fallbacks[locale] diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index faf830d00b5..058248f1241 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -1275,13 +1275,6 @@ class Plugin::Instance locale_chain = opts[:fallbackLocale] ? [locale, opts[:fallbackLocale]] : [locale] lib_locale_path = File.join(root_path, "lib/javascripts/locale") - path = File.join(lib_locale_path, "message_format") - opts[:message_format] = find_locale_file(locale_chain, path) - opts[:message_format] = JsLocaleHelper.find_message_format_locale( - locale_chain, - fallback_to_english: false, - ) unless opts[:message_format] - path = File.join(lib_locale_path, "moment_js") opts[:moment_js] = find_locale_file(locale_chain, path) opts[:moment_js] = JsLocaleHelper.find_moment_locale(locale_chain) unless opts[:moment_js] @@ -1357,8 +1350,7 @@ class Plugin::Instance def valid_locale?(custom_locale) File.exist?(custom_locale[:client_locale_file]) && File.exist?(custom_locale[:server_locale_file]) && - File.exist?(custom_locale[:js_locale_file]) && custom_locale[:message_format] && - custom_locale[:moment_js] + File.exist?(custom_locale[:js_locale_file]) && custom_locale[:moment_js] end def find_locale_file(locale_chain, path) diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index c8fe5bf123b..9066958c018 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -55,7 +55,6 @@ task "qunit:test", %i[timeout qunit_path filter] do |_, args| begin success = true - test_path = "#{Rails.root}/test" qunit_path = args[:qunit_path] filter = args[:filter] diff --git a/package.json b/package.json index 91f7c578436..c79b6e613c3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@glint/environment-ember-template-imports": "^1.4.0", "@glint/template": "^1.4.0", "@json-editor/json-editor": "2.10.0", + "@messageformat/core": "^3.3.0", "@mixer/parallel-prettier": "^2.0.3", "chart.js": "3.5.1", "chartjs-plugin-datalabels": "2.2.0", @@ -83,4 +84,4 @@ "npm": "please-use-yarn", "yarn": ">= 1.21.1" } -} \ No newline at end of file +} diff --git a/spec/lib/js_locale_helper_spec.rb b/spec/lib/js_locale_helper_spec.rb index 7d3cd760232..24bf9ec746a 100644 --- a/spec/lib/js_locale_helper_spec.rb +++ b/spec/lib/js_locale_helper_spec.rb @@ -5,23 +5,27 @@ require "mini_racer" RSpec.describe JsLocaleHelper do let(:v8_ctx) do node_modules = "#{Rails.root}/node_modules/" - transpiler = DiscourseJsProcessor::Transpiler.new - discourse_i18n = - transpiler.perform( - File.read("#{Rails.root}/app/assets/javascripts/discourse-i18n/src/index.js"), - "app/assets/javascripts/discourse", - "discourse-i18n", - ) - ctx = MiniRacer::Context.new ctx.load("#{node_modules}/loader.js/dist/loader/loader.js") ctx.eval("var window = globalThis;") - ctx.eval(discourse_i18n) + { + "@messageformat/runtime/messages": "#{node_modules}/@messageformat/runtime/esm/messages.js", + "@messageformat/runtime": "#{node_modules}/@messageformat/runtime/esm/runtime.js", + "@messageformat/runtime/lib/cardinals": + "#{node_modules}/@messageformat/runtime/esm/cardinals.js", + "make-plural/cardinals": "#{node_modules}/make-plural/cardinals.mjs", + "discourse-i18n": "#{Rails.root}/app/assets/javascripts/discourse-i18n/src/index.js", + }.each do |module_name, path| + ctx.eval(transpiler.perform(File.read(path), "", module_name.to_s)) + end ctx.eval <<~JS define("discourse/loader-shims", () => {}) JS - ctx.load("#{Rails.root}/app/assets/javascripts/locales/i18n.js") + # As there are circular references in the return value, this raises an + # error if we let MiniRacer try to convert the value to JSON. Forcing + # returning `null` from `#eval` will prevent that. + ctx.eval("#{File.read("#{Rails.root}/app/assets/javascripts/locales/i18n.js")};null") ctx end @@ -54,116 +58,6 @@ RSpec.describe JsLocaleHelper do end end - describe "message format" do - def message_format_filename(locale) - Rails.root + "lib/javascripts/locale/#{locale}.js" - end - - def setup_message_format(format) - filename = message_format_filename("en") - compiled = JsLocaleHelper.compile_message_format(filename, "en", format) - - @ctx = MiniRacer::Context.new - @ctx.eval("MessageFormat = {locale: {}};") - @ctx.load(filename) - @ctx.eval("var test = #{compiled}") - end - - def localize(opts) - @ctx.eval("test(#{opts.to_json})") - end - - it "handles plurals" do - setup_message_format( - "{NUM_RESULTS, plural, - one {1 result} - other {# results} - }", - ) - expect(localize(NUM_RESULTS: 1)).to eq("1 result") - expect(localize(NUM_RESULTS: 2)).to eq("2 results") - end - - it "handles double plurals" do - setup_message_format( - "{NUM_RESULTS, plural, - one {1 result} - other {# results} - } and {NUM_APPLES, plural, - one {1 apple} - other {# apples} - }", - ) - - expect(localize(NUM_RESULTS: 1, NUM_APPLES: 2)).to eq("1 result and 2 apples") - expect(localize(NUM_RESULTS: 2, NUM_APPLES: 1)).to eq("2 results and 1 apple") - end - - it "handles select" do - setup_message_format("{GENDER, select, male {He} female {She} other {They}} read a book") - expect(localize(GENDER: "male")).to eq("He read a book") - expect(localize(GENDER: "female")).to eq("She read a book") - expect(localize(GENDER: "none")).to eq("They read a book") - end - - it "can strip out message formats" do - hash = { "a" => "b", "c" => { "d" => { "f_MF" => "bob" } } } - expect(JsLocaleHelper.strip_out_message_formats!(hash)).to eq("c.d.f_MF" => "bob") - expect(hash["c"]["d"]).to eq({}) - end - - it "handles message format special keys" do - JsLocaleHelper.set_translations( - "en", - "en" => { - "js" => { - "hello" => "world", - "test_MF" => "{HELLO} {COUNT, plural, one {1 duck} other {# ducks}}", - "error_MF" => "{{BLA}", - "simple_MF" => "{COUNT, plural, one {1} other {#}}", - }, - "admin_js" => { - "foo_MF" => "{HELLO} {COUNT, plural, one {1 duck} other {# ducks}}", - }, - }, - ) - - ctx = MiniRacer::Context.new - ctx.eval("I18n = { pluralizationRules: {} };") - ctx.eval(JsLocaleHelper.output_locale("en")) - - expect(ctx.eval('I18n.translations["en"]["js"]["hello"]')).to eq("world") - expect(ctx.eval('I18n.translations["en"]["js"]["test_MF"]')).to eq(nil) - - expect(ctx.eval('I18n.messageFormat("test_MF", { HELLO: "hi", COUNT: 3 })')).to eq( - "hi 3 ducks", - ) - expect(ctx.eval('I18n.messageFormat("error_MF", { HELLO: "hi", COUNT: 3 })')).to match( - /Invalid Format/, - ) - expect(ctx.eval('I18n.messageFormat("missing", {})')).to match(/missing/) - expect(ctx.eval('I18n.messageFormat("simple_MF", {})')).to match(/COUNT/) # error - expect(ctx.eval('I18n.messageFormat("foo_MF", { HELLO: "hi", COUNT: 4 })')).to eq( - "hi 4 ducks", - ) - end - - it "load pluralization rules before precompile" do - message = JsLocaleHelper.compile_message_format(message_format_filename("ru"), "ru", "format") - expect(message).not_to match "Plural Function not found" - end - - it "uses message formats from fallback locale" do - translations = JsLocaleHelper.translations_for(:en_GB) - en_gb_message_formats = JsLocaleHelper.remove_message_formats!(translations, :en_GB) - expect(en_gb_message_formats).to_not be_empty - - translations = JsLocaleHelper.translations_for(:en) - en_message_formats = JsLocaleHelper.remove_message_formats!(translations, :en) - expect(en_gb_message_formats).to eq(en_message_formats) - end - end - it "performs fallbacks to English if a translation is not available" do JsLocaleHelper.set_translations( "en", @@ -235,35 +129,6 @@ RSpec.describe JsLocaleHelper do end end - it "correctly evaluates message formats in en fallback" do - allow_missing_translations do - JsLocaleHelper.set_translations("en", "en" => { "js" => { "something_MF" => "en mf" } }) - JsLocaleHelper.set_translations("de", "de" => { "js" => { "something_MF" => "de mf" } }) - - TranslationOverride.upsert!("en", "js.something_MF", <<~MF.strip) - There { - UNREAD, plural, - =0 {are no} - one {is one unread} - other {are # unread} - } - MF - - v8_ctx.eval(JsLocaleHelper.output_locale("de")) - v8_ctx.eval(JsLocaleHelper.output_client_overrides("de")) - v8_ctx.eval(<<~JS) - for (let [key, value] of Object.entries(I18n._mfOverrides || {})) { - key = key.replace(/^[a-z_]*js\./, ""); - I18n._compiledMFs[key] = value; - } - JS - - expect(v8_ctx.eval("I18n.messageFormat('something_MF', { UNREAD: 1 })")).to eq( - "There is one unread", - ) - end - end - LocaleSiteSetting.values.each do |locale| it "generates valid date helpers for #{locale[:value]} locale" do js = JsLocaleHelper.output_locale(locale[:value]) @@ -281,31 +146,86 @@ RSpec.describe JsLocaleHelper do end end - describe ".find_message_format_locale" do - it "finds locale's message format rules" do - locale, filename = - JsLocaleHelper.find_message_format_locale([:de], fallback_to_english: false) - expect(locale).to eq("de") - expect(filename).to end_with("/de.js") + describe ".output_MF" do + let(:output) { described_class.output_MF(locale).gsub(/^import.*$/, "") } + let(:generated_locales) { v8_ctx.eval("Object.keys(I18n._mfMessages._data)") } + let(:translated_message) do + v8_ctx.eval("I18n._mfMessages.get('posts_likes_MF', {count: 3, ratio: 'med'})") + end + let!(:overriden_translation) do + Fabricate( + :translation_override, + translation_key: "admin_js.admin.user.penalty_history_MF", + value: "OVERRIDEN", + ) end - it "finds locale for en_GB" do - locale, filename = - JsLocaleHelper.find_message_format_locale([:en_GB], fallback_to_english: false) - expect(locale).to eq("en") - expect(filename).to end_with("/en.js") + before { v8_ctx.eval(output) } - locale, filename = - JsLocaleHelper.find_message_format_locale(["en_GB"], fallback_to_english: false) - expect(locale).to eq("en") - expect(filename).to end_with("/en.js") + context "when locale is 'en'" do + let(:locale) { "en" } + + it "generates messages for the 'en' locale only" do + expect(generated_locales).to eq %w[en] + end + + it "translates messages properly" do + expect( + translated_message, + ).to eq "This topic has 3 replies with a very high like to post ratio\n" + end + + context "when the translation is overriden" do + let(:translated_message) do + v8_ctx.eval( + "I18n._mfMessages.get('admin.user.penalty_history_MF', { SUSPENDED: 3, SILENCED: 2 })", + ) + end + + it "returns the overriden translation" do + expect(translated_message).to eq "OVERRIDEN" + end + end end - it "falls back to en when locale doesn't have own message format rules" do - locale, filename = - JsLocaleHelper.find_message_format_locale([:nonexistent], fallback_to_english: true) - expect(locale).to eq("en") - expect(filename).to end_with("/en.js") + context "when locale is not 'en'" do + let(:locale) { "fr" } + + it "generates messages for the current locale and uses 'en' as fallback" do + expect(generated_locales).to match(%w[fr en]) + end + + it "translates messages properly" do + expect( + translated_message, + ).to eq "Ce sujet comprend 3 réponses avec un taux très élevé de « J'aime » par message\n" + end + + context "when a translation is missing" do + before { v8_ctx.eval("delete I18n._mfMessages._data.fr.posts_likes_MF") } + + it "returns the fallback translation" do + expect( + translated_message, + ).to eq "This topic has 3 replies with a very high like to post ratio\n" + end + + context "when the fallback translation is overriden" do + let(:translated_message) do + v8_ctx.eval( + "I18n._mfMessages.get('admin.user.penalty_history_MF', { SUSPENDED: 3, SILENCED: 2 })", + ) + end + + before do + v8_ctx.eval("delete I18n._mfMessages._data.fr['admin.user.penalty_history_MF']") + end + + it "returns the overriden fallback translation" do + expect(translated_message).to eq "OVERRIDEN" + end + end + end end end end diff --git a/spec/lib/plugin/instance_spec.rb b/spec/lib/plugin/instance_spec.rb index 656b52aaac6..7bf68f1d8b9 100644 --- a/spec/lib/plugin/instance_spec.rb +++ b/spec/lib/plugin/instance_spec.rb @@ -523,9 +523,6 @@ TEXT expect(DiscoursePluginRegistry.locales).to have_key(:foo_BAR) expect(locale[:fallbackLocale]).to be_nil - expect(locale[:message_format]).to eq( - ["foo_BAR", "#{plugin_path}/lib/javascripts/locale/message_format/foo_BAR.js"], - ) expect(locale[:moment_js]).to eq( ["foo_BAR", "#{plugin_path}/lib/javascripts/locale/moment_js/foo_BAR.js"], ) @@ -536,9 +533,6 @@ TEXT expect(Rails.configuration.assets.precompile).to include("locales/foo_BAR.js") - expect( - JsLocaleHelper.find_message_format_locale(["foo_BAR"], fallback_to_english: true), - ).to eq(locale[:message_format]) expect(JsLocaleHelper.find_moment_locale(["foo_BAR"])).to eq(locale[:moment_js]) expect(JsLocaleHelper.find_moment_locale(["foo_BAR"], timezone_names: true)).to eq( locale[:moment_js_timezones], @@ -552,9 +546,6 @@ TEXT expect(DiscoursePluginRegistry.locales).to have_key(:tup) expect(locale[:fallbackLocale]).to eq("pt_BR") - expect(locale[:message_format]).to eq( - ["pt_BR", "#{Rails.root}/lib/javascripts/locale/pt_BR.js"], - ) expect(locale[:moment_js]).to eq( ["pt-br", "#{Rails.root}/vendor/assets/javascripts/moment-locale/pt-br.js"], ) @@ -565,9 +556,6 @@ TEXT expect(Rails.configuration.assets.precompile).to include("locales/tup.js") - expect(JsLocaleHelper.find_message_format_locale(["tup"], fallback_to_english: true)).to eq( - locale[:message_format], - ) expect(JsLocaleHelper.find_moment_locale(["tup"])).to eq(locale[:moment_js]) end @@ -578,9 +566,6 @@ TEXT expect(DiscoursePluginRegistry.locales).to have_key(:tlh) expect(locale[:fallbackLocale]).to be_nil - expect(locale[:message_format]).to eq( - ["tlh", "#{plugin_path}/lib/javascripts/locale/message_format/tlh.js"], - ) expect(locale[:moment_js]).to eq( ["tlh", "#{Rails.root}/vendor/assets/javascripts/moment-locale/tlh.js"], ) @@ -588,9 +573,6 @@ TEXT expect(Rails.configuration.assets.precompile).to include("locales/tlh.js") - expect(JsLocaleHelper.find_message_format_locale(["tlh"], fallback_to_english: true)).to eq( - locale[:message_format], - ) expect(JsLocaleHelper.find_moment_locale(["tlh"])).to eq(locale[:moment_js]) end @@ -602,7 +584,6 @@ TEXT %w[ config/locales/client.foo_BAR.yml config/locales/server.foo_BAR.yml - lib/javascripts/locale/message_format/foo_BAR.js lib/javascripts/locale/moment_js/foo_BAR.js assets/locales/foo_BAR.js.erb ].each do |path| diff --git a/spec/models/translation_override_spec.rb b/spec/models/translation_override_spec.rb index d9080f89f79..4d5ff7d5365 100644 --- a/spec/models/translation_override_spec.rb +++ b/spec/models/translation_override_spec.rb @@ -170,20 +170,6 @@ RSpec.describe TranslationOverride do expect(ovr.value).to eq("Click here alert('TEST');") end - it "stores js for a message format key" do - I18n.backend.store_translations(:en, { some: { key_MF: "initial value" } }) - TranslationOverride.upsert!( - "ru", - "some.key_MF", - "{NUM_RESULTS, plural, one {1 result} other {many} }", - ) - - ovr = TranslationOverride.where(locale: "ru", translation_key: "some.key_MF").first - expect(ovr).to be_present - expect(ovr.compiled_js).to start_with("function") - expect(ovr.compiled_js).to_not match(/Invalid Format/i) - end - describe "site cache" do def cached_value(guardian, translation_key, locale:) types_name, name_key, attribute = translation_key.split(".") diff --git a/spec/requests/extra_locales_controller_spec.rb b/spec/requests/extra_locales_controller_spec.rb index a9573510905..c240c293f74 100644 --- a/spec/requests/extra_locales_controller_spec.rb +++ b/spec/requests/extra_locales_controller_spec.rb @@ -46,6 +46,11 @@ RSpec.describe ExtraLocalesController do expect(response.headers["Cache-Control"]).not_to include("max-age", "public", "immutable") end + it "doesn’t generate the bundle twice" do + described_class.expects(:bundle_js).returns("JS").once + get "/extra-locales/admin", params: { v: "a" * 32 } + end + context "with plugin" do before do JsLocaleHelper.clear_cache! @@ -120,7 +125,6 @@ RSpec.describe ExtraLocalesController do ctx.eval("I18n = {};") ctx.eval(response.body) - expect(ctx.eval("typeof I18n._mfOverrides['js.client_MF']")).to eq("function") expect(ctx.eval("I18n._overrides['#{I18n.locale}']['js.some_key']")).to eq( "client-side translation", ) @@ -175,15 +179,18 @@ RSpec.describe ExtraLocalesController do expect(overrides["fr"]).to eq( { "js.some_key" => "some key (fr)", "js.only_fr" => "only French" }, ) - - expect(ctx.eval("Object.keys(I18n._mfOverrides)")).to contain_exactly( - "js.some_client_MF", - "js.only_en_MF", - "js.only_fr_MF", - ) end end end + + context "when requesting MessageFormat translations" do + before { JsLocaleHelper.stubs(:output_MF).with("en").returns("MF_TRANSLATIONS") } + + it "returns the translations properly" do + get "/extra-locales/mf" + expect(response.body).to eq("MF_TRANSLATIONS") + end + end end describe ".bundle_js_hash" do @@ -233,4 +240,14 @@ RSpec.describe ExtraLocalesController do expect(ExtraLocalesController.client_overrides_exist?).to eq(true) end end + + describe ".bundle_js_with_hash" do + before { described_class.stubs(:bundle_js).with("admin").returns("JS") } + + it "returns both JS and its hash for a given bundle" do + expect(described_class.bundle_js_with_hash("admin")).to eq( + ["JS", Digest::MD5.hexdigest("JS")], + ) + end + end end diff --git a/yarn.lock b/yarn.lock index 1a0acd514e9..f93638972ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2204,6 +2204,42 @@ tslib "^2.4.1" upath "^2.0.1" +"@messageformat/core@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@messageformat/core/-/core-3.3.0.tgz#31edd52a5f7d017adad85c929809f07741dcfd3f" + integrity sha512-YcXd3remTDdeMxAlbvW6oV9d/01/DZ8DHUFwSttO3LMzIZj3iO0NRw+u1xlsNNORFI+u0EQzD52ZX3+Udi0T3g== + dependencies: + "@messageformat/date-skeleton" "^1.0.0" + "@messageformat/number-skeleton" "^1.0.0" + "@messageformat/parser" "^5.1.0" + "@messageformat/runtime" "^3.0.1" + make-plural "^7.0.0" + safe-identifier "^0.4.1" + +"@messageformat/date-skeleton@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@messageformat/date-skeleton/-/date-skeleton-1.0.1.tgz#980b8babe21a11433b6e1e8f6dc8c4cae4f5f56b" + integrity sha512-jPXy8fg+WMPIgmGjxSlnGJn68h/2InfT0TNSkVx0IGXgp4ynnvYkbZ51dGWmGySEK+pBiYUttbQdu5XEqX5CRg== + +"@messageformat/number-skeleton@^1.0.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz#e7c245c41a1b2722bc59dad68f4d454f761bc9b4" + integrity sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg== + +"@messageformat/parser@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@messageformat/parser/-/parser-5.1.0.tgz#05e4851c782d633ad735791dd0a68ee65d2a7201" + integrity sha512-jKlkls3Gewgw6qMjKZ9SFfHUpdzEVdovKFtW1qRhJ3WI4FW5R/NnGDqr8SDGz+krWDO3ki94boMmQvGke1HwUQ== + dependencies: + moo "^0.5.1" + +"@messageformat/runtime@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@messageformat/runtime/-/runtime-3.0.1.tgz#94d1f6c43265c28ef7aed98ecfcc0968c6c849ac" + integrity sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg== + dependencies: + make-plural "^7.0.0" + "@mixer/parallel-prettier@^2.0.3": version "2.0.3" resolved "https://registry.yarnpkg.com/@mixer/parallel-prettier/-/parallel-prettier-2.0.3.tgz#970902afcd38c8c71155e1d089e5444896abc253" @@ -3271,11 +3307,6 @@ async@^3.2.3: resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== -async@~0.1.18: - version "0.1.22" - resolved "https://registry.yarnpkg.com/async/-/async-0.1.22.tgz#0fc1aaa088a0e3ef0ebe2d8831bab0dcf8845061" - integrity sha512-2tEzliJmf5fHNafNwQLJXUasGzQCVctvsNkXmnlELHwypU0p08/rHohYvkqKIjyXpx+0rkrYv6QbhJ+UF4QkBg== - async@~0.2.9: version "0.2.10" resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" @@ -4467,11 +4498,6 @@ clone@^2.1.2: resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== -coffee-script@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.2.0.tgz#b5e61e55f1ca8c4a9eb87d53aa0657ea43125b91" - integrity sha512-vHxLlDOeI7/S+R/fr28ZjAhL3g+qcI+YbN0/S3N3yZa2aTh65XwFfbkeje+R3uSu1yQgXW2NvrzYJ7nznvRQaQ== - collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" @@ -7412,15 +7438,6 @@ glob@^8.0.3, glob@^8.1.0: minimatch "^5.0.1" once "^1.3.0" -glob@~3.1.9: - version "3.1.21" - resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" - integrity sha512-ANhy2V2+tFpRajE3wN4DhkNQ08KDr0Ir1qL12/cUe5+a7STEK8jkW4onUYuY8/06qAFuT5je7mjAqzx0eKI2tQ== - dependencies: - graceful-fs "~1.2.0" - inherits "1" - minimatch "~0.2.11" - global-modules@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" @@ -7544,11 +7561,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graceful-fs@~1.2.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" - integrity sha512-iiTUZ5vZ+2ZV+h71XAgwCSu6+NAizhFU3Yw8aC/hH5SQ3SnISqEqAek40imAFGtDcwJKNhXvSY+hzIolnLwcdQ== - graphemer@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" @@ -7926,11 +7938,6 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" - integrity sha512-Al67oatbRSo3RV5hRqIoln6Y5yMVbJSIn4jEJNL7VCImzq/kLr7vvb6sFRJXqr8rpHc/2kJOM+y0sPKN47VdzA== - inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" @@ -9107,11 +9114,6 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" -lru-cache@2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - integrity sha512-WpibWJ60c3AgAz8a2iYErDrcT2C7OmKnsWhIcHOjkUHFjkXncJhtLxNSqUmxRxRunpb5I8Vprd7aNSd2NtksJQ== - lru-cache@^10.2.0: version "10.2.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" @@ -9155,6 +9157,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-plural@^7.0.0, make-plural@^7.4.0: + version "7.4.0" + resolved "https://registry.yarnpkg.com/make-plural/-/make-plural-7.4.0.tgz#fa6990dd550dea4de6b20163f74e5ed83d8a8d6d" + integrity sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg== + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -9329,18 +9336,6 @@ message-bus-client@^4.3.8: resolved "https://registry.yarnpkg.com/message-bus-client/-/message-bus-client-4.3.8.tgz#5ee23c03236b250b13613034764a87881c350d4e" integrity sha512-Vvrs0tOx5YcKeEoh7l1zATLVKt49FK34Vq/sloRbgDDQUB6VAbSVJPvH8RVxQ/PZGb9ScGzCMJCMnDyVa2p8CQ== -messageformat@0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/messageformat/-/messageformat-0.1.5.tgz#c7c561de181b04ef0fad36ca89c5cb942e5bb75c" - integrity sha512-Ppf1WSwINnNdYUnbQnMaRj/3zY+QFJsGf/a2o98a62t3DX7TtUjn9O9CFDxiCkJPPtayf1W0YfvaTDYX6cBxBQ== - dependencies: - async "~0.1.18" - coffee-script "~1.2.0" - glob "~3.1.9" - nopt "~2.0.0" - underscore "~1.3.1" - watchr "~1.0.0" - methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -9454,14 +9449,6 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimatch@~0.2.11: - version "0.2.14" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" - integrity sha512-zZ+Jy8lVWlvqqeM8iZB7w7KmQkoJn8djM585z88rywrEbzoqawVa9FR5p2hwD+y74nfuKOjmNvi9gtWJNLqHvA== - dependencies: - lru-cache "2" - sigmund "~1.0.0" - minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -9527,6 +9514,11 @@ moment@2.30.1, moment@^2.29.4: resolved "https://registry.yarnpkg.com/moment/-/moment-2.30.1.tgz#f8c91c07b7a786e30c59926df530b4eac96974ae" integrity sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how== +moo@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.2.tgz#f9fe82473bc7c184b0d32e2215d3f6e67278733c" + integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q== + morgan@^1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" @@ -9721,13 +9713,6 @@ nopt@^3.0.6: dependencies: abbrev "1" -nopt@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.0.0.tgz#ca7416f20a5e3f9c3b86180f96295fa3d0b52e0d" - integrity sha512-uVTsuT8Hm3aN3VttY+BPKw4KU9lVpI0F22UAr/I1r6+kugMr3oyhMALkycikLcdfvGRsgzCYN48DYLBFcJEUVg== - dependencies: - abbrev "1" - normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -11022,6 +11007,11 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@^5.1.2, resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-identifier@^0.4.1: + version "0.4.2" + resolved "https://registry.yarnpkg.com/safe-identifier/-/safe-identifier-0.4.2.tgz#cf6bfca31c2897c588092d1750d30ef501d59fcb" + integrity sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w== + safe-json-parse@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/safe-json-parse/-/safe-json-parse-1.0.1.tgz#3e76723e38dfdda13c9b1d29a1e07ffee4b30b57" @@ -11262,11 +11252,6 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -sigmund@~1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" - integrity sha512-fCvEXfh6NWpm+YSuY2bpXb/VIihqWA6hLsgboC+0nl71Q7N7o2eaCW8mJa/NLvQhs6jpd3VZV4UiUQlV6+lc8g== - signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" @@ -12298,11 +12283,6 @@ underscore@>=1.8.3, underscore@~1.13.2: resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.6.tgz#04786a1f589dc6c09f761fc5f45b89e935136441" integrity sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A== -underscore@~1.3.1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.3.3.tgz#47ac53683daf832bfa952e1774417da47817ae42" - integrity sha512-ddgUaY7xyrznJ0tbSUZgvNdv5qbiF6XcUBTrHgdCOVUrxJYWozD5KyiRjtIwds1reZ7O1iPLv5rIyqnVAcS6gg== - unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -12620,11 +12600,6 @@ watchpack@^2.4.1: glob-to-regexp "^0.4.1" graceful-fs "^4.1.2" -watchr@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/watchr/-/watchr-1.0.0.tgz#ce023fd59edae9430523031915c1812ff2302c27" - integrity sha512-qaR72INd8EsMZ63VY+o91n6KHyy4gPUb0td2vsQQEWfgApg5NxXWBSh3zSXWoaJc7PS3imSNPggiHnQJfKxpNQ== - wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"