@nuxtjs/i18nのインストールとセットアップ。ページタイトルの翻訳化【TypeScript】
  • 2022.02.11に公開
  • ブログ構築TS
  • 7. コンテンツページ構築
  • No.8 / 8

今回達成すること

@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に登録します。

tsconfig.json
{
  ...
  "types": [
    ...
    // 追加
    "@nuxtjs/i18n"
  ]
}

これでNuxtの型に登録できました。

this.$i18nthis.$t()を使用してもタイプエラーは出なくなります。

Nuxtコンテキストからはi18nで呼び出すことが可能です。

asyncData ({ i18n }: Context) {
  console.log(i18n)
}

セットアップ

nuxt.config.js@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」ディレクトリを作成し、ja.jsonen.jsonを作成します。

% 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

2022-02-10 19-49-43

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()

my-plugin.tsに作成します。

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型に変換している。

プライバシーポリシーのタイトルを翻訳しよう

privacy.vuepageTitleを書き換えます。

pages/privacy.vue
@Component
export default class PrivacyPage extends Vue {
  // 書き換え
  pageTitle: string = this.$my.routePageTitle(this.$route.name)
  // pageTitle: string = 'Privacy Policy'
  ...
}

/privacy へアクセスしてみましょう。

日本語のタイトルが表示されました。

2022-02-10 21-37-06

/en/privacyへアクセスしてみましょう。

翻訳されたタイトルが表示されました。

2022-02-10 21-38-30

注意

localesディレクトリ以下のファイルは、Nuxtを再起動しないと変更がうまく読み込めない場合があります。翻訳が上手くいかない場合は、Nuxtを再起動してみてください。

本番環境の挙動も確認しよう

デプロイして本番環境の挙動も確認しておいてください。

% yarn netlify:deploy

generateコマンドが走ると、i18nによって/en/ルートも自動生成されます。

2022-02-11 10-55-20

今回の作業は以上です。

ブランチをマージして、GitHubにpushしておきましょう。

% git add -A
% git commit -m "Add i18n module and routePageTitle method"
% git checkout master
% git merge <ブランチ名>
% git push

まとめ

このチャプターでは、以下のことを行いました。

以上で、検索ページ以外の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
あなたの力になれること
私自身が独学でプログラミングを勉強してきたので、一人で学び続ける苦しみは痛いほど分かります。そこで、当時の私がこんなのあったら良いのにな、と思っていたサービスを立ち上げました。周りに質問できる人がいない、答えの調べ方が分からない、ここを聞きたいだけなのにスクールは高額すぎる。そんな方に向けた単発・短期間メンターサービスを行っています。
独学プログラマのサービス
ブログ構築TSの投稿
1
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #01
Nuxt.jsをローカルPCに立ち上げよう
2
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #02
Nuxt.jsプロジェクトをGitHubにPushしよう
3
  • Nuxt.js×TypeScript開発環境構築
  • /
  • #03
nuxt-property-decoratorのインストールとTypeScriptのセットアップ
1
  • Vuetifyセットアップ
  • /
  • #01
TypeScript環境のNuxt.jsにVuetifyを導入しよう
2
  • Vuetifyセットアップ
  • /
  • #02
VuetifyにカスタムCSSを追加してSASS変数を理解しよう
3
  • Vuetifyセットアップ
  • /
  • #03
VuetifyにカスタムSVGアイコンを追加しよう
1
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #01
Netlify CLIをインストールして本番環境のサイトを作成しよう
2
  • NetlifyCLIを使ったNuxtデプロイ
  • /
  • #02
netlify.tomlを使ってNuxt.jsをNetlifyに手動デプロイしよう
1
  • Contentfulモデル構築
  • /
  • #01
Contentfulの料金とCommunityプランの無料枠を理解する
2
  • Contentfulモデル構築
  • /
  • #02
Contentfulへ新規会員登録、ロケールの変更、API Keyの発行を行う
3
  • Contentfulモデル構築
  • /
  • #03
Contentful ブログカテゴリーモデルを作成しよう
4
  • Contentfulモデル構築
  • /
  • #04
Contentful カテゴリーモデルに1対1で関連づくblogPostモデルを作成しよう
5
  • Contentfulモデル構築
  • /
  • #05
Contentful ブログ記事に1対多で関連づくplogTagモデルを作成しよう
6
  • Contentfulモデル構築
  • /
  • #06
Contentful カテゴリー・ブログ記事・タグコンテンツを作成しよう
1
  • Nuxt.js×Contentfulセットアップ
  • /
  • #01
Nuxt.js×Contentfulセットアップ。モジュールのインストールからAPI Keyの登録まで
2
  • Nuxt.js×Contentfulセットアップ
  • /
  • #02
Contentful APIリクエストの実行 Nuxt.jsにブログコンテンツを表示しよう
3
  • Nuxt.js×Contentfulセットアップ
  • /
  • #03
ContentfulAPIをNetlifyにデプロイしよう【Nuxt FullStaticのasyncDataとfetch】
1
  • Vuex×TypeScriptセットアップ
  • /
  • #01
Vuexの型付け vuex-module-decoratorsとnuxt-typed-vuexどちらを使用するか
2
  • Vuex×TypeScriptセットアップ
  • /
  • #02
nuxt-typed-vuexのインストールとセットアップ。Vuexの型定義と呼び出し方
3
  • Vuex×TypeScriptセットアップ
  • /
  • #03
VuexにContentfulの型定義ファイルとnuxtServerInitを追加しよう
4
  • Vuex×TypeScriptセットアップ
  • /
  • #04
VuexにContentfulAPIレスポンスを保存してVueファイルに表示しよう
1
  • コンテンツページ構築
  • /
  • #01
ブログアプリのページ設計とNuxt.jsの動的ルーティングについて理解しよう
2
  • コンテンツページ構築
  • /
  • #02
カテゴリーのコンテンツページを作成しよう【Nuxt.js×Contentful】
3
  • コンテンツページ構築
  • /
  • #03
カテゴリーに関連付くブログ記事一覧を表示しよう【Nuxt.js×Contentful】
4
  • コンテンツページ構築
  • /
  • #04
injectを使用して共通エラー処理メソッドを作成しよう【Nuxt×TypeScript】
5
  • コンテンツページ構築
  • /
  • #05
NuxtChildを使用してブログ記事ページを作成しよう【Nuxt.js×TypeScript】
6
  • コンテンツページ構築
  • /
  • #06
タグ一覧ページとタグ関連づく記事一覧を表示しよう【Nuxt.js×TypeScript】
7
  • コンテンツページ構築
  • /
  • #07
プライバシーポリシーページを作成しよう【Nuxt.js×TypeScript】
8
  • コンテンツページ構築
  • /
  • #08
@nuxtjs/i18nのインストールとセットアップ。ページタイトルの翻訳化【TypeScript】
1
  • NetlifyFunctionsを使った検索機能
  • /
  • #01
Netlify Functionsを使ってクエリを返す関数を作成しよう【Nuxt.js×TypeScript】
2
  • NetlifyFunctionsを使った検索機能
  • /
  • #02
Netlify Functionsプロジェクトをデプロイしよう【Nuxt.js×TypeScript】
3
  • NetlifyFunctionsを使った検索機能
  • /
  • #03
Nuxt.js × axiosセットアップ Netlify Functionsにリクエストを行う準備をしよう
4
  • NetlifyFunctionsを使った検索機能
  • /
  • #04
オリジン•CORS•プリフライトリクエストを理解する【Nuxt.js×Netlify Functions】
5
  • NetlifyFunctionsを使った検索機能
  • /
  • #05
Netlify Functionsを使ってフォームバリデーション機能を構築しよう【Nuxt.js】
6
  • NetlifyFunctionsを使った検索機能
  • /
  • #06
ツールバーに表示する検索フォームを作成しよう【Nuxt.js×TypeScript】
7
  • NetlifyFunctionsを使った検索機能
  • /
  • #07
検索ページを作成しよう【Vue propsとTypeScriptの書き方 解説】
8
  • NetlifyFunctionsを使った検索機能
  • /
  • #08
Netlify FunctionsからContentfulAPIリクエストを送ろう【Nuxt.js】
9
  • NetlifyFunctionsを使った検索機能
  • /
  • #09
検索ページに「もっと見る」ボタンを実装しよう【Nuxt.js×TypeScript】
1
  • ブログMarkdown対応
  • /
  • #01
@nuxtjs/markdownitのインストールとセットアップ【Nuxt.js×TypeScript】
2
  • ブログMarkdown対応
  • /
  • #02
Nuxt.js×markdown-it 外部リンクを別タブで開くプラグインを追加しよう
3
  • ブログMarkdown対応
  • /
  • #03
Nuxt.js×markdown-it 内部リンクをVueRouterで高速にページ遷移しよう
4
  • ブログMarkdown対応
  • /
  • #04
Nuxt.js×markdown-it アンカーリンクとブログ目次を自動生成しよう
独学プログラマ
独学でも、ここまでできるってよ。
CONTACT
Nuxt.js制作のご依頼は下記メールアドレスまでお送りください。