I just realized that the recommended Rails way to set locale in your controller
before_filter :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
sets the locale globally. The code above works, but I wonder is default_locale really default if you have to type it explicitly?
What I'd expect is to have a locale per request (like we have session per request) and doing something like:
def set_locale
locale = params[:locale] if params[:locale]
end
And having I18n.default_locale used by default otherwise. This would match ideally the optional locale in path:
# config/routes.rb
scope "(:locale)", :locale => /en|nl/ do
resources :books
end
For now if for some reason I skip locale setting in some action it uses the locale set in the previous request which could be from another user!
And isn't there a potential race condition as one request can change global I18n.locale while another request (having set another locale beforehande) is in the middle of rendering?
UPDATE: Some details I found for now, from the I18n documentstion:
Sets the current locale pseudo-globally, i.e. in the Thread.current hash
def locale=(locale)
Now I want to understand if every request is a separate thread.
UPDATE 2: See my answer for explanation.
So now the final answer. TL;DR Setting locale acts as global only when you use threaded web servers, like Thin and Puma.
As I mentioned, I18n.locale=
Sets the current locale pseudo-globally, i.e. in the Thread.current hash
So it is supposed to be per-request, and it works this way in Webrick and Unicorn.
But if you use threaded web server like Thin or Puma, seems that the thread lives longer, and the value is preserved for future requests, until it is changed explicitly. Where I learned it is from the new Steve Klabnik's gem request_store:
If you need global state, you've probably reached for Thread.current.
<...>
So people are using those fancy threaded web servers, like Thin or Puma. But if you use Thread.current, and you use one of those servers, watch out! Values can stick around longer than you'd expect, and this can cause bugs.
Recommended code from above does not set locale globally it sets it by request.
before_filter :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
Code is usually place in BaseController so before each page is render it is triggered and set. There is no race conditions since every page will trigger this code and I18n locale will be calculated there. You can expand this to let's say looks for users locale, than session locale, than request params, than uses English.
def set_locale
I18n.locale = #user.locale || session[:locale] || params[:locale] || :en
end
In other words if you set local on one page let's say in home controller to german and got to dashboard controller you will see default language (english). Since change is not global. That is why code is placed in base controller. Hope it makes sense.
Related
I'm developing a internationalized application, and sometimes I need to get some messages in a language that is not my default, like this:
I18n.t("my_message.some_attribute", :locale => :ptBR)
The problem is: I have an API method that creates and active record object, and I would like to render the error message from the method full_messages, like this:
my_object.errors.full_messages.uniq.to_sentence
The problem is, in that case, I get the message for my default language. How could I get that sentence in another language?
Thanks!
Got my answer... since the language come as a param to my api, in my app controller I set the locale before_action:
before_action :set_locale
def set_locale
I18n.locale = params[:lang] || "en"
end
This works for me
I have a model with custom validations. I return the validation
errors to the view as JSON using AJAX.
I need to show the error
messages with the right locale but the messages
show in English (default) for each locale.
When I return I18n.locale
itself instead of the message (for troubleshooting) it shows "en" no matter what is the set locale.
I18n in controllers or views works as expected, this problem
occurs only in the model.
The only configuration that I've made regarding locale is setting the default locale in the application config and setting the locale before each action in the application controller.
Another thing that I've noticed is that when I use root_path without providing the locale, it's using the default locale instead keeping the current locale like it should. I think those issues are connected
Edit:
When I print the locale parameter in controller#create, I get nil. For some reason I don't have the locale parameter at all.
model file:
validate :relevant_date_time
def relevant_date_time
errors.add(:date_error, I18n.t("validations.date_error")) unless date_time_is_relevant?
end
application_controller:
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
Am I forgetting something?
Information I reviewed:
http://guides.rubyonrails.org/i18n.html
Access translation file (i18n) from inside rails model
Rails 3 translations within models in production
http://www.eq8.eu/blogs/10-translating-locales-for-rails-model-errors
https://lingohub.com/frameworks-file-formats/rails5-i18n-ruby-on-rails/
Change locale at runtime in Rails 3
If you set locale at application_controller.rb, it will only have scope in all controllers and views NOT in models.
So you have to set locale inside model too. Same as you did at controller.
At controller
Model.new(params.merge(locale: I18n.locale))
Inside model
attr_accessible :locale
before_validation() do
I18n.local = locale
end
def relevant_date_time
errors.add(:date_error, I18n.t("validations.date_error")) unless date_time_is_relevant?
end
I18n locale in controllers/views only applies to the action's context, it does not change your app's default locale setting, which is where your model is getting its locale from (as you have discovered).
The step you are missing is to pass the locale into your model from your action, which you could do by adding an attr_accessor :locale to your model, and then passing the locale through when creating/updating it in your controller:
# e.g. app/controllers/users_controller.rb
# user_params = { email: test#mail.com, password: 12345678 }
User.new(user_params.merge(locale: I18n.locale))
Then in your validation method you would be able to access the current locale with locale, so all you need to do is pass it to I18n.t:
errors.add(:date_error, I18n.t("validations.date_error", locale: locale)) unless date_time_is_relevant?
Solved by adding the following method in application controller:
def default_url_options(options={})
{ locale: I18n.locale }
end
For anyone who is struggling with I18n in model validation, the solution could be not including any custom message keys in the models but
en:
activerecord:
errors:
models:
model_name:
attributes:
attr_name:
taken: << or whatever error message like blank, too_long, too_short etc.
Credit t:
https://stackoverflow.com/a/4453350/267693
In development mode all work fine. But not in production mode on heroku.
When I select a locale and I refresh the page, it displays language by default half the time and the language I selected otherwise. I do not know what to do. I tried to clear the cache with Rails.cache.clear command, but it does not work. I guess the problem is with the cache system. I'm new to ruby on rails. Someone would have an idea how to fix this?
to understand my problem, you can go to my website, select the French language and refresh the page several times. Once the page is in French. Another time in English.
https://releaseit.herokuapp.com/
my application_controller:
before_action :set_locale
def set_locale
if params[:locale].in? %W(en fr)
I18n.locale = params[:locale]
end
end
The config files are same as here: https://github.com/starterkits/rails4-starterkit/tree/master/config
Sorry for my english (I am french and i use google translator)
I don't know (or see) where do you pass locale in URL. As far as I know in order to use params[:locale] your URL should look like:
https://releaseit.herokuapp.com/fr/something
https://releaseit.herokuapp.com/en/something
https://releaseit.herokuapp.com/en
https://releaseit.herokuapp.com?locale=fr
http://guides.rubyonrails.org/i18n.html#setting-the-locale-from-the-url-params
What is more try my set_locale method:
def set_locale
if params[:locale]
I18n.locale = params[:locale] || I18n.default_locale
else
I18n.locale = http_accept_language.compatible_language_from(
I18n.available_locales
)
end
end
set_locale method should be executed on every page reload to set proper locale each time.
It requires http_accept_language gem from: https://github.com/iain/http_accept_language
Check out also route_translator gem for creating routes with locale.
I'm trying to internationalize a Rails/Spree app using spree's own spree_i18n gem, but I can't get it to work.
I made a minimal app which recreates the problem here.
To cut a long story short, I have the following code in my ApplicationController:
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
puts I18n.locale
end
And code in my view which should be translated (<%= t("whatever") %>). But no matter what I do, the text is always output in English.
With some additional code for debugging, I can see that once set_locale is called but while execution is still within the controller, the locale is correct (e.g. if I visit the url /?locale=es, then the puts statement in the above controller code outputs es).
But by the time execution has reached the view, the locale has somehow been reset to en. (E.g. adding <% raise I18n.locale.to_s %> within the view raises "en" as an error message.)
I've opened an issue on Spree's Github because as far as I can tell I've followed their instructions exactly and it's still not working, but I may still be missing something. Why isn't the locale getting set properly?
(Note: I should add that the Spree.t doesn't work either, not just t.)
EDIT: If you look at the comment on my Github issue, you'll see that I got it working. However, I'm 99% sure that my solution is a hack and there's a better method I should be using. Bounty goes to whoever can tell me what I'm dong wrong.
Spree I18n gives a way to set default language: on config/application.rb with
config.i18n.default_locale = :es
And the possibility of setting the languages to be changed. Perhaps on config/initializers/spree_i18n.rb
SpreeI18n::Config.available_locales = [:en, :es, :de]
SpreeI18n::Config.supported_locales = [:en, :es, :de]
After that, you can remove set_locale on ApplicationController, because it has no effect.
With this at place, it works like a charm.
Edited:
I change the message of error because I want to be sure that it works:
<%= product_description(#product) rescue Spree.t(:product_has_no_description) +
' ' + Spree.t(:action) %>
And I add a new product without description. Running the server at localhost
In english I see: "This product has no description Action"
In spanish I see: "Este producto no tiene descripción Acción"
In deutsch I see: "Produkt hat keine Beschreibung Aktion"
Exactly the expected.
You can see the source with changes at github
It is unclear to me how Spree handles localization and in your routes.rb you only mounts engine.
Basically you should start localize your app within routes.rb by adding:
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
# routing and engines go here
end
Now, you need to keep your params[:locale] across requests, so add to app controller:
def default_url_options(options={})
logger.debug "default_url_options is passed options: #{options.inspect}\n"
{ locale: I18n.locale }
end
Last, detect and set locale for current request, depending on your input data:
before_filter :set_locale
def set_locale
if defined?(params) && params[:locale]
I18n.locale = params[:locale]
elsif current_user && current_user.language_id.present?
I18n.locale = current_user.language.code
elsif defined?(request)
I18n.locale = extract_locale_from_accept_language_header
end
I18n.locale ||= I18n.default_locale
I18n.locale = :en unless valid_languages.include?(I18n.locale.to_sym)
end
You may be using/setting the following in app/controllers/application_controller.rb, which doesn't work:
before_action :set_locale
def set_locale
I18n.locale =
Spree::Frontend::Config[:locale] =
Spree::Backend::Config[:locale] = :LOCALE
end
In core/lib/spree/core/controller_helpers/common.rb there is another before_filter called set_user_language. That filter gets called and re-sets the locale to the value of session[:locale], or if that's not defined it uses default locale.
To solve the issue, set session[:locale] = :LOCALE in your before_filter.
I'm trying to understand how to set locales.
Right now, my application controller has:
def set_locale
I18n.locale = user_signed_in? ? current_user.local.to_sym : :en
end
This works well, but if I want to add a quick switch button on the log in page (which won't have session info because there's no current_user...how can I do this?
You don't have to have a current_user to have a session. Every request that hits your app will have a session object available to it where you can persist data.
Your set_locale can use the locale stored in sessionas the default (falling back to :en) if the user isn't currently logged in:
def set_locale
I18n.locale = user_signed_in? ? current_user.local.to_sym : (session[:locale] || :en)
end
You just need a method which sets the locale based on the user's submitted locale.
You can make a chain that figures out the current locale from many sources with priorities. The sources are (in order):
The current GET variable lang. I like to use it when a user wants to see only the current page in another locale. It neither changes his preference locale nor session locale.
The lang variable in the session which is applied to a whole session. So a user might like to browse multiple pages in the website in a locale different from his preference one, but without having to set the GET lang variable on every request and without having to change the locale in his preferences, and this is where session comes in. It applies only to the current... session :)
The locale in his preferences.
The default locale of the application (I18n.defaul_locale) which defaults to :en if you don't set it manually.
My code looks like:
before_filter :set_locale
def set_locale
if user_signed_in?
I18n.locale = params[:lang] || session[:lang] || current_user.locale || I18n.default_locale
else
I18n.locale = params[:lang] || session[:lang] || I18n.default_locale
end
end