mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
DEV: Upgrade the MessageFormat library (JS)
This patch upgrades the MessageFormat library to version 3.3.0 from 0.1.5. Our `I18n.messageFormat` method signature is unchanged, and now uses the new API under the hood. We don’t need dedicated locale files for handling pluralization rules anymore as everything is now included by the library itself. The compilation of the messages now happens through our `messageformat-wrapper` gem. It then outputs an ES module that includes all its needed dependencies. Most of the changes happen in `JsLocaleHelper` and in the `ExtraLocales` controller. A new method called `.output_MF` has been introduced in `JsLocaleHelper`. It handles all the fetching, compiling and transpiling to generate the proper MF messages in JS. Overrides and fallbacks are also handled directly in this method. The other main change is that now the MF translations are served through the `ExtraLocales` controller instead of being statically compiled in a JS file, then having to patch the messages using overrides and fallbacks. Now the MF translations are just another bundle that is created on the fly and cached by the client.
This commit is contained in:
committed by
Loïc Guitaut
parent
6591a0654b
commit
301713ef96
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user