PERF: Much more performant, multisite aware I18n overrides

This commit is contained in:
Robin Ward 2015-11-19 16:36:59 -05:00
parent 711a7a146c
commit e168c5fde3
10 changed files with 108 additions and 36 deletions

View file

@ -3,5 +3,6 @@
require 'i18n/backend/discourse_i18n'
I18n.backend = I18n::Backend::DiscourseI18n.new
I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) }
I18n.reload!
MessageBus.subscribe("/i18n-flush") { I18n.reload! }

View file

@ -2,7 +2,7 @@ class AddLoungeCategory < ActiveRecord::Migration
def up
return if Rails.env.test?
I18n.backend.overrides_disabled do
I18n.overrides_disabled do
result = Category.exec_sql "SELECT 1 FROM site_settings where name = 'lounge_category_id'"
if result.count == 0
description = I18n.t('vip_category_description')

View file

@ -2,7 +2,7 @@ class AddMetaCategory < ActiveRecord::Migration
def up
return if Rails.env.test?
I18n.backend.overrides_disabled do
I18n.overrides_disabled do
result = Category.exec_sql "SELECT 1 FROM site_settings where name = 'meta_category_id'"
if result.count == 0
description = I18n.t('meta_category_description')

View file

@ -2,7 +2,7 @@ class AddStaffCategory < ActiveRecord::Migration
def up
return if Rails.env.test?
I18n.backend.overrides_disabled do
I18n.overrides_disabled do
result = Category.exec_sql "SELECT 1 FROM site_settings where name = 'staff_category_id'"
if result.count == 0
description = I18n.t('staff_category_description')

View file

@ -1,6 +1,6 @@
class FixTosName < ActiveRecord::Migration
def up
I18n.backend.overrides_disabled do
I18n.overrides_disabled do
execute ActiveRecord::Base.sql_fragment('UPDATE user_fields SET name = ? WHERE name = ?', I18n.t('terms_of_service.title'), I18n.t("terms_of_service.signup_form_message"))
end

View file

@ -1,7 +1,7 @@
class MigrateOldModeratorPosts < ActiveRecord::Migration
def migrate_key(action_code)
I18n.backend.overrides_disabled do
I18n.overrides_disabled do
text = I18n.t("topic_statuses.#{action_code.gsub('.', '_')}")
execute "UPDATE posts SET action_code = '#{action_code}', raw = '', cooked = '', post_type = 3 where post_type = 2 AND raw = #{ActiveRecord::Base.connection.quote(text)}"

View file

@ -1,6 +1,6 @@
class MigrateAutoClosePosts < ActiveRecord::Migration
def up
I18n.backend.overrides_disabled do
I18n.overrides_disabled do
strings = []
%w(days hours lastpost_days lastpost_hours lastpost_minutes).map do |k|
strings << I18n.t("topic_statuses.autoclosed_enabled_#{k}.one")

View file

@ -19,6 +19,10 @@ module I18n
def reload!
@loaded_locales = []
@cache = nil
@overrides_enabled = true
@overrides_by_site = {}
reload_no_cache!
end
@ -48,18 +52,60 @@ module I18n
load_locale(locale) unless @loaded_locales.include?(locale)
end
def translate(key, *args)
load_locale(config.locale) unless @loaded_locales.include?(config.locale)
# 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
def translate_no_override(key, *args)
return translate_no_cache(key, *args) if args.length > 0
@cache ||= LruRedux::ThreadSafeCache.new(LRU_CACHE_SIZE)
k = "#{key}#{config.locale}#{config.backend.object_id}#{RailsMultisite::ConnectionManagement.current_db}"
k = "#{key}#{config.locale}#{config.backend.object_id}"
@cache.getset(k) do
translate_no_cache(key).freeze
end
end
def translate(key, *args)
load_locale(config.locale) unless @loaded_locales.include?(config.locale)
if @overrides_enabled
site = RailsMultisite::ConnectionManagement.current_db
by_site = @overrides_by_site[site]
by_locale = nil
unless by_site
by_site = @overrides_by_site[site] = {}
# Load overrides
TranslationOverride.where(locale: locale).pluck(:translation_key, :value).each do |tuple|
by_locale = by_site[locale] ||= {}
by_locale[tuple[0]] = tuple[1]
end
end
by_locale = by_site[config.locale]
if by_locale
if args.size > 0 && args[0].is_a?(Hash)
args[0][:overrides] = by_locale
return backend.translate(config.locale, key, args[0])
end
if result = by_locale[key]
return result
end
end
end
translate_no_override(key, *args)
end
alias_method :t, :translate
end
end

View file

@ -6,10 +6,6 @@ module I18n
include I18n::Backend::Fallbacks
include I18n::Backend::Pluralization
def initialize
@overrides_enabled = true
end
def available_locales
# in case you are wondering this is:
# Dir.glob( File.join(Rails.root, 'config', 'locales', 'client.*.yml') )
@ -29,22 +25,10 @@ module I18n
return site_overrides[locale] if site_overrides[locale]
locale_overrides = site_overrides[locale] = {}
TranslationOverride.where(locale: locale).pluck(:translation_key, :value).each do |tuple|
locale_overrides[tuple[0]] = tuple[1]
end
locale_overrides
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
# force explicit loading
def load_translations(*filenames)
unless filenames.empty?
@ -56,8 +40,22 @@ module I18n
[locale, SiteSetting.default_locale.to_sym, :en].uniq.compact
end
def translate(locale, key, options = {})
(@overrides_enabled && overrides_for(locale)[key]) || super(locale, key, options)
def lookup(locale, key, scope = [], options = {})
# Support interpolation and pluralization of overrides
if options[:overrides]
if options[:count]
result = {}
options[:overrides].each do |k, v|
result[k.split('.').last.to_sym] = v if k != key && k.start_with?(key.to_s)
end
return result if result.size > 0
end
return options[:overrides][key] if options[:overrides][key]
end
super(locale, key, scope, options)
end
def exists?(locale, key)

View file

@ -7,17 +7,22 @@ describe I18n::Backend::DiscourseI18n do
let(:backend) { I18n::Backend::DiscourseI18n.new }
before do
backend.reload!
backend.store_translations(:en, :foo => 'Foo in :en', :bar => 'Bar in :en')
I18n.reload!
backend.store_translations(:en, :foo => 'Foo in :en', :bar => 'Bar in :en', :wat => "Hello %{count}")
backend.store_translations(:en, :items => {:one => 'one item', :other => "%{count} items" })
backend.store_translations(:de, :bar => 'Bar in :de')
backend.store_translations(:'de-AT', :baz => 'Baz in :de-AT')
end
after do
I18n.reload!
end
it 'translates the basics as expected' do
expect(backend.translate(:en, 'foo')).to eq("Foo in :en")
expect(backend.translate(:en, 'items', count: 1)).to eq("one item")
expect(backend.translate(:en, 'items', count: 3)).to eq("3 items")
expect(backend.translate(:en, 'wat', count: 3)).to eq("Hello 3")
end
describe '#exists?' do
@ -53,16 +58,38 @@ describe I18n::Backend::DiscourseI18n do
end
describe 'with overrides' do
before do
it 'returns the overriden key' do
TranslationOverride.upsert!('en', 'foo', 'Overwritten foo')
end
it 'returns the overrided key' do
expect(backend.translate(:en, 'foo')).to eq('Overwritten foo')
expect(I18n.translate('foo')).to eq('Overwritten foo')
TranslationOverride.upsert!('en', 'foo', 'new value')
backend.reload!
expect(backend.translate(:en, 'foo')).to eq('new value')
I18n.reload!
expect(I18n.translate('foo')).to eq('new value')
end
it 'supports disabling' do
TranslationOverride.upsert!('en', 'foo', 'meep')
I18n.overrides_disabled do
expect(I18n.translate('foo')).to eq('meep')
end
end
it 'supports interpolation' do
TranslationOverride.upsert!('en', 'foo', 'hello %{world}')
expect(I18n.translate('foo', world: 'foo')).to eq('hello foo')
end
it 'supports interpolation named count' do
TranslationOverride.upsert!('en', 'wat', 'goodbye %{count}')
expect(I18n.translate('wat', count: 123)).to eq('goodbye 123')
end
it 'supports one and other' do
TranslationOverride.upsert!('en', 'items.one', 'one fish')
TranslationOverride.upsert!('en', 'items.other', '%{count} fishies')
expect(I18n.translate('items', count: 13)).to eq('13 fishies')
expect(I18n.translate('items', count: 1)).to eq('one fish')
end
end