Compare commits

...

4 Commits

Author SHA1 Message Date
Simon Vareille 68d3ae4dd4
Merge branch 'feature-plural-translations' into feature-plural-fallback 2020-06-18 14:47:30 +02:00
Simon Vareille 86598190f2
Add option to fall back on default local if current translation is not found 2020-06-18 14:46:28 +02:00
Simon Vareille a2d4083e95
Update README.md 2020-06-18 14:41:34 +02:00
Simon Vareille 5d5371e739
Add support for plural forms 2020-06-18 14:24:03 +02:00
3 changed files with 248 additions and 2 deletions

122
README.md
View File

@ -11,7 +11,7 @@ koa locales, i18n solution for koa:
1. All locales resources location on `options.dirs`.
2. resources file supports: `*.js`, `*.json`, `*.yml`, `*.yaml` and `*.properties`, see [examples](test/locales/).
3. One api: `__(key[, value, ...])`.
3. Api: `__(key[, value, ...])`, `__n(key, count[, value, ...])`.
4. Auto detect request locale from `query`, `cookie` and `header: Accept-Language`.
## Installation
@ -42,8 +42,10 @@ Patch locales functions to koa app.
- {Application} app: koa app instance.
- {Object} options: optional params.
- {String} functionName: locale function name patch on koa context. Optional, default is `__`.
- {String} functionnName: locale function (with plurals management) name patch on koa context. Optional, default is `__n`.
- {String} dirs: locales resources store directories. Optional, default is `['$PWD/locales']`.
- {String} defaultLocale: default locale. Optional, default is `en-US`.
- {Boolean} fallbackToDefaultLocale: fall back to default local if current translation is not found. Optional, default is `false`.
- {String} queryField: locale field name on query. Optional, default is `locale`.
- {String} cookieField: locale field name on cookie. Optional, default is `locale`.
- {String} cookieDomain: domain on cookie. Optional, default is `''`.
@ -99,6 +101,86 @@ __('{a} {a} {b} {b} {b}', {a: 'foo', b: 'bar'})
'foo foo bar bar bar'
```
### `context.__n(key, count[, value1[, value2, ...]])`
Get current request locale text with plural.
Works with package [make-plural](https://github.com/eemeli/make-plural/tree/master/packages/plurals) package. Support approximately 200 languages.
```js
async function home(ctx) {
ctx.body = {
message: ctx.__n({one: 'I have one apple.', other: 'I have {0} apples.', 2, 2),
};
}
```
**Note:**
The `count` parameter is not part of the displayed values. To have the number
`count` displayed, one must add this parameter to the list of values.
Examples:
```js
// With english locale
__n({one: 'I have one cat.', other: 'I have not one cat.'}, 0)
=>
'I have not one cat.'
// With french locale
// Note that the parameter 0 is put two times:
// The first one is used as the count parameter and the second is displayed as
// first value ({0}).
__n({one: "J'ai {0} chat.", other: "J'ai {0} chats."}, 0, 0)
=>
"J'ai 0 chat."
// If the targeted plural is not found (here 'one'), 'other' is selected.
__n({other: '{a} {a} {b} {b} {b}'}, 1, {a: 'foo', b: 'bar'})
=>
'foo foo bar bar bar'
```
Russian json file (from [i18n](https://github.com/mashpie/i18n-node#i18n__n)
example):
```json
{
"cat_key": {
"one": "%d кошка",
"few": "%d кошки",
"many": "%d кошек",
"other": "%d кошка",
}
}
```
Use:
```js
__n('cat_key', 0, 0);
=>
'0 кошек'
__n('cat_key', 1, 1);
=>
'1 кошка'
__n('cat_key', 2, 2);
=>
'2 кошки'
__n('cat_key', 5, 5);
=>
'5 кошек'
__n('cat_key', 6, 6);
=>
'6 кошек'
__n('cat_key', 21, 21);
=>
'21 кошка'
```
### `context.__getLocale()`
Get locale from query / cookie and header.
@ -149,6 +231,44 @@ app.use(async (ctx, next) => {
});
```
### `app.__n(locale, key, count[, value1[, value2, ...]])`
Get the given locale text with plural management on application level.
```js
console.log(app.__n('zh', {other: 'Hello'}, 0));
// stdout '你好' for Chinese
```
## Usage on template
```js
this.state.__n = this.__n.bind(this);
```
[Nunjucks] example:
```html
{{ __n({other: 'Hello, %s'}, 2, user.name) }}
```
[Pug] example:
```pug
p= __n({other: 'Hello, %s'}, 2, user.name)
```
[Koa-pug] integration:
You can set the property *locals* on the KoaPug instance, where the default locals are stored.
```js
app.use(async (ctx, next) => {
koaPug.locals.__n = ctx.__n.bind(ctx);
await next();
});
```
## Debugging
If you are interested on knowing what locale was chosen and why you can enable the debug messages from [debug].

127
index.js
View File

@ -10,9 +10,11 @@ const path = require('path');
const ms = require('humanize-ms');
const assign = require('object-assign');
const yaml = require('js-yaml');
const MakePlural = require('make-plural');
const DEFAULT_OPTIONS = {
defaultLocale: 'en-US',
fallbackToDefaultLocale: 'false',
queryField: 'locale',
cookieField: 'locale',
localeAlias: {},
@ -21,11 +23,13 @@ const DEFAULT_OPTIONS = {
dir: undefined,
dirs: [path.join(process.cwd(), 'locales')],
functionName: '__',
functionnName: '__n'
};
module.exports = function (app, options) {
options = assign({}, DEFAULT_OPTIONS, options);
const defaultLocale = formatLocale(options.defaultLocale);
const fallbackToDefaultLocale = options.fallbackToDefaultLocale;
const queryField = options.queryField;
const cookieField = options.cookieField;
const cookieDomain = options.cookieDomain;
@ -35,7 +39,9 @@ module.exports = function (app, options) {
const localeDir = options.dir;
const localeDirs = options.dirs;
const functionName = options.functionName;
const functionnName = options.functionnName;
const resources = {};
const PluralsForLocale = {};
/**
* @Deprecated Use options.dirs instead.
@ -81,13 +87,16 @@ module.exports = function (app, options) {
function gettext(locale, key, value) {
if (arguments.length === 0 || arguments.length === 1) {
// __()
// --('en')
// __('en')
return '';
}
const resource = resources[locale] || {};
let text = resource[key];
if (text === undefined && fallbackToDefaultLocale === true) {
text = resources[defaultLocale][key];
}
if (text === undefined) {
text = key;
}
@ -132,6 +141,96 @@ module.exports = function (app, options) {
}
app[functionName] = gettext;
function gettextn(locale, key, count, value) {
if (arguments.length === 0 || arguments.length === 1) {
// __n()
// __n('en')
return '';
}
if (arguments.length === 2) {
// __n('en', key)
return gettext(locale, key);
}
const resource = resources[locale] || {};
// enforce number
count = Number(count);
//**************************************************************//
// Directly adapted from __n function of i18n module :
// https://github.com/mashpie/i18n-node/blob/master/i18n.js
var p;
// create a new Plural for locale
// and try to cache instance
if (PluralsForLocale[locale]) {
p = PluralsForLocale[locale];
} else {
// split locales with a region code
var lc = locale.toLowerCase().split(/[_-\s]+/)
.filter(function(el){ return true && el; });
// take the first part of locale, fallback to full locale
p = MakePlural[lc[0] || locale];
PluralsForLocale[locale] = p;
}
// fallback to 'other' on case of missing translations
let text = resource[key+"."+p(count)] || resource[key+".other"];
//**************************************************************//
if (text === undefined && isObject(key)) {
text = key[p(count)] || key["other"];
}
if (text === undefined && fallbackToDefaultLocale === true) {
text = resources[defaultLocale][key+"."+p(count)] || resources[defaultLocale][key+".other"];
}
if (text === undefined) {
text = key;
}
debugSilly('%s: %j => %j', locale, key, text);
if (!text) {
return '';
}
if (arguments.length === 3) {
// __(locale, key, count)
return text;
}
if (arguments.length === 4) {
if (isObject(value)) {
// __(locale, key, count, object)
// __('zh', {"one": '{a} {b} {b} {a}', "other": '{b} {a} {a} {b}'}, 1, {a: 'foo', b: 'bar'})
// =>
// foo bar bar foo
return formatWithObject(text, value);
}
if (Array.isArray(value)) {
// __(locale, key, array)
// __('zh', {"one": '{0} {1} {1} {0}', "other": '{1} {0} {0} {1}'}, 2, ['foo', 'bar'])
// =>
// bar foo foo bar
return formatWithArray(text, value);
}
// __(locale, key, count, value)
return util.format(text, value);
}
// __(locale, key, count, value1, ...)
const args = new Array(arguments.length - 2);
args[0] = text;
for(let i = 3; i < arguments.length; i++) {
args[i - 2] = arguments[i];
}
return util.format.apply(util, args);
}
app[functionnName] = gettextn;
app.context[functionName] = function (key, value) {
if (arguments.length === 0) {
@ -153,6 +252,32 @@ module.exports = function (app, options) {
}
return gettext.apply(this, args);
};
app.context[functionnName] = function (key, count, value) {
if (arguments.length === 0) {
// __n()
return '';
}
const locale = this.__getLocale();
if (arguments.length === 1) {
// __n(key)
return gettext(locale, key);
}
if (arguments.length === 2) {
return gettextn(locale, key, count);
}
if (arguments.length === 3) {
return gettextn(locale, key, count, value);
}
const args = new Array(arguments.length + 1);
args[0] = locale;
for(let i = 0; i < arguments.length; i++) {
args[i + 1] = arguments[i];
}
return gettextn.apply(this, args);
};
// 1. query: /?locale=en-US
// 2. cookie: locale=zh-TW

View File

@ -19,6 +19,7 @@
"humanize-ms": "^1.2.0",
"ini": "^1.3.4",
"js-yaml": "^3.13.1",
"make-plural": "^6.2.1",
"npminstall": "^3.23.0",
"object-assign": "^4.1.0"
},