lingui-best-practices について
lingui-best-practicesは、JavaScript開発者向けのLingui国際化スキルです。ReactとバニラJavaScriptアプリでのメッセージ指定、抽出、翻訳、カタログ管理の手法を扱います。
主な機能
- 01Linguiマクロの選び方
- 02複数形と動的メッセージの処理
- 03翻訳カタログの設定と読み込み
- 04抽出、コンパイル、チェックの運用
- 05カタログ保守の指針
活用シーン
- 01ReactのUIに翻訳可能なテキストを追加
- 02バニラJavaScriptの文字列を国際化
- 03カタログの抽出とコンパイルをビルドに組み込む
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lingui/ skills --skill lingui-best-practicesインストール後、Claude Code で/lingui-best-practicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/common-mistakes.mdconfiguration.mdintegration-recipes.md詳細
Lingui のベストプラクティス
Lingui は、JavaScript 向けの強力な国際化(i18n)フレームワークです。このスキルでは、React およびバニラ JavaScript アプリケーションで i18n を実装するためのベストプラクティスを説明します。
クイックスタートのワークフロー
Lingui の標準的なワークフローは、次の手順で構成されます。
- アプリを
I18nProviderでラップします - マクロ(
Trans、tなど)を使ってメッセージに翻訳対象の印を付けます - メッセージを抽出します:
lingui extract - カタログを翻訳します
- カタログをコンパイルします:
lingui compile - アプリでロケールを読み込み、有効化します
コアパッケージ
次のパッケージからインポートします。
// React macros (recommended)
import { Trans, Plural, Select, useLingui } from "@lingui/react/macro";
// Core macros for vanilla JS
import { t, msg, plural, select } from "@lingui/core/macro";
// Runtime (rarely used directly)
import { I18nProvider } from "@lingui/react";
import { i18n } from "@lingui/core";
I18nProvider のセットアップ
アプリケーションを I18nProvider でラップします。
import { I18nProvider } from "@lingui/react";
import { i18n } from "@lingui/core";
import { messages } from "./locales/en/messages";
i18n.load("en", messages);
i18n.activate("en");
function App() {
return (
<I18nProvider i18n={i18n}>
{/* Your app */}
</I18nProvider>
);
}
UI テキストの翻訳
適切なマクロの選択
このツリーは、ビルド時にマクロ変換(Babel、SWC、または Vite プラグイン)が実行されることを前提としています。ビルド手順がない環境で実行されるコード(Node.js スクリプト、サーバーレス関数、テストランナーなど)では、代わりにランタイムマクロを使用します。integration-recipes.md を参照してください。
次の質問を、上から順に確認してください。
- メッセージは数に依存しますか? →
Plural(JSX)またはplural(文字列)。数に依存する文字列を通常のTransでラップしてはいけません。メッセージに英語の複数形ルールが埋め込まれてしまいます。 - JSX のコンテンツですか? →
Trans - コンポーネント内の文字列(属性、アラート、関数の引数など)ですか? →
useLingui()+t`...` - コンポーネントの外部(モジュールスコープ、定数、設定など)で定義されていますか? →
msgディスクリプターを使い、レンダリング時にt(descriptor)または_(descriptor)で解決します - React 以外のコード内にありますか? →
@lingui/core/macroのt
文字列に翻訳者向けのコメントが必要な場合は、ツリーで選んだマクロのオブジェクト形式を使ってください。t`…` と msg`…` にはコメントを付ける場所がありません。ラップする時点で決めておけばコストはかかりませんが、後からコードベース全体を変換するのは大変です。enhanced-message-context スキルを参照してください。
Zod のメッセージ、aria-label 内の数、サーバー関数の戻り値、Intl フォーマッターなど、通常の文字列を想定していてマクロを受け付けない出力先もあります。それぞれに専用のレシピがあります。integration-recipes.md を参照してください。
JSX コンテンツには Trans を使う
Trans マクロは、JSX を翻訳する主な方法です。
import { Trans } from "@lingui/react/macro";
// Simple text
<Trans>Hello World</Trans>
// With variables
<Trans>Hello {userName}</Trans>
// With components (rich text)
<Trans>
Read the <a href="/docs">documentation</a> for more info.
</Trans>
// Extracted as: "Read the <0>documentation</0> for more info."
使用する場面:JSX 要素内の翻訳可能なテキスト全般。
JSX 以外には useLingui を使う
JSX の外部にある文字列(属性、アラート、関数呼び出しなど)には、次を使います。
import { useLingui } from "@lingui/react/macro";
function MyComponent() {
const { t } = useLingui();
const handleClick = () => {
alert(t`Action completed!`);
};
return (
<div>
<img src="..." alt={t`Image description`} />
<button onClick={handleClick}>{t`Click me`}</button>
</div>
);
}
使用する場面:要素の属性、アラート、関数のパラメーターなど、JSX 以外の文字列全般。
マクロフックは t と同様に i18n も返し、どちらも React コンテキストにバインドされています。そのため、このフックだけでアクティブなロケールの読み取り、そのロケールに基づくフォーマット、ロケール変更のコンポーネントへの反映を行えます。インポートも 1 つで済みます。@lingui/react のランタイム useLingui は、マクロ変換を行わないコードで使用します。
import { useLingui } from "@lingui/react/macro";
const { t, i18n } = useLingui();
i18n.locale; // "de-DE" — the active locale
new Intl.NumberFormat(i18n.locale) // format against it
遅延翻訳には msg を使う
メッセージをモジュールレベル、または配列やオブジェクト内で定義する必要がある場合は、次を使います。
import { msg } from "@lingui/core/macro";
import { useLingui } from "@lingui/react";
// Module-level constants
const STATUSES = {
active: msg`Active`,
inactive: msg`Inactive`,
pending: msg`Pending`,
};
function StatusList() {
const { _ } = useLingui();
return Object.entries(STATUSES).map(([key, message]) => (
<div key={key}>{_(message)}</div>
));
}
使用する場面:モジュールレベルの定数、メッセージの配列、条件に応じたメッセージの選択。
ディスクリプターによってフィールドの型が変わります
msg は string フィールドを MessageDescriptor に変換するため、TypeScript はそのフィールドを使うすべての箇所を指摘します。これにより、この変換を一括して安全に適用できます。ただし、次の 2 点には注意が必要です。
React のキーはコンパイルを通ります。 key={item.label} はオブジェクトキーになり、React はこれを [object Object] に文字列化します。すべての行で同じ値になるため、調整処理の精度が低下し、コンソールの警告だけが手がかりになります。キーには文言ではなく、識別子を使ってください。
// ❌ compiles; every key is now identical
{NAV.map((item) => <li key={item.label}>{t(item.label)}</li>)}
// ✅
{NAV.map((item) => <li key={item.to}>{t(item.label)}</li>)}
文字列メソッドは、誤った修正をしたくなるような型エラーになります。 LABELS[k].toLowerCase() は正しくコンパイルエラーになりますが、t(LABELS[k]).toLowerCase() は修正になりません。翻訳を小文字にすると、規則として大文字を使う言語(ドイツ語の名詞など)で問題が生じ、大文字・小文字の区別がない文字体系では何も変わりません。小文字の表記が本当に必要な場合は、独自のコメントを付けた別のメッセージを用意してください。
複数形
数量に応じて変わるメッセージには、Plural マクロを使用します。
import { Plural } from "@lingui/react/macro";
<Plural
value={messageCount}
one="You have # message"
other="You have # messages"
/>
# プレースホルダーは実際の値に置き換えられます。
完全一致
完全一致する数値には、_N 構文を使用します(複数形より優先されます)。
<Plural
value={count}
_0="No messages"
one="One message"
other="# messages"
/>
変数とコンポーネントとの併用
複雑なメッセージには、Trans と組み合わせます。
<Plural
value={count}
one={`You have # message, ${userName}`}
other={
<Trans>
You have <strong>#</strong> messages, {userName}
</Trans>
}
/>
日付と数値の書式設定
Intl を直接使用します。
import { useLingui } from '@lingui/react/macro';
function MyComponent() {
const { i18n } = useLingui();
const lastLogin = new Date();
const dateFormatter = useMemo(() => new Intl.DateTimeFormat(i18n.locale), [i18n.locale]);
return <Trans>Last login: {dateFormatter.format(lastLogin)}</Trans>;
}
メッセージ ID とコンテキスト
明示的な ID
メッセージキーを安定させるには、カスタム ID を指定します。
<Trans id="header.welcome">Welcome to our app</Trans>
曖昧さを解消するコンテキスト
同じテキストに異なる意味がある場合は、context を使用します。
<Trans context="direction">right</Trans>
<Trans context="correctness">right</Trans>
これにより、カタログに別々のエントリーが作成されます。
context は、同じテキストに実際に異なる翻訳が必要な場合にのみ使用してください。名前空間の仕組み(auth.login、settings.title)として使用しないでください。意味が同じ文字列は、翻訳が一度で済むよう、1 つのカタログエントリーを共有する必要があります。
翻訳者向けコメント
翻訳者向けのコンテキストを追加します。
<Trans comment="Greeting shown on homepage">Hello World</Trans>
設定
基本的な lingui.config.js:
import { defineConfig } from "@lingui/cli";
export default defineConfig({
sourceLocale: "en",
locales: ["en", "es", "fr", "de"],
catalogs: [
{
path: "<rootDir>/src/locales/{locale}/messages",
include: ["src"],
exclude: ["**/node_modules/**"],
},
],
});
詳細な設定パターンについては、configuration.md を参照してください。
Lingui 6 に関する注意事項
Lingui 6(2026 年 4 月)は ESM 専用で、Node.js ≥ 22.19(または ≥ 24)が必要です。プロジェクトがこの要件を満たせない場合は、すべての @lingui/* パッケージを ^5 に固定してください。
非推奨の文字列形式 format: "po" と formatOptions オプションは v6 で削除されました。format を省略してください(PO が引き続きデフォルトです)。または、フォーマッターのインスタンスを渡して設定します。
import { defineConfig } from "@lingui/cli";
import { formatter } from "@lingui/format-po";
export default defineConfig({
// ...
format: formatter({ lineNumbers: false }),
});
lineNumbers: false にすると、カタログの差分を小さくできます。行番号のコメントは、ソースを少し変更するたびにほぼ毎回変わるためです。
カタログの管理
抽出とコンパイルをプロジェクトに組み込み、忘れずに実行されるようにします。
{
"scripts": {
"lingui:extract": "lingui extract",
"lingui:compile": "lingui compile",
"dev": "lingui compile && vite",
"build": "lingui compile && vite build"
}
}
- 既存の
dev/buildスクリプトの先頭にlingui compile &&を追加してください。既存のスクリプトを置き換えてはいけません。また、prebuildフックに頼らないでください。pnpm ≥ 7 と Yarn Berry では、デフォルトで pre/post フックが実行されません。 - コンパイル済みカタログは、ディレクトリ単位ではなく拡張子で Git の追跡対象から除外してください。
src/locales/のようなディレクトリルールでは、翻訳の信頼できる情報源である.poファイルまで除外されます。
# ✅ compiled artifacts only
src/locales/**/messages.ts
src/locales/**/messages.js
# ❌ never — silently untracks the .po sources too
# src/locales/
git check-ignore で確認してください。コンパイル済みファイルは一致し、その .po ファイルは一致しない必要があります。コンパイル済みカタログを除外しても安全なのは、すべてのビルド前に lingui compile を実行する場合に限られます。どちらか一方だけを実行しないでください。
compileNamespaceを、アプリがカタログをインポートする方法に合わせてください。 コードで./locales/en/messagesを.tsファイルとしてインポートする場合は、lingui.configでcompileNamespace: "ts"を設定してください。これにより、単純なlingui compileで該当するアーティファクトが正確に再生成され、忘れられる可能性のある--typescriptフラグも不要になります。- Vite の代替方法:
@lingui/vite-pluginを使用すると、アプリから.poカタログを直接動的にインポートできます(await import(\./locales/${locale}/messages.po`)`)。プラグインがその場でコンパイルするため、スクリプトや Git の追跡対象外設定が必要なコンパイル済みカタログファイルはありません。 - CI の差分チェックを追加し、カタログの状態を PR の要件に含めてください(Lingui 6.8 以降。古いバージョンでは
lingui compile && lingui extract --clean && git diff --exit-code -- src/localesを使用します)。
"i18n:check": "lingui check sync"
これにより、誰かが抽出を再実行せずにメッセージを追加または編集すると、ビルドが失敗します。ファイルへの書き込みは行われません。チームが抽出時に使用するフラグを反映してください(抽出スクリプトで --clean を使用している場合は check sync --clean、--overwrite も同様です)。そうしないと、チームが生成したことのない出力と比較されます。
- 不足している翻訳は、
lingui check missingで別途チェックしてください。このコマンドは、fallbackLocalesの適用後も不足している翻訳があると失敗します(compile --strictのルールについては、configuration.md を参照してください)。TMS から翻訳が非同期で届く場合は、すべての PR ではなくリリースパイプラインで実行してください。そうしないと、翻訳者の対応が追いつくまで、機能追加の PR がすべて失敗します。
ベストプラクティス
常にマクロを使用する
実行時コンポーネントよりもマクロを優先してください。マクロはビルド時にコンパイルされるため、バンドルサイズを削減できます。
// ✅ Good - uses macro
import { Trans } from "@lingui/react/macro";
// ❌ Avoid - runtime only
import { Trans } from "@lingui/react";
メッセージはシンプルに保つ
メッセージ内では複雑な式を避けてください。複雑な式はプレースホルダーに置き換えられます。
// ❌ Bad - loses context
<Trans>Hello {user.name.toUpperCase()}</Trans>
// Extracted as: "Hello {0}"
// ✅ Good - clear variable name
const userName = user.name.toUpperCase();
<Trans>Hello {userName}</Trans>
// Extracted as: "Hello {userName}"
ローカル変数への抽出が現実的でない場合は、ph() を使ってプレースホルダーをインラインで指定します。
import { ph } from "@lingui/core/macro";
// Extracted as: "Hello {name}" instead of "Hello {0}"
t`Hello ${ph({ name: getUserName() })}`;
ph() は Trans、Plural、Select の中でも使えます。
JSX には Trans、文字列には t を使う
適切なツールを選んでください。
// ✅ For JSX content
<h1><Trans>Welcome</Trans></h1>
// ✅ For string values
const { t } = useLingui();
<img alt={t`Profile picture`} />
モジュールレベルではマクロを使わない
マクロにはコンポーネントのコンテキストが必要です。代わりに msg を使ってください。
// ❌ Bad - won't work
import { t } from "@lingui/core/macro";
const LABELS = [t`Red`, t`Green`, t`Blue`];
// ✅ Good - use msg for lazy translation
import { msg } from "@lingui/core/macro";
const LABELS = [msg`Red`, msg`Green`, msg`Blue`];
UI 以外の文字列はラップしない
すべての文字列がメッセージというわけではありません。次のものはラップしないでください。
- CSS クラスと
classNameの値 console.*/ロガーの出力、開発者向けのエラーコード- インポートパス、URL、API ルート、クエリーキー
- オブジェクトのキー、列挙型の値、ALL_CAPS 定数、
data-testidの値 - 比較対象または永続化される値(ステータス、スラッグ)
URL にロケールのプレフィックスを付けるのはルーティングの関心事であり、翻訳の関心事ではありません。パスをマクロでラップしないでください。
ESLint プラグインを使う
eslint-plugin-lingui をインストールして設定し、よくあるミスを自動的に検出できるようにしてください。
npm install --save-dev eslint-plugin-lingui
// eslint.config.js
import pluginLingui from "eslint-plugin-lingui";
export default [
pluginLingui.configs["flat/recommended"],
];
ロケールのメタデータ:単一の情報源にする
React やフレームワークのインポートを含まない共有モジュールでロケールの情報を一度だけ定義すれば、設定、ミドルウェア、テスト、コンポーネントのどこからでも安全に利用できます。
// src/i18n/locales.ts
export const locales = ["en", "es", "fr", "ar"] as const;
export type Locale = (typeof locales)[number];
export const sourceLocale: Locale = "en";
const RTL_LOCALES = new Set(["ar", "he", "fa", "ur"]);
export const getDirection = (locale: string): "ltr" | "rtl" =>
RTL_LOCALES.has(locale.split("-")[0]) ? "rtl" : "ltr";
// "Deutsch", not "German" — each language name rendered in its own language
export const localeDisplayName = (locale: string) =>
new Intl.DisplayNames([locale], { type: "language" }).of(locale) ?? locale;
// `null` is in the signature on purpose: detect() and headers.get() both return it
export function resolveLocale(candidate: string | null | undefined): Locale {
if (!candidate) return sourceLocale;
if ((locales as readonly string[]).includes(candidate)) return candidate as Locale;
const base = candidate.split("-")[0]; // es-MX → es
return (locales as readonly string[]).includes(base) ? (base as Locale) : sourceLocale;
}
問題がある場合の兆候:getDirection や Intl.DisplayNames が複数のファイルで定義されている、dir="rtl" の条件分岐があちこちにハードコードされている、言語名のマップを手作業で管理している。
レイアウト上の注意点:src/i18n.ts と src/i18n/ の両方を置かないでください。フラットなファイルがモジュール解決時にディレクトリの index.ts を隠すため、ビルドは通るのにアプリがひそかに誤動作します。どちらか一方の構成を選んでください。
よくあるパターン
ロケールの動的な切り替え
import { i18n } from "@lingui/core";
async function changeLocale(locale) {
const { messages } = await import(`./locales/${locale}/messages`);
i18n.load(locale, messages);
i18n.activate(locale);
}
カタログの動的な読み込み
import { useEffect } from "react";
import { i18n } from "@lingui/core";
function loadCatalog(locale) {
return import(`./locales/${locale}/messages`);
}
function App() {
useEffect(() => {
loadCatalog("en").then(catalog => {
i18n.load("en", catalog.messages);
i18n.activate("en");
});
}, []);
return <I18nProvider i18n={i18n}>{/* ... */}</I18nProvider>;
}
メッセージ内でアプリ全体の値を使う
さまざまなメッセージに登場する値(製品名、サポート用アドレス、アプリのバージョンなど)は、プレースホルダーとして指定します。こうすれば値を変更するだけで済み、すべてのメッセージで ID と翻訳を維持できます。Lingui 6.10 以降では、値をインスタンスに一度だけ登録し、メッセージ内ではプレースホルダーをそのまま記述します。
// once, at startup — or setupI18n({ variables }) when you create the instance
i18n.setVariables({ appName: "Acme Notes" });
// in a component
const { t } = useLingui();
t`Questions about {appName}? Ask in the help center.`;
<Trans>Welcome to {"{appName}"}</Trans>; // the string literal keeps the braces as a placeholder
- 呼び出し側で渡された値が優先されます。
undefinedとnullの場合はデフォルト値が使われます。 - 未設定または名前の誤記がある場合は、警告なしで空文字列として表示されます。最初のレンダリングの前にデフォルト値を設定してください。
- 関数のデフォルト値(
() => user.plan)はレンダリングごとに読み取られますが、その背後の値が変わっても再レンダリングは発生しません。i18n.setVariable(name, value)とsetVariablesはchangeを発行するため、I18nProviderが再レンダリングします。 - 翻訳者にはプレースホルダー
{appName}がそのまま表示されます。何を表す値なのかコメントで説明してください(enhanced-message-context)。
useLingui でのメモ化
メモ化を使う場合は、マクロ版の t 関数を使ってください。
import { useLingui } from "@lingui/react/macro";
import { msg } from "@lingui/core/macro";
import { useMemo } from "react";
const welcomeMessage = msg`Welcome!`;
function MyComponent() {
const { t } = useLingui(); // Macro version - reference changes with locale
// ✅ Safe - t reference updates with locale
const message = useMemo(() => t(welcomeMessage), [t]);
return <div>{message}</div>;
}
トラブルシューティング
問題が発生した場合は、次の点を確認してください。
- メッセージが抽出されない:
lingui.config.jsのincludeパターンを確認します - 翻訳が適用されない:カタログが
lingui compileでコンパイルされていることを確認します - 実行時エラーが発生する:アプリが
I18nProviderでラップされていることを確認します - 型エラーが発生する:TypeScript プロジェクトでは
lingui compile --typescriptを実行します
よくあるミスや落とし穴について詳しくは、common-mistakes.md を参照してください。
Lingui とプレーンな文字列を必要とするライブラリとの接続部分(バリデーションスキーマ、属性内の複数形、値を指定した i18n._()、サーバー側で組み立てるメッセージ、Intl フォーマッター)や、ビルド手順のないコードについては、integration-recipes.md を参照してください。どのケースにも、コンパイルや出荷はできても誤りがある実装例があります。各レシピでは、まずその落とし穴を取り上げます。