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"