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
Related
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
Rails 4.2 application.
In the footer of the page I have such code:
footer.footer
.container
= form_tag set_locale_path, method: :post, id: 'set_locale' do
= select_tag "locale", options_for_select(I18n.available_locales, I18n.locale.to_s), onchange: ("$('#set_locale').submit();")
And Locales controller with set_locale action.
class LocalesController < ApplicationController
skip_before_filter :authenticate_user!
def set_locale
I18n.locale = params[:locale]
# redirect_to request.referrer
end
end
I have access to previous url via request.referrer. That looks like: http://localhost:3000/en/users.
Is there any way to change locale without gsubing referrer string? Because if not - application will switch locale back to :en from referrer url.
I can't see an easy solution without gsubing the string. If the locale would be set with a get parameter instead of being part of the route it would be easier.
Example
http://localhost:3000/users?locale=en // Sets the locale to english
In that case the url would stay the same but the locale would change.
This is how i do it in my projects:
before_action :set_locale
def set_locale
I18n.locale = locale || I18n.default_locale
end
def locale
return params[:locale] if ["sv", "en"].include? params[:locale]
end
So I have been dealing with this myself. I dont want to remove the language from being part of the URL cause it does affect SEO if you are going after different languages. So I also have the language/locale as part of the url so its looks like www.website.com/en/more_url/even_more_url etc etc
I haven't found an answer anywhere else so this is my solution. I made a service that creates the url swapping out the current language or setting it if there is none picked and is not part of the URL for example when its "/"
I just grab the end part of the URL and look for the language and swap them out depending on its length and then build the URL as a string then gets returned. and used in the controller.
It works as intended the only weird edge case that I dont know how to fix though is that... lets say you are on a page that has a form. You submit the form with validation errors. Rails grabs the post url and changes the browser url to the post URL which is a rails behavior. If you then try to change the language if the browser url is on the form URL then you will be making a get request with a new language on a post URL. So it will result in an error. I thought maybe you can add an if statement saying only run this code if request.get BUT then that means when changing the language u cant use a post request to set the language so it doesnt work.
So you gotta hope the user doesnt submit a rails form with errors and then decide to change the language LOL (which seems like weird behavior anyways)
Anyways I hope this helps anyone out there that has been dealing with this :)
class LanguageUrlHelper
def self.get_url(request, locale_param)
uri= URI(request.referer).path
long_local_url = "/" + locale_param+ "/"
short_local_url = "/" + locale_param
if uri.length > 3
if uri.include?("/es/")
new_uri = uri.gsub("/es/", long_local_url)
elsif uri.include?("/en/")
new_uri = uri.gsub("/en/", long_local_url)
elsif uri.include?("/pt/")
new_uri = uri.gsub("/pt/", long_local_url)
end
elsif uri.length <= 3
if uri == "/"
new_uri = uri.gsub("/", short_local_url)
elsif uri == "/en"
new_uri = uri.gsub("/en", short_local_url)
elsif uri == "/es"
new_uri = uri.gsub("/es", short_local_url)
elsif uri == "/pt"
new_uri = uri.gsub("/pt", short_local_url)
end
end
new_url = request.base_url + new_uri
return new_url
end
end
In your controller add
redirect_to LanguageUrlHelper.get_url(request, params[:locale])
The answer I wrote above is not the best way to do this I have a newer way that I ended up using which I think solves multiple issues. Depending on how much you really want to translate your app.
In my cause I want to make sure I have the content translated as well as the urls translated which makes it friendlier for users and for marketing purposes if you are for example doing a landing pages or something.
For this to work you need to add the "route_translator" gem
Inside the Application controller add as a private method (this need to be called in a before action.)
def set_current_url_and_params
set_params = params.except("authenticity_token", "controller","action")
#path = ""
#current_page_params = set_params.to_unsafe_h
Rails.application.routes.router.recognize(request) do |route, matches, param|
#path = route.name
break
end
end
what this does is sets up the #path variable which has the path that you are currently on. This will be used inside the view for example were your menu is so you can switch languages.
like this:
=button_to "ES", set_location_path(locale:'es', current_path:#path, current_page_params:#current_page_params), class: "btn btn-primary-soft btn-block mb-1"
=button_to "EN", set_location_path(locale:'en', current_path:#path, current_page_params:#current_page_params), class: "btn btn-primary-soft btn-block mb-1"
This button send a post request to an end point that sets your "locale". It will also send the current_path which we set in the application controller.
In your routes file you will use the gem i mention above.
localized do
get "register"=> "users#new", :as=> :new_user
end
that gem will make some new paths for you depending on the locales/languages u want to use.
So in this example it creates
new_user_en
new_user_es
new_user_pt
So now lets say you are on the new_user_en_path
the block set this up so now it knows where you are. It knows the named route it will give you back a string "new_user_en"
Rails.application.routes.router.recognize(request) do |route, matches, param|
#path = route.name
break
end
When sending a request to change the locale you are also sending it with the #path(with the string value) you need to grab that and process that. I used the same service and method from my other answer but with some changes use this one its better.
class LanguageUrlHelper
#you need to include url helpers in service for this to work
class << self
include Rails.application.routes.url_helpers
end
def self.get_url(request, params)
prefix_path = params[:current_path]
locale = "_"+ params[:locale]
if prefix_path.include?("_es")
new_path = prefix_path.gsub("_es", locale)
elsif prefix_path.include?("_en")
new_path = prefix_path.gsub("_en", locale)
elsif prefix_path.include?("_pt")
new_path = prefix_path.gsub("_pt", locale)
else
new_path = prefix_path + locale
end
url = eval("#{new_path}_path(params.to_unsafe_h)")
end
end
In the change locale action controller add
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
http_accept_language.compatible_language_from(I18n.available_locales)
session[:currency] = params[:currency]
flash[:success] = t("application_controller.set_location.success")
redirect_to LanguageUrlHelper.get_url(request, params)
end
What the get_url does is it swaps out the last bit of the url_helper path that contains the locale and changes it to the new one you want.
So for example it grabs new_user_en and changes it to new_user_es if I wanted spanish.
I then interpolate it and then eval the method which gives me the actual URL.
with this line
url = eval("#{new_path}_path(params.to_unsafe_h)")
you will also want to make sure that you send the current pages params if there are any so I also store the current params inside a variable and is passed in the button as well which gets passes into this service.
Also this is important for this to work the #path need to be set from the very beginning and adding the "_en" etc needs to be a real path. so you will need to make sure the root path also uses the gem so you have to add (use your own obviously) Which give you root_en_path etc etc
root :to => 'landing_pages#buyers'
localized do
root :to => 'landing_pages#buyers'
end
So doing all this
if you are for example on
domain.com/en/new_user and want to change it to spanish it now changes it to
domain.com/es/nuevo_usario and redirect to that page which happens to be the translated page that u were already on while translating the URL as well.
This is much better than using regex or gsubing urls which seems more brittle to me. I hope this was helpful to anyone out there that has been trying to mess with translations. I couldn't find anything out there that dealt with this and spent a bunch of time trying to figure it out :)
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 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.