From bbab90bfcce86b100b2576a73db9faec0a53b79b Mon Sep 17 00:00:00 2001 From: Alexey Date: Sun, 28 Oct 2018 18:52:34 +0300 Subject: [PATCH] :zap: improvement(pluralization): Extendable pluralization by @Raiondesu --- gitbook/en/api.md | 14 +++++++ gitbook/en/pluralization.md | 83 +++++++++++++++++++++++++++++++++++++ src/index.js | 32 +++++++++++++- src/util.js | 26 ------------ test/unit/issues.test.js | 51 +++++++++++++++++++++++ types/index.d.ts | 7 ++++ 6 files changed, 185 insertions(+), 28 deletions(-) diff --git a/gitbook/en/api.md b/gitbook/en/api.md index 6e6d7aa44..262a2d3c5 100644 --- a/gitbook/en/api.md +++ b/gitbook/en/api.md @@ -38,6 +38,20 @@ Localize the locale message of `key` with pluralization. Localize in preferentially component locale messages than global locale messages. If not specified component locale messages, localize with global locale messages. If you specified `locale`, localize the locale messages of `locale`. If you will specify string value to `values`, localize the locale messages of value. If you will specify Array or Object value to `values`, you must specify with `values` of [$t](#t). +#### getChoiceIndex + +- **Arguments:** + - `{number} choice` + - `{number} choicesLength` + +- **Return:** `finalChoice {number}` + + Get pluralization index for current pluralizing number and a given amount of choices. Can be overriden through prototype mutation: + ```js + VueI18n.prototype.getChoiceIndex = /* custom implementation */ + ``` + + #### $te - **Arguments:** diff --git a/gitbook/en/pluralization.md b/gitbook/en/pluralization.md index 4020bc308..a37ae60e6 100644 --- a/gitbook/en/pluralization.md +++ b/gitbook/en/pluralization.md @@ -36,3 +36,86 @@ This will output the following HTML:

one apple

10 apples

``` + +--- + +## Custom pluralization + +Such pluralization, however, does not apply to all languages (Slavic languages, for example, have different pluralization rules). + +In order to implement these rules you can override the `VueI18n.prototype.getChoiceIndex` function. + +Very simplified example using rules for Slavic langauges (Russian, Ukrainian, etc.): +```js +/** + * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)` + * @param choiceLength {number} an overall amount of available choices + * @returns a final choice index to select plural word by +**/ +VueI18n.prototype.getChoiceIndex = function (choice, choicesLength) { + // this === VueI18n instance, so the locale property also exists here + if (this.locale !== 'ru') { + // proceed to the default implementation + } + + if (choice === 0) { + return 0; + } + + const teen = choice > 10 && choice < 20; + const endsWithOne = choice % 10 === 1; + + if (!teen && endsWithOne) { + return 1; + } + + if (!teen && choice % 10 >= 2 && choice % 10 <= 4) { + return 2; + } + + return (choicesLength < 4) ? 2 : 3; +} +``` + +This would effectively give this: + + +```javascript +const messages = { + ru: { + car: '0 машин | 1 машина | {n} машины | {n} машин', + banana: 'нет бананов | 1 банан | {n} банана | {n} бананов' + } +} +``` +Where the format is `0 things | 1 thing | few things | multiple things`. + +Your template still needs to use `$tc()`, not `$t()`: + +```html +

{{ $tc('car', 1) }}

+

{{ $tc('car', 2) }}

+

{{ $tc('car', 4) }}

+

{{ $tc('car', 12) }}

+

{{ $tc('car', 21) }}

+ +

{{ $tc('car', 0) }}

+

{{ $tc('car', 4) }}

+

{{ $tc('car', 11) }}

+

{{ $tc('car', 31) }}

+``` + +Which results in: + +```html +

1 машина

+

2 машины

+

4 машины

+

12 машин

+

21 машина

+ +

нет бананов

+

4 банана

+

11 бананов

+

31 банан

+``` diff --git a/src/index.js b/src/index.js index 1e99ef5a5..607b3f011 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,6 @@ import { warn, isNull, parseArgs, - fetchChoice, isPlainObject, isObject, looseClone, @@ -407,7 +406,36 @@ export default class VueI18n { const parsedArgs = parseArgs(...values) parsedArgs.params = Object.assign(predefined, parsedArgs.params) values = parsedArgs.locale === null ? [parsedArgs.params] : [parsedArgs.locale, parsedArgs.params] - return fetchChoice(this._t(key, _locale, messages, host, ...values), choice) + return this.fetchChoice(this._t(key, _locale, messages, host, ...values), choice) + } + + fetchChoice (message: string, choice: number): ?string { + /* istanbul ignore if */ + if (!message && typeof message !== 'string') { return null } + const choices: Array = message.split('|') + + choice = this.getChoiceIndex(choice, choices.length) + if (!choices[choice]) { return message } + return choices[choice].trim() + } + + /** + * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)` + * @param choiceLength {number} an overall amount of available choices + * @returns a final choice index + */ + getChoiceIndex (choice: number, choicesLength: number): number { + choice = Math.abs(choice) + + if (choicesLength === 2) { + return choice + ? choice > 1 + ? 1 + : 0 + : 1 + } + + return choice ? Math.min(choice, 2) : 0 } tc (key: Path, choice?: number, ...values: any): TranslateResult { diff --git a/src/util.js b/src/util.js index b661472a6..ae1c17e2d 100644 --- a/src/util.js +++ b/src/util.js @@ -50,32 +50,6 @@ export function parseArgs (...args: Array): Object { return { locale, params } } -function getOldChoiceIndexFixed (choice: number): number { - return choice - ? choice > 1 - ? 1 - : 0 - : 1 -} - -function getChoiceIndex (choice: number, choicesLength: number): number { - choice = Math.abs(choice) - - if (choicesLength === 2) { return getOldChoiceIndexFixed(choice) } - - return choice ? Math.min(choice, 2) : 0 -} - -export function fetchChoice (message: string, choice: number): ?string { - /* istanbul ignore if */ - if (!message && typeof message !== 'string') { return null } - const choices: Array = message.split('|') - - choice = getChoiceIndex(choice, choices.length) - if (!choices[choice]) { return message } - return choices[choice].trim() -} - export function looseClone (obj: Object): Object { return JSON.parse(JSON.stringify(obj)) } diff --git a/test/unit/issues.test.js b/test/unit/issues.test.js index a3f3b3b9a..04ef020e4 100644 --- a/test/unit/issues.test.js +++ b/test/unit/issues.test.js @@ -1,5 +1,6 @@ import messages from './fixture/index' import { parse } from '../../src/format' +import VueI18n from '../../src' const compiler = require('vue-template-compiler') describe('issues', () => { @@ -382,4 +383,54 @@ describe('issues', () => { ) }) }) + + describe('#78', () => { + it('should allow custom pluralization', () => { + const defaultImpl = VueI18n.prototype.getChoiceIndex + VueI18n.prototype.getChoiceIndex = function (choice, choicesLength) { + if (this.locale !== 'ru') { + return defaultImpl.apply(this, arguments) + } + + if (choice === 0) { + return 0 + } + + const teen = choice > 10 && choice < 20 + const endsWithOne = choice % 10 === 1 + + if (choicesLength < 4) { + return (!teen && endsWithOne) ? 1 : 2 + } + + if (!teen && endsWithOne) { + return 1 + } + + if (!teen && choice % 10 >= 2 && choice % 10 <= 4) { + return 2 + } + + return (choicesLength < 4) ? 2 : 3 + } + + + i18n = new VueI18n({ + locale: 'en', + messages: { + ru: { + car: '0 машин | 1 машина | {n} машины | {n} машин' + } + } + }) + vm = new Vue({ i18n }) + + assert(vm.$tc('car', 0), '0 машин') + assert(vm.$tc('car', 1), '1 машина') + assert(vm.$tc('car', 2), '2 машины') + assert(vm.$tc('car', 4), '4 машины') + assert(vm.$tc('car', 12), '12 машин') + assert(vm.$tc('car', 21), '21 машина') + }) + }) }) diff --git a/types/index.d.ts b/types/index.d.ts index 21f8b20c5..2a0219c3d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -140,6 +140,13 @@ declare class VueI18n { setNumberFormat(locale: VueI18n.Locale, format: VueI18n.NumberFormat): void; mergeNumberFormat(locale: VueI18n.Locale, format: VueI18n.NumberFormat): void; + /** + * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)` + * @param choiceLength {number} an overall amount of available choices + * @returns a final choice index + */ + getChoiceIndex: (choice: number, choicesLength: number) => number; + static install: PluginFunction; static version: string; static availabilities: VueI18n.IntlAvailability;