【Vue.js】i18nを遅延読み込みして多言語サイトを軽量化する方法

【Vue.js】i18nを遅延読み込みして多言語サイトを軽量化する方法 Vue.js

多言語対応でボトルネックになりがちなのが「全言語分の辞書を初期バンドルに含めてしまう」ことです。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とパフォーマンスを両立できます。