Todoアプリで多言語化&タイムーゾン対応やってみた
はじめに
以前、既存プロダクトの多言語化とタイムゾーン対応を行うプロジェクトに参画していましたが、色々と大変で心が折れそうになりました。そこで、改めて小さなアプリでベストプラクティスを模索しようと思い、多言語化&タイムゾーン対応したTodoアプリを作ってみました。
対象読者
- 多言語化に興味がある方
- タイムゾーン対応に興味がある方
技術スタック
- Next.js: 14.2.5
- React: 18.3.1
- Zustand: 4.5.4
- i18next: 23.11.5
- next-i18next: 15.3.0
- react-i18next: 14.1.2
- @formkit/tempo: 0.1.2
リポジトリ
今回紹介するコードは公開しています。
前提
今回のアプリで使用した言語とタイムゾーンは以下の通りです。
※OSやブラウザで設定されている言語とタイムゾーンは考慮していません。
- 言語
- 日本語
- 英語
- スペイン語
- タイムゾーン
- Asia/Tokyo(+9:00)
- Europe/London
- Asia/Yangon(+6:30)
- Pacific/Chatham(+12:45)
多言語化
多言語化にはi18nextというライブラリを使用しました。検索したらこのライブラリの内容ばかりだったので、主流のライブラリだと思います。
The easiest way to translate your Next.js apps (with pages setup).
App Routerにも対応している記事はありましたが、READMEはPage Routerだったので今回はPage Routerで実装しています。
useTranslation
import { useTranslation } from 'next-i18next';
const { t, i18n: { language } } = useTranslation();
useTranslation
というhooksを使用して、翻訳するためのt
メソッドと現在の言語であるlanguage
を取得します。
t
<button>{t('登録')}</button>
public配下に配置するjsonのkeyをt
メソッドの引数に渡してあげます。
そうすることで言語に応じて表示文字列が切り替わります。
下記のようなjsonを用意していた場合、英語を選択しているとRegister
、スペイン語だとRegistrar
、日本語だと登録
と画面に表示されます。
jsonをネストさせたりkeyを英語にしても良いんですが、コンポーネント上でt('Register')
と書いてあるよりt('登録')
のほうが圧倒的に読みやすいので、日本語をkeyにして運用するのが良さそうです。(こうやってやると良いってどこかの記事で読んだんですが見つけられなかったです。)
public/locales/en/common.json
{
"登録": "Register"
}
public/locales/es/common.json
{
"登録": "Registrar"
}
public/locales/ja/common.json
{
"登録": "登録"
}
language
現在選択されている言語が格納されている変数です。
次の項目で説明するタイムゾーン対応で日時をフォーマットするときにこの変数を渡して、各言語で一般的な表記で表示できるようにしています。
言語切替
useTranslation hookにchangeLanguage
というメソッドが提供されています。このメソッドを使用して言語切り替えを実装するとlanguage
の値は変更されましたが、画面上の文字列が変更されませんでした。
公式が提供しているサンプルコードを見るとrouterで言語切替を行うことがわかり、実装を変更しました。
const router = useRouter();
function changeLanguage(locale: string) {
const { pathname, asPath, query } = router;
router.push({ pathname, query }, asPath, { locale });
}
この実装で英語やスペイン語に切り替えると、URLに/en
や/es
が付き、画面の文字列もちゃんと翻訳されました。
タイムゾーン対応
タイムゾーン対応にはTempoという日付操作ライブラリを使用しました。選定理由は、まだバージョン0で今年リリースされたばかりのライブラリだからです。新しくリリースされたものは試してみたくなりますよね!
— including first-class support for timezone operations.
ドキュメントにもタイムゾーン対応が明記されており、今回の用途に合っていました。
主に使用したメソッドは以下の2つです。
format
tzDate
format
function format(options: FormatOptions): string
一覧に表示する期限を言語に応じて表記を変更するために使用しました。例えば、日本語では2024/07/01 9:00
という表記も、英語ではJul 1, 2024, 9:00 AM
、スペイン語では1 jul 2024, 9:00
と変換してくれます。言語に応じて一般的な表記に変更してくれるのは多言語化の文脈でも助かります。
tzDate
function tzDate(
date: string | Date,
tz: string // IANA timezone, ex: "America/Los_Angeles"
): Date
第1引数に渡した日時を第2引数で渡したタイムゾーンを考慮してDateオブジェクトを返却してくれる関数です。Todoを登録する際、ユーザーが入力した期限を選択されているタイムゾーンを考慮して登録するためにこの関数を使用しました。
例
タイムゾーンを東京に設定した状態で、期限2024/07/01 9:00
のTodoを作成すると、ロンドンでは2024/07/01 1:00
、ヤンゴンでは2024/07/01 6:30
、チャタム島では2024/07/01 12:45
になります。
サマータイム
上記の例で、ロンドンが2024/07/01 0:00
ではないの?と思った方もいるかもしれません。私も最初は間違っていると思いましたが、サマータイムが考慮されているため1時間ずれるのが正しいのです。そこまで考慮されているので、タイムゾーン対応のライブラリとしては安心感があります。
おわりに
Todoアプリのような小さなアプリでも、多言語化とタイムゾーン対応の難しさと重要性を改めて実感しました。特に、最近のプロダクトではユーザーの多様なニーズに対応することが求められています。グローバルに使用されるプロダクトでは、多言語化とタイムゾーン対応の両方が必要になるので、引き続きアンテナを張っていきたいと思います。
最後まで読んでいただき、ありがとうございました!
余談
せっかくなので新しい状態管理ライブラリを使おうと思い、Zustandを使ってみました。ドキュメントのサンプルコードがわかりやすく、Reduxを使ったことがあったので学習コストはほとんどかからず実装できました。まだ使ったことがない方はぜひ試してみてください!
Discussion