Merge pull request #47105 from ghiculescu/i18n-raise

Make `raise_on_missing_translations` raise on any missing translation
This commit is contained in:
Rafael Mendonça França 2023-01-23 14:10:50 -05:00 committed by GitHub
commit 4af571c22d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 164 additions and 36 deletions

@ -54,7 +54,7 @@
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.

@ -41,7 +41,7 @@
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.

@ -54,7 +54,7 @@
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names

@ -41,7 +41,7 @@
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names

@ -43,7 +43,7 @@
# Suppress logger output for asset requests.
config.assets.quiet = true
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names

@ -36,7 +36,7 @@
# Store uploaded files on the local file system in a temporary directory
config.active_storage.service = :test
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names

@ -1,3 +1,13 @@
* `config.i18n.raise_on_missing_translations = true` now raises on any missing translation.
Previously it would only raise when called in a view or controller. Now it will raise
anytime `I18n.t` is provided an unrecognised key.
If you do not want this behaviour, you can customise the i18n exception handler. See the
upgrading guide or i18n guide for more information.
*Alex Ghiculescu*
* `ActiveSupport::CurrentAttributes` now raises if a restricted attribute name is used.
Attributes such as `set` and `reset` cannot be used as they clash with the

@ -85,6 +85,15 @@ def self.forward_raise_on_missing_translations_config(app)
ActiveSupport.on_load(:action_controller) do
AbstractController::Translation.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
end
if app.config.i18n.raise_on_missing_translations &&
I18n.exception_handler.is_a?(I18n::ExceptionHandler) # Only override the i18n gem's default exception handler.
I18n.exception_handler = ->(exception, *) {
exception = exception.to_exception if exception.is_a?(I18n::MissingTranslation)
raise exception
}
end
end
def self.include_fallbacks_module

@ -813,8 +813,7 @@ Sets the path Rails uses to look for locale files. Defaults to `config/locales/*
#### `config.i18n.raise_on_missing_translations`
Determines whether an error should be raised for missing translations
in controllers and views. This defaults to `false`.
Determines whether an error should be raised for missing translations. This defaults to `false`.
#### `config.i18n.fallbacks`

@ -1148,47 +1148,34 @@ The I18n API defines the following exceptions that will be raised by backends wh
| `I18n::ReservedInterpolationKey` | the translation contains a reserved interpolation variable name (i.e. one of: `scope`, `default`) |
| `I18n::UnknownFileType` | the backend does not know how to handle a file type that was added to `I18n.load_path` |
The I18n API will catch all of these exceptions when they are thrown in the backend and pass them to the default_exception_handler method. This method will re-raise all exceptions except for `MissingTranslationData` exceptions. When a `MissingTranslationData` exception has been caught, it will return the exception's error message string containing the missing key/scope.
#### Customizing how `I18n::MissingTranslationData` is handled
The reason for this is that during development you'd usually want your views to still render even though a translation is missing.
If `config.i18n.raise_on_missing_translations` is `true`, `I18n::MissingTranslationData` errors will be raised. It's a good idea to turn this on in your test environment, so you can catch places where missing translations are requested.
In other contexts you might want to change this behavior, though. E.g. the default exception handling does not allow to catch missing translations during automated tests easily. For this purpose a different exception handler can be specified. The specified exception handler must be a method on the I18n module or a class with a `call` method:
If `config.i18n.raise_on_missing_translations` is `false` (the default in all environments), the exception's error message will be printed. This contains the missing key/scope so you can fix your code.
If you want to customize this behaviour further, you should set `config.i18n.raise_on_missing_translations = false` and then implement a `I18n.exception_handler`. The custom exception handler can be a proc or a class with a `call` method:
```ruby
# config/initializers/i18n.rb
module I18n
class JustRaiseExceptionHandler < ExceptionHandler
class RaiseExceptForSpecificKeyExceptionHandler
def call(exception, locale, key, options)
if exception.is_a?(MissingTranslation)
if key == "special.key"
"translation missing!" # return this, don't raise it
elsif exception.is_a?(MissingTranslation)
raise exception.to_exception
else
super
raise exception
end
end
end
end
I18n.exception_handler = I18n::JustRaiseExceptionHandler.new
I18n.exception_handler = I18n::RaiseExceptForSpecificKeyExceptionHandler.new
```
This would re-raise only the `MissingTranslationData` exception, passing all other input to the default exception handler.
However, if you are using `I18n::Backend::Pluralization` this handler will also raise `I18n::MissingTranslationData: translation missing: en.i18n.plural.rule` exception that should normally be ignored to fall back to the default pluralization rule for English locale. To avoid this you may use an additional check for the translation key:
```ruby
if exception.is_a?(MissingTranslation) && key.to_s != 'i18n.plural.rule'
raise exception.to_exception
else
super
end
```
Another example where the default behavior is less desirable is the Rails TranslationHelper which provides the method `#t` (as well as `#translate`). When a `MissingTranslationData` exception occurs in this context, the helper wraps the message into a span with the CSS class `translation_missing`.
To do so, the helper forces `I18n#translate` to raise exceptions no matter what exception handler is defined by setting the `:raise` option:
```ruby
I18n.t :foo, raise: true # always re-raises exceptions from the backend
```
This would raise all exceptions the same way the default handler would, except in the case of `I18n.t("special.key")`.
Translating Model Content
-------------------------

@ -297,6 +297,37 @@ Option `config.action_mailer.preview_path` is deprecated in favor of `config.act
config.action_mailer.preview_paths << "#{Rails.root}/lib/mailer_previews"
```
### `config.i18n.raise_on_missing_translations = true` now raises on any missing translation.
Previously it would only raise when called in a view or controller. Now it will raise anytime `I18n.t` is provided an unrecognised key.
```ruby
# with config.i18n.raise_on_missing_translations = true
# in a view or controller:
t("missing.key") # raises in 7.0, raises in 7.1
I18n.t("missing.key") # didn't raise in 7.0, raises in 7.1
# anywhere:
I18n.t("missing.key") # didn't raise in 7.0, raises in 7.1
```
If you don't want this behaviour, you can set `config.i18n.raise_on_missing_translations = false`:
```ruby
# with config.i18n.raise_on_missing_translations = false
# in a view or controller:
t("missing.key") # didn't raise in 7.0, doesn't raise in 7.1
I18n.t("missing.key") # didn't raise in 7.0, doesn't raise in 7.1
# anywhere:
I18n.t("missing.key") # didn't raise in 7.0, doesn't raise in 7.1
```
Alternatively, you can customise the `I18n.exception_handler`.
See the [i18n guide](https://guides.rubyonrails.org/v7.1/i18n.html#using-different-exception-handlers) for more information.
Upgrading from Rails 6.1 to Rails 7.0
-------------------------------------

@ -69,7 +69,7 @@ Rails.application.configure do
config.assets.quiet = true
<%- end -%>
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.

@ -57,7 +57,7 @@ Rails.application.configure do
# Tell Active Support which deprecation messages to disallow.
config.active_support.disallowed_deprecation_warnings = []
# Raises error for missing translations in controllers and views.
# Raises error for missing translations.
# config.i18n.raise_on_missing_translations = true
# Annotate rendered view with file names.

@ -4335,6 +4335,98 @@ def new(app); self; end
assert_match(/The `legacy_connection_handling` setter was deprecated in 7.0 and removed in 7.1, but is still defined in your configuration. Please remove this call as it no longer has any effect./, error.message)
end
test "raise_on_missing_translations = true" do
add_to_config "config.i18n.raise_on_missing_translations = true"
app "development"
assert_equal true, Rails.application.config.i18n.raise_on_missing_translations
assert_raise(I18n::MissingTranslationData) do
I18n.t("translations.missing")
end
end
test "raise_on_missing_translations = false" do
add_to_config "config.i18n.raise_on_missing_translations = false"
app "development"
assert_equal false, Rails.application.config.i18n.raise_on_missing_translations
assert_nothing_raised do
I18n.t("translations.missing")
end
end
test "raise_on_missing_translations = true and custom exception handler in initializer" do
add_to_config "config.i18n.raise_on_missing_translations = true"
app_file "config/initializers/i18n.rb", <<~RUBY
I18n.exception_handler = ->(exception, *) {
if exception.is_a?(I18n::MissingTranslation)
"handled I18n::MissingTranslation"
else
raise exception
end
}
RUBY
app "development"
assert_equal true, Rails.application.config.i18n.raise_on_missing_translations
assert_equal "handled I18n::MissingTranslation", I18n.t("translations.missing")
assert_raise(I18n::InvalidLocale) do
I18n.t("en.errors.messages.required", locale: "dsafdsafdsa")
end
end
test "raise_on_missing_translations = false and custom exception handler in initializer" do
add_to_config "config.i18n.raise_on_missing_translations = false"
app_file "config/initializers/i18n.rb", <<~RUBY
I18n.exception_handler = ->(exception, *) {
if exception.is_a?(I18n::MissingTranslation)
"handled I18n::MissingTranslation"
else
raise exception
end
}
RUBY
app "development"
assert_equal false, Rails.application.config.i18n.raise_on_missing_translations
assert_equal "handled I18n::MissingTranslation", I18n.t("translations.missing")
assert_raise(I18n::InvalidLocale) do
I18n.t("en.errors.messages.required", locale: "dsafdsafdsa")
end
end
test "i18n custom exception handler in initializer and pluralization backend" do
app_file "config/initializers/i18n.rb", <<~RUBY
I18n.exception_handler = ->(exception, *) {
if exception.is_a?(I18n::MissingTranslation)
"handled I18n::MissingTranslation"
else
raise exception
end
}
Rails.application.config.after_initialize do
I18n.backend.class.include(I18n::Backend::Pluralization)
I18n.backend.send(:init_translations)
I18n.backend.store_translations :en, i18n: { plural: { rule: lambda { |n| [0, 1].include?(n) ? :one : :other } } }
I18n.backend.store_translations :en, apples: { one: 'one or none', other: 'more than one' }
I18n.backend.store_translations :en, pears: { pear: "pear", pears: "pears" }
end
RUBY
app "development"
assert I18n.backend.class.include?(I18n::Backend::Pluralization)
assert_equal "one or none", I18n.t(:apples, count: 0)
assert_raises I18n::InvalidPluralizationData do
assert_equal "pears", I18n.t(:pears, count: 0)
end
end
private
def set_custom_config(contents, config_source = "custom".inspect)
app_file "config/custom.yml", contents