# frozen_string_literal: true # This patch performs 2 functions # # 1. It caches all translations which drastically improves # translation performance in an LRU cache # # 2. It patches I18n so it only loads the translations it needs # on demand # # This patch depends on the convention that locale yml files must be named [locale_name].yml module I18n # this accelerates translation a tiny bit (halves the time it takes) class << self alias_method :translate_no_cache, :translate alias_method :exists_no_cache?, :exists? alias_method :reload_no_cache!, :reload! alias_method :locale_no_cache=, :locale= LRU_CACHE_SIZE = 400 def init_accelerator!(overrides_enabled: true) @overrides_enabled = overrides_enabled execute_reload end def reload! @requires_reload = true end LOAD_MUTEX = Mutex.new def load_locale(locale) locale = locale.to_sym LOAD_MUTEX.synchronize do return if @loaded_locales.include?(locale) if @loaded_locales.empty? # load all rb files I18n.backend.load_translations(I18n.load_path.grep(/\.rb$/)) # load plural rules from plugins DiscoursePluginRegistry.locales.each do |plugin_locale, options| if options[:plural] I18n.backend.store_translations( plugin_locale, i18n: { plural: options[:plural] } ) end end end # load it I18n.backend.load_translations(I18n.load_path.grep(/\.#{Regexp.escape locale}\.yml$/)) if Rails.env.development? I18n.backend.load_translations(I18n.load_path.grep(/.*faker.*\/#{Regexp.escape locale}\.yml$/)) I18n.backend.load_translations(I18n.load_path.grep(/.*faker.*\/#{Regexp.escape locale}\/.*\.yml$/)) end @loaded_locales << locale end end def ensure_all_loaded! I18n.fallbacks[locale].each { |l| ensure_loaded!(l) } end def search(query, opts = {}) execute_reload if @requires_reload locale = (opts[:locale] || config.locale).to_sym load_locale(locale) unless @loaded_locales.include?(locale) opts ||= {} target = opts[:backend] || backend results = opts[:overridden] ? {} : target.search(locale, query) regexp = I18n::Backend::DiscourseI18n.create_search_regexp(query) (overrides_by_locale(locale) || {}).each do |k, v| results.delete(k) results[k] = v if (k =~ regexp || v =~ regexp) end results end def ensure_loaded!(locale) locale = locale.to_sym @loaded_locales ||= [] load_locale(locale) unless @loaded_locales.include?(locale) end # In some environments such as migrations we don't want to use overrides. # Use this to disable them over a block of ruby code def overrides_disabled @overrides_enabled = false yield ensure @overrides_enabled = true end class MissingTranslation; end def translate_no_override(key, options) # note we skip cache for :format and :count should_raise = false locale = nil dup_options = nil if options dup_options = options.dup should_raise = dup_options.delete(:raise) locale = dup_options.delete(:locale) end if dup_options.present? return translate_no_cache(key, **options) end locale ||= config.locale locale = locale.to_sym @cache ||= LruRedux::ThreadSafeCache.new(LRU_CACHE_SIZE) k = "#{key}#{locale}#{config.backend.object_id}" val = @cache.getset(k) do begin translate_no_cache(key, locale: locale, raise: true).freeze rescue I18n::MissingTranslationData MissingTranslation end end if val != MissingTranslation val elsif should_raise raise I18n::MissingTranslationData.new(locale, key) else -"translation missing: #{locale}.#{key}" end end def overrides_by_locale(locale) return unless @overrides_enabled return {} if GlobalSetting.skip_db? locale = locale.to_sym execute_reload if @requires_reload site = RailsMultisite::ConnectionManagement.current_db by_site = @overrides_by_site[site] by_site ||= {} if !by_site.has_key?(locale) # Load overrides translations_overrides = TranslationOverride.where(locale: locale).pluck(:translation_key, :value, :compiled_js) if translations_overrides.empty? by_site[locale] = {} else translations_overrides.each do |tuple| by_locale = by_site[locale] ||= {} by_locale[tuple[0]] = tuple[2] || tuple[1] end end @overrides_by_site[site] = by_site end by_site[locale].with_indifferent_access rescue ActiveRecord::StatementInvalid => e if PG::UndefinedTable === e.cause || PG::UndefinedColumn === e.cause {} else raise end end def translate(*args) execute_reload if @requires_reload options = args.last.is_a?(Hash) ? args.pop.dup : {} key = args.shift locale = (options[:locale] || config.locale).to_sym load_locale(locale) unless @loaded_locales.include?(locale) if @overrides_enabled overrides = {} # for now lets do all the expensive work for keys with count # no choice really has_override = !!options[:count] I18n.fallbacks[locale].each do |l| override = overrides[l] = overrides_by_locale(l) has_override ||= override.key?(key) end if has_override && overrides.present? if options.present? options[:overrides] = overrides # I18n likes to use throw... catch(:exception) do return backend.translate(locale, key, options) end else overrides.each do |_k, v| if result = v[key] return result end end end end end translate_no_override(key, options) end alias_method :t, :translate def exists?(key, locale = nil) execute_reload if @requires_reload locale ||= config.locale locale = locale.to_sym load_locale(locale) unless @loaded_locales.include?(locale) exists_no_cache?(key, locale) end def locale=(value) value = value.to_sym execute_reload if @requires_reload self.locale_no_cache = value end private RELOAD_MUTEX = Mutex.new def execute_reload RELOAD_MUTEX.synchronize do return unless @requires_reload @loaded_locales = [] @cache = nil @overrides_by_site = {} reload_no_cache! ensure_all_loaded! @requires_reload = false end end end end