Simple i18n for JavaScript Apps

        
//  Adds data that is used to translate
i18n.translator.add({
  values:{
    "Hello": "こんにちは"
  }
})

//  Then translate something
i18n("Hello");  // -> こんにちは
        
      

Language Files

In reality we define all of the translations in a separate JSON file that is loaded and passed to the i18n.add method.

Here is an example of what the JSON file might look like.

        
{
  "values":{
    "Yes": "はい",
    "No": "いいえ",
    "Ok": "Ok",
    "Cancel": "キャンセル"
  }
}
        
      

Then you can use your preffered method to load the JSON, pass it into i18n and away you go.

        
//  Load the JSON File
$.ajax("ja.json").done(function(text){
  //  Parse it
  data = JSON.parse(text);
  //  Set the data
  i18n.translator.add(data);
  //  Translate away
  i18n("Yes");          // -> はい
  i18n("No");           // -> いいえ
})
        
      

Pluralisation

Pluralisation is how we take care of all the funny rules langauges have when talking about a number of things.

Even if you are not translating your application, i18n can help you make the English on your site sound more natural.

        
{
  "values":{
    "%n comments":[
      [0, 0, "%n comments"],
      [1, 1, "%n comment"],
      [2, null, "%n comments"]
    ]
  }
}
        
      

Instead of just supplying a single translation we supply a series of translations. As we supply each translation we supply the range which tells i18n when to use it.

        
i18n("%n comments", 0);    //  -> 0 comments
i18n("%n comments", 1);    //  -> 1 comment
i18n("%n comments", 2);    //  -> 2 comments
        
      

Notice that the keys passed in for each of the above examples are identical.

In Japanese it is all the same so we only need to provide single entry.

        
{
  "values":{
    "%n comments":[
      [0, null, "%n コメント"]
    ]
  }
}
        
      
        
i18n("%n comments", 0);    //  -> 0 コメント
i18n("%n comments", 1);    //  -> 1 コメント
i18n("%n comments", 2);    //  -> 2 コメント
        
      

Fun with Pluralisation

You can also do more creative things with pluralisation

        
{
  "values":{
    "Due in %n days":[
      [null, -2, "Due -%n days ago"],
      [-1, -1, "Due Yesterday"],
      [0, 0, "Due Today"],
      [1, 1, "Due Tomorrow"],
      [2, null, "Due in %n days"]
    ]
  }
}
        
      
        
i18n("Due in %n days", 2);    //  -> Due in 2 days
i18n("Due in %n days", 1);    //  -> Due Tomorrow
i18n("Due in %n days", 0);    //  -> Due Today
i18n("Due in %n days", -1);   //  -> Due Yesterday
i18n("Due in %n days", -2);   //  -> Due 2 days ago
        
      

Formatting

Sometimes you want to mix variables into the text you are translating.

        
i18n("Welcome %{name}",
  {
    name:"John"
  }
);    //  -> Welcome John
        
      

Just pass in a "formatting" object with the keys matching the replacements.

Contexts

And then there will be times when you want to pass in extra information to be used when translating. A common example is working with genders.

        
{
  "values":{
    "Yes": "Yes",
    "No": "No"
  },
  "contexts":[
    {
      "matches":{
        "gender":"male"
      },
      "values":{
          "%{name} updated their profile":
          "%{name} updated his profile"
      }
    },
    {
      "matches":{
        "gender":"female"
      },
      "values":{
        "%{name} updated their profile":
        "%{name} updated her profile"
      }
    }
  ]
}
        
      

And then to use the context...

        
i18n("%{name} updated their profile",
  {
    name:"John"
  },
  {
    gender:"male"
  }
); //  ->  John updated his profile

i18n("%{name} updated their profile",
  {
    name:"Jane"
  },
  {
    gender:"female"
  }
); //  ->  Jane updated her profile
        
      

All Together Now

And of course, you can combine all of these concepts to achieve very sophisticated translation.

        
{
  "values":{
    "Yes": "Yes",
    "No": "No"
  },
  "contexts":[
    {
      "matches":{
        "gender":"male"
      },
      "values":{
        "%{name} uploaded %n photos to their %{album} album":[
          [0, 0, "%{name} uploaded %n photos to his %{album} album"],
          [1, 1, "%{name} uploaded %n photo to his %{album} album"],
          [2, null, "%{name} uploaded %n photos to his %{album} album"]
        ]
      }
    },
    {
      "matches":{
        "gender":"female"
      },
      "values":{
        "%{name} uploaded %n photos to their %{album} album":[
          [0, 0, "%{name} uploaded %n photos to her %{album} album"],
          [1, 1, "%{name} uploaded %n photo to her %{album} album"],
          [2, null, "%{name} uploaded %n photos to her %{album} album"]
        ]
      }
    }
  ]
}
        
      
        
i18n("%{name} uploaded %n photos to their %{album} album", 1,
  {
    name:"John",
    album:"Buck's Night"
  },
  {
    gender:"male"
  }
); //  ->  John uploaded 1 photo to his Buck's Night album

i18n("%{name} uploaded %n photos to their %{album} album", 4,
  {
    name:"Jane"
    album:"Hen's Night"
  },
  {
    gender:"female"
  }
); //  ->  Jane uploaded 4 photos to her Hen's Night album
        
      

Remind me... Why?

So, that may look like a lot of work for a bit of string concatenation. At this point let me remind you of the original objective of i18njs -> internationalisation.

So, here is the equivalent Japanese language file.

        
{
  "values":{
    "Yes": "はい",
    "No": "いいえ"
  },
  "contexts":[
    {
      "matches":{
        "gender":"male"
      },
      "values":{
        "%{name} uploaded %n photos to their %{album} album":[
          [0, 0, "%{name}は彼の%{album}アルバムに写真%n枚をアップロードしました"]
        ]
      }
    },
    {
      "matches":{
        "gender":"female"
      },
      "values":{
        "%{name} uploaded %n photos to their %{album} album":[
          [0, 0, "%{name}は彼女の%{album}アルバムに写真%n枚をアップロードしました"]
        ]
      }
    }
  ]
}
        
      

And then without changing any of our application code we get a beautifully translated sentence with pluralisation, formatting and context.

You may also notice that the word order in Japanese is completely different compared to English. This is why it is so important to use formatting rather than string concatenation.

        
i18n("%{name} uploaded %n photos to their %{album} album", 1,
  {
    name:"John",
    album:"Buck's Night"
  },
  {
    gender:"male"
  }
); //  ->  Johnは彼のBuck's Nightアルバムに写真1枚をアップロードしました

i18n("%{name} uploaded %n photos to their %{album} album", 4,
  {
    name:"Jane"
    album:"Hen's Night"
  },
  {
    gender:"female"
  }
); //  ->  Janeは彼女のHen's Nightアルバムに写真4枚をアップロードしました
        
      

A Note on Keys

In all of the examples above I have used the actual English text as the keys for the translation. You don't have to do it this way. It is perfectly legitimate to do the following.

        
{
  "values":{
    "loginFail":
    "There was an error logging in, please check your email and password."
  }
}
        
      

And then you would grab the translation like so.

        
i18n("loginFail");
// -> There was an error logging in, please check your email and password.
        
      

Advantages:

You can also provide a default string in case the key can not be found.

        
i18n("emailRequired", "We require an email when signing up.");
// -> We require an email when signing up.
        
      

Multiple Languages

Sometimes you may want to have multiple languages working simultaneously.

You can create additional instances of i18n like so.

        
en = i18n.create({
  values:{
    "Hello":"Hello"
  }
})

ja = i18n.create({
  values:{
    "Hello":"こんにちは"
  }
})

pt = i18n.create({
  values:{
    "Hello":"Olá"
  }
})
        
      

And then call

        
en("Hello")
// -> Hello

ja("Hello")
// -> こんにちは

pt("Hello")
// -> Olá
        
      

This is particularly useful in a node environment when you will want to have all of the languages initialised and then simply select a particular language instance for each incoming request.

Extensions

In most cases the features above will suffice. But every now and then you will have more complex translation requirements. This is where extensions can help. The extension will be called when the value for a given key is an "object". How you structure that object is entirely up to you. The expectation is that the extension will simply return a string that will then have the same formatting, context and pluralisation replacements applied to it. Extensions have the limitation of a single extension being available per instance of i18n. That is, each language may have it's own extension, but a single language can only support one.

The following is an example of using extensions to handle the pluralisation rules for the Russian language.

        
let ru = i18n.create({
  values:{
    // Note, the value of the key '%n results' is an 
    // object which will trigger the extension to be called.
    "%n results":{
      "zero": "нет результатов",
      "one": "%n результат",
      "few": "%n результата",
      "many": "%n результатов",
      "other": "%n результаты"
    }
  }
)

function getPluralisationKey(num) {
  if (!num) {
    return 'zero'
  }
  if (num % 10 == 1 && num % 100 != 11) {
    return 'one'
  }
  if ([2, 3, 4].indexOf(num % 10) >= 0 
    && [12, 13, 14].indexOf(num % 100) < 0) {
    return 'few'
  }
  if (num % 10 == 0 || [5, 6, 7, 8, 9].indexOf(num % 10) >= 0 
    || [11, 12, 13, 14].indexOf(num % 100) >= 0) {
    return 'many'
  }
  return 'other'
}

function russianExtension(text, num, formatting, data){
  let key = getPluralisationKey(num)
  return data[key]
}

ru.extend(russianExtension)

ru('%n results', 0)
// -> нет результатов
ru('%n results', 1)
// -> 1 результат
ru('%n results', 11)
// -> 11 результатов
ru('%n results', 11)
// -> 4 результата
ru('%n results', 101)
// -> 101 результат