R18n

R18n is a i18n tool to translate your Ruby application to several languages.

Use r18n-rails, sinatra-r18n or teamon’s merb_i18n to localize Web applications and r18n-desktop to localize desktop application.

Features

Ruby-style Syntax

R18n uses hierarchical not English-centrist YAML format for translations by default:

user:
  edit: Edit user
  name: User name is %1
  count: !!pl
    1: There is 1 user
    n: There are %1 users

To access translation you can call methods with same names:

t.user.edit         #=> "Edit user"
t.user.name('John') #=> "User name is John"
t.user.count(5)     #=> "There are 5 users"

t.not.exists | 'default' #=> "default"
t.not.exists.translated? #=> false

If translation key has name of Object method you can use another way:

t[:methods] #=> "Methods"

Filters

You can add custom filters for some YAML type or any translated strings. Filters are cascade and can communicate with each other.

R18n already has filters for HTML escaping, lambdas, Textile and Markdown:

hi: !!markdown |
  **Hi**, people!
greater: !!escape
  1 < 2 is true

t.hi      #=> "<p><strong>Hi</strong>, people!</p>"
t.greater #=> "1 &lt; 2 is true"

Flexibility

Translation variables and pluralization (“1 comment”, “5 comments”) are filters too. So you can extend or replace it. For example, you can use named variables filter from r18n-rails-api gem:

greeting: "Hi, {{name}}"

R18n::Filters.on(:named_variables)
t.greeting(name: 'John') #=> "Hi, John"

Flexible Locales

Locale can extend Locale class, so locales are very flexible. For example, English locale extend time formatters:

l Date.now, :full #=> "30th of November, 2009"

Or Russian has built-in different pluralization without any lambdas in YAML:

t.user.count(1) #=> "1 пользователь"
t.user.count(2) #=> "2 пользователя"
t.user.count(5) #=> "5 пользователей"

Loaders

R18n can load translations from any places, not just from YAML files. You just need to create loader object with 2 methods: available and load:

class DBLoader
  def available
    Translation.find(:all).map(&:locale)
  end
  def load(locale)
    Translation.find(locale).to_hash
  end
end

R18n.set R18n::I18n.new(user_locales, DBLoader.new)

You can also set a list of different translation places or set extension places, which will be used only with application translation (useful for plugins).

Object Translation

You can translate any class, including ORM models:

require 'r18n-core/translated'

class Product < ActiveRecord::Base
  include R18n::Translated
  # Model has two usual property: title_en and title_ru
  translations :title
end

# For English user
product.title #=> "Anthrax"

# For Russian user
product.title #=> "Сибирская язва"

Localization

R18n can localize numbers and time:

l -5000                 #=> "−5,000"
l Time.now              #=> "30/11/2009 14:36"
l Time.now, :full       #=> "30th of November, 2009 14:37"
l Time.now - 60, :human #=> "1 minute ago"

Several User Languages

Lack of translation in user language isn’t exception for R18n (because translation to not primary language done by enthusiasts, it can be out of date). R18n just automatically take next user language (browser send a list of locales) and for cultures with two officially languages (e.g., exUSSR) it take second language (e.g., if translation isn’t available in Kazakh R18n will see in Russian):

i18n = R18n::I18n.new(['kk', 'de'], 'dir/with/translations')

i18n.locales    #=> [Locale kk (Қазақша), Locale de (Deutsch),
                #    Locale ru (Русский), Locale en (English)]

i18n.kazakh  #=> "Қазақша", main user language
i18n.deutsch #=> "Deutsch", not in Kazakh, use next user locale
i18n.russian #=> "Русский", not in kk and de, use Kazakh sublocale
i18n.english #=> "English", not in any user locales, use default

Agnostic

R18n has a agnostic core package and plugins with out-of-box support for Sinatra, Merb and desktop applications.

Usage

Translation

Translation files use YAML format and has name like en.yml (English) or en-us.yml (USA English dialect) with language/country code (RFC 3066).

In translation you can use:

  • Strings

    robot: This is robot
    percent: "Percent sign (%)"
    
  • Numbers

    number: 123
    float: 12.45
    
  • Pluralizable messages

    robots: !!pl
      0: No robots
      1: One robot
      n: %1 robots
    
  • Filters

    filtered: !!custom_type
      This content will be processed by filter
    

To get translated string use method with key name or square brackets [] for keys, which is same with Object methods (class, inspect, etc):

t.robot    #=> "This is robot"
t[:robot]  #=> "This is robot"

Translation may be hierarchical:

t.post.add     #=> "Add post"
t[:post][:add] #=> "Add post"

If locale willn’t be found in user locale R18n will search it in they sublocales or in another locale, which user know:

t.no.in.english #=> "В английском нет"

Translated string has locale method and you can get it locale (Locale instance or code string if locale is’t supported in R18n):

i18n.no.in.english.locale #=> Locale ru (Русский)

You can replace some parameters in translated string by put it as arguments:

name: "My name is %1"

t.name('John') #=> "My name is John"

Pluralizable messages get item count from first argument:

t.robots(0)  #=> "No robots"
t.robots(1)  #=> "One robot"
t.robots(50) #=> "50 robots"

If there isn’t pluralization for some number, translation will be use n. If there isn’t locale file for translation, it will be use English pluralization rule (0, 1 and n).

You can check, is message has translation:

t.post.add.translated?   #=> true
t.not.exists.translated? #=> false

For untranslated strings you can set default value:

t.not.exists | 'default' #=> "default"

You can get translation keys, to analyze translation:

t.counties._keys.each do |county|
  puts t.counties[county]
end

R18n already has translation for common words for most supported locales. See base/ in dir in gem.

t.yes    #=> "Yes"
t.cancel #=> "Cancel"
t.delete #=> "Delete"

Filters

You can also add you own filter for translations: escape HTML entries, convert from Markdown syntax, etc. Filters can be passive to process translation only on loading.

R18n::Filters.add('custom_type', :filter_name) do |content, config, replace|
  content.gsub(' ', replace)
end
R18n::Filters.add('custom_type', :passive => true) do |content, config|
  content + '!'
end

t.filtered('_') #=> "This_content_will_be_processed_by_filter!"

You can also add global filters for all translated strings:

R18n::Filters.add(String, :capitalize_ruby) do |content, config|
  content.gsub(/ruby/i) { |i| i.swapcase }
end

HTML Escape

R18n contain 2 filters to escape HTML entries: by YAML type and global. If you need to escape HTML in some translations, just set !!escape YAML type:

greater: !!escape
  1 < 2 is true

t.greater #=> "1 &lt; 2 is true"

If you develop web application and want to escape HTML in all translations, just activate global escape filter:

R18n::Filters.on(:global_escape_html)

If you enable global HTML escape, you may use !!html YAML type to disable escaping in some special value.

warning: !!html
  <b>Warning</b>

R18n::Filters.on(:global_escape_html)
t.warning #=> "<b>Warning</b>"

Markdown

To use Markdown in your translations you must install maruku gem:

hi: !!markdown
  **Hi**, people!

t.hi #=> "<p><strong>Hi</strong>, people!</p>"

Textile

To use Textile in your translations you must install RedCloth gem:

alarm: !!textile
  It will delete _all_ users!

t.alarm #=> "<p>It will delete <em>all</em> users!</p>"

Lambdas

You can use lambdas in your translations.

sum: !!proc |x, y| x + y

t.sum(1, 2) #=> 3

If it isn’t secure in your application (for example, user can change translations), you can disable it:

R18n::Filters.off(:procedure)

Localization

You can print number and float according to the rules of the user locale:

l -12000.5 #=> "−12,000.5"

Number and float formatters will also put real typographic minus and put non-break thin spaces (for locale, which use it as digit separator).

You can translate months and week days names in Time, Date and DateTime by strftime method:

l Time.now, '%B'  #=> "September"

R18n has some time formats for locales: :human, :full and :standard (by default):

l Time.now, :human #=> "now"
l Time.now, :full  #=> "August 9th, 2009 21:47"
l Time.now         #=> "08/09/2009 21:41"
l Time.now.to_date #=> "08/09/2009"

Model

You can add i18n support to any classes, including ORM models:

require 'r18n-core/translated'

class Product
  include DataMapper::Resource
  property :title_ru, String
  property :title_en, String

  include R18n::Translated
  translations :title
end

# For example, user know only Russian

# Set value to English (default) title
product.title_en = "Anthrax"
product.title #=> "Anthrax"

# Set value to title on user locale (Russian)
product.title = "Сибирская язва"
product.title #=> "Сибирская язва"

product.title_en #=> "Anthrax"
product.title_ru #=> "Сибирская язва"

See R18n::Translated for documentation.

Locale

All supported locales are storage in R18n gem at locales dir. If you want to add your locale, please write me to [email protected].

To get information about locale create R18n::Locale instance:

locale = R18n::Locale.load('en')

You can get from locale:

  • Locale title and RFC 3066 code:

    locale.title #=> "English"
    locale.code  #=> "en"
    
  • Language direction (left to right, or right to left for Arabic and Hebrew):

    locale.ltr? #=> true
    
  • Week start day (:monday or :sunday):

    locale.week_start #=> :sunday
    

Loaders

You can load translations from any ways, not just from YAML files. To load translation you must create loader class with 2 methods:

  • available – return array of locales of available translations;

  • load(locale) – return Hash of translation.

And put it instance to R18n::I18n.new:

R18n.set R18n::I18n.new('en', MyLoader.new(loader_param))

You can set your loader as default and pass to R18n::I18n.new only constructor argument:

R18n.default_loader = MyLoader
R18n.set R18n::I18n.new('en', loader_param)

If you want to load translation with some type for filter, use R18n::Typed struct:

# Loader load method return something like:
{ 'users' => R18n::Typed.new('pl', { 1 => '1 user', 'n' => '%1 users' }) }

# To use pluralization filter (+pl+ type):
t.users(5) #=> "5 users"

Extension Translations

For r18n plugin you can add loaders with translations, which will be used with application translations. For example, DB plugin may place translations for error messages in extension dir. R18n contain translations for base words as extension dir too.

R18n.extension_places << R18n::Loader::YAML.new('./error_messages/')

Add Locale

If R18n hasn’t locale file for your language, please add it. It’s very simple:

  • Create in locales/ file code.rb for your language and describe locale. Just copy from another locale and change different values.

    • If in your country people mostly know another language (like in exUSSR countries people know Russian), add sublocales %{another_locale en}.

  • Create in base/ file code.yml for your language and translate base messages. Just copy file from language, which you know, and rewrite values.

  • If you language need some special logic (for example, different pluralization or time formatters) you can extend Locale class methods.

  • Push files by GitHub (github.com/ai/r18n) or just write e-mail with this files to me ([email protected]).

Code is RFC 3066 code for your language (for example, “en” for English and “fr-CA” for Canadian French). You can send to my e-mail any questions (on sitnik.ru you find another contact addresses).

License

R18n is licensed under the GNU Lesser General Public License version 3. You can read it in LICENSE file or in www.gnu.org/licenses/lgpl.html.

Author

Andrey “A.I.” Sitnik <[email protected]>