Rails production: locales switching - ruby-on-rails

In my app I've made switching locales with sessions. The logic keeps in controller:
class SetLanguageController < ApplicationController
def russian
I18n.locale = :ru
set_session_and_redirect
end
def english
I18n.locale = :en
set_session_and_redirect
end
private
def set_session_and_redirect
session[:locale] = I18n.locale
redirect_to :back
rescue ActionController::RedirectBackError
redirect_to :root
end
end
switching works with links:
link_to_unless I18n.locale == :ru, "Русский", rus_locale_path
link_to_unless I18n.locale == :en, "English", eng_locale_path
code for locales in routes (unnecessary for question, but if you interesting)
get 'rus_locale' => 'set_language#russian'
get 'eng_locale' => 'set_language#english'
It works in development perfectly, but on production
application.rb
config.i18n.load_path += Dir[Rails.root.join('config','locales', '*.yml').to_s]
config.i18n.default_locale = :ru
How can I make it working on production? Thanks

I solved problem with setting a before_filter in application controller like
before_filter :set_locale
def set_locale
I18n.locale = session[:locale] ? session[:locale] : I18n.default_locale
end
But this solution is dummy because it doesn't solve essence of problem actually - previous code worked in development, but in production. If you know how to fix it more smartly you are wellcome

Robust I18n implementation:
Add a locales.rb initializer to define what I18n.available_locales you support:
# config/initializers/locales.rb
# Permitted locales available for the application
I18n.available_locales = [:en, :fr]
Set a language_name value in each language's locale file (e.g. fr.yml):
fr:
language_name: "Français"
As you add more languages, this ERB will let you generically switch between them:
// app/views/layouts/_languages.html.erb
<span class="languages">
<% I18n.available_locales.each do |locale| %>
<% if I18n.locale == locale %>
<%= link_to I18n.t('language_name', locale: locale), url_for( params.clone.permit!.merge(locale: locale, only_path: true ), {style: "display:none" } %>
<% else %>
<%= link_to I18n.t('language_name', locale: locale), url_for( params.clone.permit!.merge(locale: locale, only_path: true ) %>
<% end %>
<% end %>
</span>
For the controller, we automatically find the correct language for the user by detecting their browser's Accept-Language HTTP header (using the http_accept_language gem).
Set a session cookie to preserve locale across requests.
Or optionally, use default_url_options to insert the ?locale= param into your app's url. I do both.
Controller:
class ApplicationController < ActionController::Base
before_action :set_locale
private
def set_locale
I18n.locale = begin
extract_locale ||
session[:locale] ||
http_accept_language.compatible_language_from(I18n.available_locales) ||
I18n.default_locale
end
session[:locale] = I18n.locale
end
def extract_locale
parsed_locale = params[:locale].dup
I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end
def default_url_options
{ locale: I18n.locale }
end
end

Related

Rails I18n - How can I change other locale with the same page [duplicate]

I am adding I18N to my rails application by passing the locale using url params. My urls are looking like http://example.com/en/users and http://example.com/ar/users (for the english and arabic locales respectively).
In my routes file, I have defined my routes with a :path_prefix option:
map.resources :users, :path_prefix => '/:locale'
And locale is being set using a before_filter defined in ApplicationController
def set_locale
I18n.locale = params[:locale]
end
I also defined ApplicationController#default_url_options, to add locale to all urls generated by the application:
def default_url_options(options={})
{:locale => I18n.locale}
end
What I want is to add a link in the layout header (displayed in all pages) that would link to the same page but with the other locale.
For instance, if I am browsing the arabic locale, I want a "English" link in the header, that will redirect me back to my current page, and set the locale to english. Is there a way to do this in rails?
Took me a while to find this but here is my solution:
link_to 'English', url_for( :locale => 'en' )
link_to 'Deutch', url_for( :locale => 'de' )
From the docs here: http://api.rubyonrails.org/classes/ActionController/Base.html#M000649
When generating a new URL, missing
values may be filled in from the
current request‘s parameters. For
example, url_for :action =>
‘some_action‘ will retain the current
controller, as expected. This behavior
extends to other parameters, including
:controller, :id, and any other
parameters that are placed into a
Route‘s path.
So using url_for will default to the current request's parameters, just change the one's you want in your code. In this case all I changed was :locale, so everything else stays the same.
Note this also works for "hidden" :parameters. So if you have:
map.my_map ':locale/my_map', :controller => 'home', :action => 'my_map'
using the above url_for in the page /en/my_map will not have 'home' in the url (ie /en/home/my_map). Bonus.
So I found a way to more explicitly do this with out relying on (as much) rails magic.
url_for(params.merge({:your_new_parameter => value}))
This should work in any link_to.
All its doing is taking the current request's parameters and merging your new desired hash into them and then creating a new url for that.
Link to current page with different locales
Tested on Rails 4
Hello all.
After some time of research I decide to write my own solution for this.
link_to 'English', url_for( :locale => 'en' )
link_to 'Deutch', url_for( :locale => 'de' )
This works perfect, but it allows XSS Vulnerability just passing parameters in your URL like below:
http://localhost:3000/en/about?host=www.fishingsiteorbadurl.com/%23&port=80
Or worst case:
http://localhost:3000/en/about?host=%D0%BE%D1%87%D0%B5%D0%BD%D1%8C%D0%BF%D0%BB%D0%BE%D1%85%D0%BE%D0%B9%D1%81%D0%B0%D0%B9%D1%82.%D1%80%D1%84
Check out what URLs you will get after going through this link in your application.
My production solution.
Method "change language" redirects to any page with proper locale just using HTTP_REFERER in request object.
Please note: URI.path method for get only path, not whole url
Make "change language" method in any controller:
def change_lang
if request.referer.nil?
refer = root_url
else
uri = URI(request.referer)
refer = uri.path
end
lang = params[:lang]
cookies[:locale] = lang
redirect_to refer
end
application_controller.rb
before_action :set_locale
def set_locale
# -- Get lang from cookies or url parameter locale
user_locale = cookies[:locale] || params[:locale]
# -- If present
if user_locale.present?
# -- If it is has 2 symbols
user_locale = user_locale.scan(/[a-zA-Z]{2}/)
else
# -- If no - use default en locale
user_locale = 'en'
end
# -- Check, is this locale available for using.
# Please note: this needed for disable invalid locale warning.
if I18n.available_locales.include?(user_locale[0].to_sym)
I18n.locale = user_locale[0]
else
I18n.locale = "en"
end
end
add this to your layout
<%= link_to 'English', change_lang_path('en') %> <%= link_to 'Russian', change_lang_path('ru') %>
config/routes.rb
scope "(:locale)", locale: /[a-zA-Z]{2}/ do
get "change_lang/:lang" => "users#change_lang", :as => "change_lang"
end
There is no need to use params.merge or any monkey-patch solution.
I hope this helps, because I personally spent a lot of time to solve it.
A much quicker avenue - and convenient if you have many parameters that change in different places... avoid the clutter with an anchor tag that just merges the new locale param to the existing ones (and actually killing the old locale param).
<%= link_to "ру", request.params.merge( locale: 'ru' ) %>
But yes, one needs to whitelist parameters at that point, according to application's context.
You can parse request_uri, and replace your locale in the path with regular expression
Ok, here is helper example. If I correctly understand the goal
def locale_url(url, locale)
url.gsub(/\/\w*$/, "/#{locale}")
end
url = "http://www.domain.com/products/1/ru" # or request.request_uri
locale = "en"
locale_url(url, locale) #=> "http://www.domain.com/products/1/en"
This is a start point, so you can make some different stuff that you need
You can safely use url_for to switch locales with url params if you set only_path: true:
<%= link_to I18n.t('language_name', locale: I18n.locale), url_for( params.clone.permit!.merge(locale: locale, only_path: true ) %>
We .clone the params before permitting them all (.permit!), to preserve strong parameters elsewhere. The only more secure solution I could find would be to time consumingly whitelist all params instead...
Robust I18n implementation:
Add a locales.rb initializer to define what I18n.available_locales you support:
# config/initializers/locales.rb
# Permitted locales available for the application
I18n.available_locales = [:en, :fr]
Set a language_name value in each language's locale file (e.g. fr.yml):
fr:
language_name: "Français"
As you add more languages, this ERB will let you generically switch between them:
// app/views/layouts/_languages.html.erb
<span class="languages">
<% I18n.available_locales.each do |locale| %>
<% if I18n.locale == locale %>
<%= link_to I18n.t('language_name', locale: locale), url_for( params.clone.permit!.merge(locale: locale, only_path: true ), {style: "display:none" } %>
<% else %>
<%= link_to I18n.t('language_name', locale: locale), url_for( params.clone.permit!.merge(locale: locale, only_path: true ) %>
<% end %>
<% end %>
</span>
For the controller, we automatically find the correct language for the user by detecting their browser's Accept-Language HTTP header (using the http_accept_language gem).
Set a session cookie to preserve locale across requests.
Or optionally, use default_url_options to insert the ?locale= param into your app's url. I do both.
Controller:
class ApplicationController < ActionController::Base
before_action :set_locale
private
def set_locale
I18n.locale = begin
extract_locale ||
session[:locale] ||
http_accept_language.compatible_language_from(I18n.available_locales) ||
I18n.default_locale
end
session[:locale] = I18n.locale
end
def extract_locale
parsed_locale = params[:locale].dup
I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end
def default_url_options
{ locale: I18n.locale }
end
end
This is what worked for me, which preserves params and protects against xss:
= link_to_unless_current "English", url_for( request.params.merge(locale: 'en'))
You could use link_to instead of link_to_unless_current
Have a look at this, though it may not be DRY and proper one, but works perfectly for me. It reads all the parameters you supplied replacing only the locale
EX urls : http://example.com:3000/us/users?t=123&m=343 etc
def us_link
link_to "US", form_locale_url("/us")
end
def jp_link
link_to "Japan",form_locale_url("/jp")
end
def form_locale_url(locale)
new_url = request.request_uri
new_locale_url = new_us_url = new_jp_url = new_url
if new_url == "/"
new_locale_url.sub!(/\//,locale)
elsif (new_url =~/\/us/) == 0
new_us_url.sub!(/\/us/,locale)
elsif (new_url =~/\/jp/) == 0
new_jp_url.sub!(/\/jp/,locale)
end
end

How to keep URL parameters when changing the locale?

In my Rails application I have this method through which a user can change their locale:
# locales_controller.rb
class LocalesController < ApplicationController
def change_locale
new_locale = params[:set_locale]
if new_locale
session[:locale] = new_locale
url_hash = Rails.application.routes.recognize_path URI(request.referer).path
url_hash[:locale] = new_locale
redirect_to url_hash
end
end
end
# routes.rb
MyApp::Application.routes.draw do
scope "(:locale)", locale: /#{DEFINED_LANGUAGES.join("|")}/ do
get 'change_locale', :to => 'locales#change_locale'
...
end
# application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || session[:locale] || extract_locale_from_accept_language_header || I18n.default_locale
session[:locale] = I18n.locale
end
def extract_locale_from_accept_language_header
http_accept_language.compatible_language_from(DEFINED_LANGUAGES) # => e.g. 'de'
end
...
end
# application_helper.rb
def locale_switcher
form_tag url_for(:controller => 'locales', :action => 'change_locale'), :method => 'get', :id => 'locale_switcher' do
select_tag 'set_locale', options_for_select(LANGUAGES, I18n.locale.to_s)
end
end
Unfortunately, when a user selects another locale, all other parameters that were present in the URL at that time, are lost. Is there a way to circumvent that?
Currently your locale switcher posts the desired locale to a new route. That makes it very hard to redirect the user back to the page from where the request was posted and - at the same time - change parts of the URL.
Furthermore, it is not possible to have a form action point to a URL that includes query parameters and have this form send its content via the get method. The form submit would simply remove all existing query parameters and only add key/value pairs that were defined within the form.
To fix this we need to do two things:
Send the form to the current URLs instead of a dedicated locale controller
Duplicate all existing query parameters as hidden tags into the form
To avoid sanitized HTML in nested content tags in Rails helpers, I suggest adding a new partial to the app. Note that I inlined the onchange JavaScript to remove external dependencies.
# in app/views/shared/_locale_switcher.html.erb
<%= form_tag url_for(params), method: 'get' do %>
<% request.query_parameters.each do |key, value| %>
<%= hidden_field_tag(key, value) %>
<% end %>
<%= select_tag(:new_locale,
options_for_select(LANGUAGES, I18n.locale.to_s),
onchange: 'this.form.submit()') %>
<% end %>
You can render the new locale_switcher partial into every other view or layout file with this line:
<%= render 'shared/locale_switcher' %>
Last step: Add a before_action method to your ApplicationContoller that triggers a redirect if the new and the current locale not match:
before_action :check_for_locale_change
before_action :set_locale
def check_for_locale_change
new_locale = params[:new_locale]
if new_locale.present? && new_locale != params[:locale]
redirect_to url_for(params.merge(locale: new_locale, new_locale: nil))
end
end
def set_locale
I18n.locale = params[:locale] || session[:locale] || extract_locale_from_accept_language_header || I18n.default_locale
session[:locale] = I18n.locale
end

How do I call a method in application_controller?

I have this method in my application controller:
before_action :set_locale
def set_locale
locale = params[:locale].to_s.strip.to_sym
I18n.locale = I18n.available_locales.include?(locale) ?
locale :
I18n.default_locale
end
def set_locale_or_redirect
unless no_locale_needed?
if params[:locale].present?
#current_locale = I18n.locale = params[:locale]
else
redirect_to locale: 'fr'
end
end
end
What I would like to do is put a link in the layouts/mylayout.html.erb (I have my navigation menu there too) pointing to the current url but setting a locale in params and calling set_locale.
How do I do that ?
You'd need to point a link to the current page. This quick little hack might work for you
link_to 'English', url_for('?locale=eng')
This was the solution:
<%= link_to 'nl', url_for( :locale => 'nl') %>

How to internationalize Rails app from url when logged out and user profile when logged in?

I'm trying to implement internationalization on the RailsTutorial Sample App.
I managed to store a locale attribute for each user, and URL localization like myurl.com/en and myurl.com/es. The point is how to combine both methods and internationalize my app with URL params when the user is logged out, otherwise use his own preference.
I have this so far. At this point, the user preference overrides the URL param, but if a user selects es language and navigates to myapp.com/en the content is displayed in Spanish. I want the URL to not have that param if the user is logged in.
# application_controller.rb
class ApplicationController < ActionController::Base
include SessionsHelper
before_action :set_locale
def default_url_options(options = {})
{locale: I18n.locale}.merge options
end
def set_locale
I18n.locale = (current_user&.locale) || params[:locale] || I18n.default_locale
end
end
# users_controller.rb
def change_locale
user = current_user
user.locale = params[:locale]
user.save
redirect_to request.referer
end
# routes.rb
post 'international/:locale' => 'users#change_locale', as: 'international'
scope "(:locale)", locale: /en|es/ do
# All my routes
end
get '/:locale' => 'static_pages#home' # Fix the home route
# _footer.html.erb
...
<li>
<%= link_to t('spanish'), international_path(locale: :es), :method => :post %>
</li>
<li>
<%= link_to t('english'), international_path(locale: :en), :method => :post %>
</li>
...
I18n.available_locales output
[:en, :"da-DK", :"en-GB", :he, :"en-NZ", :"en-au-ocker", :uk, :"en-CA", :ja, :"en-BORK", :"zh-CN", :ru, :de, :"en-AU", :"de-AT", :pl, :"ca-CAT", :fr, :"fi-FI", :vi, :"en-UG", :nl, :"en-SG", :nep, :pt, :ko, :es, :sv, :ca, :"de-CH", :"zh-TW", :sk, :"en-IND", :it, :"en-US", :"pt-BR", :fa, :"nb-NO"]
My view code looks as this (right now there are only two links)
<li>
<%= link_to "Español", international_path(locale: :es), :metho d => :post %>
</li>
<li>
<%= link_to "English", international_path(locale: :en), :method => :post %>
</li>
PryRpl debugging:
When I'm logged in: If I write manually the URL it always go to the user stored language despite which URL I write. It always displays the user preference (:en or :es)
When i'm logged in and I click on an explicit view locale link (one of the above): The user preference is changed so it loads the new language stored to the user pref.
When I'm not logged in: It uses the URL param.
Found the solution thanks to a coworker.
def set_locale
# Change current_user&.locale to enable ruby <2.3 compatibility
I18n.locale = (current_user.locale if current_user) || params[:locale] || I18n.default_locale
redirect_to locale: I18n.locale if request.get? && params[:locale] != I18n.locale.to_s
end
If anyone knows a better solution please post an answer with it.
def set_locale
if user_signed_in?
I18n.locale = current_user.language
else
I18n.locale = params[:lang] || locale_from_header || I18n.default_locale
end
end
def locale_from_header
request.env.fetch('HTTP_ACCEPT_LANGUAGE', '').scan(/[a-z]{2}/).first
end

Locale not switching in Rails 4

My Rails App is on rails 4.0.2 and I have a problem switching translations with the locale variable and the params[:locale] from the url scheme following the official rails guide. I have a single page website at my site.
My routes for Internationalization:
scope "(:locale)", locale: /en|de/ do
#my routes here
end
My application controller
before_filter :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
#Rails.application.routes.default_url_options[:locale]= I18n.locale
end
# app/controllers/application_controller.rb
def default_url_options(options = {})
{ locale: I18n.locale }.merge options
end
The links to change the locale variables in the view:
<%= link_to_unless I18n.locale == :en, "English", locale: :en %>
|
<%= link_to_unless I18n.locale == :de, "Deutsch", locale: :de %>
What happens: the locale variable is set correctly but the translations are not switching. If I remove one of the translation files (currently for english and german) the languages switches to the remaining translation file. When I put back the other translation file and try to switch to it by changing the locale variable it never switches to the other language.
Why is my code not changing the translations?
I had the same issues and maybe it would be a solution for you:
in routes.rb change
scope "(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
#your routes here
end
get '*path', to: redirect("/#{I18n.default_locale}/%{path}")
get '', to: redirect("/#{I18n.default_locale}")
in application_controller.rb
def set_locale
I18n.locale = params[:locale] if params[:locale].present?
end
def default_url_options(options = {})
{locale: I18n.locale}
end
p.s.
in config/locales/en.yml something like this:
en:
languages:
en: "English"
de: "Deutsch"
and in config/locales/de.yml in German
in view
<%= link_to_unless_current t('languages.en'), locale: :en %>
|
<%= link_to_unless_current t('languages.de'), locale: :de %>
I think you need to define the constraint on the locale more explicit:
scope path: '(:locale)', constraints: { locale: /en|de/ } do
# routes you want to localize
end

Resources