- Vue.js
vue-i18nの使い方|useI18nで多言語対応と言語切り替え
vue-i18nとは、Vueアプリの文言・日時・数値を言語ごとに出し分けるためのVue向け国際化ライブラリで、createI18nで作ったインスタンスをapp.useで登録し、コンポーネントではuseI18nが返すt(文言)・d(日時)・n(数値)を呼ぶだけで多言語対応ができます。 言語の切り替えは、useI18nが返すlocale(ref)に 'en' などを代入するだけで、画面全体が自動で描き直されます。
この記事では、vue-i18n 11.4.13 と Vue 3.5.43 で実際に動かした出力をもとに、最小構成、メッセージの書き方と出力の一覧表、言語ファイルの遅延ロード、初期言語の決め方、キーが無いときの警告文、TypeScriptでキーを検査する方法までを順に説明します。コードはそのまま貼って動く形にしてあります。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / vue 3.5.43 / vue-i18n 11.4.13 / Vite 8.3.2 / TypeScript 7.0.2 / jsdom 30.1.1 / Chromium 141(Playwright 1.56.0)。コンポーネントの出力は @vue/test-utils 2.5.1 と jsdom で、遅延ロードは Vite でビルドした成果物を Chromium で開いて確かめています。
vue-i18nの早見表
| やりたいこと | 書き方 | 出力例(ja) |
|---|---|---|
| インスタンスを作る | createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages }) |
― |
| アプリに登録する | createApp(App).use(i18n).mount('#app') |
― |
| コンポーネントで使う | const { t, d, n, locale } = useI18n() |
― |
| 文言を出す | t('title') |
タスク一覧 |
| 値を埋め込む(名前付き補間) | t('hello', { name: 'Sato' }) |
こんにちは、Satoさん |
| 件数で文言を変える(複数形) | t('item', 5) |
品物5件 |
| 日時を書式化する | d(date, 'short') |
2026年10月3日 |
| 数値・通貨を書式化する | n(1280, 'currency') |
¥1,280 |
| 言語を切り替える | locale.value = 'en' |
画面全体が英語に変わる |
| setupの外から切り替える | i18n.global.locale.value = 'en' |
同上 |
| 言語ファイルを後から足す | i18n.global.setLocaleMessage('en', messages) |
― |
| キーがあるか調べる | te('title') |
true |
| 読み込み済みの言語を調べる | i18n.global.availableLocales |
['ja'] |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「vue-i18nの使い方|useI18nで多言語対応と言語切り替え」
vue-i18n 11系で押さえておく3つの前提
最初に、現在のvue-i18nで知っておくと迷わない点を3つにまとめます。
| 前提 | 内容 | この記事での扱い |
|---|---|---|
| 2つのモードがある | Composition APIモード(useI18nを使う)と、Legacy APIモード(this.$i18nを使う従来の書き方) | Composition APIモードだけを使う |
| Legacy APIモードは11系で非推奨 | 公式の移行ガイド(Breaking Changes in v11)に、Legacy APIモードとv-tディレクティブは11系で非推奨、12系で取り除くと書かれている | 新しく書くコードは legacy: false にする |
| 既定値はまだlegacy | 11.4.13では、legacy を書かないとLegacy APIモードで起動し、警告が1つ出る |
legacy: false を必ず書く |
3つ目は実際に確かめた結果です。legacy を省いて createI18n を呼ぶと、次の警告が出て、モードは legacy になりました。
[intlify] Legacy API mode has been deprecated in v11. Use Composition API mode instead.
About how to use the Composition API mode, see https://vue-i18n.intlify.dev/guide/advanced/composition.html
mode(default)= legacy
mode(legacy:false)= composition
「useI18n を使うなら legacy: false を書く」と覚えておけば十分です。npmのタグを見ると、次の系列(12.0.0-alpha)がすでに公開されていて、Composition APIモードへ一本化していく流れが続いています。今から書くコードをuseI18nでそろえておくと、次の版へもそのまま進めます。
インストールと最小構成(createI18n → useI18n → t・d・n)
インストール
npm install vue-i18n
この記事の時点では vue-i18n 11.4.13 が入りました。入った版は npm ls vue-i18n で確かめられます。
3ファイルの最小構成
Viteで作ったVueプロジェクトを前提に、i18n.js(設定)・main.js(登録)・App.vue(利用) の3つに分けます。
// src/i18n.js
import { createI18n } from 'vue-i18n'
export const i18n = createI18n({
legacy: false, // Composition API モード
locale: 'ja', // 最初に表示する言語
fallbackLocale: 'en', // キーが無いときに探す言語
messages: {
ja: { title: 'タスク一覧', hello: 'こんにちは、{name}さん' },
en: { title: 'Task list', hello: 'Hello, {name}!' },
},
datetimeFormats: {
ja: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
en: { short: { year: 'numeric', month: 'short', day: 'numeric' } },
},
numberFormats: {
ja: { currency: { style: 'currency', currency: 'JPY' } },
en: { currency: { style: 'currency', currency: 'USD' } },
},
})
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import { i18n } from './i18n'
createApp(App).use(i18n).mount('#app')
<!-- src/App.vue -->
<script setup>
import { useI18n } from 'vue-i18n'
const { t, d, n, locale } = useI18n()
const date = new Date(2026, 9, 3)
</script>
<template>
<h1>{{ t('title') }}</h1>
<p>{{ t('hello', { name: 'Sato' }) }}</p>
<p>{{ d(date, 'short') }}</p>
<p>{{ n(1280, 'currency') }}</p>
<select v-model="locale">
<option value="ja">日本語</option>
<option value="en">English</option>
</select>
</template>
実行結果
同じ内容のコンポーネントを @vue/test-utils でマウントし、見出しと段落の文字を1行に並べて出力しました。1行目が初期表示、2行目がセレクトボックスで en を選んだあと、3行目が i18n.global.locale.value = 'ja' で戻したあとです。
タスク一覧 | こんにちは、Satoさん | 2026年10月3日 | ¥1,280
Task list | Hello, Sato! | Oct 3, 2026 | $1,280.00
タスク一覧 | こんにちは、Satoさん | 2026年10月3日 | ¥1,280
localeはrefなので、v-modelにそのままつなげます。 値を変えるだけで、t・d・n を使っている箇所がすべて描き直されます。再描画を自分で指示する必要はありません。
createI18nの主なオプション
| オプション | 役割 | 書かない場合 |
|---|---|---|
legacy |
false でComposition APIモード | Legacy APIモード(警告が出る) |
locale |
現在の言語 | 'en-US' |
fallbackLocale |
キーが無いときに探す言語 | locale と同じ値(別の言語は探さない) |
messages |
言語ごとの文言 | 空 |
datetimeFormats |
d で使う日時書式の名前と定義 | 名前付き書式は使えない |
numberFormats |
n で使う数値書式の名前と定義 | 名前付き書式は使えない |
missingWarn / fallbackWarn |
開発時の警告を出すかどうか | 出す |
missing |
キーが無いときに呼ばれる関数 | なし |
globalInjection |
テンプレートで $t などを使えるようにする |
使える |
useI18nが返す主なもの
| 名前 | 種類 | 用途 |
|---|---|---|
t |
関数 | 文言を取り出す(補間・複数形もこれ) |
d |
関数 | 日時を書式化する |
n |
関数 | 数値を書式化する |
locale |
ref | 現在の言語。代入で切り替え |
te |
関数 | キーがあるかどうかを true / false で返す |
tm |
関数 | メッセージの入れ子ごと(配列やオブジェクト)取り出す |
availableLocales |
配列 | 読み込み済みの言語 |
setLocaleMessage |
関数 | 言語のメッセージを登録する |
何も渡さずに useI18n() を呼ぶと、アプリ全体で共有するグローバルスコープが返ります。 そのため、どのコンポーネントで locale を変えても、アプリ全体の言語が変わります。
メッセージ定義→出力の一覧表(補間・複数形・日時・数値)
ここがこの記事の中心です。メッセージの書き方ごとに、呼び出し方と実際の出力を並べました。 すべて vue-i18n 11.4.13 で実行した結果です。
文言(t)の一覧
| 種類 | メッセージ定義 | 呼び出し | 出力 |
|---|---|---|---|
| 名前付き補間 | hello: 'こんにちは、{name}さん' |
t('hello', { name: 'Sato' }) |
こんにちは、Satoさん |
| リスト補間 | range: '{0}から{1}まで' |
t('range', [1, 5]) |
1から5まで |
| 入れ子のキー | nested: { save: '保存' } |
t('nested.save') |
保存 |
| 別のメッセージを参照 | app: 'タスク帳'、welcome: '@:app へようこそ' |
t('welcome') |
タスク帳 へようこそ |
| 参照+大文字化 | app: 'task book'、shout: '@.upper:app' |
t('shout') |
TASK BOOK |
| 記号をそのまま出す | "info{'@'}example.com" |
t('mail') |
info@example.com |
| 波かっこをそのまま出す | "{'{'}name{'}'}" |
t('mail') |
{name} |
| 既定の文言を渡す | (キーなし) | t('nothing', '(未設定)') |
(未設定) |
@ と波かっこはメッセージ構文で特別な意味を持つので、文字として出したいときは {'@'} のようにリテラル補間で囲みます。メールアドレスを文言に入れるときによく出会う箇所です。
複数形の一覧
複数形は、縦棒(パイプ)で区切って書き、t の2番目の引数に件数を渡します。
| メッセージ定義 | 呼び出し | 出力 |
|---|---|---|
item: '品物なし | 品物1件 | 品物{count}件' |
t('item', 0) |
品物なし |
| 同上 | t('item', 1) |
品物1件 |
| 同上 | t('item', 5) |
品物5件 |
| 同上 | t('item', { count: 12 }) |
品物12件 |
item: 'no items | one item | {count} items' |
t('item', 5) |
5 items |
apple: '{n} apple | {n} apples' |
t('apple', 1) |
1 apple |
| 同上 | t('apple', 3) |
3 apples |
区切りの数で選ばれ方が変わります。
| 区切った数 | 選ばれ方 |
|---|---|
| 2つ | 1のとき1番目、それ以外は2番目 |
| 3つ | 0のとき1番目、1のとき2番目、2以上は3番目 |
件数は、メッセージの中で {count} と {n} のどちらでも参照できます。日本語は単数・複数で形が変わりませんが、「0件のときだけ別の文言にしたい」場面で3つ区切りが役に立ちます。
日時(d)の一覧
日時の書式は、datetimeFormats に名前を付けて定義します。中身は JavaScript 標準の Intl.DateTimeFormat に渡すオプションと同じです。
datetimeFormats: {
ja: {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric' },
},
en: {
short: { year: 'numeric', month: 'short', day: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short', hour: 'numeric', minute: 'numeric' },
},
}
const date = new Date(2026, 9, 3, 14, 5) を渡したときの出力です。
| 呼び出し | ja の出力 | en の出力 |
|---|---|---|
d(date, 'short') |
2026年10月3日 | Oct 3, 2026 |
d(date, 'long') |
2026年10月3日(土) 14:05 | Sat, October 3, 2026 at 2:05 PM |
d(date, 'short', 'ja')(言語を指定) |
2026年10月3日 | 2026年10月3日 |
数値(n)の一覧
数値の書式は numberFormats に定義します。こちらは Intl.NumberFormat のオプションと同じです。
numberFormats: {
ja: {
currency: { style: 'currency', currency: 'JPY' },
decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent' },
},
en: {
currency: { style: 'currency', currency: 'USD' },
decimal: { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent' },
},
}
| 呼び出し | ja の出力 | en の出力 |
|---|---|---|
n(1234567.891) |
1,234,567.891 | 1,234,567.891 |
n(1280, 'currency') |
¥1,280 | $1,280.00 |
n(3.14159, 'decimal') |
3.14 | 3.14 |
n(0.256, 'percent') |
26% | 26% |
n(1280, { style: 'currency', currency: 'EUR' }) |
― | €1,280.00 |
n(1280, 'currency', 'ja')(言語を指定) |
¥1,280 | ¥1,280 |
n は表示の形を変えるだけで、金額の換算はしません。 1280 を currency で出すと、ja では ¥1,280、en では $1,280.00 になります。言語によって通貨単位を変えたい場合は、数値そのものもアプリ側で用意します。
また、d と n の書式名も言語ごとに定義が必要です。ja に short を定義して en に定義しなかった場合、en では書式が見つからず、後述の警告が出て空文字になります。
テンプレートでの書き方3通り
テンプレートから文言を出す方法は3つあります。
| 書き方 | 例 | 向いている場面 |
|---|---|---|
useI18nの t |
{{ t('title') }} |
基本。script setup内でも同じ関数を使える |
グローバルの $t |
{{ $t('title') }} |
useI18nを呼ばずにテンプレートだけで済ませたいとき |
i18n-t コンポーネント |
下のコード | 文の途中にリンクなどの要素を挟みたいとき |
$t・$d・$n
legacy: false でも、テンプレートでは $t・$d・$n がそのまま使えます(globalInjection が既定で有効なため)。
<template>
<h1>{{ $t('title') }}</h1>
<p>{{ $n(1234.5) }}</p>
</template>
<h1>タスク一覧</h1><p>1,234.5</p>
文の途中に要素を入れる(i18n-t)
「利用規約に同意します」の「利用規約」だけをリンクにしたい、という場面です。言語によって語順が変わるので、文を分割せず、メッセージ側に差し込み位置 {link} を書きます。
messages: {
ja: { agree: '{link}に同意します', terms: '利用規約' },
en: { agree: 'I agree to the {link}', terms: 'Terms' },
}
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
</script>
<template>
<i18n-t keypath="agree" tag="p">
<template #link>
<a href="/terms">{{ t('terms') }}</a>
</template>
</i18n-t>
</template>
出力(上が ja、下が en に切り替えたあと):
<p><a href="/terms">利用規約</a>に同意します</p>
<p>I agree to the <a href="/terms">Terms</a></p>
日本語ではリンクが文頭に、英語では文末に入りました。語順の違いをメッセージ側で吸収できるのがこの書き方の良いところです。
そのコンポーネントだけの文言(ローカルスコープ)
共通の文言とは別に、その部品だけで使う文言を持たせることもできます。
<script setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n({
useScope: 'local',
messages: {
ja: { caption: 'この部品だけの文言' },
en: { caption: 'Local text' },
},
})
</script>
<template>
<p>{{ t('caption') }}</p>
</template>
<p>この部品だけの文言</p>
| スコープ | 指定 | メッセージの置き場所 | 使いどころ |
|---|---|---|---|
| グローバル | useI18n() |
createI18n の messages | アプリ全体で使う文言。まずはこちら |
| ローカル | useI18n({ useScope: 'local', messages }) |
そのコンポーネント | 配布する部品など、文言を部品と一緒に持ち運びたいとき |
言語切り替えの作り方
セレクトボックスで切り替える
最小構成で見たとおり、locale を v-model につなぐだけで切り替えられます。
<script setup>
import { useI18n } from 'vue-i18n'
const { locale } = useI18n()
</script>
<template>
<select v-model="locale">
<option value="ja">日本語</option>
<option value="en">English</option>
</select>
</template>
setupの外(ルーターやストア)から切り替える
useI18n は setup の中でしか呼べません。ルーターのガードや通常の関数から切り替えるときは、createI18n で作ったインスタンスの global を使います。
import { i18n } from './i18n'
i18n.global.locale.value = 'en'
legacy: false のとき、i18n.global.locale は ref です。.value を付けて代入します。
切り替えと一緒にやっておくと良いこと
| すること | コード | 理由 |
|---|---|---|
| html の lang 属性を合わせる | document.documentElement.setAttribute('lang', locale) |
読み上げソフトや検索エンジンが言語を判別しやすくなる |
| 選んだ言語を保存する | localStorage.setItem('locale', locale) |
次に開いたときも同じ言語で表示できる |
| その言語のメッセージを読み込む | 次の章の setLocale | 使う言語だけを配信できる |
この3つを1つの関数にまとめたのが、次の章の setLocale です。
言語ファイルを遅延ロードする(動的import)
言語が増えると、全言語のメッセージを最初から配るのは通信量の面でもったいなくなります。最初の言語だけを含めておき、ほかの言語は選ばれたときに動的importで取りに行くようにします。考え方は公式ドキュメントの Lazy loading のページに沿っています。
ファイル構成
src/
├── i18n.js
├── main.js
├── App.vue
└── locales/
├── ja.json
├── en.json
└── fr.json
{ "title": "タスク一覧", "add": "追加" }
(en.json は { "title": "Task list", "add": "Add" }、fr.json は { "title": "Liste des tâches", "add": "Ajouter" } です。)
i18n.js(setLocaleつき)
// src/i18n.js
import { nextTick } from 'vue'
import { createI18n } from 'vue-i18n'
import ja from './locales/ja.json' // 最初の言語だけ静的に読み込む
export const SUPPORTED = ['ja', 'en', 'fr']
export const i18n = createI18n({
legacy: false,
locale: 'ja',
fallbackLocale: 'ja',
messages: { ja },
})
export async function setLocale(locale) {
if (!SUPPORTED.includes(locale)) return false
// まだ読み込んでいない言語だけ、動的 import で取りに行く
if (!i18n.global.availableLocales.includes(locale)) {
const messages = await import(`./locales/${locale}.json`)
i18n.global.setLocaleMessage(locale, messages.default)
}
i18n.global.locale.value = locale
document.documentElement.setAttribute('lang', locale)
localStorage.setItem('locale', locale)
await nextTick()
return true
}
<!-- src/App.vue -->
<script setup>
import { useI18n } from 'vue-i18n'
import { setLocale, SUPPORTED } from './i18n'
const { t, locale } = useI18n()
</script>
<template>
<h1>{{ t('title') }}</h1>
<button>{{ t('add') }}</button>
<select :value="locale" @change="setLocale($event.target.value)">
<option v-for="l in SUPPORTED" :key="l" :value="l">{{ l }}</option>
</select>
</template>
ここでは v-model ではなく、:value と @change に分けています。 v-model で locale を直接書き換えると、メッセージが届く前に言語だけが切り替わってしまうためです。setLocale の中で「読み込み→切り替え」の順に進めます。
ビルド結果:言語ごとにファイルが分かれる
npx vite build(Vite 8.3.2)の出力です。
dist/index.html 0.21 kB │ gzip: 0.17 kB
dist/assets/en-CX98zutB.js 0.08 kB │ gzip: 0.09 kB
dist/assets/fr-PPcmznbm.js 0.09 kB │ gzip: 0.10 kB
dist/assets/index-CRSsrpfu.js 121.73 kB │ gzip: 44.05 kB
en と fr が別ファイル(チャンク)になりました。 ja は静的importしているので index 側に含まれます。このときViteは次のメッセージも出します。ja.json が「動的importの対象でもあり、静的importもされている」ことを知らせる案内で、動作には影響しません。
[INEFFECTIVE_DYNAMIC_IMPORT] src/locales/ja.json is dynamically imported by src/i18n.js but also statically imported by src/i18n.js, dynamic import will not move module into another chunk.
ブラウザで確かめる:選んだときだけ取得される
ビルドした成果物を vite preview で配信し、Chromium 141 で開いて、取得されたJSファイルを記録しました。
初期表示 | h1 = タスク一覧 | button = 追加 | lang = ja | 取得したJS: ['index-CRSsrpfu.js']
en を選択 | h1 = Task list | button = Add | lang = en | 取得したJS: ['index-CRSsrpfu.js', 'en-CX98zutB.js']
fr を選択 | h1 = Liste des tâches | button = Ajouter | lang = fr | 取得したJS: ['index-CRSsrpfu.js', 'en-CX98zutB.js', 'fr-PPcmznbm.js']
en に戻す | h1 = Task list | button = Add | lang = en | 取得したJS: ['index-CRSsrpfu.js', 'en-CX98zutB.js', 'fr-PPcmznbm.js']
localStorage.locale = en
読み取れることは3つです。
- 初期表示では index だけが取得され、en・fr のファイルは取りに行かない
- en を選んだ時点で en のファイルが1回だけ取得される
- en に戻したときは再取得しない(availableLocales に入っているため)
遅延ロードの流れ
flowchart TD
A[言語を選ぶ] --> B{対応している言語か}
B -- いいえ --> X[何もしない false を返す]
B -- はい --> C{読み込み済みか}
C -- いいえ --> D[動的 import で取得]
D --> E[setLocaleMessage で登録]
C -- はい --> F[locale.value を変更]
E --> F
F --> G[html の lang と localStorage を更新]
静的に全部含める方法との使い分け
| 方法 | 向いている場合 | 気を付ける点 |
|---|---|---|
| 全言語を静的import | 言語が2〜3個で文言も少ない | 言語が増えるほど初期のJSが大きくなる |
| 動的importで遅延ロード | 言語が多い、文言が多い | 切り替えが非同期になるので、読み込み→切り替えの順を守る |
| 画面(ルート)単位でも分ける | 画面ごとの文言が多い | キーの置き場所の決まりをチームでそろえる |
初期言語を決める(localStorage と navigator.language)
初めて開いたときの言語は、「前回選んだ言語 → ブラウザの言語設定 → 既定の言語」の順で決めると自然です。
// src/detect.js
export const SUPPORTED = ['ja', 'en', 'fr']
export const DEFAULT_LOCALE = 'ja'
export function detectLocale() {
// 1. 前回選んだ言語(localStorage)
const saved = localStorage.getItem('locale')
if (saved && SUPPORTED.includes(saved)) return saved
// 2. ブラウザの言語設定(優先順の配列)。'en-US' は 'en' に丸めて探す
const candidates = navigator.languages?.length ? navigator.languages : [navigator.language]
for (const lang of candidates) {
const base = lang.toLowerCase().split('-')[0]
if (SUPPORTED.includes(base)) return base
}
// 3. どれにも当てはまらなければ既定の言語
return DEFAULT_LOCALE
}
localStorage と navigator.languages の値を入れ替えながら実行した結果です。
localStorage=null navigator.languages=["en-US","en"] => en
localStorage=null navigator.languages=["fr-CA","en"] => fr
localStorage=null navigator.languages=["de-DE","en-GB"] => en
localStorage=null navigator.languages=["zh-TW","ko"] => ja
localStorage=fr navigator.languages=["en-US"] => fr
localStorage=xx navigator.languages=["en-US"] => en
| 行 | 読み取れること |
|---|---|
| 1〜2行目 | 'en-US' や 'fr-CA' のような地域つきの値は、ハイフンの前だけを見て対応言語に当てはめる |
| 3行目 | 第1希望(de)に対応していなくても、第2希望(en-GB)で見つかる |
| 4行目 | どれにも対応していなければ既定の ja |
| 5行目 | 保存した言語があれば、ブラウザの設定より優先 |
| 6行目 | 保存値が対応言語に無い値(xx)なら読み飛ばして次へ進む |
navigator.language は第1希望の1つだけ、navigator.languages は優先順の配列です。配列のほうを見ると、3行目のように第2希望まで拾えます。
起動時に組み合わせる
遅延ロードと合わせる場合は、マウントの前に setLocale を待つと、最初の描画から目的の言語で表示できます。
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import { i18n, setLocale } from './i18n'
import { detectLocale } from './detect'
await setLocale(detectLocale())
createApp(App).use(i18n).mount('#app')
この main.js でビルドし、Chromium の言語設定を変えて開いた結果です。
fr-CA => Liste des tâches | lang = fr | languages = ['fr-CA']
en-US => Task list | lang = en | languages = ['en-US']
fr を選んで再読み込み => Liste des tâches
de-DE => タスク一覧 | lang = ja | languages = ['de-DE']
ブラウザがフランス語なら最初からフランス語で表示され、選び直した言語は再読み込みのあとも保たれました。 対応していないドイツ語の設定では、既定の日本語になります。
キーが無いときの警告文と対処
実際に出る警告
ja に無く en にだけあるキー(onlyEn)と、どちらにも無いキー(nothing)を呼んだ結果です。[warn] の行が console.warn に出た内容です。
(1) 既定の設定
[warn] [intlify] Not found 'onlyEn' key in 'ja' locale messages.
[warn] [intlify] Fall back to translate 'onlyEn' key with 'en' locale.
t('onlyEn') => English only
[warn] [intlify] Not found 'nothing' key in 'ja' locale messages.
[warn] [intlify] Fall back to translate 'nothing' key with 'en' locale.
[warn] [intlify] Not found 'nothing' key in 'en' locale messages.
t('nothing') => nothing
| 警告文 | 意味 | 対処 |
|---|---|---|
Not found 'xxx' key in 'ja' locale messages. |
ja のメッセージにそのキーが無い | ja にキーを足す。つづりを見直す |
Fall back to translate 'xxx' key with 'en' locale. |
fallbackLocale の en を探しに行った | ja 側の訳を足せば出なくなる |
Fall back to datetime format 'short' key with 'en' locale. |
d の書式名がその言語に無い | datetimeFormats に同じ名前を定義する |
どの言語にも無いキーは、キーの文字列そのもの(nothing)が画面に出ます。 画面に英字のキーがそのまま見えたら、まずつづりと言語ファイルを見直します。
警告を止める・記録する
// (2) 警告を出さない
createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages,
missingWarn: false, fallbackWarn: false })
// (3) 欠けているキーを自分で記録する
const missed = []
createI18n({ legacy: false, locale: 'ja', fallbackLocale: 'en', messages,
missing: (locale, key) => { missed.push(`${locale}:${key}`) } })
(2) missingWarn: false, fallbackWarn: false
t('onlyEn') => English only
t('nothing') => nothing
(3) missing ハンドラで記録する
[warn] [intlify] Fall back to translate 'nothing' key with 'en' locale.
t('nothing') => nothing
[warn] [intlify] Fall back to translate 'onlyEn' key with 'en' locale.
t('onlyEn') => English only
記録: ["ja:nothing","en:nothing","ja:onlyEn"]
missing を渡すと「Not found」の警告は出なくなり、代わりに自分の関数が呼ばれます。開発中に欠けているキーの一覧を集めたいときに便利です。
なお、NODE_ENV=production で同じスクリプトを実行すると、警告は1行も出ませんでした。出力(フォールバックした文言やキー文字列)と missing の記録は同じです。警告は開発時の手がかりで、公開用のビルドでは静かになります。
よく出会うエラー文
| エラー文 | 原因 | 直し方 |
|---|---|---|
SyntaxError: Must be called at the top of a setup function |
useI18n を setup の外(通常の関数やモジュール直下)で呼んだ | setup 内で呼ぶ。外では i18n.global を使う |
SyntaxError: Need to install with app.use function |
app.use(i18n) をしていない |
main.js で createApp(App).use(i18n) とする |
[intlify] Legacy API mode has been deprecated in v11. |
legacy: false を書いていない |
createI18n に legacy: false を足す |
[intlify] Not found parent scope. use the global scope. |
i18n-t などが親のスコープを探したが見つからず、グローバルを使った | 案内のメッセージ。グローバルの文言で良ければそのままで動く |
(1行目のエラー文では、setup の部分が実際にはバッククォートで囲まれて表示されます。)
型でキーを検査する(TypeScript)
TypeScriptでは、「言語ファイルどうしでキーがそろっているか」と「t に渡すキーが存在するか」の2つを検査できます。TypeScript 7.0.2 の tsc --noEmit で確かめました。
言語ファイルのキーのそろい具合を検査する
日本語の定義を型の正本にして、createI18n の型引数に渡します。書き方は公式ドキュメントの TypeScript Support のページに沿っています。
// src/locales/ja.ts
export default {
title: 'タスク一覧',
hello: 'こんにちは、{name}さん',
nested: { save: '保存' },
}
// src/i18n.ts
import { createI18n } from 'vue-i18n'
import ja from './locales/ja'
// 日本語の定義を「型の正本」にする
export type MessageSchema = typeof ja
export const i18n = createI18n<[MessageSchema], 'ja' | 'en'>({
legacy: false,
locale: 'ja',
fallbackLocale: 'en',
messages: {
ja,
en: {
title: 'Task list',
hello: 'Hello, {name}!',
// nested.save を書き忘れている
},
},
})
ts/i18n.ts(13,5): error TS2741: Property 'nested' is missing in type '{ title: string; hello: string; }' but required in type '{ title: string; hello: string; nested: { save: string; }; }'.
en に nested が無いことを、ビルド前に教えてくれます。 en に nested: { save: 'Save' } を足すとエラーは消えました。
t に渡すキーを検査する
ここは実際に試してわかった点です。上の設定をしたうえで i18n.global.t('titel')(つづりの誤り)や、型引数つきの useI18n が返す t に t('titel') を書いても、tsc はエラーを出しませんでした。 t の引数は任意の文字列も受け取れる型になっていて、型の情報は主にエディタでのキー補完に使われます。
呼び出し側でも検査したい場合は、キーの文字列の型を自分で作り、その型だけを受け取る薄い関数を用意します。
// src/keys.ts
import { i18n, type MessageSchema } from './i18n'
// 'title' | 'hello' | 'nested.save' のような、キーの文字列の型を作る
type Leaves<T, P extends string = ''> = {
[K in keyof T & string]: T[K] extends string ? `${P}${K}` : Leaves<T[K], `${P}${K}.`>
}[keyof T & string]
export type MessageKey = Leaves<MessageSchema>
// キーを MessageKey に絞った t
export function tk(key: MessageKey, named: Record<string, unknown> = {}): string {
return i18n.global.t(key, named)
}
tk('title')
tk('nested.save')
tk('hello', { name: 'Sato' })
tk('titel') // つづりの誤り
tk('nested.sav') // 入れ子のキーの誤り
ts/keys.ts(18,4): error TS2345: Argument of type '"titel"' is not assignable to parameter of type 'MessageKey'.
ts/keys.ts(19,4): error TS2345: Argument of type '"nested.sav"' is not assignable to parameter of type 'MessageKey'.
正しいキーの3行は通り、誤った2行だけがエラーになりました。 tk の中身は i18n.global.t を呼んでいるだけなので、言語を切り替えたときの再描画もそのまま働きます。
エディタの補完を効かせる(グローバルの型定義)
useI18n を呼ぶたびに型引数を書かなくて済むよう、vue-i18n の DefineLocaleMessage を拡張しておく方法もあります。
// src/i18n.d.ts
import type { MessageSchema } from './i18n'
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends MessageSchema {}
}
日時と数値の書式名には、同じ要領で DefineDateTimeFormat と DefineNumberFormat が用意されています。
型による検査のまとめ
| 検査したいこと | 方法 | tsc の結果 |
|---|---|---|
| 言語どうしでキーがそろっているか | createI18n の型引数に [MessageSchema] と言語名を渡す |
不足があれば TS2741 |
| t に渡すキーのつづり | 素の t('titel') |
エラーにならない(補完向け) |
| t に渡すキーのつづり | キーの型(MessageKey)を受け取る関数 tk を通す | 誤りがあれば TS2345 |
| 毎回の型引数を省く | DefineLocaleMessage を拡張する | ― |
従来の書き方からの置き換え表
以前のバージョンの記事やコードを読むときに役立つ対応表です。
| 従来の書き方 | 現在の書き方(Composition APIモード) |
|---|---|
new VueI18n({ ... })(Vue 2向けのv8) |
createI18n({ legacy: false, ... }) |
this.$i18n.locale = 'en' |
const { locale } = useI18n() → locale.value = 'en' |
this.$t('key') |
const { t } = useI18n() → t('key')(テンプレートでは $t も可) |
$tc('item', 5) |
t('item', 5)(公式の移行ガイドで tc と $tc は11系で取り除かれたと案内されている) |
v-t ディレクティブ |
{{ t('key') }}(v-t は11系で非推奨) |
切り替え後に $forceUpdate() |
不要。locale は ref なので自動で描き直される |
切り替えたのに画面が変わらないときは、再描画を指示するより先に、locale が同じインスタンスのものかを見直すと解決が早いです。たとえばローカルスコープの locale を変えても、グローバルスコープの文言は変わりません。アプリ全体の言語を変えるなら、引数なしの useI18n() か i18n.global を使います。
つまずきやすい点チェック表
| 症状 | 見直す箇所 |
|---|---|
| キーの文字列がそのまま画面に出る | キーのつづり、言語ファイルの読み込み、fallbackLocale の設定 |
| 切り替えても文言が変わらない | legacy: false か、.value に代入しているか、グローバルスコープか |
| 日付が空文字になる | その言語の datetimeFormats に書式名があるか |
| @ を含む文言でエラーになる | {'@'} のリテラル補間にしているか |
| 遅延ロードで一瞬キーが見える | 読み込み完了を待ってから locale を変えているか |
| 再読み込みで言語が戻る | localStorage への保存と、起動時の detectLocale |
| 起動時に Legacy の警告が出る | createI18n に legacy: false があるか |
よくある質問
Q. $t と useI18n の t は、どちらを使えばよいですか? A. どちらも同じグローバルの文言を返します。script setup の中でも文言を使う(タイトルの設定や通知など)なら useI18n の t に統一すると見通しが良くなります。テンプレートだけで済む小さな部品なら $t でも十分です。
Q. 言語を切り替えたあと、ページを再読み込みすると元に戻ります。 A. locale はメモリ上の値なので、再読み込みで初期値に戻ります。切り替え時に localStorage へ保存し、起動時に detectLocale のような関数で読み出して初期言語に使うと、同じ言語で表示が続きます。
Q. 遅延ロードの言語ファイルはJSONでないといけませんか? A. JSONでもJavaScript(export default のオブジェクト)でも動きます。この記事では、Node.js上の確認では .mjs、Viteのビルドでは .json を使い、どちらも setLocaleMessage で登録できました。翻訳の担当者とファイルをやり取りするなら、JSONのほうが扱いやすいことが多いです。
Q. 日時や数値の書式は、自分でライブラリを足す必要がありますか? A. 追加は不要です。d と n は JavaScript 標準の Intl.DateTimeFormat と Intl.NumberFormat を使うので、datetimeFormats と numberFormats にオプションを書くだけで言語に合った表記になります。
まとめ
- createI18n に
legacy: falseを書き、app.use で登録し、useI18n の t・d・n を使うのが現在の基本形。11.4.13 では legacy を省くと Legacy APIモードで起動し、非推奨の警告が出る - 言語の切り替えは
locale.value = 'en'。setup の外からはi18n.global.locale.value。再描画の指示は不要 - 補間は {name}、複数形は縦棒区切り、@ や波かっこはリテラル補間。日時と数値は datetimeFormats と numberFormats に名前付きで定義する
- 遅延ロードは「動的import → setLocaleMessage → locale 変更」の順。Viteのビルドで言語ごとのファイルに分かれ、選んだときに1回だけ取得されることを確かめた
- 初期言語は localStorage → navigator.languages → 既定の言語の順で決めると、前回の選択とブラウザの設定の両方を生かせる
- キーが無いと Not found と Fall back の警告が出て、キー文字列がそのまま表示される。missing ハンドラで一覧を集められる
- 型の検査は2段構え。言語どうしのキーのそろい具合は createI18n の型引数で、t に渡すキーのつづりはキーの型を受け取る関数で検査できる
参考
- Vue I18n 公式ドキュメント「Lazy loading」 https://vue-i18n.intlify.dev/guide/advanced/lazy.html
- Vue I18n 公式ドキュメント「TypeScript Support」 https://vue-i18n.intlify.dev/guide/advanced/typescript.html
- Vue I18n 公式ドキュメント「Breaking Changes in v11」 https://vue-i18n.intlify.dev/guide/migration/breaking11.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「vue-i18nの使い方|useI18nで多言語対応と言語切り替え」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。