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
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 :)
I am trying to set the local of my users via a field in my user model called locale.
However, the I18n methods do not seem to be working. I'm not sure if I've missed some configuration or something. (I'm still new to rails.) I find that this also happens with many gems that I try to use. Possible that I am initializing things incorrectly?
undefined method default_locale for nil:NilClass
In my Application Controller:
before_filter :set_locale
private
def set_locale # Sets the users language via the locale index.
I18n.default_local = 'en'
I18n.locale = current_user.locale if current_user.locale.present? || I18n.default_locale
end
I am pretty sure that your current_user is nil. Just make sure that the current_user is set before you call locale on it.
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.
when our app sends SMS messages, we'd like to give users the option to specify their preferred language.
Using Spanish as an example, I see how to add a new locale yml file to config/locales.
And I see how to replace any hardcoded strings such as "text STOP to opt-out" with :stop_opt_out in my app.
what I don't see is how to select the language used on a user-by-user basis.
specifically if my code is currently:
msg_out = "Thank you!"
and after internationalization I have :thank_you defined in multiple locale yml files
and if in my user record I have user.locale = "en" or "sp" (or should I enumerate locales 0,1,2?)
how would I do a user specific
msg_out = t :thank_you
that would apply to every message created for that user in the current session?
If all your messages are generated when the user is using your site (i.e. all messages are sent as responses to user actions), you can just read the preferred locale out of the User model in a before_filter on your Application Controller (more on this here):
class ApplicationController < ActionController::Base
before_filter :load_user
before_filter :load_locale
#...
def load_locale
I18n.locale = (#user && #user.locale)? #user.locale : I18n.default_locale
end
#...
end
If you're sending your messages asynchronously (e.g. nightly mass-mailings), you'll have to load it per user:
User.find_each do |user|
I18n.locale = user.locale || I18n.default_locale
# Send your message...
end
And if you're sending generic mass-emails (i.e. with no per-user customizations), you can probably speed the above code up drastically by loading lists of users grouped by locale, and then sending a single message to all of them at once.
Hope this helps!