Skip to content

Commit

Permalink
* Added support for contextual translations
Browse files Browse the repository at this point in the history
* Changed behavior or parameters replacement
  • Loading branch information
naholyr committed Jan 30, 2011
1 parent 0fe74aa commit 3552797
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 39 deletions.
108 changes: 88 additions & 20 deletions README.md
@@ -1,7 +1,8 @@
Usage
=====

In any type of application:
Default usage
-------------

// Load module:
var i18n = require('/path/to/i18n');
Expand All @@ -23,10 +24,11 @@ In any type of application:
// Go translate :)
console.log(i18n.translate('Chicken')); // "Poulet"
console.log(i18n.translate('Chicken', 'it')); // "Pollo"
console.log(i18n.translate('Chicken %name%', {"%name%": "KFC"})); // "Poulet KFC"
console.log(i18n.translate('Chicken %name%', {"%name%": "KFC"}, 'it')); // "Pollo KFC"
console.log(i18n.translate('Chicken {name}', {name: "KFC"})); // "Poulet KFC"
console.log(i18n.translate('Chicken {name}', {name: "KFC"}, 'it')); // "Pollo KFC"

In Express.js:
Integration with Express.js
---------------------------

// ... app initialized ...
// Load module:
Expand All @@ -47,26 +49,50 @@ In Express.js:
req.locales() // returns the list of user's accept-language, ordered by preference
req.locale() // returns current user's chosen locale, stored in session if available
// Your templates gain new helpers:
...<%= _('You have :nb: messages', {":nb:": 3}) %>...
...<%= _('Hello, {name}', {name: userName}) %>...
...<%= plural('You have {n} messages', nbMessages) %>...

Store your messages
===================

There is only one messages store currently supported is "store-module", which means you store your messages as a Node.js module.
There is only one messages store currently supported is "module", which means you store your messages as a Node.js module.

Samples:
Store: module
-------------

A whole catalogue in a single file:

// Module name: "./i18n-data/%catalogue%"
// ./i18n-data/messages.js
module.exports = {
"fr": { "Chicken": "Poulet", "Chicken %name%": "Poulet %name%" },
"it": { "Chicken": "Pollo", "Chicken %name%": "Pollo %name%" },
};
// will be loaded with i18n.load('messages');

Or split by locale:

// Module name: "./i18n-data/%catalogue%/%locale%"
// ./i18n-data/messages/fr.js
module.exports = { "Chicken": "Poulet", "Chicken %name%": "Poulet %name%" };
// ./i18n-data/messages/it.js
module.exports = { "Chicken": "Pollo", "Chicken %name%": "Pollo %name%" };
// will be loaded with i18n.load('messages', ['fr', 'it']);

Note that you can customize the path to i18n-data modules:

i18n.i18nDataModuleName.__default__ = process.cwd() + "/i18n-data";

Store: file
-----------

Soon available (format: ini).

Store: db
---------

Soon available (redis, mongodb, mysql...).

Plural forms
============

Expand Down Expand Up @@ -111,24 +137,65 @@ Example in a template:
// _("You have %n% messages") returns "[0]No message|[1]One message|[2,+Inf)%n% messages"
// plural("[0]No message|[1]One message|[2,+Inf)%n% messages", 3) returns "3 messages"

Contextual translations
=======================

You may sometimes need to translate a sentence differently depending on a unpredictible context. Usual case is the gender (male/female).
This is handled using a special parameter named "context", and a special translation "context:message".

For example, supposing you want to say "hello, {name}" differently depending on civility ("mr", "mrs", "miss"), you will provide these translations in the store:

{
"hello, {name}": "hello, {name}", // default translation, no context
"mr:hello, {name}": "hello, Mister {name}", // translation for civility "mr"
"mrs:hello, {name}": "hello, Mrs. {name}", // translation for civility "mrs"
"miss:hello, {name}": "hello, Miss {name}" // translation for civility "miss"
}

You will then be able to translate "hello, {name}" differently depending on provided context:

i18n.translate("hello, {name}", {name: "Jones", context: "mr"}); // hello, Mister Jones
i18n.translate("hello, {name}", {name: "Jones", context: "mrs"}); // hello, Mrs. Jones

Configuration
=============

* Customize the session key to store user's locale:
i18n.localeSessKey = 'locale';

i18n.localeSessKey = 'locale';

* Customize the messages store:
// Embedded store
i18n.setStore('module', options, function(err, i18n) {
...
});
// You custom store module
i18n.setStore(require('/path/to/my/store'), options, function(err, i18n) {
...
});
//

// Embedded store
i18n.setStore('module', options, function(err, i18n) {
...
});
// You custom store module
i18n.setStore(require('/path/to/my/store'), options, function(err, i18n) {
...
});

Beware you must call "i18n.load(...)" again if you had already loaded another store.
You can use only one store at a time.

* Customize default locale:

i18n.defaultLocale = 'en';

* Default catalogue to load and search translations from:

i18n.defaultCatalogue = 'messages';

* Change format of replaced parameters in your messages:

i18n.replaceFormat = '{...}';
// i18n.replaceFormat = ':...';
// and i18n.translate('hello, :name', {name: 'John'}) will work as expected

* In plural forms, the parameter 'n' is replaced by the number, you can change this name:

i18n.defaultPluralReplace = 'n';

Write your own store
--------------------

Expand All @@ -139,7 +206,7 @@ You must write a module that will expose at least two self-explanatory methods:
* callback expects following parameters: (errors, loadedLocales, this)
* get(key, locale, catalogue, i18n)
* all parameters will always be provided by i18n module.
* if no translation is found, you're expected to return null, false, or undefined.
* if no translation is found, you MUST return undefined.
* this function HAS TO BE synchronous.
* locales(prefix, callback)
* callback expects following parameters: (err, array of locales starting with prefix, this)
Expand Down Expand Up @@ -181,7 +248,6 @@ Stupid example (will always translate "js", and only this one, into "rox"):
TODO
====

* Context like gender (original idea from dialect).
* Fix the data loading when we specify the list of loaded locales.
* Provide more stores (at least Redis).
* Better documentation.
Expand All @@ -190,4 +256,6 @@ TODO
* Better support for locales "lang_COUNTRY" (loads messages for locales "lang" and "lang-country")
* All these things I didn't think about yet.

* DONE <del>Plural forms, including ranges and expressions recognition</del>

* Done: <del>Plural forms, including ranges and expressions recognition</del>.
* Done: <del>Context like gender (original idea from dialect)</del>.
38 changes: 27 additions & 11 deletions index.js
Expand Up @@ -78,7 +78,8 @@ exports.store = require('./stores/module');
exports.pluralHandler = require('./plural-form');
exports.defaultLocale = 'en';
exports.defaultCatalogue = 'messages';
exports.defaultPluralReplace = '%n%';
exports.replaceFormat = '{...}';
exports.defaultPluralReplace = 'n';
exports.availableLocales = undefined; // any locale available in store

exports.setStore = function(store, config, callback) {
Expand All @@ -104,6 +105,15 @@ exports.setStore = function(store, config, callback) {
return true;
};

function replaceParams(string, replacements) {
if (typeof replacements == 'object') {
for (var key in replacements) {
string = string.replace(exports.replaceFormat.replace("...", key), replacements[key]);
}
}
return string;
}

exports.translate = function translate(msg, params, locale, catalogue) {
if (typeof params == 'string') {
if (typeof locale != 'undefined') {
Expand All @@ -113,17 +123,21 @@ exports.translate = function translate(msg, params, locale, catalogue) {
params = undefined;
}
// Find translation
var translation = this.store.get(msg, locale || this.defaultLocale, catalogue || this.defaultCatalogue, this);
var translated = typeof translation != 'undefined';
if (!translated) {
var translation;
// Search with context
if (typeof params.context != 'undefined') {
translation = this.store.get(params.context+":"+msg, locale || this.defaultLocale, catalogue || this.defaultCatalogue, this);
}
// No context, or no translation for this context, search with no context
if (typeof translation == 'undefined') {
var translation = this.store.get(msg, locale || this.defaultLocale, catalogue || this.defaultCatalogue, this);
}
// No translation found, just keep original message
if (typeof translation == 'undefined') {
translation = msg;
}
// Apply parameters
if (typeof params == 'object') {
for (var replace in params) {
translation = translation.replace(replace, params[replace]);
}
}
translation = replaceParams(translation, params);
// Debug ?
if (!translated && this.debugInfo) {
translation = this.debugInfo.prefix + translation + this.debugInfo.suffix;
Expand All @@ -140,7 +154,7 @@ exports.plural = function plural(msg, number, params, locale, catalogue) {
if (typeof params != 'undefined' || typeof locale != 'undefined' || typeof catalogue != 'undefined') {
msg = this.translate(msg, params, locale, catalogue);
}
// "number" can be a number (we'll replace "%n%" in this case), or an object like '{"%count%": 33}'
// "number" can be a number (we'll replace "n" in this case), or an object like '{"count": 33}'
var paramName = this.defaultPluralReplace;
if (typeof number == 'object') {
var foundKey = null;
Expand All @@ -159,7 +173,9 @@ exports.plural = function plural(msg, number, params, locale, catalogue) {
// Handle plural form
msg = this.pluralHandler(msg, number);
// Replace in result
return msg.replace(paramName, number);
var replacements = {};
replacements[paramName] = String(number);
return replaceParams(msg, replacements);
};

exports.dynamicHelpers = {
Expand Down
9 changes: 6 additions & 3 deletions test/i18n-data/messages.js
@@ -1,9 +1,12 @@
exports.fr = {
'guy': 'mec',
'You have %n% messages': "[0]Vous n'avez aucun message|[1]Vous avez un message|[2-+Inf]Vous avez %n% messages"
'x': 'x (en français)',
'You have {n} messages': "[0]Vous n'avez aucun message|[1]Vous avez un message|[2-+Inf]Vous avez {n} messages",
'Hello, {name}': "Bonjour, {name}",
'female:Hello, {name}': "Bonjour, mademoiselle {name}",
'male:Hello, {name}': "Bonjour, monsieur {name}"
};

exports.en = {
'guy': 'some guy'
'x': 'x (in English)'
};

13 changes: 8 additions & 5 deletions test/index.js
Expand Up @@ -8,10 +8,13 @@ i18n.load("messages", function(err, locales) {
throw err;
}
console.log("Loaded locales", locales);
console.log(i18n.translate('guy'));
console.log(i18n.translate('guys'));
console.log(i18n.translate('guy', 'en'));
for (var n=0; n<5; n++) {
console.log(i18n.plural('You have %n% messages', n, 'fr'));
console.log(i18n.translate('x')); // x (en français)
console.log(i18n.translate('y')); // y
console.log(i18n.translate('x', 'en')); // x (in English)
for (var n=0; n<6; n++) {
console.log(i18n.plural('You have {n} messages', n, 'fr'));
}
console.log(i18n.translate('Hello, {name}', {name:'Jones', context:'male'})); // Bonjour, monsieur Jones
console.log(i18n.translate('Hello, {name}', {name:'Jones', context:'female'})); // Bonjour, mademoiselle Jones
console.log(i18n.translate('Hello, {name}', {name:'Jones', context:'unknown'})); // Bonjour, Jones
});

0 comments on commit 3552797

Please sign in to comment.