Rails Localization with Subdomains and IP Address

January 15, 2014

In this post I will explain how we are using Rails redirect to a localized subdomain based on a visitors IP address while also allowing a visitor to override these defaults.

The Problem

Recently one of our clients asked us to set up some localization on their site (sellrest.com). They asked that the locale be set via different subdomains.

My first thought was "Great! Rails provides this sort of functionality right out of the box with their i18 gem." Soon after they added another requirement. They wanted visitors be redirected to a specific subdomain based on their location in the world and then be able to select a specific subdomains to be their default. My response imediately changed to "Humm I don't know how to do that so let me get back to you." After some searching and tinkering I found a nice solution and added another skill to my developer tool belt.

Step 1: Setting up the Localized Subdomains

First we need to set the locale based on a subdomain. We are going to implement locales for canada, france, and the uk. Our default locale will be en. The Rails guides provides some nice documentation for this. I prefer testing so here are my specs.

  # /spec/controllers/application_controller_spec.rb

require 'spec_helper'

describe ApplicationController do

  describe '#set_locale' do
    controller do
      def index
        render :text => 'Ruby is awesome.'
      end
    end

    ['canada', 'uk', 'france'].each do |locale|
      it "will set #{locale} subdomain to the #{locale.to_sym} locale}" do
        request.host = "#{locale}.example.com"
        get :index
        expect(response.status).to eq(200)
        expect(I18n.locale).to eq(locale.to_sym)
      end
    end
    it 'will default to en if no subdomain' do
      get :index
      expect(response.status).to eq(200)
      expect(I18n.locale).to eq(:en)
    end
  end
end
  

In this spec notice that we are setting up a mock index action for ApplicationController because it does not have any actions itself and I want to only test this class. In the first it block I am looping through my subdomains, setting the request host, calling the index action, and then verifying the response was ok. Most importantly we expect to see that the locale was set correctly. In the second it block we are making a request without a subdomain and expecting to see our en default.

To get these tests passing we have to add our locale files and add a before action to ApplicationController.

The config/locales directory holds all of your various defined locales. These yaml files will be read when Rails boots up and added to the I18n.available_locales array. For example a file with the name uk.yml will be listed in the array as the :uk symbol. In our example we will be adding the following files to the directory canada.yml, france.yml, and uk.yml. In each file make sure you start the base nodes of the file or Rails will complain that it could not create a hash from the yaml file. For example our uk file could be.

uk:
  hello: "Get local"

Now that we have the necessary yaml files in place we need to have our app load the appropriate locale based on the subdomain. Let's add this ability to our ApplicationController.

    # /app/controllers/application_controller.rb

class ApplicationController < ActionController::Base

  before_action :set_locale

  def set_locale
    I18n.locale = extract_locale_from_subdomain || I18n.default_locale
  end

  private

  def extract_locale_from_subdomain
    parsed_locale = request.subdomains.first.try(:to_sym)
    I18n.available_locales.include?(parsed_locale) ? parsed_locale : nil
  end

end
    

Notice that .try(:to_sym) in the extract_locale_from_subdomain method? This prevents Rails from throwing NoMethodError: undefined method `to_sym' for nil:NilClass in the event that there is no subdomain.

From here our tests should be passing and and we are free to use translate and localize methods. Information on how to use these can be found in the guide and in the api docs.

Step 2: IP Address Detecting