diff --git a/decls/i18n.js b/decls/i18n.js
index af44ae745..e45e4ba1e 100644
--- a/decls/i18n.js
+++ b/decls/i18n.js
@@ -61,7 +61,10 @@ declare type I18nOptions = {
root?: I18n, // for internal
fallbackRoot?: boolean,
sync?: boolean,
- silentTranslationWarn?: boolean
+ silentTranslationWarn?: boolean,
+ pluralizationRules?: {
+ [lang: string]: (choice: number, choicesLength: number) => number,
+ },
};
declare type IntlAvailability = {
@@ -100,7 +103,10 @@ declare interface I18n {
getNumberFormat (locale: Locale): NumberFormat,
setNumberFormat (locale: Locale, format: NumberFormat): void,
mergeNumberFormat (locale: Locale, format: NumberFormat): void,
- n (value: number, ...args: any): NumberFormatResult
+ n (value: number, ...args: any): NumberFormatResult,
+ pluralizationRules: {
+ [lang: string]: (choice: number, choicesLength: number) => number
+ }
};
declare interface Formatter {
diff --git a/gitbook/en/api.md b/gitbook/en/api.md
index 4ec610b03..d4da72e20 100644
--- a/gitbook/en/api.md
+++ b/gitbook/en/api.md
@@ -38,6 +38,8 @@
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).
+ If default pluralization does not suit your needs, see [pluralization rules in constructor options](#pluralizationrules) and [custom pluralization](pluralization.md).
+
#### getChoiceIndex
- **Arguments:**
@@ -237,7 +239,7 @@ You can specify the below some options of `I18nOptions` constructor options of [
If `false`, regardless of the root level locale, localize for each component locale.
-### silentTranslationWarn
+#### silentTranslationWarn
> 6.1+
@@ -249,6 +251,26 @@ You can specify the below some options of `I18nOptions` constructor options of [
If `true`, supress localization fail warnings.
+#### pluralizationRules
+
+> 8.5+
+
+- **Type:** `Object`
+
+- **Default:** `{}`
+
+ A set of rules for word pluralization in a following format:
+ ```js
+ {
+ // Key - locale for the rule to be applied to.
+ // Value - mapping function that maps a choice index from `$tc` to the actual choice of the plural word.
+
+ 'ru': function (choice, choiceIndex) => Number/* index of the plural word */;
+ 'en': function (choice, choiceIndex) => Number/* index of the plural word */;
+ 'jp': function (choice, choiceIndex) => Number/* index of the plural word */;
+ }
+ ```
+
### Properties
#### locale
@@ -321,6 +343,16 @@ You can specify the below some options of `I18nOptions` constructor options of [
Whether suppress warnings outputted when localization fails.
+#### pluralizationRules
+
+> 8.5+
+
+- **Type:** `Object`
+
+- **Default:** `{}`
+
+ A set of rules for word pluralization. Key is a locale, value is the rule function for that locale.
+
### Methods
#### getLocaleMessage( locale )
diff --git a/gitbook/en/pluralization.md b/gitbook/en/pluralization.md
index 4d7fa2743..0b0f016b6 100644
--- a/gitbook/en/pluralization.md
+++ b/gitbook/en/pluralization.md
@@ -43,43 +43,46 @@ This will output the following HTML:
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.
+In order to implement these rules you can pass an optional `pluralizationRules` object into `VueI18n` constructor options.
Very simplified example using rules for Slavic languages (Russian, Ukrainian, etc.):
```js
-/**
- * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
- * @param choicesLength {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
+new VueI18n({
+ pluralizationRules: {
+ /** Key - language to use the rule for, 'ru', in this case */
+ /** Value - function
+ * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
+ * @param choicesLength {number} an overall amount of available choices
+ * @returns a final choice index to select plural word by
+ **/
+ 'ru': function (choice, choicesLength) {
+ // this === VueI18n instance, so the locale property also exists here
+
+ 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;
+ }
}
-
- 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: {
@@ -119,3 +122,7 @@ Which results in:
11 бананов
31 банан
```
+
+### Default pluralization
+
+If your current locale is not found in a pluralization map, the [default](#pluralization) rule of the english langugage will be used.
diff --git a/src/index.js b/src/index.js
index 9b80435a1..75a4fe8e9 100644
--- a/src/index.js
+++ b/src/index.js
@@ -56,6 +56,9 @@ export default class VueI18n {
_numberFormatters: Object
_path: I18nPath
_dataListeners: Array
+ pluralizationRules: {
+ [lang: string]: (choice: number, choicesLength: number) => number
+ }
constructor (options: I18nOptions = {}) {
// Auto install if it is not done yet and `window` has `Vue`.
@@ -88,6 +91,8 @@ export default class VueI18n {
this._path = new I18nPath()
this._dataListeners = []
+ this.pluralizationRules = options.pluralizationRules || {}
+
this._exist = (message: Object, key: Path): boolean => {
if (!message || !key) { return false }
return !isNull(this._path.getPathValue(message, key))
@@ -435,17 +440,26 @@ export default class VueI18n {
* @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
+ // Default (old) getChoiceIndex implementation - english-compatible
+ const defaultImpl = (_choice: number, _choicesLength: number) => {
+ _choice = Math.abs(_choice)
+
+ if (_choicesLength === 2) {
+ return _choice
+ ? _choice > 1
+ ? 1
+ : 0
+ : 1
+ }
+
+ return _choice ? Math.min(_choice, 2) : 0
}
- return choice ? Math.min(choice, 2) : 0
+ if (this.locale in this.pluralizationRules) {
+ return this.pluralizationRules[this.locale].apply(this, [choice, choicesLength])
+ } else {
+ return defaultImpl(choice, choicesLength)
+ }
}
tc (key: Path, choice?: number, ...values: any): TranslateResult {
diff --git a/test/unit/issues.test.js b/test/unit/issues.test.js
index fbfdecea9..df9abf4da 100644
--- a/test/unit/issues.test.js
+++ b/test/unit/issues.test.js
@@ -384,56 +384,6 @@ 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 машина')
- })
- })
-
describe('#450', () => {
it('shoulbe be translated with v-t', done => {
const vm = new Vue({
@@ -539,4 +489,145 @@ describe('issues', () => {
}).then(done)
})
})
+
+ describe('#78, #464', () => {
+ it('should fallback to default pluralization', () => {
+ // / Test default pluralization rule (english)
+ const i18n = new VueI18n({
+ locale: 'en',
+ messages: {
+ 'en': {
+ test: 'no tests | 1 test | {n} tests'
+ }
+ }
+ })
+
+ assert.strictEqual(i18n.tc('test', 1), '1 test')
+ assert.strictEqual(i18n.tc('test', 0), 'no tests')
+ assert.strictEqual(i18n.tc('test', 10), '10 tests')
+ })
+
+ it('should use custom pluralization if available', () => {
+ // Test custom pluralization rule (slavic languages)
+ function slavicPluralization (choice, choicesLength) {
+ 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
+ }
+
+ let i18n = new VueI18n({
+ locale: 'en',
+ messages: {
+ 'en': {
+ test: 'no tests | 1 test | {n} tests'
+ },
+ 'ru': {
+ test: 'нет тестов | 1 тест | {n} теста | {n} тестов'
+ }
+ },
+ pluralizationRules: {
+ 'ru': slavicPluralization
+ }
+ })
+
+ assert.strictEqual(i18n.tc('test', 1), '1 test')
+ assert.strictEqual(i18n.tc('test', 0), 'no tests')
+ assert.strictEqual(i18n.tc('test', 10), '10 tests')
+
+ i18n.locale = 'ru'
+
+ assert.strictEqual(i18n.tc('test', 1), '1 тест')
+ assert.strictEqual(i18n.tc('test', 3), '3 теста')
+ assert.strictEqual(i18n.tc('test', 0), 'нет тестов')
+ assert.strictEqual(i18n.tc('test', 10), '10 тестов')
+
+ i18n = new VueI18n({
+ locale: 'ru',
+ messages: {
+ ru: {
+ car: '0 машин | 1 машина | {n} машины | {n} машин'
+ }
+ },
+ pluralizationRules: {
+ ru: slavicPluralization
+ }
+ })
+ 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 машина')
+ })
+
+ it('ensures backward-compatibility with #451', () => {
+ 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 машина')
+
+ // Set the default implementation back
+ VueI18n.prototype.getChoiceIndex = defaultImpl
+ })
+ })
})
diff --git a/types/index.d.ts b/types/index.d.ts
index 047a66df6..0fec49b02 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -44,6 +44,14 @@ declare namespace VueI18n {
interface NumberFormat { [key: string]: NumberFormatOptions; }
interface NumberFormats { [key: string]: NumberFormat; }
type NumberFormatResult = string;
+ type PluralizationRulesMap = {
+ /**
+ * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)`
+ * @param choicesLength {number} an overall amount of available choices
+ * @returns a final choice index
+ */
+ [lang: string]: (choice: number, choicesLength: number) => number;
+ };
interface Formatter {
interpolate(message: string, values?: Values): any[];
@@ -68,6 +76,7 @@ declare namespace VueI18n {
fallbackRoot?: boolean;
sync?: boolean;
silentTranslationWarn?: boolean;
+ pluralizationRules?: PluralizationRulesMap;
}
}
@@ -103,6 +112,7 @@ export declare interface IVueI18n {
missing: VueI18n.MissingHandler;
formatter: VueI18n.Formatter;
silentTranslationWarn: boolean;
+ pluralizationRules: VueI18n.PluralizationRulesMap;
}
declare class VueI18n {