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' require 'i18n/backend/discourse_i18n'
I18n.backend = I18n::Backend::DiscourseI18n.new I18n.backend = I18n::Backend::DiscourseI18n.new
I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) } I18n.config.missing_interpolation_argument_handler = proc { throw(:exception) }
I18n.reload!
MessageBus.subscribe("/i18n-flush") { I18n.reload! } MessageBus.subscribe("/i18n-flush") { I18n.reload! }

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
class FixTosName < ActiveRecord::Migration class FixTosName < ActiveRecord::Migration
def up 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")) 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 end

View File

@ -1,7 +1,7 @@
class MigrateOldModeratorPosts < ActiveRecord::Migration class MigrateOldModeratorPosts < ActiveRecord::Migration
def migrate_key(action_code) def migrate_key(action_code)
I18n.backend.overrides_disabled do I18n.overrides_disabled do
text = I18n.t("topic_statuses.#{action_code.gsub('.', '_')}") 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)}" 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 class MigrateAutoClosePosts < ActiveRecord::Migration
def up def up
I18n.backend.overrides_disabled do I18n.overrides_disabled do
strings = [] strings = []
%w(days hours lastpost_days lastpost_hours lastpost_minutes).map do |k| %w(days hours lastpost_days lastpost_hours lastpost_minutes).map do |k|
strings << I18n.t("topic_statuses.autoclosed_enabled_#{k}.one") strings << I18n.t("topic_statuses.autoclosed_enabled_#{k}.one")

View File

@ -19,6 +19,10 @@ module I18n
def reload! def reload!
@loaded_locales = [] @loaded_locales = []
@cache = nil @cache = nil
@overrides_enabled = true
@overrides_by_site = {}
reload_no_cache! reload_no_cache!
end end
@ -48,18 +52,60 @@ module I18n
load_locale(locale) unless @loaded_locales.include?(locale) load_locale(locale) unless @loaded_locales.include?(locale)
end end
def translate(key, *args) # In some environments such as migrations we don't want to use overrides.
load_locale(config.locale) unless @loaded_locales.include?(config.locale) # 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 return translate_no_cache(key, *args) if args.length > 0
@cache ||= LruRedux::ThreadSafeCache.new(LRU_CACHE_SIZE) @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 @cache.getset(k) do
translate_no_cache(key).freeze translate_no_cache(key).freeze
end end
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 alias_method :t, :translate
end end
end end

View File

@ -6,10 +6,6 @@ module I18n
include I18n::Backend::Fallbacks include I18n::Backend::Fallbacks
include I18n::Backend::Pluralization include I18n::Backend::Pluralization
def initialize
@overrides_enabled = true
end
def available_locales def available_locales
# in case you are wondering this is: # in case you are wondering this is:
# Dir.glob( File.join(Rails.root, 'config', 'locales', 'client.*.yml') ) # Dir.glob( File.join(Rails.root, 'config', 'locales', 'client.*.yml') )
@ -29,22 +25,10 @@ module I18n
return site_overrides[locale] if site_overrides[locale] return site_overrides[locale] if site_overrides[locale]
locale_overrides = 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 locale_overrides
end 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 # force explicit loading
def load_translations(*filenames) def load_translations(*filenames)
unless filenames.empty? unless filenames.empty?
@ -56,8 +40,22 @@ module I18n
[locale, SiteSetting.default_locale.to_sym, :en].uniq.compact [locale, SiteSetting.default_locale.to_sym, :en].uniq.compact
end end
def translate(locale, key, options = {}) def lookup(locale, key, scope = [], options = {})
(@overrides_enabled && overrides_for(locale)[key]) || super(locale, key, 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 end
def exists?(locale, key) def exists?(locale, key)

View File

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