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