Jed

Gettext Style i18n for Modern JavaScript Apps

var i18n = new Jed({ locale_data: { ... } });

// Use the chainable API

i18n.translate("There is one translation.")
    .onDomain("messages")
    .withContext("male")
    .ifPlural( num, "There are %d translations." )
    .fetch( num );

// Or kick it old school

var out = i18n.dnpgettext(
  "messages",
  "male",
  "There is one translation.",
  "There are %d translations.",
  num
);

Jed.sprintf( out, num );

Gettext?

Gettext is an old translation standard. It solves a unique set of problems when dealing with things like pluralization and positional interpolation. There are implementations of gettext in nearly every language (including javascript). I wasn't happy with the quality, safety, or speed of other javascript implementations and thus Jed was born. You can read more about Gettext here: gnu.org/software/gettext/

Safety and Speed

Jed parses plural forms using a grammar instead of running eval. This results in both safer and faster plural-form function generations. The jison grammar that was used to generate the parser is included in the source.

You can find a fairly comprehensive list of plural forms here: translate.sourceforge.net/wiki/l10n/pluralforms

Sane API Wrappers

Sometimes using gettext directly is fine, but knowing which gettext function to call at runtime can often be cumbersome. Since JavaScript supports things like chaining and function overloading, we can wrap the original API with one that feels a bit more modern.

Jed API

The core available functions on an instantiated Jed object are:

Jed also exposes all the standard Gettext functions on an instance. Those behave according to spec and are detailed further down in the document. Here's how you use these "more-friendly-than-direct-gettext" functions:


// Create a new instance
var i18n = new Jed({

  // You can choose to set the domain at instantiation time
  // If you don't, then "messages" will be used by default
  "domain" : "the_domain",

  // This callback is called when a key is missing
  "missing_key_callback" : function(key) {
    // Do something with the missing key
    // e.g. send key to web service or
    console.error(key)
  },

  // This is the translation data, which is often generated by
  // a po2json converter. You would ideally have one per locale
  // and only pull in the locale_data that you need.
  "locale_data" : {

    // This is the domain key
    "the_domain" : {

      // The empty string key is used as the configuration
      // block for each domain
      "" : {
        // Domain name
        "domain" : "the_domain",

        // Language code
        "lang" : "en",

        // Plural form function for language
        "plural_forms" : "nplurals=2; plural=(n != 1);"
      },

      // Other keys in a domain contain arrays as values
      // that map to the translations for that key.
      // Gettext suggests that you use english as your keys
      // in case the key isn't found, and it can just pass
      // the value directly through.
      // Note: by convention, the 0-index location of the translations
      // is never accessed. It's just a thing, I guess.
      "a key" : [ null, "the translation", "the plural translations", ... ],

      // The plural form string is converted into a function
      // and the value that's passed into the gettext call
      // is passed into the plural form function. It's result
      // (plus 1) is the index that the translation comes from.

      // We're using sprintf interpolation on our keys so we can
      // then sub in the _actual_ values into the result.
      "%d key" : [ null, "%d key", "%d keys" ],

      // Contexts are keys that are just prefixed with a context string
      // with a unicode \u0004 as the delimiter.
      // You can use it for anything. Usually it's just for being content aware
      // in some way (e.g. male vs. female, product vs. category)
      "context\u0004%d key": [ null, "context %d key", "context %d keys" ]
    }
  }
});

Now that you have an instance that's attached to a set of locale data, and often a default domain, you'll want to pull out translations

// The translate function is chained, and doesn't
// end until you call 'fetch'
i18n.translate('a key').fetch();
> "the translation"

// Most apps will only require a single Jed instance, but 
// occasionally you may need more. You can use the same
// instance to make as many queries as you want.

// Keys that don't exist are passed through directly, so in the case of missing
// translations, you still get the default language.

var n = 5;
i18n.translate("%d key doesnt exist").ifPlural( n, "%d keys dont exist" ).fetch();
> "%d keys dont exist"

// You can pass in sprintf arguments to fetch to do value interpolation

i18n.translate("%d key doesnt exist").ifPlural( n, "%d keys dont exist" ).fetch( n );
> "5 keys dont exist"

// It's important that you still give the default language plural
// version in the `ifPlural` call for cases when the key doesn't 
// exist, however, normally, the values in the translation object
// are the returned values.
i18n.translate("%d key").ifPlural( n, "not used, but still important" ).fetch( n );
> "5 keys"

// We can specify a context in our chain
i18n.translate("%d key").withContext("context").ifPlural( n, "default %d keys" ).fetch( n );
> "context 5 keys"

// We can also change/set our domain if we have more than one
i18n.translate("a key").onDomain("other_domain").fetch();
> "Whatever the value was on that domain ;)"

// We can use the sprintf positional capabilities in our replacement
// This is a super contrived example of that:
i18n.translate("I like the %1$s %2$s.").fetch( "red", "shirt" );
> "I like the red shirt."

// Then in our spanish language file, we'd have the key:
{ ...
"I like the %1$s %2$s." : [ null, "Me gusta la %2$s %1$s.", ... ]
... }

// Notice the reverse numbering, so our result on that locale_data would be:
i18n.translate("I like the %1$s %2$s.").fetch( "roja", "camisa" );
> "Me gusta la camisa roja."

Installation

Node

npm install jed

Then in your app:

var Jed = require('jed');

Webpage ( global )

<script src="jed.js"></script>
<script>
var i18n = new Jed({
  // Generally output by a .po file conversion
  locale_data : {
    "messages" : {
      "" : {
        "domain" : "messages",
        "lang"   : "en",
        "plural_forms" : "nplurals=2; plural=(n != 1);"
      },
      "some key" : [ null, "some value"]
    }
  },
  "domain" : "messages"
});

alert( i18n.gettext( "some key" ) ); // alerts "some value"
</script>

AMD Module

require(['jed'], function ( Jed ) {
  var i18n = new Jed({
    // Generally output by a .po file conversion
    locale_data : {
      "messages" : {
        "" : {
          "domain" : "messages",
          "lang"   : "en",
          "plural_forms" : "nplurals=2; plural=(n != 1);"
        },
        "some key" : [ null, "some value"]
      }
    },
    "domain" : "messages"
  });

  alert( i18n.gettext( "some key" ) ); // alerts "some value"
  });
});

Details

The GNU gettext stuff defines 4 modifiers for gettext

Note :: I took the same path as `gettext.js` and just ignore any 'Category' information. The methods are exposed but the information is ignored. I'd recommend not using them at all, but I figured I'd be as API compatible with gettext.js as possible.

These map to the properties in the translation files.

At its base, Jed exposes (nearly) every combination of these four letters as functions

gettext = function ( key )
dgettext = function ( domain, key )
dcgettext = function ( domain, key, category )
ngettext = function ( singular_key, plural_key, value )
dngettext = function ( domain, singular_ley, plural_key, value )
dcngettext = function ( domain, singular_key, plural_key, value, category )
pgettext = function ( context, key )
dpgettext = function ( domain, context, key )
npgettext = function ( context, singular_key, plural_key, value )
dnpgettext = function ( domain, context, singular_key, plural_key, value )
dcnpgettext = function ( domain, context, singular_key, plural_key, value, category )

In the code, every function ends up calling dcnpgettext since it's the most specific. It just handles the missing values in the correct manner.

Jed intentionally made many of the same API choices as gettext.js for these lower level calls in order to offer a step up from gettext.js

That means in order instantiate an object to call these functions, you need to create a new `Jed` instance:

var i18n = new Jed( options );

Then you'll have the methods available to you. e.g. : i18n.gettext( 'key' );

The options object generally contains 1 or 2 keys: domain and locale_data

The domain setting is which group inside of locale_data that the keys will be looked up in.

The locale_data is the output from your po2json converter. The tests have a few good examples of what these can look like if you are in need of more examples. Here's one to boot:

var locale_data_multi = {
  "messages_3": {
    "": {
      "domain": "messages_3",
      "lang": "en",
      "plural-forms": "nplurals=2; plural=(n != 1);"
    },
    "test": [null, "test_1"],
    "test singular": ["test plural", "test_1 singular", "test_1 plural"],
    "context\u0004test": [null, "test_1 context"],
    "context\u0004test singular": ["test context plural", "test_1 context singular", "test_1 context plural"]
  },
  "messages_4": {
    "": {
      "domain": "messages_4",
      "lang": "en",
      "plural-forms": "nplurals=2; plural=(n != 1);"
    },
    "test": [null, "test_2"],
    "test singular": ["test plural", "test_2 singular", "test_2 plural"],
    "context\u0004test": [null, "test_2 context"],
    "context\u0004test singular": ["test context plural", "test_2 context singular", "test_2 context plural"]
  }
};

Other Functions

Jed.sprintf

This is the sprintf found at www.diveintojavascript.com/projects/javascript-sprintf - Courtesy of Alexandru Marasteanu.

It lives as both a 'static' method on the Jed function and on an individual instance. It is used for variable replacement after a translation happens. It supports reordering of values as well.

The english translation returns: "There are %1$d %2$s crayons."

Then with sprintf, we can do: alert( Jed.sprintf( "I like your %1$s %2$s.", 'red', 'shirt' ) );

This alerts I like your red shirt.

But in spanish it would look more like this:

alert( Jed.sprintf( "Me gusta tu %2$s %1$s.", 'roja', 'camisa' ) );
// This alerts "Me gusta tu camisa roja."

Translation files (as json)

There are quite a few available .po to .json converters out there. Gettext .po files are standard output from most decent translation companies, as it's an old standard.

I currently use: po2json

However, I'd like to add this functionality to a separate Jed module in a future version.

License

You may use this software under the WTFPL.

You may contribute to this software under the Dojo CLA - http://dojofoundation.org/about/cla

Tests

npm install
make test
make test-browser

The name

The name jed.js is an homage to Jed Schmidt - the JavaScript community member who is a japanese translator by day, and a "hobbyist" JavaScript programmer by night. Give your kids three character names and they'll probably get software named after them too.

Not coincidentally, his project locale could be a good plug into jed.js.

Why gettext?

Internationalization is hard. Sun created gettext back in the day as a way to make things a little easier.

Many apps that try to internationalize start out with simple key replacements.

<h1>{{i18n_helper "some_key"}}</h1>

Then they just map each locale to a different object

{
  en_us : {
    some_key : "This is a title."
  },
  en_ca : {
    some_key : "This is a title, eh?"
  }
}

That works for a little while, until you get into a situation where pluralization changes the structure of the sentence.

Consider: "I have a toaster" vs. "I have 3 toasters"

Some people choose to solve this with pre/postfix data:

"I have the following amount of toasters: " + num_toasters

This is not ideal from a UX standpoint, and it doesn't work in every language, so some people try to do this logic themselves:

if ( num_toasters === 1) {
  return i18n('single_toaster_key');
}
else {
  return i18n('plural_toaster_key');
}

That seems to be a good solution until you consider languages like Polish and Russian that have _much_ more complex rulesets for pluralization than `if not 1`. Splitting the logic on each language hardly makes it sane or decoupled, so that method is a bust.

Gettext + sprintf solve these problems.

alert( sprintf( ngettext('I have one toaster.', 'I have %1$d toasters.', num_toasters), num_toasters ) )

This would look up the translation for the 'I have one toaster' string, evaluate, the `num_toasters` value against it's `plural_forms` rule, choose the appropriate sprintf-able string to return, then the sprintf will sub in the correct data, and output a happy translated string. Since sprintf can handle argument reordering words can be mixed around based on languages own rules.

Bonus

You should use gettext because it works, but also, most major translation support the delivery of `.po` files which are the input to gettext based apps. Different languages vary in their implementations, slightly, but for the most part, these `.po` files can be converted and used nearly untouched.

References

Credits

Jed is brought to you by


Alex Sexton
yepnope.js / Modernizr / Bazaarvoice
@slexaxton


Jed would not be possible with out the prior work of

Joshua I. Miller (his work on gettext.js)
and Alexandru Marasteanu (his sprintf implementation).