虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Nuxt.js で多言語対応 ~ nuxt-i18n を導入~

こんにちは、虎の穴ラボのはっとりです。

つい最近、Nuxtを使用したアプリケーションで多言語化対応をしました。

Nuxtの多言語化ライブラリである nuxt-i18n の使い方を紹介します。

対応するアプリケーションの主なバージョン

  • Nuxt v2.x
  • TypeScript v3.9

コンポーネント部分は Vue.extend を使用した書き方を採用しています。

セットアップ

公式サイトに載っている方法通りです。

https://i18n.nuxtjs.org/setup/

  • nuxt-i18n を追加します
npm install nuxt-i18n
  • nuxt.config.jsに以下の設定を追加します。
export default {
  // ...
  modules: [
    '@nuxtjs/axios',
    // ...
+    'nuxt-i18n',
  ],
  // ...
+  i18n: {
+  },
  // ...
};

i18n 部分はもう少し詳細に設定するので後述します。

  • TypeScriptで使用するので tsconfig.json に以下の設定を追加します。
{
  // ...
  "compilerOptions": {
    // ...
    "paths": {
      "~/*": [
        "./*"
      ]
    },
    "types": [
      "@types/node",
      "@nuxtjs/axios",
+      "@nuxt/types",
+      "nuxt-i18n"
    ]
  },
  // ...
}

nuxt-i18n の詳しい設定

セットアップの説明の中で省略した nuxt.config.js の i18n 部分を詳しく設定していきます。

最終的にはこのようになります

export default {
  // ...
  i18n: {
    defaultLocale: 'ja',
    detectBrowserLanguage: {
      cookieDomain: process.env.COOKIE_DOMAIN || null,
    },
    langDir: 'locales/',
    lazy: true,
    locales: [
      { code: 'ja', iso: 'ja-JP', file: 'ja.json' },
      { code: 'en', iso: 'en-US', file: 'en.json' },
      { code: 'zh-cn', iso: 'zh-CN', file: 'zh-cn.json' },
      { code: 'zh-tw', iso: 'zh-TW', file: 'zh-tw.json' },
    ],
    strategy: 'no_prefix',
    vueI18n: {
      fallbackLocale: 'ja',
      formatFallbackMessages: true,
    },
  },
  // ...
};

解説しながら一つ一つ設定を追加していきます。

まず、対応する言語の一覧を追加します。
今回のアプリでは 日本語、英語、中国語(簡体字・繁体字)に対応するため locales を設定します。

※今回はSEOの機能を使わなかったのでisoは不要でしたがこのまま入れておきます。

    locales: [
      { code: 'ja', iso: 'ja-JP' },
      { code: 'en', iso: 'en-US' },
      { code: 'zh-cn', iso: 'zh-CN' },
      { code: 'zh-tw', iso: 'zh-TW' },
    ],

次に言語ファイルの場所を設定します。
各コンポーネントに言語情報を記載する方法も取れるのですが、
開発者自身が各言語に精通していないため、言語ごとに1つのファイルにまとめる方式を取ることにしました。
また、あとに出てくる lazy: true の設定をする場合にもこの構成にする必要があるようです。

今回は各言語ファイルを以下の場所に置くことにします。

locales/ja.json
locales/en.json
locales/zh-cn.json
locales/zh-tw.json

言語ファイルを使用するように設定を追加します。
langDir に言語ファイルを置くディレクトリ、
localesfile に 各言語ファイル名を追加します。

    langDir: 'locales/',
    locales: [
      { code: 'ja', iso: 'ja-JP', file: 'ja.json' },
      { code: 'en', iso: 'en-US', file: 'en.json' },
      { code: 'zh-cn', iso: 'zh-CN', file: 'zh-cn.json' },
      { code: 'zh-tw', iso: 'zh-TW', file: 'zh-tw.json' },
    ],

デフォルトの言語を決めます。日本語をデフォルトにするため 以下を追加します。

    defaultLocale: 'ja',

パスの戦略を決めます。デフォルトだと strategy: 'prefix_except_default' となり、
defaultLocale 以外の言語を選択すると /path/en/path のようになります。
SEO的にはその方が良いようなのですが
今回はパスを変えられない事情があり、どの言語でも同じパスになるように以下の設定を追加します。

    strategy: 'no_prefix',

対応する言語が多くなると言語ファイルだけでかなりのサイズになります。
大抵のユーザーは1言語分しか使わないので毎回全言語分ロードさせるのは無駄です。
以下の設定を入れると選択している言語のみロードされます。

    lazy: true,

次の設定は翻訳が一部だけ、間に合っていない場合などに使える設定です。
こちらは nuxt-i18n が使用している vue-18n の設定になります。

選択している言語の言語ファイルに、該当のキーの文言がない場合は日本語を使用する場合は以下を追加します。

    vueI18n: {
      fallbackLocale: 'ja',
    },

更に以下を追加すると
キーに埋め込み変数を使用して、かつ言語ファイルに該当キーが見つからない場合でも埋め込み変数が正しく処理されます。
詳しい解説はこちらにあります。
https://kazupon.github.io/vue-i18n/guide/fallback.html#fallback-interpolation

    vueI18n: {
      fallbackLocale: 'ja',
      formatFallbackMessages: true,
    },

同じサブドメインを持つサイトと言語の選択状態を共用したかったため、以下のようにcookieドメインの設定を入れます。

    detectBrowserLanguage: {
      cookieDomain: process.env.COOKIE_DOMAIN || null,
    },

言語の選択状態はcookieに保存されるので、COOKIE_DOMAIN=example.com としておけば、
aaa.example.com でも bbb.example.com でも両方で同じ言語を選択した状態にできます。

Cookieのドメインを環境変数にした場合、注意があります。
npm run generate (nuxt-ts generate) する時に 環境変数COOKIE_DOMAINを設定しておく必要があります。
cookieDomain の設定は バンドルされる js ファイルに含まれてしまうので実行時にCOOKIE_DOMAINを設定してもクライアントサイドでは使用されません。
(これに気が付かずにハマりました・・・)

以上です。

その他のオプションや各オプションの詳細については下記にあります。
(英語なので頑張って読まないといけませんが・・・)

https://i18n.nuxtjs.org/options-reference

実装部分

言語切替UI処理

言語を切り替えるUIを用意します。

<template>
  <div>
    <a @click.prevent="switchLocale('ja')" :class="{ active: $i18n.locale === 'ja' }">日本語</a>
    <a @click.prevent="switchLocale('en')" :class="{ active: $i18n.locale === 'en' }">English</a>
    <a @click.prevent="switchLocale('zh-cn')" :class="{ active: $i18n.locale === 'zh-cn' }">简体中文</a>
    <a @click.prevent="switchLocale('zh-tw')" :class="{ active: $i18n.locale === 'zh-tw' }">繁體中文</a>
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  methods: {
    switchLocale(localeCode: string) {
      this.$i18n.setLocale(localeCode);
    },
  },
});
</script>

$i18n.setLocale で言語の切り替え、 $i18n.locale で現在選択中の言語のcodeが取得できます。

言語切替部分・基本

まずは基本です。変更前はこちら。

<template>
  <div>
    こんにちは!
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  method: {
    sayHello() {
      alert('こんにちは!');
    },
  },
});

template 部分は $t メソッドを使用します。

<template>
  <div>
    {{ $t('こんにちは!') }}
  </div>
</template>

ja.json 、 en.jsonは以下のようにします。(省略しますがzh-cn.jsonやzh-tw.jsonも同じ様に設定します)

{
  "こんにちは!": "こんにちは!"
}
{
  "こんにちは!": "Hello!"
}

※キーには日本語(マルチバイト文字)も使用できます。

script 部分でも $t メソッドは使用できるのですが、戻り値が string型 ではありません。
これだとTypeScriptで扱う際に不便な場面が多いです。
$t の代わりに $tc を使うと string型 で扱えます。

<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  method: {
    sayHello() {
      alert(this.$tc('こんにちは!'));
    },
  },
});

$tc は template部分でも使用できます。

<template>
  <div>
    {{ $tc('こんにちは!') }}
  </div>
</template>

言語切替部分・埋め込み

次は埋め込みがあるメッセージです。変更前はこちら。

<template>
  <div>
    こんにちは、{{ userName }}さん
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  computed: {
    userName() {
      return '虎の穴ラボ',
    },
  },
  method: {
    sayHello() {
      alert(`こんにちは、${this.userName}さん`);
    },
  },
});

埋め込みが必要な場合は以下の様にします。

<template>
  <div>
    {{ $t('こんにちは、{name}さん', { name: userName }) }}
    <!-- $tc の場合 -->
    {{ $tc('こんにちは、{name}さん', 0, { name: userName }) }}
  </div>
</template>
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
  computed: {
    userName() {
      return '虎の穴ラボ',
    },
  },
  method: {
    sayHello() {
      alert(this.$tc('こんにちは、{name}さん', 0, { name: userName }));
    },
  },
});

言語ファイルはこの様になります。

{
  "こんにちは、{name}さん": "こんにちは、{name}さん"
}
{
  "こんにちは、{name}さん": "Hello, {name}."
}

$tc の場合、第2引数が必要になります。
これは何かというと同じ単語でも複数形などいくつかの表現ある言語などで
どれを使うかを選択させる場合に使用します。
1つしか表現がない場合はどんな数字を入れても同じです。

詳しい使い方はこちらを御覧ください。 https://kazupon.github.io/vue-i18n/guide/pluralization.html#accessing-the-number-via-the-pre-defined-argument

言語切替部分・HTML埋め込み

次は少しだけ複雑です。変更前はこちら。

※ここでは userLink はURL形式であること、内部リンクであることが保証されているとします。

<template>
  <div class="main">
    <a v-bind:href="userLink">{{ userName }}</a>さんにフォローされました。
  </div>
</template>

単純に $t や $tc を使った場合こうなります。

<template>
  <div class="main">
    {{ $t('message1-1') }}<a v-bind:href="userLink">{{ userName }}</a>{{ $t('message1-2') }}
  </div>
</template>
// ja.json
// {
//   "message1-1": "",
//   "message1-2": "さんにフォローされました。"
// }
// en.json
// {
//   "message1-1": "Followed by ",
//   "message1-2": "."
// }

メッセージキーを分割して前後に挿入します。
埋め込みが1つくらいならこれで良いかもしれませんが、2つ3つ埋め込み箇所があり、かつ埋め込み場所が言語により前後する場合には対応できません。

v-htmlを使うとメッセージキーを1つにまとめることができますが、
クロスサイトスクリプティング の危険性があります。

<template>
  <div class="main" v-html="$t('message1', { link: userLink, name: userName })" />
</template>
// ja.json
// {
//   "message1": "<a href=\"{link}\">{name}</a>さんにフォローされました。"
// }

この方法を使う場合は、 userName の値をサニタイズして使用する必要があります。

安全にHTMLを埋め込みたい場合は i18n コンポーネントを使用します。

<template>
  <i18n path="message1" class="main" tag="div">
    <template v-slot:name>
      <a v-bind:href="userLink">{{ userName }}</a>
    </template>
  </i18n>
</template>
// ja.json
// {
//   "message1": "{name}さんにフォローされました。"
// }

i18n コンポーネントを使うと柔軟な埋め込みが可能になります。

<template>
  <i18n path="message2" class="main" tag="div">
    <template v-slot:newline><br /></template>
    <template v-slot:name>{{ userName }}</template>
  </i18n>
  <!-- 表示される内容 -->
  <!--
  <div class="main">
    こんにちは、<br />虎の穴ラボさん<br />ようこそ!
  </div>
  -->
</template>
// ja.json
// {
//   "message2": "こんにちは、{newline}{name}さん{newline}ようこそ!"
// }

まとめ

Nuxt の多言語化ライブラリ nuxt-i18n の使い方を紹介しました。
今回は既存アプリケーションに後付したので置き換え量が多く大変でした。
多言語化をする場合はなるべく早い段階で、アプリケーションが大きくなりすぎる前に(できれば始めから)対応することをお勧めします。

P.S.

3/19(金)19:30~ 【オンライン】3/19 とらのあなエンジニア&マーケター採用説明会【地方勤務可能!!】 yumenosora.connpass.com

採用情報
募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です
お申し込みはこちら!
news.toranoana.jp

ToraLab.fmスタートしました!
メンバーによるPodcastを配信中!是非スキマ時間に聞いて頂けると嬉しいです。

anchor.fm

Twitterもフォローしてくださいね!
ツイッターでも随時情報発信をしています
twitter.com