Skip to content
This repository was archived by the owner on Dec 31, 2024. It is now read-only.

Commit bbab90b

Browse files
Raiondesukazupon
authored andcommitted
⚡ improvement(pluralization): Extendable pluralization by @Raiondesu
1 parent 3a57895 commit bbab90b

6 files changed

Lines changed: 185 additions & 28 deletions

File tree

gitbook/en/api.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,20 @@
3838

3939
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).
4040

41+
#### getChoiceIndex
42+
43+
- **Arguments:**
44+
- `{number} choice`
45+
- `{number} choicesLength`
46+
47+
- **Return:** `finalChoice {number}`
48+
49+
Get pluralization index for current pluralizing number and a given amount of choices. Can be overriden through prototype mutation:
50+
```js
51+
VueI18n.prototype.getChoiceIndex = /* custom implementation */
52+
```
53+
54+
4155
#### $te
4256

4357
- **Arguments:**

gitbook/en/pluralization.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,86 @@ This will output the following HTML:
3636
<p>one apple</p>
3737
<p>10 apples</p>
3838
```
39+
40+
---
41+
42+
## Custom pluralization
43+
44+
Such pluralization, however, does not apply to all languages (Slavic languages, for example, have different pluralization rules).
45+
46+
In order to implement these rules you can override the `VueI18n.prototype.getChoiceIndex` function.
47+
48+
Very simplified example using rules for Slavic langauges (Russian, Ukrainian, etc.):
49+
```js
50+
/**
51+
* @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
52+
* @param choiceLength {number} an overall amount of available choices
53+
* @returns a final choice index to select plural word by
54+
**/
55+
VueI18n.prototype.getChoiceIndex = function (choice, choicesLength) {
56+
// this === VueI18n instance, so the locale property also exists here
57+
if (this.locale !== 'ru') {
58+
// proceed to the default implementation
59+
}
60+
61+
if (choice === 0) {
62+
return 0;
63+
}
64+
65+
const teen = choice > 10 && choice < 20;
66+
const endsWithOne = choice % 10 === 1;
67+
68+
if (!teen && endsWithOne) {
69+
return 1;
70+
}
71+
72+
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
73+
return 2;
74+
}
75+
76+
return (choicesLength < 4) ? 2 : 3;
77+
}
78+
```
79+
80+
This would effectively give this:
81+
82+
83+
```javascript
84+
const messages = {
85+
ru: {
86+
car: '0 машин | 1 машина | {n} машины | {n} машин',
87+
banana: 'нет бананов | 1 банан | {n} банана | {n} бананов'
88+
}
89+
}
90+
```
91+
Where the format is `0 things | 1 thing | few things | multiple things`.
92+
93+
Your template still needs to use `$tc()`, not `$t()`:
94+
95+
```html
96+
<p>{{ $tc('car', 1) }}</p>
97+
<p>{{ $tc('car', 2) }}</p>
98+
<p>{{ $tc('car', 4) }}</p>
99+
<p>{{ $tc('car', 12) }}</p>
100+
<p>{{ $tc('car', 21) }}</p>
101+
102+
<p>{{ $tc('car', 0) }}</p>
103+
<p>{{ $tc('car', 4) }}</p>
104+
<p>{{ $tc('car', 11) }}</p>
105+
<p>{{ $tc('car', 31) }}</p>
106+
```
107+
108+
Which results in:
109+
110+
```html
111+
<p>1 машина</p>
112+
<p>2 машины</p>
113+
<p>4 машины</p>
114+
<p>12 машин</p>
115+
<p>21 машина</p>
116+
117+
<p>нет бананов</p>
118+
<p>4 банана</p>
119+
<p>11 бананов</p>
120+
<p>31 банан</p>
121+
```

src/index.js

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
warn,
66
isNull,
77
parseArgs,
8-
fetchChoice,
98
isPlainObject,
109
isObject,
1110
looseClone,
@@ -407,7 +406,36 @@ export default class VueI18n {
407406
const parsedArgs = parseArgs(...values)
408407
parsedArgs.params = Object.assign(predefined, parsedArgs.params)
409408
values = parsedArgs.locale === null ? [parsedArgs.params] : [parsedArgs.locale, parsedArgs.params]
410-
return fetchChoice(this._t(key, _locale, messages, host, ...values), choice)
409+
return this.fetchChoice(this._t(key, _locale, messages, host, ...values), choice)
410+
}
411+
412+
fetchChoice (message: string, choice: number): ?string {
413+
/* istanbul ignore if */
414+
if (!message && typeof message !== 'string') { return null }
415+
const choices: Array<string> = message.split('|')
416+
417+
choice = this.getChoiceIndex(choice, choices.length)
418+
if (!choices[choice]) { return message }
419+
return choices[choice].trim()
420+
}
421+
422+
/**
423+
* @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
424+
* @param choiceLength {number} an overall amount of available choices
425+
* @returns a final choice index
426+
*/
427+
getChoiceIndex (choice: number, choicesLength: number): number {
428+
choice = Math.abs(choice)
429+
430+
if (choicesLength === 2) {
431+
return choice
432+
? choice > 1
433+
? 1
434+
: 0
435+
: 1
436+
}
437+
438+
return choice ? Math.min(choice, 2) : 0
411439
}
412440

413441
tc (key: Path, choice?: number, ...values: any): TranslateResult {

src/util.js

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,32 +50,6 @@ export function parseArgs (...args: Array<mixed>): Object {
5050
return { locale, params }
5151
}
5252

53-
function getOldChoiceIndexFixed (choice: number): number {
54-
return choice
55-
? choice > 1
56-
? 1
57-
: 0
58-
: 1
59-
}
60-
61-
function getChoiceIndex (choice: number, choicesLength: number): number {
62-
choice = Math.abs(choice)
63-
64-
if (choicesLength === 2) { return getOldChoiceIndexFixed(choice) }
65-
66-
return choice ? Math.min(choice, 2) : 0
67-
}
68-
69-
export function fetchChoice (message: string, choice: number): ?string {
70-
/* istanbul ignore if */
71-
if (!message && typeof message !== 'string') { return null }
72-
const choices: Array<string> = message.split('|')
73-
74-
choice = getChoiceIndex(choice, choices.length)
75-
if (!choices[choice]) { return message }
76-
return choices[choice].trim()
77-
}
78-
7953
export function looseClone (obj: Object): Object {
8054
return JSON.parse(JSON.stringify(obj))
8155
}

test/unit/issues.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import messages from './fixture/index'
22
import { parse } from '../../src/format'
3+
import VueI18n from '../../src'
34
const compiler = require('vue-template-compiler')
45

56
describe('issues', () => {
@@ -382,4 +383,54 @@ describe('issues', () => {
382383
)
383384
})
384385
})
386+
387+
describe('#78', () => {
388+
it('should allow custom pluralization', () => {
389+
const defaultImpl = VueI18n.prototype.getChoiceIndex
390+
VueI18n.prototype.getChoiceIndex = function (choice, choicesLength) {
391+
if (this.locale !== 'ru') {
392+
return defaultImpl.apply(this, arguments)
393+
}
394+
395+
if (choice === 0) {
396+
return 0
397+
}
398+
399+
const teen = choice > 10 && choice < 20
400+
const endsWithOne = choice % 10 === 1
401+
402+
if (choicesLength < 4) {
403+
return (!teen && endsWithOne) ? 1 : 2
404+
}
405+
406+
if (!teen && endsWithOne) {
407+
return 1
408+
}
409+
410+
if (!teen && choice % 10 >= 2 && choice % 10 <= 4) {
411+
return 2
412+
}
413+
414+
return (choicesLength < 4) ? 2 : 3
415+
}
416+
417+
418+
i18n = new VueI18n({
419+
locale: 'en',
420+
messages: {
421+
ru: {
422+
car: '0 машин | 1 машина | {n} машины | {n} машин'
423+
}
424+
}
425+
})
426+
vm = new Vue({ i18n })
427+
428+
assert(vm.$tc('car', 0), '0 машин')
429+
assert(vm.$tc('car', 1), '1 машина')
430+
assert(vm.$tc('car', 2), '2 машины')
431+
assert(vm.$tc('car', 4), '4 машины')
432+
assert(vm.$tc('car', 12), '12 машин')
433+
assert(vm.$tc('car', 21), '21 машина')
434+
})
435+
})
385436
})

types/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,13 @@ declare class VueI18n {
140140
setNumberFormat(locale: VueI18n.Locale, format: VueI18n.NumberFormat): void;
141141
mergeNumberFormat(locale: VueI18n.Locale, format: VueI18n.NumberFormat): void;
142142

143+
/**
144+
* @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
145+
* @param choiceLength {number} an overall amount of available choices
146+
* @returns a final choice index
147+
*/
148+
getChoiceIndex: (choice: number, choicesLength: number) => number;
149+
143150
static install: PluginFunction<never>;
144151
static version: string;
145152
static availabilities: VueI18n.IntlAvailability;

0 commit comments

Comments
 (0)