Skip to content

Commit

Permalink
Allow to prevent calls outside of I18n context by setting locale to `…
Browse files Browse the repository at this point in the history
…false`
  • Loading branch information
byroot committed Feb 28, 2019
1 parent 70daba7 commit 5eeaad7
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 27 deletions.
60 changes: 34 additions & 26 deletions lib/i18n.rb
Original file line number Diff line number Diff line change
Expand Up @@ -176,23 +176,26 @@ def eager_load!
# from the argument values passed to #translate. Therefor your lambdas should
# always return the same translations/values per unique combination of argument
# values.
def translate(*args)
options = args.last.is_a?(Hash) ? args.pop.dup : {}
key = args.shift
backend = config.backend
locale = options.delete(:locale) || config.locale
handling = options.delete(:throw) && :throw || options.delete(:raise) && :raise # TODO deprecate :raise

def translate(key = nil, *, throw: false, raise: false, locale: nil, **options) # TODO deprecate :raise
locale ||= config.locale
raise Disabled.new('t') if locale == false
enforce_available_locales!(locale)

backend = config.backend

result = catch(:exception) do
if key.is_a?(Array)
key.map { |k| backend.translate(locale, k, options) }
else
backend.translate(locale, key, options)
end
end
result.is_a?(MissingTranslation) ? handle_exception(handling, result, locale, key, options) : result

if result.is_a?(MissingTranslation)
handle_exception((throw && :throw || raise && :raise), result, locale, key, options)
else
result
end
end
alias :t :translate

Expand All @@ -204,7 +207,9 @@ def translate!(key, options = EMPTY_HASH)
alias :t! :translate!

# Returns true if a translation exists for a given key, otherwise returns false.
def exists?(key, locale = config.locale)
def exists?(key, _locale = nil, locale: _locale)
locale ||= config.locale
raise Disabled.new('exists?') if locale == false
raise I18n::ArgumentError if key.is_a?(String) && key.empty?
config.backend.exists?(locale, key)
end
Expand Down Expand Up @@ -260,37 +265,40 @@ def exists?(key, locale = config.locale)
# I18n.transliterate("Jürgen") # => "Juergen"
# I18n.transliterate("Jürgen", :locale => :en) # => "Jurgen"
# I18n.transliterate("Jürgen", :locale => :de) # => "Juergen"
def transliterate(*args)
options = args.pop.dup if args.last.is_a?(Hash)
key = args.shift
locale = options && options.delete(:locale) || config.locale
handling = options && (options.delete(:throw) && :throw || options.delete(:raise) && :raise)
replacement = options && options.delete(:replacement)
def transliterate(key, *, throw: false, raise: false, locale: nil, replacement: nil, **options)
locale ||= config.locale
raise Disabled.new('transliterate') if locale == false
enforce_available_locales!(locale)

config.backend.transliterate(locale, key, replacement)
rescue I18n::ArgumentError => exception
handle_exception(handling, exception, locale, key, options || {})
handle_exception((throw && :throw || raise && :raise), exception, locale, key, options)
end

# Localizes certain objects, such as dates and numbers to local formatting.
def localize(object, options = nil)
options = options ? options.dup : {}
locale = options.delete(:locale) || config.locale
format = options.delete(:format) || :default
def localize(object, locale: nil, format: nil, **options)
locale ||= config.locale
raise Disabled.new('l') if locale == false
enforce_available_locales!(locale)

format ||= :default
config.backend.localize(locale, object, format, options)
end
alias :l :localize

# Executes block with given I18n.locale set.
def with_locale(tmp_locale = nil)
if tmp_locale
if tmp_locale == nil
yield
else
current_locale = self.locale
self.locale = tmp_locale
self.locale = tmp_locale
begin
yield
ensure
self.locale = current_locale
end
end
yield
ensure
self.locale = current_locale if tmp_locale
end

# Merges the given locale, key and scope into a single array of keys.
Expand All @@ -314,7 +322,7 @@ def locale_available?(locale)

# Raises an InvalidLocale exception when the passed locale is not available.
def enforce_available_locales!(locale)
if config.enforce_available_locales
if locale != false && config.enforce_available_locales
raise I18n::InvalidLocale.new(locale) if !locale_available?(locale)
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/i18n/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class Config
# The only configuration value that is not global and scoped to thread is :locale.
# It defaults to the default_locale.
def locale
defined?(@locale) && @locale ? @locale : default_locale
defined?(@locale) && @locale != nil ? @locale : default_locale
end

# Sets the current locale pseudo-globally, i.e. in the Thread.current hash.
Expand Down
14 changes: 14 additions & 0 deletions lib/i18n/exceptions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ def call(exception, _locale, _key, _options)

class ArgumentError < ::ArgumentError; end

class Disabled < ArgumentError
def initialize(method)
super(<<~MESSAGE)
I18n.#{method} is currently disabled, likely because your application is still in its loading phase.
This method is meant to display text in the user locale, so calling it before the user locale has
been set is likely to display text from the wrong locale to some users.
If you have a legitimate reason to access i18n data outside of the user flow, you can do so by passing
the desired locale explictly with the `locale` argument, e.g. `I18n.#{method}(..., locale: :en)`
MESSAGE
end
end

class InvalidLocale < ArgumentError
attr_reader :locale
def initialize(locale)
Expand Down
28 changes: 28 additions & 0 deletions test/i18n_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,16 @@ def setup
assert_equal "No", I18n.t(false)
end

test "translate raises Disabled if locale is false" do
I18n.with_locale(false) do
assert_raise I18n::Disabled do
I18n.t('foo')
end

assert_equal 'translation missing: en.foo', I18n.t('foo', locale: :en)
end
end

test "available_locales can be replaced at runtime" do
begin
I18n.config.enforce_available_locales = true
Expand Down Expand Up @@ -290,6 +300,16 @@ def setup
assert_equal false, I18n.exists?(:bogus, :nl)
end

test "exists? raises Disabled if locale is false" do
I18n.with_locale(false) do
assert_raise I18n::Disabled do
I18n.exists?(:bogus)
end

assert_equal false, I18n.exists?(:bogus, :nl)
end
end

test "localize given nil raises an I18n::ArgumentError" do
assert_raise(I18n::ArgumentError) { I18n.l nil }
end
Expand All @@ -311,6 +331,14 @@ def setup
end
end

test "localize raises Disabled if locale is false" do
I18n.with_locale(false) do
assert_raise I18n::Disabled do
I18n.l(Time.now)
end
end
end

test "can use a lambda as an exception handler" do
begin
previous_exception_handler = I18n.exception_handler
Expand Down

0 comments on commit 5eeaad7

Please sign in to comment.