3

I am trying to use request.location (geocoder gem), to set the locale appropriately to the clients IP address.


This is what I've got:

app/controllers/application_controller.rb

before_action :set_locale

private

def set_locale
    # get location with geocoder
    location = request.location

    # set locale through URL
    if params.has_key?(:locale)
        I18n.locale = params[:locale] || I18n.default_locale
    # set locale through user preference
    elsif current_user && current_user.locale.present?
        I18n.locale = current_user.try(:locale) || I18n.default_locale
    # set locale through geocoder detection of location
    elsif location.present? && location.country_code.present? && I18n.available_locales.include?(location.country_code)
        if location.country_code.include? "-"
            I18n.locale = location.country_code
        else
            I18n.locale = location.country_code.downcase
        end
    # set locale through HTTP detection of location
    elsif (request.env["HTTP_ACCEPT_LANGUAGE"] || "en").scan(/^[a-z]{2}/).first.present? && I18n.available_locales.include?((request.env["HTTP_ACCEPT_LANGUAGE"] || "en").scan(/^[a-z]{2}/).first)
        I18n.locale = (request.env["HTTP_ACCEPT_LANGUAGE"] || "en").scan(/^[a-z]{2}/).first
    end
end


config/application.rb

# i18n Translations
## load the subfolders in the locales
config.i18n.load_path += Dir["#{Rails.root.to_s}/config/locales/**/**/**/**/**/*.{rb,yml}"]
## set default locale
config.i18n.default_locale = 'en'
## provide locale fallbacks
config.i18n.enforce_available_locales = false
config.i18n.fallbacks = {
    'de-AT' => 'de', 'de-CH' => 'de', 'de-DE' => 'de',
    'en-AU' => 'en', 'en-CA' => 'en', 'en-GB' => 'en', 'en-IE' => 'en', 'en-IN' => 'en', 'en-NZ' => 'en', 'en-US' => 'en', 'en-ZA' => 'en'
}

Using the parameter params[:locale], everything works just fine. But without the parameter it just defaults to en, always.

What am I doing wrong here?

heroxav
  • 1,387
  • 1
  • 22
  • 65

1 Answers1

6

I18n.locale isn't remembered between requests by default - you'll need to implement that yourself by saving to it to the session. After I18n.locale is set, you can use:

session[:locale] = I18n.locale

And then to pull it back:

I18n.locale = params[:locale] || session[:locale] || I18n.default_locale
# explicit params take precedence
# otherwise use the remembered session value
# fallback to the default for new users

Sidenote: consider moving your location = request.location so that it doesn't always run. You're taking a small performance hit (and a geocoding service lookup) on every request - even if you're not using the data.


By way of example, here is one way you could do this:

def set_locale
    # explicit param can always override existing setting
    # otherwise, make sure to allow a user preference to override any automatic detection
    # then detect by location, and header
    # if all else fails, fall back to default
    I18n.locale = params[:locale] || user_pref_locale || session[:locale] || location_detected_locale || header_detected_locale || I18n.default_locale

    # save to session
    session[:locale] = I18n.locale
end

# these could potentially do with a bit of tidying up
# remember to return `nil` to indicate no match

def user_pref_locale
    return nil unless current_user && current_user.locale.present?
    current_user.locale
end

def location_detected_locale
    location = request.location
    return nil unless location.present? && location.country_code.present? && I18n.available_locales.include?(location.country_code)
    location.country_code.include?("-") ? location.country_code : location.country_code.downcase
end

def header_detected_locale
    return nil unless (request.env["HTTP_ACCEPT_LANGUAGE"] || "en").scan(/^[a-z]{2}/).first.present? && I18n.available_locales.include?((request.env["HTTP_ACCEPT_LANGUAGE"] || "en").scan(/^[a-z]{2}/).first)
    (request.env["HTTP_ACCEPT_LANGUAGE"] || "en").scan(/^[a-z]{2}/).first
end
heroxav
  • 1,387
  • 1
  • 22
  • 65
gwcodes
  • 5,632
  • 1
  • 11
  • 20
  • Weird is that if I add the following: `session[:locale] = params[:locale] if params[:locale] != session[:locale]` to update the session data, when a new locale has been set, causes the pplication to fallback to `en` on every request. Is rails setting this parameter in the background? – heroxav Mar 11 '17 at 12:00
  • @jonhue: `session` will be remembered between request, but your `params` may or may not include `locale`. To include it on every page would require you to pass it from your controller to your views when you generate URLs. The best way is to treat `params[:locale]` as a *explicit, one-off override* of any existing settings, and otherwise always rely on `session[:locale]` if it is set – gwcodes Mar 11 '17 at 12:06
  • Incidentally, there are good code samples for this in the I18n documentation section (http://stackoverflow.com/documentation/ruby-on-rails/2772/i18n-internationalization/9336/set-locale-through-requests#t=201703111203188547129), which might help to give a fuller explanation – gwcodes Mar 11 '17 at 12:07
  • Yea, after your post I tried to do it that way. The problem I am encountering is that, when `session[:locale]` has already been set. I cannot override it, by just checking `unless session[:locale]`. I need to use some sort of `if params[:locale]` to be ready for that. And it feels like rails is automatically setting that in the background, because even when the param is not set in the URL, it will run that code. – heroxav Mar 11 '17 at 12:11
  • @jonhue: I've updated the answer to include an example now, which hopefully makes things clearer! Caveat emptor: this isn't tested, but the principles should be sound. – gwcodes Mar 11 '17 at 12:17