多言語対応でボトルネックになりがちなのが「全言語分の辞書を初期バンドルに含めてしまう」ことです。Vue 3 + vue-i18n(v9)では、言語ごとの辞書を動的インポートして必要になった時だけ読み込む構成にすることで、初期表示を軽量化できます。本記事では、i18n辞書の遅延読み込み(コードスプリッティング)と、言語切替時のスムーズなUX実装を解説します。
準備:vue-i18nの導入
npm i vue-i18n
基本構成:i18nインスタンスを作成(デフォルト言語のみ同梱)
初期バンドルには既定言語(例:ja)だけを含め、他言語(en, zh など)は後から読み込みます。
// src/plugins/i18n.ts
import { createI18n } from 'vue-i18n'
import ja from '@/locales/ja.json' // 既定言語のみ静的インポート
export const i18n = createI18n({
legacy: false,
locale: 'ja',
fallbackLocale: 'ja',
messages: { ja }
})
アプリ組み込み
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import { i18n } from './plugins/i18n'
createApp(App).use(i18n).mount('#app')
コア:言語を遅延読み込みするユーティリティ
言語切替時にだけ該当辞書を読み込み、i18nにセットします。読み込み済み判定とローカル保存も行います。
// src/utils/i18nLoader.ts
import { i18n } from '@/plugins/i18n'
// 読み込み済みロケールをキャッシュ
const loaded = new Set<string>(['ja'])
export async function setLocale(lang: string) {
const instance = i18n.global
if (!loaded.has(lang)) {
// Vite/webpack ともにチャンク分割される動的インポート
const messages = await import(
/* webpackChunkName: "locale-[request]" */
`@/locales/${lang}.json`
)
instance.setLocaleMessage(lang, messages.default || messages)
loaded.add(lang)
}
instance.locale.value = lang
localStorage.setItem('locale', lang)
}
UI:言語切替コンポーネント
<template>
<select :value="current" @change="onChange">
<option value="ja">日本語</option>
<option value="en">English</option>
<option value="zh">中文</option>
</select>
</template>
<script setup lang="ts">
import { computed, ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { setLocale } from '@/utils/i18nLoader'
const { locale } = useI18n()
const current = computed(() => locale.value)
onMounted(async () => {
const saved = localStorage.getItem('locale')
if (saved && saved !== locale.value) {
await setLocale(saved)
}
})
async function onChange(e: Event) {
const lang = (e.target as HTMLSelectElement).value
await setLocale(lang)
}
</script>
UX向上:切替中のローディング表示(Suspense不要で簡易実装)
切替処理は非同期なので「一瞬の空白」を避けるためのフラグを用意します。
<template>
<button @click="change('en')" :disabled="loading">
{{ loading ? $t('loading') : 'English' }}
</button>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { setLocale } from '@/utils/i18nLoader'
const loading = ref(false)
async function change(lang: string) {
loading.value = true
try { await setLocale(lang) } finally { loading.value = false }
}
</script>
ルーティングと連携:URLで言語を切り替える
/en/products
のように言語をパスで管理する場合、ナビゲーションガードで遅延読込します。
// src/router/guards/i18nGuard.ts
import type { Router } from 'vue-router'
import { setLocale } from '@/utils/i18nLoader'
export function registerI18nGuard(router: Router) {
router.beforeEach(async (to, _from, next) => {
const lang = (to.params.lang as string) || 'ja'
await setLocale(lang)
next()
})
}
プリフェッチ戦略:ユーザー操作を先読み
メニューを開いたタイミングで関連言語を先読みして体感速度を上げます(回線と端末に配慮)。
import { setLocale } from '@/utils/i18nLoader'
function onLangMenuHover() {
// 例:英語を先読み(連続呼び出し保護は適宜)
setLocale('en')
}
エッジケースとベストプラクティス
- フォールバック文言:未翻訳キーは
fallbackLocale
で既定言語に委譲。 - 安全なキー運用:辞書キーはネームスペース化(
home.title
等)で衝突回避。 - 巨大辞書の分割:ページ/ドメイン別にJSONを分割して更に遅延化。
- キャッシュ:HTTPキャッシュ + ハッシュ付きファイルで長期キャッシュを活用。
- SSR/Nuxt:サーバ側で該当言語を先読みし、クライアントはハイドレートのみ。
よくあるトラブル対策
- 「Cannot find module ‘@/locales/<lang>.json’」:ビルドが動的パスを解決できるよう、固定ディレクトリ直下に配置し、テンプレートリテラルの形を維持。
- 初回切替が重い:スケルトンやプリフェッチで体感改善。巨大辞書は機能別に分割。
- 一瞬既定言語が見える:切替中フラグでUIロック、または
v-cloak
的なCSSでチラつき抑制。
まとめ
vue-i18n の遅延読み込みは、初期バンドルの削減と体感速度の改善に直結します。既定言語のみ同梱し、言語切替時に動的インポート + キャッシュ、必要ならプリフェッチを組み合わせるだけで、運用しやすく高速な多言語サイトを構築できます。SSR(Nuxt)でも同じ考え方で、ルートの言語に合わせて辞書を事前ロードすれば、SEOとパフォーマンスを両立できます。