i18nfp

i18n support with factualization and pluralization

npm install i18nfp
4 downloads in the last week
16 downloads in the last month

i18nfp

This is project for JavaScript i18n with a simple solution for factualization and pluralization.

Introduction

Installation

> npm install i18nfp -g

What we offer?

We are offering a solution for those developers:

  1. who have to deals PROPERTIES file as resource bundles due to legacy translation process requirements
  2. who want to handle i18n in JavaScript, either in browser or in backend like NodeJS, in a most convenient way
  3. who have to deal with factualization for different domains (e.g. '.com' and '.co.uk')
  4. who have to consider to deal with pluralization

And particularly, we are offering below set of tools for different purposes

  1. A global command line tool. It defines a set of rules for composing your PROPERTIES files, and provides corresponding tools to generate JavaScript-friendly resources from PROPERTIES files. See Command line tool.
  2. A Connect compliant plugin. It provides a simple way to use these generated resources files in NodeJS server like ExpressJS. See Work with Connect
  3. A browser side JavaScript file. It is designed to work with RequireJS and its i18n plugin. You can find the file under client folder, and put it to your project folder. See Work with RequireJS

Why we want this?

We can search out lots of i18n tools over internet, but none of them could handle factualization.

What's Factualization? Why we need this?

Factualization is a special i18n case for different display logic on different domains. For example

on US domain site (.com), use below entry to display price:

ticket.price.all=Price in all: {0}

but on UK domain site (.co.uk), you need to add VAT to the price display, so the format looks like:

ticket.price.all=Price in all: {0} + VAT {1}

When we use some set of resource bundles, we need to distinguish these difference between domains. One solution which is using by StubHub is to add a prefix:

ticket.price.all=Price in all: {0}
//For US domain logic
us.ticket.price.all=Price in all: {0}
//For UK domain logic
uk.ticket.price.all=Price in all: {0} + VAT {1}

All these entries are contained in the dafault resource bundle file, like message.properties. When this file is sent to for translation, people in that process could understand better for the translation.

Command line tool

Introduction

This tool is aimed to provide an easy way to pre-compile all resource bundle properties files into JSON-format javascript files, which could be used in NodeJS or client side.

Installation

Make sure you have installed i18nfp into global. If you're Mac or Linux user, please use sudo ahead of the command if necessary.

> npm install i18nfp -g

Usage

# get how to use via
> i18nfp --help

# compile all properties file under i18n/ to target folder
> i18nfp -s ./i18n -t ./output

What will be generated?

Take above command for example, under the target output folder, it generated a nls folder, and a few files. The files under nls are used for RequreJS i18n plugins. The files under output folder could be used by NodeJS server and RequireJS.

- output
    |- nls
    |    |- _com
    |    |    |- fr-fr
    |    |    |    |~ messages.js
    |    |    |~ messages.js
    |    |- fr-fr
    |    |    |~ messages.js
    |    |~ messages.js
    |    |~ messages_client.js
    |    |~ messages_fr-FR.js
    |    |~ messages_fr-FR_client.js
    |- properties
        |~ messages.properties
        |~ messages_fr_FR.properties

To generate resource bundles for RequireJS, you need mark [i18nfpclient] in properties file, see below section Handling Scope for usage.

As i18nfp is designed to support factualization, the nls folder has more contents than what RequireJS i18n plugin needs. We also enhanced the i18n plugin for RequireJS to support domain specific resource bundles. The new i18n plugin could be download at A new i18n plugin for RequireJS or find it at client/i18n.js;

From the above figure, you can find that there is a _com folder under nls. All the resource bundles for .com site will be placed here. To support more site domains, before executing the command, please find the i18nfp at global node_modules folder (where are global node_modules?) or local workspace node_modules folder, then edit lib/config.js.

factulizationPrefix : {
    'us' : '_com',
    'uk': '_co_uk',
    'eu': '_eu'
} 

Add or change factualization prefix to map its domain suffix, with replacing dot . to underscore _. More public domain suffix, please refer to Public Suffix List.

Now only a few Top-level domains are added by default as list below:

Handling peroperties file

Factualization

Add prefix to entries for factualization. The prefix indicates the domain for the entry.

ticket.price.all=Price in all: {0}
//For US domain logic
us.ticket.price.all=Price in all: {0}
//For UK domain logic
uk.ticket.price.all=Price in all: {0} + VAT {1}

User only needs to use the entry name, without mentioning the domain. The domain and prefix mapping could be set in config file.

Pluralization and Logic set

i18nfp provides a specific expression to allow you handling pluralization and simple logic set in properites file. Please see below samples to better understand it.

For an entry that have different expression for handling pluraization, follow below grammar:

# Sample 1 - Properties file
# displaying a message with one placeholde    
tickets = {0} tickets

# use '__' to as i18nfp specific separator
tickets__1 = {0} ticket

Then in JavaScript codes, only need to care about 'tickets' like below codes:

// Sample 1 - JavaScript code

var i18nfp = require('./i18nfp'),
    msg = require('./nls/message.js'),
    getMsg = i18nfp(msg);

console.log(getMsg('tickets', 0)); //print '0 tickets'
console.log(getMsg('tickets', 1)); //print '1 ticket'
console.log(getMsg('tickets', 2)); //print '2 tickets'

From above sample, you can see that we could allow you to append a __ and a number to entry key to form a logic set. If argument equals to 1, then we use entry tickets__1, otherwise, we use entry tickets as default. You can also use other number as threshold, this means if the paramenter is equal or larger than the number, use the mapping entry. Please note that number 0 and 1 are preserved for handling parameter that must be equal to 0 or 1. See below sample:

# Sample 2 - Properties file
# displaying a message with one placeholde    
tickets = {0} tickets

# use '__' to as i18nfp specific separator
tickets__0 = no ticket
tickets__1 = {0} ticket
tickets__5 = a few tickets
tickets__10 = plenty of tickets

With above entries defined in properties file, you can avoid logic control in your JavaScript codes, and just use tickets as key to get the corresponding message.

// Sample 2 - JavaScript code

var i18nfp = require('./i18nfp'),
    msg = require('./nls/message.js'),
    getMsg = i18nfp(msg);

console.log(getMsg('tickets', 0)); //print no ticket', use '__0'
console.log(getMsg('tickets', 1)); //print '1 ticket', use '__1'
console.log(getMsg('tickets', 2)); //print '2 tickets', use default    
console.log(getMsg('tickets', 3)); //print '3 tickets', use default
console.log(getMsg('tickets', 6)); //print 'a few tickets', use '__5'
console.log(getMsg('tickets', 20)); //print 'plenty of tickets', use '__10' threshold

There is another few use cases to use this feature with i18nfp:

  1. Use a parameter to control logic, and second parameter for passing in data. In this case, use {1} rather than {0} in your entry value
  2. Use more than one parameters to control logic, then you can append another _ with a number, like tickets__0_1. Once you have two or more parameter for logic control, you can define default value in case i18nfp can't find any mapping entry. e.g. define tickets__0 as default value for any other entries tickets__0_\d(*). See example like below:
    tickets.attibute__1 = only one ticket
    tickets.attibute__1_1 = one ticket with one parking pass
    tickets.attibute__2 = {0} tickets
    tickets.attibute__2_1 = {0} tickets with one parking pass
    tickets.attibute__2_2 = {0} tickets with {1} parking passes

*Note: i18nfp is trying to resolve simple logic in properties file, but once more than two parameters are used in logic control, you may need to define a large set of entries for all conditions. And there must be some conditions that can not be handled well with i18nfp.

Handling Scope

When you define a properties file, some of them are used in server side, while some in client side. You don't need to serve all property entries to client side JavaScript codes. In this case, you can put all your client side entries in the bottom of properties file, with a specific entry name to separate them

# Scope sample - Properties file
# Below are used in Server side
...
...

# Below are used in Client side, [i18nfpclient] is required
[i18nfpclient]
...
...

Usage and samples

Work with Connect

Usage

Take ExpressJS for example, in ExpressJS app.js

//Require this module into app
var i18nfp = require('i18nfp');

Create a config variable for setting up where the nls files, and which files will be used, and accepted locales. domainMapping provides a way to map hostname to a specific factualization prefix. With this, we could test UK site by mapping localhost to uk.

var i18nfp_config = {
    nlsRoot: path.join(__dirname, "nls"),
      nlsFiles: ['messages', 'domainConstrants', 'localeConstrants'],
      acceptLocales: ['en-us', 'en-gb'],
      structureType: 1, // keep it as 1 by now, will support more later
      // set hostname & factualization prefix mapping
      domainMapping: {
        'localhost': 'us.',
        '.co.uk': 'uk.'
    }
};

Init i18nfp with the config variable.

i18nfp.init(i18nfp_config);
var app = express();
app.configure(function() {
    app.set('port', process.env.PORT || 3000);
    app.set('views', __dirname + '/views');
    app.set('view engine', 'jade');
    app.use(express.favicon());
    app.use(express.logger('dev'));
    app.use(express.bodyParser());
    //Add a handle for app
    app.use(i18nfp.handle);
    app.use(express.methodOverride());
    app.use(app.router);
    app.use(express.static(path.join(__dirname, 'public')));
});
// register a helper
i18nfp.registerAppHelper(app);

Then do something in template file, e.g. in below jade file. message and domainMsg are exposed for use (they are the same). No need to worry about whether it is domain-specific.

//- headMeta.jade
meta(charset="utf-8")
meta(name="keywords", content=message("sh.meta.keywords"))
meta(name="description", content='#{domainMsg("sh.meta.description")}')

If using DustJS as template, a good way to use i18nfp is to create a dust helper. Sample codes as below:

...
i18nfp.init(i18nfp_config);
// Register dust helper for template rendering
var dust_i18n_helperGen = require('./lib/dust-i18n-helperGen');
dust.registerHelper('i18n', function(chunk, context, bodies, params){
      //context.message will be registered later in below by using i18nfp.handle
    var msg = dust_i18n_helperGen(context.get("message"));
      return msg(chunk, context, bodies, params);
});

// create app
var app = express();
app.engine('dust', dust);
...

Note: dust.registerHelper is not a official method of any DustJS library for ExpressJS, it's customized to set a helper to Dust. dust-i18n-helperGen codes could be found in sample/server/ folder.

How to use the dust helper, see Dust i18n helper usage

Reference

inspired by i18next

Work with RequireJS

As mentioned in Command line tool, i18nfp command will generate resource bundles for RequireJS to use. Copy client/i18nfp.js to your project javascript folder.

With RequireJS i18n plugin

define('yourModule', ['lib/i18nfp', 'i18n!output/nls/messages'], 
    function(i18nfp, message){
        var getMsg = i18nfp.t(message);
        console.log(getMsg('tickets', 0));
        console.log(getMsg('helloword', 'RequireJS and i18nfp'));
    }
);

In above sample, the i18n plugin of RequireJS will help you get the expected resource bundles according to domain and locale, and i18nfp will help you deal with pluralization.

Without RequireJS i18n plugin

If you don't want to use i18n plugin, you can use below ways to set factualization for i18nfp if all factualized property entries are defined in a single file.

define('yourModule', ['lib/i18nfp', 'output/messages_client'], 
    function(i18nfp, message){
        var config = {
            domainMapping:{
                'yoursite.com':'us.',
                'yoursite.co.uk':'uk.',
                'yoursite.au':'au.',
                'localhost:':'us.' // you can map your dev env to any domain
            }
        };
        i18nfp.init(config);
        var getMsg = i18nfp.t(message);
        console.log(getMsg('tickets', 0));
        console.log(getMsg('helloword', 'RequireJS and i18nfp'));
    }
);

Work with DustJS on Client side

sample/client/dust-i18n-helper.js shows a sample to work with RequireJS, i18n, and DustJS. It provides a simple way to bind resource bundles to DustJS helper.

Dust i18n helper usage

After integrating i18nfp with DustJS, you can specify message in dust tempalte file directly in below ways:

<!-- use a static key -->
<div>{@i18n key="sh.menu.home" /}</div>

<!-- give a default value by using text attribute -->
<div>{@i18n key="sh.menu.home" text="Höme" /}</div>

<!-- use a variable key -->
<div>{@i18n key="sh.menu.{type}" /}</div>
<div>{@i18n key=somevariable /}</div>

<!-- use a parameter from context -->
<div>{@i18n key="sh.search.results.count" p0=nums /}</div>

<!-- use multiple parameter from context or string -->
<div>{@i18n key="sh.search.results.count.in.region" p0=rows p1=geoName p2=numFound/}</div>
npm loves you