今回達成すること
@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