I18n locale disregarding fallbacks - ruby-on-rails

I asked a previous question regarding locale-setting. I am trying to set up fallbacks of various Norwegian languages to Norwegian Bokmal (:nb). The desired behaviour is that if a browser passes nn or no as locale requests, the I18n.locale will be set to either :nn or :no, and then in the absence of translations for these locales, :nb will be served to the browser.
Based on the answer to my previous question, I have this line in my application initialiser:
config.i18n.default_locale = :en
config.i18n.fallbacks = {:nn => [:nb], :no => [:nb]}
In rails console, this gives me the following results:
> I18n.fallbacks
=> {:en=>[:en]}
> I18n.fallbacks[:nn]
=> [:nn, :nb, :en]
> I18n.fallbacks[:no]
=> [:no, :nb, :en]
Using a browser that has only nn & no in the language list, this does not work- it falls back to the default locale of :en instead. Here's the request headers:
Accept-Language: "nn,no;q=0.5"
If I add :nb to the browser language stack, I am correctly served Norwegian content.
Is there something I am missing in this process?

You need to set I18n.locale based on the browser setting.
def set_locale
I18n.locale = extract_locale_from_accept_language_header
end
private
def extract_locale_from_accept_language_header
request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
end
Taken from: http://guides.rubyonrails.org/i18n.html#setting-the-locale-from-the-client-supplied-information

Related

Crash in Rails ActionView related to I18n.fallbacks when invoked from delayed_job

I am trying to send an email from my rails 4 app like so (condensed version from the console):
> ActionMailer::Base.mail(from: 'mail#example.com', to: 'foo#example.com', subject: 'test', body: "Hello, you've got mail!").deliver_later
The mail would be sent by delayed_lob, in my local test setup I trigger it like so:
> Delayed::Job.last.invoke_job
However the job crashes with the following message:
Devise::Mailer#invitation_instructions: processed outbound mail in 56234.1ms
Performed ActionMailer::DeliveryJob from DelayedJob(mailers) in 56880.04ms
TypeError: no implicit conversion of nil into Array
from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:51:in `concat'
from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:51:in `block in <class:LookupContext>'
from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:39:in `initialize_details'
from /Users/de/.rvm/gems/ruby-2.4.0/gems/actionview-4.2.10/lib/action_view/lookup_context.rb:205:in `initialize'
...
I have looked into the code of lookup_context.rb:51 GitHub, the problem is here:
register_detail(:locale) do
locales = [I18n.locale]
locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks # < crashes here
# from the debugger I got:
# I18n.locale => :de
# I18n.fallbacks => {:en=>[]}
So obviously fallbacks does not contain my locale (:de) which results in a nil exception.
Apparently I18n.fallbacks is not configured correctly.
Question: How can I fix this?
I got the answer with the help of this blog post I found: https://sjoker.net/2013/12/30/delayed_job-and-localization/
It contains half of what I need. The proposed solution goes like this:
In order to propagate state from the time of creating a job, that state has to be transferred to the time the job is invoked by storing it on the job object in the database.
In the blog post, the author only stores the current locale to localize the mail at invitation time. However I also needed to store the fallbacks which required a little bit of serialization.
Here is my solution:
# Add a state attributes to delayed_jobs Table
class AddLocaleToDelayedJobs < ActiveRecord::Migration
def change
change_table :delayed_jobs do |t|
t.string :locale # will hold the current locale when the job is invoked
t.string :fallbacks # ...
end
end
end
# store the state when creating the job
Delayed::Worker.lifecycle.before(:enqueue) do |job|
# If Locale is not set
if(job.locale.nil? || job.locale.empty? && I18n.locale.to_s != I18n.default_locale.to_s)
job.locale = I18n.locale
job.fallbacks = I18n.fallbacks.to_json
end
end
# retrieve the state when invoking the job
Delayed::Worker.lifecycle.around(:invoke_job) do |job, &block|
# Store locale of worker
savedLocale = I18n.locale
savedFallbacks = I18n.fallbacks
begin
# Set locale from job or if not set use the default
if(job.locale.nil?)
I18n.locale = I18n.default_locale
else
h = JSON.parse(job.fallbacks, {:symbolize_names => true})
I18n.fallbacks = h.each { |k, v| h[k] = v.map(&:to_sym) } # any idea how parse this more elegantly?
I18n.locale = job.locale
end
# now really perform the job
block.call(job)
ensure
# Clean state from before setting locale
I18n.locale = savedLocale
I18n.fallbacks = savedFallbacks
end
end
You need to configure your default and fallback locale something like below
config.i18n.default_locale = :de
config.i18n.fallbacks = { de: :en }
Please try this. It will fallback to :en if :de is not found

How to fix: i18n always translate to default locale

I'm trying out the internationalization of a Rails app with i18n. I did some small tests with 2 languages: english and french.
The problem I have is that i18n always translate to the default locale. So if it's english, everything will be in english, and the same with french.
Here is what I tried:
config/initializers/locales.rb
# Permitted locales available for the application
I18n.available_locales = [:en, :fr]
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
def default_url_options
{ locale: I18n.locale }
end
end
config/application.rb
module LanguageApp
class Application < Rails::Application
...
config.i18n.load_path += Dir["#{Rails.root.to_s}/config/locales/**/*.{rb,yml}"]
config.i18n.default_locale = :en
# I change the default locale here to :fr or :en
end
end
config/routes.rb
root to: "home#index"
get '/:locale/about' => 'about#index'
get '/:locale' => 'home#index'
I organized my yml files like this:
config/locales/views/about/en.yml
en:
about: "This page is about us."
config/locales/views/about/fr.yml
fr:
about: "Cette page est à propos de nous."
config/locales/views/home/en.yml
en:
welcome: "Hello world"
config/locales/views/home/fr.yml
fr:
welcome: "Bonjour le monde"
And finally my views:
app/views/about/index.html.erb
About us page. <%= t(:about) %>
app/views/home/index.html.erb
This is the homepage. <%= t(:welcome) %>
I think the problem may come from the way I organized my yml files but I don't understand why i18n only translate to the default locale and 'ignore' the other language.
EDIT:
To try this out in the browser with the rails server running, I tried to visit these URL:
localhost:3000
localhost:3000/en
localhost:3000/fr
These 3 URL give me the same content, so the :fr locale doesn't actually work (it returns the same translation as :en)
Same for
localhost:3000/en/about
localhost:3000/fr/about
I also tried it in the rails console:
> I18n.t(:welcome, :en)
"Hello world"
> I18n.t(:welcome, :fr)
"Hello world"
First set the locale for the request:
class ApplicationController < ActionController::Base
around_action :switch_locale
def switch_locale(&action)
I18n.with_locale(params[:locale] || I18n.default_locale, &action)
end
def default_url_options
{ locale: I18n.locale }
end
end
Don't use I18n.locale= as many older answers / tutorials do.
I18n.locale can leak into subsequent requests served by the same
thread/process if it is not consistently set in every controller. For
example executing I18n.locale = :es in one POST requests will have
effects for all later requests to controllers that don't set the
locale, but only in that particular thread/process. For that reason,
instead of I18n.locale = you can use I18n.with_locale which does not
have this leak issue.
Rails Guides
If you want to create translations for specific views you should nest the keys instead of just using flat hashes:
en:
home:
welcome: "Hello World"
fr:
home:
welcome: "Bonjour le monde"
And then use an implicit lookup in the view:
<h1><%= t('.welcome') %></h1>
This resolves the key to home.welcome.

How to use English text when corresponding language text doesn't exist

I have text like this
config/locales/en.yml
record_your_voice: 'Record your voice?'
When I'm seeing the page in Japanese local config, the text Record Your Voice is shown - the capitalized left side of code.
How can I use the text "Record your voice?" when corresponding Japanese text failed.
The answer is given here
in your application.rb file, you have the following options :
# rails will fallback to config.i18n.default_locale translation
config.i18n.fallbacks = true
# rails will fallback to en, no matter what is set as config.i18n.default_locale
config.i18n.fallbacks = [:en]
# fallbacks value can also be a hash - a map of fallbacks if you will
# missing translations of es and fr languages will fallback to english
# missing translations in german will fallback to french ('de' => 'fr')
config.i18n.fallbacks = {'es' => 'en', 'fr' => 'en', 'de' => 'fr'}

Avoid I18n Fallback to default locale

I have the following configured in my application.rb
config.i18n.available_locales = [:at, :de, :ch_de, :ch_fr, :fr, :int_en, :int_fr]
config.i18n.default_locale = :at
My default locale is set to :at (Austria). Which I require for Route Translation. Rails server won't start without it and to be fair it makes sense.
I now created a fallback map, which works just fine:
config.i18n.fallbacks = {'de' => 'at', 'ch_de' => 'at', 'ch_fr' => 'fr', 'int_fr' => 'fr', 'fr' => 'fr', 'int_en' => 'int_en'}
So basically I want all German speaking countries to fallback on :at, whilst all French speaking countries fall back to :fr.
However, I do NOT under any circumstances want :fr to fallback on :at. This is for SEO purposes as some french pages do not have metadata configured. So now the french pages would display the Austrian :at metadata entry. Which is wrong.
If I turn of fallbacks completely:
config.i18n.fallbacks = false
the following works fine in my views:
t('.metatitle', :default => "")
In that case if there is no translation available then nothing is displayed. However, the rest of the site that already exists relies on fallbacks - so this is not an option, considering the effort to implement the change.
Is there a way turn off fallbacks for individual translations?
Or can I implement the fallback map and make sure that the map doesn't fallback to it's default locale if for example no french :fr translation exists?
PS: The route translating gem that requires the default locale is this one here.
Thank you for your help !
Figured it out - and thought of sharing it with you:
If you wish to avoid fallback to the default locale on individual translation you simply have to send a empty fallback array like this:
t('.metatitle', :default => "", :fallback => [])
Et Voila !
This is tricky in Rails up to 6.1 because you need to beat the logic in the Railtie initializer which desperately wants to fallback to the default_locale.
To set the default fallback locale to nil you need to use the following code:
config.i18n.default_locale = "de-AT"
config.i18n.fallbacks.defaults = [[]] # If you just write [], then default_locale is used
config.i18n.fallbacks.map = {
:de => "de-AT",
"de-CH" => "de-AT",
}
Let's check:
$ rails console
2.7.2 :001 > I18n.fallbacks["de"]
=> [:de, :"de-AT"]
2.7.2 :002 > I18n.fallbacks["fr"]
=> [:fr]
2.7.2 :003 > I18n.fallbacks["de-CH"]
=> [:"de-CH", :de, :"de-AT"]
2.7.2 :004 > I18n.fallbacks["de-AT"]
=> [:"de-AT", :de]
2.7.2 :005 >
Not 100% what you want, but there just seems to be no way to prevent the fallbacks to go from country specific locale to generic language locale, when fallbacks are enabled.
Note #1: Your locales are a bit non-standard. AFAIK there is no 'at' locale, but only "de-AT".
Note #2: Some more subtleties and notes in this answer.

Rails I18n: How to set fallback locale?

I'm on Rails 4, and I'm following how to use rails i18n fallback features
My website only has 2 lang, zh, and en, and I want other language such as de, fr fallback to en, so I set as suggested above:
config.i18n.fallbacks =[:en]
But when I visiting http://localhost:3000/?locale=de it still reports
"de" is not a valid locale. (locale for en and zh works fine)
How can I fix this?
UPDATE: this is how I receive locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
Add this to your config/application.rb
I18n.config.enforce_available_locales = false

Resources