hi18nとは
hi18n は現在Wantedlyで開発中の、TypeScript/JavaScript向け翻訳テキスト管理ライブラリ (i18nライブラリの一種) です。
開発の動機や設計思想、詳しい特徴などは別の記事で紹介していますが、大まかには以下の特徴があります。
- 翻訳IDや翻訳の引数に正しく型がつく。
- Reactのような宣言的な設計との相性がよい。
- JavaScriptの既存の開発環境 (Webpackなど) に自然に統合される。
- たとえば、hi18nのために特別な設定をしなくても、ホットリロードが使えるようになる。
- 翻訳データの分割ロードが必要な場合も、モジュールバンドラーの機能を使って行う。
本稿では実際に使いたい・試したい人のための情報として、導入手順と使い方を紹介します。
導入とセットアップ
ここではReact + TypeScript環境を想定した手順を紹介します。まず必要なパッケージを入れます。
npm install @hi18n/core @hi18n/react-context @hi18n/react
npm install -D @hi18n/cli
# または:
yarn add @hi18n/core @hi18n/react-context @hi18n/react
yarn add -D @hi18n/cli
次に翻訳ファイルを用意します。1ファイル構成も可能ですが、ここでは複数ファイル構成で紹介します。
// src/locale/index.ts
// (他の名前でもOK)
// 翻訳可能な文字列の一覧をここに定義する。
import { Book, Message } from "@hi18n/core";
import catalogEn from "./en";
import catalogJa from "./ja";
export type Vocabulary = {
// 凡例: "翻訳ID": Message<{ 引数 }>; (引数がないときは単に Message でOK)
"example/greeting": Message<{ name: string }>;
};
// 各言語の翻訳データをまとめたオブジェクト
export const book = new Book<Vocabulary>({
en: catalogEn,
ja: catalogJa,
});
// src/locale/en.ts
// (他の名前でもOK)
// 各言語での翻訳をここに定義する。
import { Catalog, msg } from "@hi18n/core";
import type { Vocabulary } from ".";
export default new Catalog<Vocabulary>({
// 凡例: "翻訳ID": msg(翻訳文字列),
"example/greeting": msg("Hello, {name}!"),
});
// src/locale/ja.ts (enと同様)
import { Catalog, msg } from "@hi18n/core";
import type { Vocabulary } from ".";
export default new Catalog<Vocabulary>({
"example/greeting": msg("こんにちは、{name}さん!"),
});
翻訳IDの同期のためのコマンドを用意します。
// package.json
{
"scripts": {
"i18n:sync": "hi18n sync 'src/**/*.ts' 'src/**/*.tsx'"
}
}
これで基本の導入は完了です。 (ESLintを設定している場合はESLint pluginの設定もおすすめしていますが、導入方法の説明は省略します)
翻訳の呼び出し
定義した翻訳を呼び出すにはいくつかの方法があります。
useI18n を使う方法
Reactを使う場合の基本の方法です。まずルート付近でロケールを設定します。
// 自前でReactDOMを呼んでいる場合の例
import { LocaleProvider } from "@hi18n/react";
const root = ReactDOMClient.createRoot(/* ... */);
root.render(
<LocaleProvider locales="ja">
{/* ... */}
</LocaleProvider>
);
利用側ではuseI18nを呼び出します。
import { useI18n } from "@hi18n/react";
// 最初に定義したBookインスタンス (翻訳データ) を明示的にimportする
import { book } from "../../locale";
const Greet: React.FC = () => {
// 現在のロケールと指定したbookを使った翻訳を開始する
const { t } = useI18n(book);
return <>{t("example/greeting", { name: "太郎" })}</>;
};
getTranslatorを使う方法
Reactに依存しない方法です。ロケール情報は自前で管理する必要があります。
import { getTranslator } from "@hi18n/core";
// 最初に定義したBookインスタンス (翻訳データ) を明示的にimportする
import { book } from "../../locale";
const { t } = getTranslator(book, "en");
console.log(t("example/greeting", { name: "太郎" }));
<Translate> を使う方法
<Translate> はReactの要素を含むコンテンツの翻訳に適した方法です。
import { Translate } from "@hi18n/react";
// 最初に定義したBookインスタンス (翻訳データ) を明示的にimportする
import { book } from "../../locale";
const Greet: React.FC = () => {
return <Translate book={book} id="example/greeting" name="太郎" />;
};
<Translate> では翻訳の引数としてReactの要素を渡すことができます。
const UnreadMessages: React.FC = () => {
const unreadCount = 2;
if (unreadCount === 0) return null;
// en: "You have <link>{count,plural,one{# unread message}other{# unread messages}}</link>"
// ja: "<link>{count,number}通の未読メッセージ</link>があります"
return <Translate book={book} id="example/unread" count={unreadCount}>
<a key="link" href="https://example.com/inbox" />
</Translate>;
};
翻訳IDを同期する
hi18n sync
コマンドを使うことで翻訳IDを同期できます。
hi18n sync <globs...> [--exclude <glob>]
たとえば、package.jsonで以下のようにコマンドを定義している場合
// package.json
{
"scripts": {
"i18n:sync": "hi18n sync 'src/**/*.ts' 'src/**/*.tsx'"
}
}
次のようにして翻訳IDの同期を行えます。
npm run i18n:sync
# または:
yarn i18n:sync
この処理はプロジェクト中のTypeScript/JavaScriptソースを書き換えるので注意してください。これにより以下の2つの作業が行われます。
- 使われていない翻訳の削除 (コメントアウト)
- 必要だが定義されていない翻訳のためのコードを生成
もし誤って利用箇所を消してしまい、翻訳がコメントアウトしてしまっても戻すのは簡単です。利用箇所を戻して再度同期処理を行えば、コメントの中身から翻訳が復元されます。
翻訳の同期処理は翻訳の型部分 (Message<{ ... }>
の {
... }
の部分) に関してはノータッチです。ここはプログラマーが直接編集することが想定されています。
--check
オプションをつけると、変更が必要なときに変更を適用するのではなくエラーにすることができます。これはGitHub ActionsなどのCIで同期がとれていることを確認するのに便利です。
npm run i18n:sync --check
# または:
yarn i18n:sync --check
翻訳IDを追加する
新しい翻訳が必要なとき、手動でそれらを追加してもいいですが、ある程度ツールに任せることもできます。
コードを書くときに、 t
のかわりに t.todo
を使い、 Translate
のかわりに Translate.Todo
を使うようにします。
t.todo("example/new");
<Translate.Todo book={book} id="example/new" />;
その後同期を行い、翻訳の中身を追加したら "TODO" を削除します。
その他できること
- 複数形による分岐と数値フォーマット。 Intl が使える環境である必要があります。
- 複数の翻訳IDから動的に内容を選択するには `t.dynamic` または `Translate.Dynamic` を `translationId` と組み合わせて使います。
- ESLintプラグイン `@hi18n/eslint-plugin` を使うことで、hi18nの正しい使い方をある程度強制することができます。
- Linguiからの移行支援用のルールなども含まれています。
- ページやコンポーネント単位で翻訳データを分割するには、単に複数の独立したBookインスタンスを作ればOKです。あとはモジュールバンドラーがうまくやってくれるはずです。
次に読む