Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to prevent calls outside of I18n context by setting locale to false #471

Merged
merged 1 commit into from
Mar 3, 2019

Conversation

casperisfine
Copy link

Context

We recently discovered a common error pattern in our application. I18n.t and similar API would get called during the boot phase, and the result stored in constant or similar class level variables. e.g.:

class Foo < ActiveRecord::Base
  validates :bar, presence: { message: I18n.t('foo.bar.missing') }
end

Of course this is a simple programming mistake, but I believe it could be prevented with a relatively simple change.

Solution

By allowing frameworks like Rails to set I18n.locale = false early in the boot phase, and whenever they load code, it could catch this type of mistake early.

That safety is totally opt-in, so it wouldn't break existing usage.

Also if for some reason you need to access a translation during those phases, you can explicitly specify the locale you want:

I18n.with_locale(false) { I18n.t('foo.bar', locale: 'en') }

Alternative solution

For now we've implemented a similar feature by simply setting a fake I18n backend. However since the locale selection is done inside I18n itself, the backend can't know wether the locale was passed explicitly or not, so it's hard to opt-out of that safety in the few case you need it.

cc @rafaelfranca

@radar what do you think?

@rafaelfranca
Copy link
Contributor

I like the plan. I believe those kind of mistakes are easy to do and having protection around it in the framework.

@radar
Copy link
Collaborator

radar commented Feb 22, 2019

Activerecord already does validation messages without having to specify the message like this. See section 4.5 here: https://guides.rubyonrails.org/i18n.html

@radar radar closed this Feb 22, 2019
@rafaelfranca
Copy link
Contributor

rafaelfranca commented Feb 22, 2019

This is not about Active Record. It is about any code that calls I18n.t at load time and in really are just mistakes.

Things like:

class Bar
  SOME_ERROR_MESSAGE = I18n.t('something')
class Foo
  class_attribute :name, default: I18n.t('name')

@radar
Copy link
Collaborator

radar commented Feb 22, 2019

Define these as class methods instead so they’re evaluated on call rather than on compilation.

I am loathe to further complicate or add features to the i18n codebase.

@casperisfine
Copy link
Author

Define these as class methods instead so they’re evaluated on call rather than on compilation.

Yes, there is way to do this properly. The point here is not to allow something that was impossible before, but to eagerly catch programmers mistakes.

@rafaelfranca
Copy link
Contributor

This is not a code to allow strange usage of I18n. It is a feature to direct people with less experience on how Ruby and I18n work to the right direction.

This came out from one real application where people called I18n.t on compilation. Code review didn't catch. The feature stayed on production only in english for days. All of this could be avoided if I18n had a way to disallow calls on compile time.

I understand and agree with the hesitation to add new feature to this codebase, but I believe this one will help many experienced and non-experienced developers to catch real problems earlier.

@radar can you please reconsider in this optic?

@radar radar reopened this Feb 26, 2019
@radar
Copy link
Collaborator

radar commented Feb 26, 2019

@rafaelfranca I can definitely see the advantages of that. I think setting I18n.locale = false is a decent enough workaround for this issue and, from what I can tell, it will raise exceptions in the case where people are incorrectly using it.

One last thing: I do wonder if the MissingLocale error message is useful enough to developers who encounter this issue. Think about a junior developer who might see this error message for their first-ever time. Does "No locale specified" really convey enough information to them? How can they discover how to remedy this exception?

Maybe we could make that exception's default error message clearer? Something like "I18n.locale is set to false, but must be configured to be a locale symbol. <list available locales here?>".

I am not sure entirely. What do you think would be useful error message to display to a junior developer? I think "No locale specified" is a good start, but we could probably do a better "Developer UX" here.

@rafaelfranca
Copy link
Contributor

This was the image we used in our custom backend:

        Can't call I18n.#{method} before the application is fully initialized.
        I18n.#{method} is meant to display text in the user locale, so by invoking it during the boot phase
        you risk leaking english strings to non-english speaking users.

The context on our implementation is different so I don't think this message fully apply to this patch.

When you do I18n.locale = false is not as obvious that we want to disable translations on boot, so maybe we can have something like:

        Can't call I18n.#{method} while I18n is disabled. It is usually disabled when loading your Ruby
        code.

        I18n.#{method} is meant to display text in the user locale, so by invoking it during the code
        loading phase you risk leaking english strings to non-english speaking users.

We can complete the message with explanation on how to solve some of those cases and how to opt-out the protection in case you really want to use translated text when loading the code.

@casperisfine
Copy link
Author

I indeed didn't port the exception message as is, because it makes some assuptions about the default locale etc, that might not hold in raw I18n.

I'll try to come up with something better though.

@casperisfine
Copy link
Author

I just updated the exception name and error message, let me know how it looks.

@@ -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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also added this. While crafting the error message I figured that exists? was inconsistent with the others, as it would take a locale as second param instead of a named one.

Having all methods accepting it as named param allows for a more insightful error message.

class Disabled < ArgumentError
def initialize(method)
super(<<~MESSAGE)
I18n.#{method} is currently disabled, likely because your application is still in it's loading phase.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
I18n.#{method} is currently disabled, likely because your application is still in it's loading phase.
I18n.#{method} is currently disabled, likely because your application is still in its loading phase.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed. Thanks!

@radar
Copy link
Collaborator

radar commented Mar 3, 2019

Error message looks great. Thanks for taking the time to update that. I will merge this now, and do a new release tomorrow AM (Eastern Australian time)

@radar radar merged commit 6f3d428 into ruby-i18n:master Mar 3, 2019
@goldenson goldenson deleted the allow-disabling-i18n branch March 4, 2019 14:42
kamipo added a commit to kamipo/i18n that referenced this pull request Jan 1, 2021
Related ruby-i18n#501, ruby-i18n#500, and ruby-i18n#471.

In Ruby 3.0, the behavior of `I18n.t("key", { value: "foo" })` has
silently changed to `{ value: "foo" }` is no longer regarded as
`**options` but regarded as noop slpat arguments `*`.

https://github.com/ruby-i18n/i18n/blob/4709391dceab9096d5988576f93935843023a6ef/lib/i18n.rb#L195

So allowing (discarding) noop arguments will be more harmful in Ruby 3.0.

This removes the allowing noop arguments, it makes `{ value: "foo" }`
will raise ArgumentError instead of silently ignored the options.
radar pushed a commit that referenced this pull request Jan 1, 2021
Related #501, #500, and #471.

In Ruby 3.0, the behavior of `I18n.t("key", { value: "foo" })` has
silently changed to `{ value: "foo" }` is no longer regarded as
`**options` but regarded as noop slpat arguments `*`.

https://github.com/ruby-i18n/i18n/blob/4709391dceab9096d5988576f93935843023a6ef/lib/i18n.rb#L195

So allowing (discarding) noop arguments will be more harmful in Ruby 3.0.

This removes the allowing noop arguments, it makes `{ value: "foo" }`
will raise ArgumentError instead of silently ignored the options.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants