i18n
Lightweight simple translation module with dynamic JSON storage. Supports plain vanilla Node.js apps and should work with any framework (like Express, restify and probably more) that exposes an app.use()
method passing in res
and req
objects.
Uses common __('...') syntax in app and templates.
Stores language files in json files compatible to webtranslateit json format.
Adds new strings on-the-fly when first used in your app.
No extra parsing needed.
Install
npm install i18n --save
Synopsis
const http = require('http')
const path = require('path')
const { I18n } = require('i18n')
const i18n = new I18n({
locales: ['en', 'de'],
directory: path.join(__dirname, 'locales')
})
const app = http.createServer((req, res) => {
i18n.init(req, res)
res.end(res.__('Hello'))
})
app.listen(3000, '127.0.0.1')
This wires up a plain http server and return "Hello" or "Hallo" depending on browsers 'Accept-Language'. The first configured locale 'en' will default in case the browser doesn't include any of those locales in his request.
Usage
With 0.12.0 i18n
now provides options to be used as instance or singleton.
- Instances allow to work with multiple different configurations and encapsulate resources and states.
- Singletons allow to share configuration, state and resources across multiple requires, modules or files.
Before 0.12.0 singleton usage was the only option. Instances give much more intuitive control and should be considered the "better practice" in complex setups.
As Instance
Minimal example, just setup two locales and a project specific directory.
const { I18n } = require('i18n')
const i18n = new I18n({
locales: ['en', 'de'],
directory: path.join(__dirname, 'locales')
})
Alternatively split creation and configuration, useful when split up into different modules for bootstrapping.
const { I18n } = require('i18n')
const i18n = new I18n()
i18n.configure({
locales: ['en', 'de'],
directory: path.join(__dirname, '/locales')
})
As Singleton
Same Minimal example, just setup two locales and a project specific directory.
const i18n = require('i18n')
i18n.configure({
locales: ['en', 'de'],
directory: path.join(__dirname, '/locales')
})
Now you are ready to use a global i18n.__('Hello')
.
Require i18n
in another file reuses same configuration and shares state:
const i18n = require('i18n')
module.exports = () => {
console.log(i18n.__('Hello'))
}
CLI within global scope
In your cli, when not registered to a specific object:
var greeting = i18n.__('Hello')
Global assumes you share a common state of localization in any time and any part of your app. This is usually fine in cli-style scripts. When serving responses to http requests you'll need to make sure that scope is NOT shared globally but attached to your request object.
Middleware in express.js
In an express app, you might use i18n.init to gather language settings of your visitors and also bind your helpers to response object honoring request objects locale, ie:
app.configure(function () {
app.use(i18n.init)
})
in your apps methods:
app.get('/de', function (req, res) {
var greeting = res.__('Hello')
})
in your templates (depending on your template engine)
<%= __('Hello') %>
${__('Hello')}
Some examples for common setups
See tested examples inside /examples
for some inspiration in node and express or browse these gists:
PLEASE NOTE: Those gist examples worked until node 0.12.x only
For serving the same static files with different language url, you could:
app.use(express.static(__dirname + '/www'))
app.use('/en', express.static(__dirname + '/www'))
app.use('/de', express.static(__dirname + '/www'))
API
The api is subject of incremental development. That means, it should not change nor remove any aspect of the current api but new features and options will get added that don't break compatibility backwards within a major version.
i18n.configure()
You should configure your application once to bootstrap all aspects of i18n
. You should not configure i18n in each loop when used in an http based scenario. During configuration, i18n
reads all known locales into memory and prepares to keep that superfast object in sync with your files in filesystem as configured
i18n.configure({
locales: ['en', 'de'],
directory: path.join(__dirname, 'locales')
})
Since 0.7.0 you may even omit the locales
setting and just configure a directory
. i18n
will read all files within that directory and detect all given locales by their filenames.
i18n.configure({
directory: path.join(__dirname, 'locales')
});
list of all configuration options
i18n.configure({
locales: ['en', 'de'],
fallbacks: { nl: 'de', 'de-*': 'de' },
defaultLocale: 'en',
retryInDefaultLocale: false,
cookie: 'yourcookiename',
header: 'accept-language',
queryParameter: 'lang',
directory: './mylocales',
directoryPermissions: '755',
autoReload: true,
updateFiles: false,
syncFiles: false,
indent: '\t',
extension: '.json',
prefix: 'webapp-',
objectNotation: false,
logDebugFn: function (msg) {
console.log('debug', msg)
},
logWarnFn: function (msg) {
console.log('warn', msg)
},
logErrorFn: function (msg) {
console.log('error', msg)
},
missingKeyFn: function (locale, value) {
return value
},
register: global,
api: {
__: 't',
__n: 'tn'
},
preserveLegacyCase: true,
staticCatalog: {
de: {
}
},
mustacheConfig: {
tags: ['{{', '}}'],
disable: false
},
parser: JSON
})
The locale itself is gathered directly from the browser by header, cookie or query parameter depending on your setup.
In case of cookie you will also need to enable cookies for your application. For express this done by adding app.use(express.cookieParser())
). Now use the same cookie name when setting it in the user preferred language, like here:
res.cookie('yourcookiename', 'de', { maxAge: 900000, httpOnly: true })
After this and until the cookie expires, i18n.init()
will get the value of the cookie to set that language instead of default for every page.
Some words on register
option
Used especially in a CLI-like script. You won't use any i18n.init()
to guess language settings from your user, thus i18n
won't bind itself to any res
or req
object and will work like a static module.
var anyObject = {}
i18n.configure({
locales: ['en', 'de'],
register: anyObject
})
anyObject.setLocale('de')
anyObject.__('Hallo')
Cli usage is a special use case, as we won't need to maintain any transaction / concurrency aware setting of locale, so you could even choose to bind i18n
to global scope of node:
i18n.configure({
locales: ['en', 'de'],
register: global
})
i18n.setLocale('de')
__('Hello')
Some words on staticCatalog
option
Instead of letting i18n load translations from a given directory you may pass translations as static js object right on configuration. This supports any method that returns a key:value
translation object ({ Hello: 'Hallo', Cat: 'Katze' }
). So you might even mix native json with js modules and parsed yaml files, like so:
const yaml = require('js-yaml')
const fs = require('fs')
i18n.configure({
staticCatalog: {
de: require('../../locales/de-as-json.json'),
en: require('../../locales/en-as-module.js'),
fr: yaml.safeLoad(fs.readFileSync('../../locales/fr-as-yaml.yml', 'utf8'))
},
defaultLocale: 'de'
})
NOTE: Enabling staticCatalog
disables all other fs realated options such as updateFiles
, autoReload
and syncFiles
Some words on parser
option
Instead of parsing all file contents as JSON, you can parse them as YAML or any other format you like
const YAML = require('yaml')
i18n.configure({
extension: '.yml',
parser: YAML
})
i18n.init()
When used as middleware in frameworks like express to setup the current environment for each loop. In contrast to configure the i18n.init()
should be called within each request-response-cycle.
var app = express()
app.use(cookieParser())
app.use(i18n.init)
When i18n is used like this, the i18n.init()
tries to
- guess the language of a visitor by it's browser settings, cookie or query parameter
- set that language in any of the "usual" objects provided by the framework
Express would call i18n.init(req, res, next)
, which is "classic" and adopted by many frameworks. Thus i18n
will attach it's api to that schema:
{
req: {
locals: {},
res: {
locals: {},
}
}
}
and add it's extra attributes and methods, like so:
{
req: {
locals: {
locale: "de",
__: [function],
__n: [function],
[...]
},
res: {
locals: {
locale: "de",
__: [function],
__n: [function],
[...]
},
locale: "de",
__: [function],
__n: [function],
[...]
},
locale: "de",
__: [function],
__n: [function],
[...]
}
}
Now each local object (ie. res.locals) is setup with it's own "private" locale and methods to get the appropriate translation from the global catalog.
i18n.__()
Translates a single phrase and adds it to locales if unknown. Returns translated parsed and substituted string.
__('Hello')
__('Hello %s', 'Marcus')
__('Hello {{name}}', { name: 'Marcus' })
req.__('Hello')
req.__('Hello %s', 'Marcus')
req.__('Hello {{name}}', { name: 'Marcus' })
res.__('Hello')
res.__('Hello %s', 'Marcus')
res.__('Hello {{name}}', { name: 'Marcus' })
__({ phrase: 'Hello', locale: 'fr' })
__({ phrase: 'Hello %s', locale: 'fr' }, 'Marcus')
__({ phrase: 'Hello {{name}}', locale: 'fr' }, { name: 'Marcus' })
i18n.__n()
Plurals translation of a single phrase. Singular and plural forms will get added to locales if unknown. Returns translated parsed and substituted string based on last count
parameter.
__n('%s cat', 1)
__n('%s cat', 3)
__n('%s cat', '%s cats', 1)
__n('%s cat', '%s cats', 3)
req.__n('%s cat', 1)
req.__n('%s cat', 3)
res.__n('%s cat', 1)
res.__n('%s cat', 3)
__n({ singular: '%s cat', plural: '%s cats', locale: 'fr' }, 1)
__n({ singular: '%s cat', plural: '%s cats', locale: 'fr' }, 3)
__n({ singular: '%s cat', plural: '%s cats', locale: 'fr', count: 1 })
__n({ singular: '%s cat', plural: '%s cats', locale: 'fr', count: 3 })
When used in short form like __n(phrase, count)
the following will get added to your json files:
__n('%s dog', 1)
{
"%s dog": {
"one": "%s dog",
"other": "%s dog"
}
}
When used in long form like __n(singular, plural, count)
you benefit form passing defaults to both forms:
__n('%s kitty', '%s kittens', 0)
{
"%s kitty": {
"one": "%s kitty",
"other": "%s kittens"
}
}
You might now add extra forms to certain json files to support the complete set of plural forms, like for example in russian:
{
"%s cat": {
"one": "%d кошка",
"few": "%d кошки",
"many": "%d кошек",
"other": "%d кошка",
}
}
and let __n()
select the correct form for you:
__n('%s cat', 0)
__n('%s cat', 1)
__n('%s cat', 2)
__n('%s cat', 5)
__n('%s cat', 6)
__n('%s cat', 21)
Note i18n.__n() will add a blueprint ("one, other" or "one, few, other" for example) for each locale to your json on updateFiles in a future version.
i18n.__mf()
Supports the advanced MessageFormat as provided by excellent messageformat module. You should definetly head over to messageformat.github.io for a guide to MessageFormat. i18n takes care of new MessageFormat('en').compile(msg);
with the current msg
loaded from it's json files and cache that complied fn in memory. So in short you might use it similar to __()
plus extra object to accomplish MessageFormat's formatting. Ok, some examples:
res.setLocale('de')
res.__mf('Hello')
res.__mf('Hello {name}', { name: 'Marcus' })
res.__mf('Hello {name}, how was your %s?', 'test', { name: 'Marcus' })
res.__mf('{N, selectordinal, one{# cat} two{# cats} few{# cats} other{# cats}}', {
N: 1
})
Take a look at Mozilla to quickly get an idea of what pluralization has to deal with. With __mf()
you get a very powerful tool, but you need to handle it correctly.
But MessageFormat can handle more! You get ability to process:
- Simple Variable Replacement (similar to mustache placeholders)
- SelectFormat (ie. switch msg based on gender)
- PluralFormat (see above and ranges)
Combinations of those give superpower, but should get tested well (contribute your use case, please!) on integration.
i18n.__l()
Returns a list of translations for a given phrase in each language.
i18n.__l('Hello')
This will be usefull when setting up localized routes for example (kudos to @xpepermint, #150):
app.get(__l('/:locale/products/:id?'), function (req, res) {
})
i18n.__ln() to get plurals will come up in another release...
i18n.__h()
Returns a hashed list of translations for a given phrase in each language.
i18n.__h('Hello')
i18n.__hn() to get plurals will come up in another release...
i18n.setLocale()
Setting the current locale (ie.: en
) globally or in current scope.
setLocale('de')
setLocale(req, 'de')
req.setLocale('de')
Use setLocale to change any initial locale that was set in i18n.init()
. You get more control on how when and which objects get setup with a given locale. Locale values are inherited within the given schema like in i18n.init()
Let's see some examples:
i18n.setLocale(req, 'ar')
i18n.setLocale(res, 'ar')
i18n.setLocale(res.locals, 'ar')
You'll get even more control when passing an array of objects:
i18n.setLocale([req, res.locals], req.params.lang)
or disable inheritance by passing true as third parameter:
i18n.setLocale(res, 'ar', true)
i18n.getLocale()
Getting the current locale (ie.: en
) from current scope or globally.
getLocale()
getLocale(req)
req.getLocale()
i18n.getLocales()
Returns a list with all configured locales.
i18n.getLocales()
i18n.getCatalog()
Returns a whole catalog optionally based on current scope and locale.
getCatalog()
getCatalog('de')
getCatalog(req)
getCatalog(req, 'de')
req.getCatalog()
req.getCatalog('de')
Attaching helpers for template engines
In general i18n has to be attached to the response object to let it's public api get accessible in your templates and methods. As of 0.4.0 i18n tries to do so internally via i18n.init
, as if you were doing it in app.configure
on your own:
app.use(function (req, res, next) {
res.locals.__ = res.__ = function () {
return i18n.__.apply(req, arguments)
}
next()
})
Different engines need different implementations, so yours might miss or not work with the current default helpers. This one showing an example for mustache in express:
app.use(function (req, res, next) {
res.locals.__ = function () {
return function (text, render) {
return i18n.__.apply(req, arguments)
}
}
next()
})
You could still setup your own implementation. Please refer to Examples below, post an issue or contribute your setup.
Output formats
As inspired by gettext there is currently support for sprintf-style expressions. You can also use mustache syntax for named parameters.
sprintf support
var greeting = __('Hello %s, how are you today?', 'Marcus')
this puts Hello Marcus, how are you today?. You might add endless arguments and even nest it.
var greeting = __('Hello %s, how are you today? How was your %s.', 'Marcus', __('weekend'))
which puts Hello Marcus, how are you today? How was your weekend.
You might need to have repeated references to the same argument, which can be done with sprintf.
var example = __('%1$s, %1$s, %1$s', 'repeat')
which puts
repeat, repeat, repeat
In some cases the argument order will need to be switched for different locales. The arguments can be strings, floats, numbers, etc.
var example = __('%2$d then %1$s then %3$.2f', 'First', 2, 333.333)
which puts
2 then First then 333.33
mustache support
You may also use mustache syntax for your message strings. To pass named parameters to your message, just provide an object as the last parameter. You can still pass unnamed parameters by adding additional arguments.
var greeting = __('Hello {{name}}, how are you today?', { name: 'Marcus' })
this puts Hello Marcus, how are you today?. You might also combine it with sprintf arguments...
var greeting = __('Hello {{name}}, how was your %s.', __('weekend'), { name: 'Marcus' })
and even nest it...
var greeting = __( __('Hello {{name}}, how was your %s?', { name: 'Marcus' }), __('weekend') )
which both put Hello Marcus, how was your weekend.
how about markup?
Including markup in translation and/or variables is considered to be bad practice, as it leads to side effects (translators need to understand it, might break it, inject malformed markup or worse). But well, mustache supports unescaped markup out-of-the-box (Quote from https://mustache.github.io/mustache.5.html):
All variables are HTML escaped by default. If you want to return unescaped HTML, use the triple mustache: {{{name}}}.
So this will work
var greeting = __('Hello {{{name}}}, how are you today?', { name: '<u>Marcus</u>' })
as expected:
Hello <u>Marcus</u>, how are you today
basic plural support
two different plural forms are supported as response to count
:
var singular = __n('%s cat', '%s cats', 1)
var plural = __n('%s cat', '%s cats', 3)
this puts 1 cat or 3 cats
and again these could get nested:
var singular = __n('There is one monkey in the %%s', 'There are %d monkeys in the %%s', 1, 'tree')
var plural = __n('There is one monkey in the %%s', 'There are %d monkeys in the %%s', 3, 'tree')
putting There is one monkey in the tree or There are 3 monkeys in the tree. Passing all 3 parameters would write a one
and other
to your json. For reading you might just use 2 parameters, too:
__n('%s cat', 1)
__n('%s cat', 3)
ranged interval support
use mathematical intervals to declare any own plural rules based on ISO 31-11 notation. Let's assume the following json snippet:
"dogs": {
"one": "one dog",
"other": "[0] no dog|[2,5] some dogs|[6,11] many dogs|[12,36] dozens of dogs|a horde of %s dogs|[100,] too many dogs"
}
this will result in
__n('dogs', 0)
__n('dogs', 1)
__n('dogs', 2)
__n('dogs', 10)
__n('dogs', 25)
__n('dogs', 42)
__n('dogs', 199)
The rules are parsed in sequenced order, so the first match will skip any extra rules. Example:
{
"dogs":"[0]no dog|[1]one dog|[,10[ less than ten dogs|[,20[ less than 20 dogs|too many dogs"
}
results in
__n('dogs', 0)
__n('dogs', 1)
__n('dogs', 2)
__n('dogs', 9)
__n('dogs', 10)
__n('dogs', 19)
__n('dogs', 20)
__n('dogs', 199)
__('dogs')
See en.json example inside /locales
for some inspiration on use cases. Each phrase might get decorated further with mustache and sprintf expressions:
{
"example":"[0] %s is zero rule for {{me}}|[2,5] %s is between two and five for {{me}}|and a catchall rule for {{me}} to get my number %s"
}
will put (as taken from tests):
__('example', {me: 'marcus'})
__('example', ['one'], {me: 'marcus'})
__n('example', 1, {me: 'marcus'})
__n('example', 2, {me: 'marcus'})
__n('example', 5, {me: 'marcus'})
__n('example', 3, {me: 'marcus'})
__n('example', 6, {me: 'marcus'})
Notice: the "example" object in your json doesn't use any "one", "other" subnodes although you could use and even combine them. Currently "one" referres to the value of exactly 1 while "other" referres to every other value (think of 0, -10, null, false)
variable support
you might even use dynamic variables as they get interpreted on the fly. Better make sure no user input finds it's way to that point as they all get added to the en.js
file if not yet existing.
var greetings = ['Hi', 'Hello', 'Howdy']
for (var i = 0; i < greetings.length; i++) {
console.log(__(greetings[i]))
}
which puts
Hi
Hello
Howdy
Object notation
In addition to the traditional, linear translation lists, i18n also supports hierarchical translation catalogs.
To enable this feature, be sure to set objectNotation
to true
in your configure()
call. Note: If you can't or don't want to use .
as a delimiter, set objectNotation
to any other delimiter you like.
Instead of calling __("Hello")
you might call __("greeting.formal")
to retrieve a formal greeting from a translation document like this one:
{
"greeting": {
"formal": "Hello",
"informal": "Hi",
"placeholder": {
"formal": "Hello %s",
"informal": "Hi %s"
}
}
}
In the document, the translation terms, which include placeholders, are nested inside the "greeting" translation. They can be accessed and used in the same way, like so __('greeting.placeholder.informal', 'Marcus')
.
Pluralization
Object notation also supports pluralization. When making use of it, the "one" and "other" entries are used implicitly for an object in the translation document. For example, consider the following document:
{
"pets": {
"cat": {
"one": "Katze",
"other": "Katzen"
}
}
}
When accessing these, you would use __n("pets.cat", "pets.cat", 3)
to tell i18n to use both the singular and plural form of the "cat" entry. Naturally, you could also access these members explicitly with __("pets.cat.one")
and __("pets.cat.other")
.
Defaults
When starting a project from scratch, your translation documents will probably be empty. i18n takes care of filling your translation documents for you. Whenever you use an unknown object, it is added to the translation documents.
By default, when using object notation, the provided string literal will be inserted and returned as the default string. As an example, this is what the "greeting" object shown earlier would look like by default:
{
"greeting": {
"formal": "greeting.formal",
"informal": "greeting.informal"
}
}
In case you would prefer to have a default string automatically inserted and returned, you can provide that default string by appending it to your object literal, delimited by a :
. For example:
__('greeting.formal:Hello')
__('greeting.placeholder.informal:Hi %s')
Storage
Will get modular support for different storage engines, currently just json files are stored in filesystem.
json file
the above will automatically generate a en.json
by default inside ./locales/
which looks like
{
"Hello": "Hello",
"Hello %s, how are you today?": "Hello %s, how are you today?",
"weekend": "weekend",
"Hello %s, how are you today? How was your %s.": "Hello %s, how are you today? How was your %s.",
"Hi": "Hi",
"Howdy": "Howdy",
"%s cat": {
"one": "%s cat",
"other": "%s cats"
},
"There is one monkey in the %%s": {
"one": "There is one monkey in the %%s",
"other": "There are %d monkeys in the %%s"
},
"tree": "tree",
"%s dog": {
"one": "one dog",
"other": "[0] no dog|[2,5] some dogs|[6,11] many dogs|[12,36] dozens of dogs|a horde of %s dogs"
}
}
that file can be edited or just uploaded to webtranslateit for any kind of collaborative translation workflow:
{
"Hello": "Hallo",
"Hello %s, how are you today?": "Hallo %s, wie geht es dir heute?",
"weekend": "Wochenende",
"Hello %s, how are you today? How was your %s.": "Hallo %s, wie geht es dir heute? Wie war dein %s.",
"Hi": "Hi",
"Howdy": "Hallöchen",
"%s cat": {
"one": "%s Katze",
"other": "%s Katzen"
},
"There is one monkey in the %%s": {
"one": "Im %%s sitzt ein Affe",
"other": "Im %%s sitzen %d Affen"
},
"tree": "Baum",
"%s dog": {
"one": "Ein Hund",
"other": "[0] Kein Hund|[2,5] Ein paar Hunde|[6,11] Viele Hunde|[12,36] Dutzende Hunde|Ein Rudel von %s Hunden"
}
}
Logging & Debugging
Logging any kind of output is moved to debug module. To let i18n output anything run your app with DEBUG
env set like so:
$ DEBUG=i18n:* node app.js
i18n exposes three log-levels:
- i18n:debug
- i18n:warn
- i18n:error
if you only want to get errors and warnings reported start your node server like so:
$ DEBUG=i18n:warn,i18n:error node app.js
Combine those settings with you existing application if any of you other modules or libs also uses debug
Using custom logger
You can configure i18n to use a custom logger. For example attach some simple console
-logging:
i18n.configure({
logDebugFn: function (msg) {
console.log('debug', msg)
},
logWarnFn: function (msg) {
console.log('warn', msg)
},
logErrorFn: function (msg) {
console.log('error', msg)
}
})
Test
npm test
Roadmap
- add a storage adapter (to support .yaml, .js, memory)
- improved fallbacks
- refactor dot notation (ie. configurable delimiters)
- refactor to ES6
- refactor to standard + prettier
- move docs + examples to github pages
Changelog
For current release notes see GitHub Release Notes. Changes until 0.8.3 are filed as Changelog Summary.