今回達成すること
@nuxtjs/i18nを導入し、サイトを多言語化対応する設定を行います。
また、ルート名から翻訳したページタイトルを表示するメソッドも作成します。
i18nとは
アプリを多言語化するために使用されるモジュールで、@nuxtjs/i18nはNuxt用のi18nモジュールとなります。
i18nが用意しているメソッドに、JSONファイルのパスを渡せばそのパスの値が表示されます。
ja.json
{
  "pages": {
    "privacy": "プライバシーポリシー"
  }
}
en.json
{
  "pages": {
    "privacy": "Privacy Policy"
  }
}
i18nの$t()メソッドにJSONファイルのパスを渡す
console.log(this.$t('pages.privacy'))
// path => /privacy
=> 'プライバシーポリシー'
// path => /en/privacy
=> 'Privacy Policy'
@nuxtjs/i18nのインストールとセットアップ
@nuxtjs/i18nのインストールを行い、使用するのセットアップを行います。
インストール
まずはインストールしましょう。
% yarn add @nuxtjs/i18n
型の登録
@nuxtjs/i18nはデフォルトで型が用意されているので、
tsconfig.json
{
  ...
  "types": [
    ...
    // 追加
    "@nuxtjs/i18n"
  ]
}
これでNuxtの型に登録できました。
this.$i18nやthis.$t()を使用してもタイプエラーは出なくなります。
Nuxtコンテキストからはi18nで呼び出すことが可能です。
asyncData ({ i18n }: Context) {
  console.log(i18n)
}
セットアップ
@nuxtjs/i18nの初期設定を行います。
nuxt.config.js
// 追加
import localeJa from './locales/ja.json'
import localeEn from './locales/en.json'
export default {
  ...
  modules: [
    ...
    // 追加
    '@nuxtjs/i18n'
  ],
  // 以下、追加
  // Doc: https://i18n.nuxtjs.org/setup
  i18n: {
    /*
      Doc: https://i18n.nuxtjs.org/strategies/
      no_prefix:
        プレフィックスを付与しない
        Path: /privacy, $route.name: 'privacy'
      prefix_except_default:
        defaultLocaleの言語を除いた全てのルートにプレフィックスを追加
        Path: /privacy, $route.name: 'privacy___ja'
      prefix:
        全てのルートにプレフィックスを付与
        Path: /ja/privacy, $route.name: 'privacy___ja'
      prefix_and_default:
        defaultLocaleの言語を除いた全てのルートにプレフィックスを追加
        Path: /privacy, $route.name: privacy___ja___default
    */
    strategy: 'prefix_except_default',
    // デフォルトで使用する言語を指定
    defaultLocale: 'ja',
    // アプリが対応する言語を配列で指定
    locales: ['ja', 'en'],
    // Doc: https://kazupon.github.io/vue-i18n/api/#properties
    vueI18n: {
      // 翻訳対象のキーがない場合に参照(フォールバック)される言語
      fallbackLocale: 'ja',
      // true => フォールバック時に翻訳のキーが存在しない場合、警告を発生させる(default: false)
      silentFallbackWarn: true,
      // 翻訳データ
      messages: {
        ja: localeJa,
        en: localeEn
      }
    }
  },
  build: {
  }
}
- 
import localeJa from './locales/ja.json'... 翻訳言語を用意するJSONファイルをインポート。nuxt.config.js では、ファイルインポートの際に~を使用するとエラーになる。そこで.を使用している。
- 
messages: { en }... 翻訳化のため英語のロケールを追加している。
i18nのstrategyプロパティ
strategyプロパティには以下4つの値が指定でき、値によって生成されるパスとルート名が変わります。
| i18n.strategy | パス | $route.name | 
|---|---|---|
| no_prefix | /privacy | privacy | 
| prefix_except_default | /privacy | privacy___ja | 
| prefix | /ja/privacy | privacy___ja | 
| prefix_and_default | /privacy | privacy___ja___default | 
今回は2番目のprefix_except_defaultを指定しています。
これにより、
- 日本語の場合のパスは/privacy
- 英語の場合のパスは/en/privacy
となります。
翻訳ファイルを作成する
今回は下記2つの翻訳ファイルを用意します。
- 日本語に対応する => ja.json 
- 英語に対応する => en.json 
ルートディレクトリ直下に「locales」ディレクトリを作成し、
% mkdir locales && touch $_/{ja.json,en.json}
それぞれのファイルを編集します。
locales/ja.json
{
  "pages": {
    "privacy": "プライバシーポリシー"
  }
}
locales/en.json
{
  "pages": {
    "privacy": "Privacy Policy"
  }
}
これで@nuxtjs/i18nの設定は完了です。
linkTo()メソッドを書き換える
@nuxtjs/i18nによって、$route.nameに___jaが付与されるようになりました。
ここで作成したlinkTo()メソッドと、使用している型を書き換えます。
plugins/my-plugin.ts
// local typesの追加 & 書き換え
/*
  local types
*/
// コンテンツページのルート名
type Locale = 'ja' | 'en'
type CategorySlug<T extends Locale> = `categories-slug___${T}`
type PostSlug<T extends Locale> = `posts-category-slug___${T}`
type TagSlug<T extends Locale> = `tags-slug___${T}`
// 各ルート名の型をオブジェクト化
interface Slug {
  category: CategorySlug<'ja'> | CategorySlug<'en'>
  post: PostSlug<'ja'> | PostSlug<'en'>
  tag: TagSlug<'ja'> | TagSlug<'en'>
}
// LinkTo()のデフォルト戻り値
type LinkToDefaultName = Slug['category'] | Slug['tag'] | ''
interface LinkToDefault {
  name: LinkToDefaultName
  params: {
    slug: string
  }
}
// LinkTo()のblogPost戻り値
interface LinkToPost {
  name: Slug['post']
  params: {
    category: string
    slug: string
  }
}
/*
  class types
*/
export interface MyPluginInterface {
  ...
  // 追加
  contentfulRouteName (entryType: string): LinkToDefaultName
  linkTo (entry: BlogCategory | BlogPost | BlogTag): LinkToDefault | LinkToPost
}
class MyPlugin implements MyPluginInterface {
  error
  // 追加
  i18n
  constructor (ctx: Context) {
    this.error = ctx.error
    // 追加
    this.i18n = ctx.i18n
  }
  ...
  // 追加
  // Contentfulのコンテンツタイプからルート名を返す
  contentfulRouteName (entryType: string) {
    const categorySlug = `categories-slug___${this.i18n.locale}` as Slug['category']
    const tagSlug = `tags-slug___${this.i18n.locale}` as Slug['tag']
    switch (entryType) {
      case 'blogCategory': return categorySlug
      case 'blogTag': return tagSlug
      /* 動的ルートネームはここに追加する */
      default: return ''
    }
  }
  // 書き換え
  // 引数のリンクオブジェクトを返す
  linkTo (entry: BlogCategory | BlogPost | BlogTag) {
    const entryType: string = entry.sys.contentType.sys.id
    const slug: string = entry.fields.slug
    // blogPostPath: /posts/<category.fields.slug>/<post.fields.slug>
    if (entryType === 'blogPost') {
      const name = `posts-category-slug___${this.i18n.locale}` as Slug['post']
      const category: string = (entry as BlogPost).fields.category.fields.slug
      return { name, params: { category, slug } }
    }
    const name: LinkToDefaultName = this.contentfulRouteName(entryType)
    return { name, params: { slug } }
  }
}
- type CategorySlug<T extends Locale>... TypeScriptのジェネリクスを使用し、引数- Tにロケールを渡す方法を採用。- CategorySlug<'ja'>の場合、- categories-slug___jaの型に変換される。
- interface Slug {}... ルート名の型をオブジェクト化し、型を扱いやすくしている。
- as Slug['category']... JavaScriptの文字列展開- posts-category-slug___${this.i18n.locale}を使用すると強制的にstring型となる。そこで、値を宣言した後に- asを使用し、型を上書きしている。
- const category: string = (entry as BlogPost)...- const postを削除して、- ()内で型付けを行う方法に変更。
挙動の確認
Vueファイルで使用されているlinkTo()メソッドの挙動を確認してください。
マウスホーバーでリンクが表示されていれば成功です。
http://localhost:3000/tags

this.i18n.localeが返す値
this.i18n.localeは、現在のルートのロケールを返すi18nが用意しているプロパティです。
デフォルト値はdefaultLocaleで指定した値なので、'ja'が返ってきます。
パスが/en/privacyの場合は'en'が返ってきます。
console.log(this.$i18n.locale)
// /privacy
=> 'ja'
// /en/privacy
=> 'en'
翻訳したページタイトルを返すメソッドの作成
$route.nameで取得できるルート名(privacy___ja)を、
- 純粋なルート名とロケールに分けるメソッド、currentRoute()と
- 引数のルート名から翻訳したページタイトルを返すroutePageTitle()
を
plugins/my-plugin.ts
最終的なmy-plugin.tsは、このページ下部に置いています。
...
export interface MyPluginInterface {
  ...
  // 追加
  currentRoute (routeName: string): { name: string, locale: string }
  routePageTitle (routeName: string | null | undefined): string
}
class MyPlugin implements MyPluginInterface {
  ...
  // 以下、追加
  // route.nameからルート名とロケールを別々に返す
  currentRoute (routeName: string) {
    const splitTarget: string = '___'
    const [name, locale]: string[] = routeName.split(splitTarget)
    return { name, locale }
  }
  // 引数のルート名の翻訳後のページタイトルを返す
  // vue-i18nv9未満の場合t()はTranslateResult型が返るのでstringに変換する
  routePageTitle (routeName: string | null | undefined) {
    if (!routeName) {
      return ''
    }
    const translationPath: string =
      `pages.${this.currentRoute(routeName).name}`
    return String(this.i18n.t(translationPath))
  }
}
- currentRoute()- const [name, locale]...- split()で区切られた配列を、それぞれの変数に代入するJavaScriptの書き方。
 
- routePageTitle()- routeName: string | null | undefined... 引数に渡す- $route.nameのデフォルトの型。
- this.i18n.t()... 翻訳化を行う- t()メソッドは- TranslateResultの型を返す。string型を返した方がVueファイルで扱いやすくなるため、- String()でstring型に変換している。
 
プライバシーポリシーのタイトルを翻訳しよう
pageTitleを書き換えます。
pages/privacy.vue
@Component
export default class PrivacyPage extends Vue {
  // 書き換え
  pageTitle: string = this.$my.routePageTitle(this.$route.name)
  // pageTitle: string = 'Privacy Policy'
  ...
}
/privacy へアクセスしてみましょう。
日本語のタイトルが表示されました。

/en/privacyへアクセスしてみましょう。
翻訳されたタイトルが表示されました。

注意
localesディレクトリ以下のファイルは、Nuxtを再起動しないと変更がうまく読み込めない場合があります。翻訳が上手くいかない場合は、Nuxtを再起動してみてください。
本番環境の挙動も確認しよう
デプロイして本番環境の挙動も確認しておいてください。
% yarn netlify:deploy
generateコマンドが走ると、i18nによって/en/ルートも自動生成されます。

今回の作業は以上です。
ブランチをマージして、GitHubにpushしておきましょう。
% git add -A
% git commit -m "Add i18n module and routePageTitle method"
% git checkout master
% git merge <ブランチ名>
% git push
まとめ
このチャプターでは、以下のことを行いました。
- カテゴリーコンテンツページの作成
- ブログ記事コンテンツページの作成
- タグ一覧とタグコンテンツページの作成
- プライバシーポリシーページの作成
- i18nを使用したページタイトルの翻訳化(今ここ)
以上で、検索ページ以外の5つのページが作成できました。
| ページ名 | パス | pagesファイル | |
|---|---|---|---|
| 1 | カテゴリーコンテンツページ | /categories/<category.fields.slug> | /categories/_slug.vue | 
| 2 | ブログ記事コンテンツページ | /posts/<category.fields.slug>/<post.fields.slug> | /posts/_category/_slug.vue | 
| 3 | タグ一覧ページ | /tags | /tags/index.vue | 
| 4 | タグコンテンツページ | /tags/<tag.fields.slug> | /tags/_slug.vue | 
| 5 | プライバシーポリシーページ | /privacy | /privacy.vue | 
次回は?
次回から、検索ページを作成していきます。
検索にはNetlify functionsを使用して、サーバーサイドの検索機能を実装します。
お楽しみに。
最終的なmy-plugin.ts
plugins/myplugin.ts
// Plugin Doc: https://typescript.nuxtjs.org/cookbook/plugins/
import { Plugin, Context } from '@nuxt/types'
import { BlogCategory, BlogPost, BlogTag } from '~/store/types'
/*
  local types
*/
// コンテンツページのルート名
type Locale = 'ja' | 'en'
type CategorySlug<T extends Locale> = `categories-slug___${T}`
type PostSlug<T extends Locale> = `posts-category-slug___${T}`
type TagSlug<T extends Locale> = `tags-slug___${T}`
// 各ルート名の型をオブジェクト化
interface Slug {
  category: CategorySlug<'ja'> | CategorySlug<'en'>
  post: PostSlug<'ja'> | PostSlug<'en'>
  tag: TagSlug<'ja'> | TagSlug<'en'>
}
// LinkTo()のデフォルト戻り値
type LinkToDefaultName = Slug['category'] | Slug['tag'] | ''
interface LinkToDefault {
  name: LinkToDefaultName
  params: {
    slug: string
  }
}
// LinkTo()のblogPost戻り値
interface LinkToPost {
  name: Slug['post']
  params: {
    category: string
    slug: string
  }
}
/*
  class types
*/
export interface MyPluginInterface {
  errorHandler (statusCode: number): void
  errorMessage (statusCode: number): string
  contentfulRouteName (entryType: string): LinkToDefaultName
  linkTo (entry: BlogCategory | BlogPost | BlogTag): LinkToDefault | LinkToPost
  currentRoute (routeName: string): { name: string, locale: string }
  routePageTitle (routeName: string | null | undefined): string
}
class MyPlugin implements MyPluginInterface {
  error
  i18n
  constructor (ctx: Context) {
    this.error = ctx.error
    this.i18n = ctx.i18n
  }
  // 引数のエラーコードのエラーを発生させる
  errorHandler (statusCode: number) {
    this.error({
      statusCode,
      message: this.errorMessage(statusCode)
    })
  }
  // 引数のエラーコードによってエラーメッセージを切り替える
  errorMessage (statusCode: number) {
    switch (statusCode) {
      /* ここに出力したいメッセージを追加する */
      case 404: return 'This page could not be found'
      case 500: return 'Server error'
      default: return 'Error'
    }
  }
  // Contentfulのコンテンツタイプからルート名を返す
  contentfulRouteName (entryType: string) {
    const categorySlug = `categories-slug___${this.i18n.locale}` as Slug['category']
    const tagSlug = `tags-slug___${this.i18n.locale}` as Slug['tag']
    switch (entryType) {
      case 'blogCategory': return categorySlug
      case 'blogTag': return tagSlug
      /* 動的ルートネームはここに追加する */
      default: return ''
    }
  }
  // 引数のリンクオブジェクトを返す
  linkTo (entry: BlogCategory | BlogPost | BlogTag) {
    const entryType: string = entry.sys.contentType.sys.id
    const slug: string = entry.fields.slug
    // blogPostPath: /posts/<category.fields.slug>/<post.fields.slug>
    if (entryType === 'blogPost') {
      const name = `posts-category-slug___${this.i18n.locale}` as Slug['post']
      const category: string = (entry as BlogPost).fields.category.fields.slug
      return { name, params: { category, slug } }
    }
    const name: LinkToDefaultName = this.contentfulRouteName(entryType)
    return { name, params: { slug } }
  }
  // route.nameからルート名とロケールを別々に返す
  currentRoute (routeName: string) {
    const splitTarget: string = '___'
    const [name, locale]: string[] = routeName.split(splitTarget)
    return { name, locale }
  }
  // 引数のルート名の翻訳後のページタイトルを返す
  // vue-i18nv9未満の場合t()はTranslateResult型が返るのでstringに変換する
  routePageTitle (routeName: string | null | undefined) {
    if (!routeName) {
      return ''
    }
    const translationPath: string =
      `pages.${this.currentRoute(routeName).name}`
    return String(this.i18n.t(translationPath))
  }
}
const myPlugin: Plugin = (context: Context, inject) => {
  inject('my', new MyPlugin(context))
}
export default myPlugin