discourse/lib/js_locale_helper.rb
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

287 lines
9.5 KiB
Ruby

# frozen_string_literal: true
module JsLocaleHelper
def self.plugin_client_files(locale_str)
Dir["#{Rails.root}/plugins/*/config/locales/client.#{locale_str}.yml"]
end
def self.reloadable_plugins(locale, ctx)
return unless Rails.env.development?
plugin_client_files(locale.to_s).each do |file|
ctx.depend_on(file)
end
end
def self.plugin_translations(locale_str)
@plugin_translations ||= HashWithIndifferentAccess.new
@plugin_translations[locale_str] ||= begin
translations = {}
plugin_client_files(locale_str).each do |file|
if plugin_translations = YAML.load_file(file)[locale_str]
translations.deep_merge!(plugin_translations)
end
end
translations
end
end
def self.load_translations(locale)
@loaded_translations ||= HashWithIndifferentAccess.new
@loaded_translations[locale] ||= begin
locale_str = locale.to_s
# load default translations
yml_file = "#{Rails.root}/config/locales/client.#{locale_str}.yml"
if File.exist?(yml_file)
translations = YAML.load_file(yml_file)
else
# If we can't find a base file in Discourse, it might only exist in a plugin
# so let's start with a basic object we can merge into
translations = {
locale_str => {
'js' => {},
'admin_js' => {},
'wizard_js' => {}
}
}
end
# merge translations (plugin translations overwrite default translations)
if translations[locale_str] && plugin_translations(locale_str)
translations[locale_str]['js'] ||= {}
translations[locale_str]['admin_js'] ||= {}
translations[locale_str]['wizard_js'] ||= {}
translations[locale_str]['js'].deep_merge!(plugin_translations(locale_str)['js']) if plugin_translations(locale_str)['js']
translations[locale_str]['admin_js'].deep_merge!(plugin_translations(locale_str)['admin_js']) if plugin_translations(locale_str)['admin_js']
translations[locale_str]['wizard_js'].deep_merge!(plugin_translations(locale_str)['wizard_js']) if plugin_translations(locale_str)['wizard_js']
end
translations
end
end
# deeply removes keys from "deleting_from" that are already present in "checking_hashes"
def self.deep_delete_matches(deleting_from, checking_hashes)
checking_hashes.compact!
new_hash = deleting_from.dup
deleting_from.each do |key, value|
if value.is_a?(Hash)
new_at_key = deep_delete_matches(deleting_from[key], checking_hashes.map { |h| h[key] })
if new_at_key.empty?
new_hash.delete(key)
else
new_hash[key] = new_at_key
end
else
if checking_hashes.any? { |h| h.include?(key) }
new_hash.delete(key)
end
end
end
new_hash
end
def self.load_translations_merged(*locales)
locales = locales.uniq.compact
@loaded_merges ||= {}
@loaded_merges[locales.join('-')] ||= begin
all_translations = {}
merged_translations = {}
loaded_locales = []
locales.map(&:to_s).each do |locale|
all_translations[locale] = load_translations(locale)
merged_translations[locale] = deep_delete_matches(all_translations[locale][locale], loaded_locales.map { |l| merged_translations[l] })
loaded_locales << locale
end
merged_translations
end
end
def self.translations_for(locale_str)
current_locale = I18n.locale
locale_sym = locale_str.to_sym
site_locale = SiteSetting.default_locale.to_sym
fallback_locale = LocaleSiteSetting.fallback_locale(locale_str)
I18n.locale = locale_sym
if Rails.env.development?
@loaded_translations = nil
@plugin_translations = nil
@loaded_merges = nil
end
translations =
if locale_sym == :en
load_translations(locale_sym)
elsif locale_sym == site_locale || site_locale == :en
load_translations_merged(locale_sym, fallback_locale, :en)
else
load_translations_merged(locale_sym, fallback_locale, site_locale, :en)
end
I18n.locale = current_locale
translations
end
def self.output_locale(locale)
locale_str = locale.to_s
fallback_locale_str = LocaleSiteSetting.fallback_locale(locale_str)&.to_s
translations = Marshal.load(Marshal.dump(translations_for(locale_str)))
message_formats = strip_out_message_formats!(translations[locale_str]['js'])
message_formats.merge!(strip_out_message_formats!(translations[locale_str]['admin_js']))
mf_locale, mf_filename = find_message_format_locale([locale_str], fallback_to_english: true)
result = generate_message_format(message_formats, mf_locale, mf_filename)
translations.keys.each do |l|
translations[l].keys.each do |k|
translations[l].delete(k) unless k == "js"
end
end
# I18n
result << "I18n.translations = #{translations.to_json};\n"
result << "I18n.locale = '#{locale_str}';\n"
result << "I18n.fallbackLocale = '#{fallback_locale_str}';\n" if fallback_locale_str && fallback_locale_str != "en"
result << "I18n.pluralizationRules.#{locale_str} = MessageFormat.locale.#{mf_locale};\n" if mf_locale != "en"
# moment
result << File.read("#{Rails.root}/vendor/assets/javascripts/moment.js")
result << File.read("#{Rails.root}/vendor/assets/javascripts/moment-timezone-with-data.js")
result << moment_locale(locale_str)
result << moment_locale(locale_str, timezone_names: true)
result << moment_formats
result
end
MOMENT_LOCALE_MAPPING ||= {
"hy" => "hy-am"
}
def self.find_moment_locale(locale_chain, timezone_names: false)
if timezone_names
path = "#{Rails.root}/vendor/assets/javascripts/moment-timezone-names-locale"
type = :moment_js_timezones
else
path = "#{Rails.root}/vendor/assets/javascripts/moment-locale"
type = :moment_js
end
find_locale(locale_chain, path, type, fallback_to_english: false) do |locale|
locale = MOMENT_LOCALE_MAPPING[locale] if MOMENT_LOCALE_MAPPING.key?(locale)
# moment.js uses a different naming scheme for locale files
locale.tr('_', '-').downcase
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.each do |locale|
plugin_locale = DiscoursePluginRegistry.locales[locale]
return plugin_locale[type] if plugin_locale&.has_key?(type)
locale = yield(locale) if block_given?
filename = File.join(path, "#{locale}.js")
return [locale, filename] if File.exist?(filename)
end
locale_chain.map! { |locale| yield(locale) } if block_given?
# try again, but this time only with the language itself
locale_chain = locale_chain.map { |l| l.split(/[-_]/)[0] }
.uniq.reject { |l| locale_chain.include?(l) }
if locale_chain.any?
locale_data = find_locale(locale_chain, path, type, fallback_to_english: false)
return locale_data if locale_data
end
# English should alyways work
["en", File.join(path, "en.js")] if fallback_to_english
end
def self.moment_formats
result = +""
result << moment_format_function('short_date_no_year')
result << moment_format_function('short_date')
result << moment_format_function('long_date')
result << "moment.fn.relativeAge = function(opts){ return Discourse.Formatter.relativeAge(this.toDate(), opts)};\n"
end
def self.moment_format_function(name)
format = I18n.t("dates.#{name}")
"moment.fn.#{name.camelize(:lower)} = function(){ return this.format('#{format}'); };\n"
end
def self.moment_locale(locale, timezone_names: false)
_, filename = find_moment_locale([locale], timezone_names: timezone_names)
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"
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: 15000)
ctx.load("#{Rails.root}/lib/javascripts/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.strip_out_message_formats!(hash, prefix = "", rval = {})
if hash.is_a?(Hash)
hash.each do |key, value|
if value.is_a?(Hash)
rval.merge!(strip_out_message_formats!(value, prefix + (prefix.length > 0 ? "." : "") << key, rval))
elsif key.to_s.end_with?("_MF")
rval[prefix + (prefix.length > 0 ? "." : "") << key] = value
hash.delete(key)
end
end
end
rval
end
end