Skip to content

Commit

Permalink
⚡ improvement(pluralization): Extendable pluralization by @Raiondesu
Browse files Browse the repository at this point in the history
  • Loading branch information
Raiondesu authored and kazupon committed Oct 28, 2018
1 parent 3a57895 commit bbab90b
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 28 deletions.
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

0 comments on commit bbab90b

Please sign in to comment.