From 5d5371e739094a21b4e8af9f5b7b800b907eabff Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Thu, 18 Jun 2020 14:24:03 +0200 Subject: [PATCH] Add support for plural forms --- README.md | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 120 ++++++++++++++++++++++++++++++++++++++++++++++++++- package.json | 1 + 3 files changed, 238 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a41519..d9c68f1 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,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 +229,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]. diff --git a/index.js b/index.js index 52b88e2..750e21a 100644 --- a/index.js +++ b/index.js @@ -10,6 +10,7 @@ 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', @@ -21,6 +22,7 @@ const DEFAULT_OPTIONS = { dir: undefined, dirs: [path.join(process.cwd(), 'locales')], functionName: '__', + functionnName: '__n' }; module.exports = function (app, options) { @@ -35,7 +37,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,7 +85,7 @@ module.exports = function (app, options) { function gettext(locale, key, value) { if (arguments.length === 0 || arguments.length === 1) { // __() - // --('en') + // __('en') return ''; } @@ -132,6 +136,94 @@ 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) { + 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 +245,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 diff --git a/package.json b/package.json index 18e243b..757d8e4 100644 --- a/package.json +++ b/package.json @@ -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" },