Skip to content

Commit bf68f2e

Browse files
authored
feat!: Support automatic pluralization with Intl.PluralRules (#2400)
1 parent 09b43b6 commit bf68f2e

File tree

15 files changed

+408
-33
lines changed

15 files changed

+408
-33
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ docs/jp/api/vue
2424
docs/jp/api/index.md
2525
!docs/api/typedoc-sidebar.json
2626
.notes
27+
.data
28+
.playwright-cli
29+
.report
30+
.claude
2731

2832
*.log
2933
*.swp

docs/guide/advanced/component.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,15 @@ const url = ref('/term')
5151
<template>
5252
<div id="app">
5353
<!-- ... -->
54-
<i18n-t keypath="term" tag="label" for="tos">
55-
<a :href="url" target="_blank">{{ t('tos') }}</a>
54+
<i18n-t
55+
keypath="term"
56+
tag="label"
57+
for="tos"
58+
>
59+
<a
60+
:href="url"
61+
target="_blank"
62+
>{{ t('tos') }}</a>
5663
</i18n-t>
5764
<!-- ... -->
5865
</div>
@@ -128,11 +135,14 @@ const refundLimit = ref(30)
128135
<template>
129136
<div id="app">
130137
<!-- ... -->
131-
<i18n-t keypath="info" tag="p">
132-
<template v-slot:limit>
138+
<i18n-t
139+
keypath="info"
140+
tag="p"
141+
>
142+
<template #limit>
133143
<span>{{ changeLimit }}</span>
134144
</template>
135-
<template v-slot:action>
145+
<template #action>
136146
<a :href="changeUrl">{{ t('change') }}</a>
137147
</template>
138148
</i18n-t>

docs/guide/essentials/local.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ const { t } = useI18n({
157157
<p>This is good service</p>
158158
</div>
159159
<div class="footer">
160-
<button type="button">{{ t('buttons.save') }}</button>
160+
<button type="button">
161+
{{ t('buttons.save') }}
162+
</button>
161163
</div>
162164
</div>
163165
</template>

docs/guide/essentials/pluralization.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,44 @@ As result the below:
114114
<p>too many bananas</p>
115115
```
116116

117-
## Custom Pluralization
117+
## Automatic Pluralization with `Intl.PluralRules`
118+
119+
Vue I18n automatically uses [`Intl.PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) to select the correct plural form based on the current locale. This means that for most languages, you don't need to write custom pluralization rules — just provide the correct number of message cases in CLDR plural category order: `zero | one | two | few | many | other`.
120+
121+
For example, Russian has 4 plural categories (`one`, `few`, `many`, `other`):
122+
123+
```js
124+
const i18n = createI18n({
125+
locale: 'ru',
126+
messages: {
127+
ru: {
128+
car: '{n} машина | {n} машины | {n} машин | {n} машин',
129+
// one few many other
130+
}
131+
}
132+
})
133+
```
134+
135+
Vue I18n will automatically select the correct form:
118136

119-
Such pluralization, however, does not apply to all languages (Slavic languages, for example, have different pluralization rules).
137+
| Value | `Intl.PluralRules` category | Selected case |
138+
|---|---|---|
139+
| 1 | `one` | `{n} машина` |
140+
| 2 | `few` | `{n} машины` |
141+
| 5 | `many` | `{n} машин` |
142+
| 21 | `one` | `{n} машина` |
143+
144+
:::tip NOTE
145+
When the number of message cases exceeds the number of plural categories for the locale, Vue I18n falls back to the default rule (suitable for English).
146+
:::
147+
148+
:::tip NOTE
149+
If `Intl.PluralRules` is not available in the runtime environment, Vue I18n falls back to the default English-based rule.
150+
:::
151+
152+
## Custom Pluralization
120153

121-
To implement these rules you can pass an optional `pluralRules` object into `createI18n` options.
154+
While automatic pluralization via `Intl.PluralRules` works for most languages, you may need custom logic for special cases. You can pass an optional `pluralRules` object into `createI18n` options to override the automatic behavior for specific locales.
122155

123156
Very simplified example using rules for Slavic languages (Russian, Ukrainian, etc.):
124157

docs/guide/migration/breaking12.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,40 @@ Replace all `v-t` directive usage with `$t()` (global scope) or `t()` from `useI
507507

508508
You can use the `@intlify/vue-i18n/no-deprecated-v-t` rule to detect all `v-t` usage in your codebase.
509509

510+
## Default pluralization now uses `Intl.PluralRules`
511+
512+
**Reason**: The previous default pluralization rule was a simple English-only implementation that did not correctly handle languages with complex plural categories (e.g., Russian, Arabic, Polish). Vue I18n v12 now uses `Intl.PluralRules` to automatically select the correct plural form based on the current locale.
513+
514+
### What changed
515+
516+
- When no custom `pluralRules` is set for a locale, Vue I18n automatically uses `Intl.PluralRules` to determine the correct plural category (zero, one, two, few, many, other)
517+
- Message cases must be ordered according to the CLDR plural category order: `zero | one | two | few | many | other` (only include categories that exist for the locale)
518+
- If the number of message cases exceeds the locale's plural category count, Vue I18n falls back to the previous default rule
519+
- If `Intl.PluralRules` is not available in the runtime environment, Vue I18n falls back to the previous default rule
520+
521+
### Migration
522+
523+
If you were relying on the previous default rule for non-English locales **without** custom `pluralRules`, you need to reorder your message cases to match the CLDR plural category order for the locale.
524+
525+
**Before (v11) — Russian with custom `pluralRules`:**
526+
527+
No change needed. Custom `pluralRules` take priority and continue to work as before.
528+
529+
**After (v12) — Russian (automatic, no custom `pluralRules` needed):**
530+
531+
```js
532+
const i18n = createI18n({
533+
locale: 'ru',
534+
// No pluralRules needed — Intl.PluralRules handles it automatically
535+
messages: {
536+
ru: {
537+
// Order: one | few | many | other (CLDR order for Russian)
538+
car: '{n} машина | {n} машины | {n} машин | {n} машин'
539+
}
540+
}
541+
})
542+
```
543+
510544
## Change `MissingHandler` signature
511545

512546
**Reason**: Vue 3.6+ deprecates `getCurrentInstance()` API. The `MissingHandler` type previously received a `ComponentInternalInstance` as the third parameter, but this is no longer available.

docs/jp/guide/advanced/component.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,15 @@ const url = ref('/term')
5151
<template>
5252
<div id="app">
5353
<!-- ... -->
54-
<i18n-t keypath="term" tag="label" for="tos">
55-
<a :href="url" target="_blank">{{ t('tos') }}</a>
54+
<i18n-t
55+
keypath="term"
56+
tag="label"
57+
for="tos"
58+
>
59+
<a
60+
:href="url"
61+
target="_blank"
62+
>{{ t('tos') }}</a>
5663
</i18n-t>
5764
<!-- ... -->
5865
</div>
@@ -128,11 +135,14 @@ const refundLimit = ref(30)
128135
<template>
129136
<div id="app">
130137
<!-- ... -->
131-
<i18n-t keypath="info" tag="p">
132-
<template v-slot:limit>
138+
<i18n-t
139+
keypath="info"
140+
tag="p"
141+
>
142+
<template #limit>
133143
<span>{{ changeLimit }}</span>
134144
</template>
135-
<template v-slot:action>
145+
<template #action>
136146
<a :href="changeUrl">{{ t('change') }}</a>
137147
</template>
138148
</i18n-t>

docs/jp/guide/essentials/local.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ const { t } = useI18n({
157157
<p>This is good service</p>
158158
</div>
159159
<div class="footer">
160-
<button type="button">{{ t('buttons.save') }}</button>
160+
<button type="button">
161+
{{ t('buttons.save') }}
162+
</button>
161163
</div>
162164
</div>
163165
</template>

docs/jp/guide/essentials/pluralization.md

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,44 @@ const messages = {
114114
<p>too many bananas</p>
115115
```
116116

117-
## カスタム複数化
117+
## `Intl.PluralRules` による自動複数化
118+
119+
Vue I18n は現在のロケールに基づいて正しい複数形を自動的に選択するために [`Intl.PluralRules`](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) を使用します。これにより、ほとんどの言語ではカスタムの複数化ルールを書く必要がありません。CLDR 複数形カテゴリの順序でメッセージのケースを正しい数だけ提供するだけです: `zero | one | two | few | many | other`
120+
121+
例えば、ロシア語には4つの複数形カテゴリがあります(`one`, `few`, `many`, `other`):
122+
123+
```js
124+
const i18n = createI18n({
125+
locale: 'ru',
126+
messages: {
127+
ru: {
128+
car: '{n} машина | {n} машины | {n} машин | {n} машин',
129+
// one few many other
130+
}
131+
}
132+
})
133+
```
134+
135+
Vue I18n は自動的に正しい形式を選択します:
118136

119-
ただし、このような複数化はすべての言語に適用されるわけではありません(たとえば、スラブ語派の言語には異なる複数化ルールがあります)。
137+
|| `Intl.PluralRules` カテゴリ | 選択されるケース |
138+
|---|---|---|
139+
| 1 | `one` | `{n} машина` |
140+
| 2 | `few` | `{n} машины` |
141+
| 5 | `many` | `{n} машин` |
142+
| 21 | `one` | `{n} машина` |
143+
144+
:::tip NOTE
145+
メッセージのケース数がロケールの複数形カテゴリ数を超える場合、Vue I18n はデフォルトルール(英語に適したもの)にフォールバックします。
146+
:::
147+
148+
:::tip NOTE
149+
ランタイム環境で `Intl.PluralRules` が利用できない場合、Vue I18n はデフォルトの英語ベースルールにフォールバックします。
150+
:::
151+
152+
## カスタム複数化
120153

121-
これらのルールを実装するには、オプションの `pluralRules` オブジェクトを `createI18n` オプションに渡すことができます
154+
`Intl.PluralRules` による自動複数化はほとんどの言語で機能しますが、特殊なケースにはカスタムロジックが必要な場合があります。特定のロケールの自動動作をオーバーライドするために、`createI18n` オプションにオプションの `pluralRules` オブジェクトを渡すことができます
122155

123156
スラブ語派の言語(ロシア語、ウクライナ語など)のルールを使用した非常に単純化された例:
124157

docs/jp/guide/migration/breaking12.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,40 @@ v11 Legacy API では、`i18n.global` は `VueI18n` インスタンスを返し
507507

508508
`@intlify/vue-i18n/no-deprecated-v-t` ルールを使用して、コードベース内のすべての `v-t` の使用箇所を検出できます。
509509

510+
## デフォルトの複数形が `Intl.PluralRules` を使用するようになりました
511+
512+
**理由**: 以前のデフォルト複数形ルールは英語専用の単純な実装で、複雑な複数形カテゴリを持つ言語(ロシア語、アラビア語、ポーランド語など)を正しく処理できませんでした。Vue I18n v12 では、現在のロケールに基づいて正しい複数形を自動的に選択するために `Intl.PluralRules` を使用します。
513+
514+
### 変更点
515+
516+
- ロケールにカスタム `pluralRules` が設定されていない場合、Vue I18n は自動的に `Intl.PluralRules` を使用して正しい複数形カテゴリ(zero, one, two, few, many, other)を決定します
517+
- メッセージのケースは CLDR 複数形カテゴリの順序に従って並べる必要があります: `zero | one | two | few | many | other`(ロケールに存在するカテゴリのみ含む)
518+
- メッセージのケース数がロケールの複数形カテゴリ数を超える場合、Vue I18n は以前のデフォルトルールにフォールバックします
519+
- ランタイム環境で `Intl.PluralRules` が利用できない場合、Vue I18n は以前のデフォルトルールにフォールバックします
520+
521+
### 移行
522+
523+
カスタム `pluralRules` **なし**で英語以外のロケールの以前のデフォルトルールに依存していた場合、メッセージのケースをロケールの CLDR 複数形カテゴリの順序に合わせて並べ替える必要があります。
524+
525+
**以前 (v11) — カスタム `pluralRules` 付きのロシア語:**
526+
527+
変更不要。カスタム `pluralRules` が優先され、以前と同様に動作します。
528+
529+
**以後 (v12) — ロシア語(自動、カスタム `pluralRules` 不要):**
530+
531+
```js
532+
const i18n = createI18n({
533+
locale: 'ru',
534+
// pluralRules 不要 — Intl.PluralRules が自動的に処理します
535+
messages: {
536+
ru: {
537+
// 順序: one | few | many | other(ロシア語の CLDR 順序)
538+
car: '{n} машина | {n} машины | {n} машин | {n} машин'
539+
}
540+
}
541+
})
542+
```
543+
510544
## `MissingHandler` シグネチャの変更
511545

512546
**理由**: Vue 3.6+ では `getCurrentInstance()` API が非推奨になります。`MissingHandler` 型は以前、3 番目のパラメータとして `ComponentInternalInstance` を受け取っていましたが、これは使用できなくなりました。

docs/zh/guide/advanced/component.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,15 @@ const url = ref('/term')
5151
<template>
5252
<div id="app">
5353
<!-- ... -->
54-
<i18n-t keypath="term" tag="label" for="tos">
55-
<a :href="url" target="_blank">{{ t('tos') }}</a>
54+
<i18n-t
55+
keypath="term"
56+
tag="label"
57+
for="tos"
58+
>
59+
<a
60+
:href="url"
61+
target="_blank"
62+
>{{ t('tos') }}</a>
5663
</i18n-t>
5764
<!-- ... -->
5865
</div>
@@ -128,11 +135,14 @@ const refundLimit = ref(30)
128135
<template>
129136
<div id="app">
130137
<!-- ... -->
131-
<i18n-t keypath="info" tag="p">
132-
<template v-slot:limit>
138+
<i18n-t
139+
keypath="info"
140+
tag="p"
141+
>
142+
<template #limit>
133143
<span>{{ changeLimit }}</span>
134144
</template>
135-
<template v-slot:action>
145+
<template #action>
136146
<a :href="changeUrl">{{ t('change') }}</a>
137147
</template>
138148
</i18n-t>

0 commit comments

Comments
 (0)