POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook
Átila Fassina

本記事は、原著者の許諾のもとに翻訳・掲載しております。

クイックサマリー ‐ 国際化ルーティングは、厳密にはNext.jsの新機能ではありません。(v.10以降搭載されています。)この記事では、この機能のメリットだけではなく、こうした機能を利用して最高のユーザ体験と円滑な開発者体験を実現する方法についても見ていきます。自己文書化コードやバンドルサイズの削減、さらにはランタイムエラーではなくコンパイル時エラーに興味のある方は、是非このまま読み進めてください。 開発中のアプリにおいて、ロケール(または国、あるいは両方)ごとにルートを設定したい場合、Next.jsで簡単に対応できるようになりました。プロジェクトのrootディレクトリにnext.config.jsがない場合、新たに作成してください。このスニペットからコピーしても構いません。

/** @type {import('next').NextConfig} */
 
module.exports = {
  reactStrictMode: true,
  i18n: {
    locales: ['en', 'gc'],
    defaultLocale: 'en',
  }
}

注記:1行目はTSサーバ(TypeScriptプロジェクトに参画している場合、またはVSCodeを使用している場合)を許可するためのもので、構成オブジェクトでサポートされる属性です。必須ではありませんが良い機能です。

i18nオブジェクトの中にプロパティが2つあります。

  • locales 開発中のアプリがサポートする全ロケールのリスト。文字列の配列。

  • defaultLocale メインrootのロケール。ユーザ設定がない場合やrootが強制的に設定される場合のデフォルト設定。

これらのプロパティ値がルートを決定するため、あまり凝りすぎない方が良いでしょう。ロケールコード国コードを使用して有効な属性値を作成し、すぐにurlが作成されるため小文字を使用しましょう。

アプリが複数のロケールに対応したら、Next.jsで認識しておくべきことが最後に1つあります。全てのロケールに全てのルートが存在し、これらが同一であることをフレームワークは認識しています。特定のロケールに移動したい場合、Linkコンポーネントにlocale propsを設定する必要があります。これを行わないと、ブラウザのAccept-Languageヘッダに基づきデフォルトに戻ります。

<Link href="/" locale="de"><a>Home page in German</a></Link>

最終的には、ユーザが選択したロケールに従い、適切なルートに転送するアンカーを記述するのが良いでしょう。これは、Next.jsからuseRouterカスタムフックを使用することで容易に行うことができ、objectが返され、選択したlocalekeyとなります。

import type { FC } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'

const Anchor: FC<{ href: string }> = ({ href, children }) => {
  const { locale } = useRouter()

  return (
    <Link href={href} locale={locale}>
      <a>{children}</a>
    </Link>
  )
}

Next.jsはこれで国際化の準備が整い、以下を行うようになります。

  • Next.jsの機能を使用し、リクエストのAccepted-Languagesヘッダからユーザが指定したロケールを取得する
  • 上で作成したAnchorコンポーネントを使用し、ユーザが指定したルートに常にユーザを転送する
  • 必要に応じてデフォルト言語に戻る

最後に行うべきことは、翻訳を確実に扱えるようにすることです。現時点では、ルーティングは問題なく機能しているものの、各ページのコンテンツを調整する方法がありません。

ディクショナリの作成

翻訳管理サービスを使用している場合でも、他の方法でテキストを入手している場合でも、最終的に欲しいのはJavaScriptがランタイム中に処理するJSONオブジェクトです。Next.jsは3つの異なるランタイムを提供します。

  • クライアントサイド
  • サーバサイド
  • コンパイル時

これについては一旦置いておきましょう。まずはデータを構造化する必要があります。

翻訳の対象となるデータは、周辺のツールに応じてさまざまな形態を取りますが、最終的にはロケール、キー、値に絞られます。したがって、まずはこれらから見ていきます。筆者のロケールは、英語のenとポルトガル語のptです。

module.exports = {
  en: {
    hello: 'hello world'
  },
  pt: {
    hello: 'oi mundo'
  }
}

翻訳カスタムフック

ロケールが決まったら、次は翻訳カスタムフックを作成します。

import { useRouter } from 'next/router'
import dictionary from './dictionary'

export const useTranslation = () => {
  const { locales = [], defaultLocale, ...nextRouter} = useRouter()
  const locale = locales.includes(nextRouter.locale || '')
    ? nextRouter.locale
    : defaultLocale
  
  return {
    translate: (term) => {
      const translation = dictionary[locale][term]

      return Boolean(translation) ? translation : term
    }
  }
}

上の内容を細かく見ていきましょう。

  1. useRouterは、利用可能な全ロケール、デフォルトのロケール、現在のロケールを取得するために使用します。
  2. これらを取得したら、有効なロケールがあるかを確認し、なければデフォルトのロケールにフォールバックします。
  3. 次に、translateメソッドを返します。termとディクショナリからのフェッチを指定されたロケールに届けます。値がない場合、再度翻訳termを返します。

これで、Next.jsアプリは少なくとも一般的で初歩的なケースを翻訳する準備が整いました。ここでは完璧な翻訳ライブラリを構築しようとしているわけではありません。我々のカスタムフックには、挿入句、複数形、性別などの重要な要素が多数欠けています。

スケーリングの時間

カスタムフックの要素の欠如は、直ちに必要でなければ許容できます。実際に必要になってから実装することはいつでも可能であり、その方が良い場合もあります。しかし、現行の戦略において懸念される根本的問題が1つあります。それは、Next.jsの同型要素を活用していないことです。

ローカライズされたアプリの規模拡大において最も好ましくないのは、翻訳作業そのものを管理しないことです。これまでに何度も起こっていることなので、ある程度は予測できます。問題は、ブラウザに送信される無数のディクショナリの肥大化に対処することであり、ディクショナリはアプリで対応が求められる言語数とともに増えていく一方です。これは、エンドユーザにとって使い物にならないデータとなることが多く、言語を切り替える際に新しいキーや値をフェッチする必要がある場合にパフォーマンスに影響を及ぼします。ユーザ体験について1つの大きな真理があるとしたら、それは、ユーザの行動を全て予期することはできないということです。

ユーザが言語を切り替えるのか、いつ切り替えるのか、追加のキーを必要とするのか、いつ必要なのかを予想することはできません。理想を言えば、特定のルートにデータが読み込まれたときに、アプリに全ての翻訳が用意されていることがベストです。現時点では、ページのレンダリング結果と状態の考え得るバリエーションに基づき、ディクショナリのチャンクを分割する必要があります。非常に気の遠くなるような話です。

サーバサイド・プリレンダリング

では、拡張性に関する新規要件を整理しましょう。

  1. クライアントサイドに送るデータは最小限に留める
  2. ユーザインタラクションに基づく追加のリクエストは避ける
  3. 既に翻訳済みの最初のレンダリングをユーザに送る

Next.jsページのgetStaticPropsメソッドのお陰で、コンパイラの構成に一切踏み込むことなくこれを実現できます。この特殊なサーバレス関数にディクショナリ全体をインポートし、各キーの翻訳を保持する特殊オブジェクトのリストをページに送ります。

SSR翻訳の設定

アプリに話を戻しますが、新しいメソッドを作成します。/utils/helpersといったディレクトリを設定し、その中のどこかに以下を置きます。

export function ssrI18n(key, dictionary) {
  return Object.keys(dictionary)
    .reduce((keySet, locale) => {
      keySet[locale] = (dictionary[locale as keyof typeof dictionary][key])
      return keySet
    , {})
}

細かく見ていきましょう。

  1. 翻訳keyまたはtermdictionaryを取得する
  2. dictionaryオブジェクトをそのkeyの配列に変換する
  3. ディクショナリの各キーはlocaleであるため、keyの名前を用いてオブジェクトを作成し、各localeはその特定の言語の値となります。

そのメソッドの出力例は以下のような形になります。

{
  'hello': {
    'en': 'Hello World',
    'pt': 'Oi Mundo',
    'de': 'Hallo Welt'
  }
}

では、Next.jsページに移動しましょう。

import { ssrI18n } from '../utils/ssrI18n'
import { DICTIONARY } from '../dictionary'
import { useRouter } from 'next/router'

const Home = ({ hello }) => {
  const router = useRouter()
  const i18nLocale = getLocale(router)

  return (
    <h1 className={styles.title}>
      {hello[i18nLocale]}
    </h1>
  )
}

export const getStaticProps = async () => ({
  props: {
    hello: ssrI18n('hello', DICTIONARY),
    // add another entry to each translation key
  }
})

これで終わりです!ページには各言語で必要な翻訳しか送られません。逆に、途中で言語を切り替えても外部にリクエストは送られないため、非常に迅速です。

設定を全て省略する

このままでも十分素晴らしいですが、まだまだ改善の余地があります。開発者の皆さん注目です。ブートストラップが多く使われているにもかかわらず、依然としてタイプミスがないことを前提としています。翻訳されたアプリを使用したことのある人なら、タイプミスを含むキーがどこかに紛れ込んでいるものだということをご存じでしょう。したがって、TypeScriptによる安全なタイピングを翻訳手法にもたらすことができます。 この設定を省略し、TypeScriptの安全性と自動補完を手に入れるには、next-g11nを使用できます。これは、上記で行ったのと全く同じことを行う小さなライブラリですが、型といくつかのオプション機能が追加されます。

まとめ

この記事が、Next.jsの国際化ルーティングがグローバル化に向けてアプリにどのようなメリットをもたらし、現在のWebにおけるローカライズされたアプリで最高のユーザ体験を提供するということが何を意味するのかについて、知見を深める一助となれば幸いです。是非以下のコメント欄からご意見やご感想をお聞かせください。ツイートも歓迎いたします。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。