Simple Internationalization in Ruby Sinatra

Internationalization is such a long word that people commonly refer to it simply as “i18n”. If it’s not already obvious, the number “18” refers to the number of characters between the “i” and the final “n”. It’s a complicated word, and similarly, internationalization can be a complicated beast to tame in your web pages, assuming you want to appeal to an international audience.

Some people approach the internationalization problem by creating entirely new web pages, which essentially results in a parallel website for each language. I don’t know about the latest version of Drupal, but Drupal 5.0 forced you into this very paradigm. What a nightmare to implement this approach and maintain a parallel website for each language! The situation gets really complicated when you want to link to another page within a different language. You have to go through and change each link for each of your parallel websites. It gets even more complicated if a target page in a specific language is not yet available because it hasn’t been translated yet.

I had to implement this process in Drupal a couple years ago. It was tedious and error-prone. And it was frustrating for our international sales offices that had to constantly beg for translated material. It was through this process that I learned how to swear in seven different languages.

We can simplify “Internationalization” to “i18n,” and in much the same way we can simplify its implementation in Ruby. It just takes a bit of preparation.

The concept presented here is relatively simple. I was inspired to create this concept based on Microsoft’s implementation of resource files when programming Windows. The implementation in Ruby uses the same concept of swapping resources. Specifically, a user, after having selected a language, stores a cookie with the standard 2-letter country code (as per the ISO standard). When processing subsequent page selections, Ruby simply reads the cookie and uses it to reference the appropriate translation from an internal resource hash. If the requested language is not available, it reverts back to a default language — English, in this case.

We start by creating an I18n class as follows:

require 'singleton'

class I18n
  include Singleton
  attr_reader :supported_languages
  attr_reader :language
  attr_reader :default

  def initialize
    @default = :en
    @supported_languages = {
        en: {
            language: 'English',
        },
        es: {
            language: 'Spanish',
        },
        nl: {
            language: 'Netherlands',
        },
        it: {
            language: 'Italian',
        },
        de: {
            language: 'German',
        },
        fr: {
            language: 'French',
        }
    }

The supported_languages variable is there for convenience. After instantiating this singleton class, one can query what languages it supports. It’s a simple matter to put additional languages into this structure. We continue now to build the resource hash, called @text.

    @text = {
        top_menu_my_account: {
            en: 'MY ACCOUNT',
            nl: 'MIJN ACCOUNT',
            es: 'MI CUENTA',
            it: 'IL MIO CONTO',
            de: 'MEIN KONTO',
            fr: 'MON COMPTE'
        },
        top_menu_wishlist: {
            en: 'WISHLIST',
            es: 'DESEOS',
            de: 'WUNSCHLISTE',
            fr: 'LISTE',
            nl: 'WENSLIJST'
        },
        top_menu_signup: {
            en: 'SIGNUP',
            nl: 'MELD JE AAN',
            es: 'CONTRATAR',
            it: 'ISCRIVITI',
            de: 'ANMELDUNG',
            fr: 'INSCRIPTION'
        } ...

    }

This resource hash is very big, because it includes all the menu items, push button labels and any other control you can think of. It also contains some very long passages of text; in fact, any text that you want translated should go into this hash. Admittedly, this could make the hash very large, so if your site gets very big, you may want to consider splitting up the hash into different files, and simply retrieving the necessary file when the page is rendered. The above code throws everything into a simple hash for demonstration purposes.

We now continue developing this class with a set_default and a set_language method. This allows you to set the default language (in this case, English) and then set the class-global variable @language so that it can be used to reference the correct phrase from the above @text hash.

  def set_default
    # Set the default language to English
    @default = :en
  end

  def set_language(lang)
    # Sets a new language. Returns true if the new language is different than the existing language
    temp_language = @language
    case (lang)
      when 'en'
        @language = :en
      when 'es'
        @language = :es
      when 'nl'
        @language = :nl
      when 'fr'
        @language = :fr
      when 'it'
        @language = :it
      when 'de'
        @language = :de
      else
        @language = :en
    end
    temp_language != @language # Returns true if language has changed
  end

Notice that the set_language method returns “true” if the language has changed from its previous setting. This may or may not be important to the calling routine. In my implementation, I keep track of whether the language changed so that I can re-send a new cookie out to the client browser when the visitor has decided to switch languages.

Finally, the moment you’ve been waiting for has arrived! We use a very simple get_text routine that cross-references the Ruby symbol with the appropriate text in the language of your choice. Note that if a specific language is not available, the default translation is provided.

  def get_text(ref)
    return '' unless @text.has_key?(ref) && @text[ref].has_key?(@default) # return empty if no entry with default baseline
    return (@text[ref].has_key?(@language)) ? @text[ref][@language] : @text[ref][@default]
  end

Though this explanation is terse, it should help inspire you to think about implementing i18n right from the beginning of your Sinatra programming effort. The only thing you have to keep in mind is to create a unique Ruby symbol for every control and every passage on your website. In cases where there is a huge amount of material, consider using this same concept, but scaling it up to use resource files instead of a single resource hash. The same algorithms would be used in either case.

One more note: discipline is essential here. As the web developer, you must ensure that people cannot simply post their pages in their own language. You must ensure that they post their pages into the appropriate resource file structure so as to ensure that translations, should they become available some day, can easily be added. The end result is that you don’t have to create a parallel website for each language. Everything uses the same website and simply pulls in different resources, depending on the language information in the viewer’s cookie.