PERF: Much more performant, multisite aware I18n overrides
This commit is contained in:
parent
711a7a146c
commit
e168c5fde3
|
@ -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! }
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)}"
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue