Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extendable pluralization #451

Merged
merged 9 commits into from
Oct 28, 2018
14 changes: 14 additions & 0 deletions gitbook/en/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
83 changes: 83 additions & 0 deletions gitbook/en/pluralization.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,86 @@ This will output the following HTML:
<p>one apple</p>
<p>10 apples</p>
```

---

## 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
<p>{{ $tc('car', 1) }}</p>
<p>{{ $tc('car', 2) }}</p>
<p>{{ $tc('car', 4) }}</p>
<p>{{ $tc('car', 12) }}</p>
<p>{{ $tc('car', 21) }}</p>

<p>{{ $tc('car', 0) }}</p>
<p>{{ $tc('car', 4) }}</p>
<p>{{ $tc('car', 11) }}</p>
<p>{{ $tc('car', 31) }}</p>
```

Which results in:

```html
<p>1 машина</p>
<p>2 машины</p>
<p>4 машины</p>
<p>12 машин</p>
<p>21 машина</p>

<p>нет бананов</p>
<p>4 банана</p>
<p>11 бананов</p>
<p>31 банан</p>
```
32 changes: 30 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
warn,
isNull,
parseArgs,
fetchChoice,
isPlainObject,
isObject,
looseClone,
Expand Down Expand Up @@ -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<string> = 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 {
Expand Down
26 changes: 0 additions & 26 deletions src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,32 +50,6 @@ export function parseArgs (...args: Array<mixed>): 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<string> = 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))
}
Expand Down
51 changes: 51 additions & 0 deletions test/unit/issues.test.js
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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 машина')
})
})
})
7 changes: 7 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<never>;
static version: string;
static availabilities: VueI18n.IntlAvailability;
Expand Down