find-unwrapped-strings について
find-unwrapped-stringsは、Linguiプロジェクトでローカライズ用のラッパーがないユーザー向け文字列を監査するスキルです。ESLintでの検出、用途の確認、範囲を区切った修正、結果報告の手順を示します。
主な機能
- 01LinguiルールによるESLintスキャン
- 02文字列の役割に基づく検査
- 03マクロで文字列をラップする指針
- 04修正回数を限定した再スキャン
- 05対応状況と残件の報告
活用シーン
- 01Lingui移行後のローカライズ監査
- 02カタログ確認だけでは見つからない文字列の検出
- 03データや設定内の文字列の確認
- 04未解決の監査結果の追跡
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ lingui/ skills --skill find-unwrapped-stringsインストール後、Claude Code で/find-unwrapped-stringsを入力して読み込まれたか確認します。
同梱ファイルreferences/eslint-config.md
詳細
ラップされていない文字列を見つける
実行するタイミング
- Lingui のセットアップ後、または Lingui への移行後。どちらも変更した箇所はラップしますが、コードベースの残りの部分まで対応済みだとは証明できません。
- 「ビルドは成功してカタログも充実しているのに、どのロケールでもこの画面に英語が表示される」という報告があったとき。
- i18n がすでに確立されているプロジェクトで、定期的にカバレッジを監査するとき。
見落としはランダムではなく、系統的に発生します。JSX 内の文字列は UI に見えるためラップされますが、それ以外は残ります。たとえば、データモジュール内の表示テキスト(export const products = [{ name: "AeroPress Go" }])、トーストやエラーヘルパー内のメッセージマップ、設定オブジェクト内のラベルなどです。
スキャンの対象範囲は、意図的に広めに設定しています。 精度は、ヒットをスキップリストに照らして判断することで確保します。スキャナーが静かになるまで調整するのではありません。静かなスキャナーは、クリーンなコードベースと見分けがつきません。そして、価値があるのはそのうち一方だけです。
言語ではなく、役割で判断してください。スキャナーは文字列リテラルを構造的に検出し、スキップリストによってユーザー向けかどうかを判断します。文字列がどの自然言語で書かれているように見えるかは、その判断材料ではなく、推測するのはまったくの無駄です。
ステップ 1 — ガードレールを整える
eslint-plugin-lingui をインストールし、references/eslint-config.md に記載された調整済みオプションを使って、no-unlocalized-strings を warn で有効にします。
npm install --save-dev eslint-plugin-lingui
最新のリリースを使ってください。これは lint 専用の開発依存関係なので、バージョン範囲を固定しても保護できるものはありません。
このインストールは恒久的なものです。 監査は一度きりの総点検ですが、このルールが次の再発を防ぎます。30 個の文字列を見つけても、ガードレールを残さない監査では、数週間分の効果しかありません。
ルールは明示的に有効にしてください。プラグインの recommended および flat/recommended プリセットには含まれていないため、プリセットを拡張するだけではカバレッジを確保できません。詳しくはリファレンスを参照してください。
ユーザーが新しい開発依存関係の追加を断った場合は、1 回限りの方法があります。調整済みの設定をリポジトリの外に書き、そこを ESLint に指定すれば、プロジェクトには変更を加えずに済みます。
npx eslint -c /tmp/lingui-audit.config.js --no-config-lookup 'src/**/*.{ts,tsx}' --format json
この方法を提案するのは、ユーザーが追加を断った後だけにしてください。また、代償も伝えます。監査は実行できますが、次にラップされていない文字列が追加されるのを防ぐものはなく、次の監査はゼロから始まります。インストールを推奨してください。
いずれの場合も、スキャンが実行され、ルールがレポートを出せば、このステップは完了です。
ステップ 2 — スキャンする
npx eslint 'src/**/*.{ts,tsx}' --format json > /tmp/lingui-scan.json
ruleId === "lingui/no-unlocalized-strings" で絞り込み、結果を file:line と該当するソース行にまとめます。jq のレシピはリファレンスにあります。このリストが作業リストです。上から順に対応してください。
ルールのオプションは設定ファイルに記述したままにし、CLI ではルールのフラグを指定せずに実行してください。オプション付きの --rule は、設定済みのオプションとマージされるのではなく置き換えるため、クラス名やログ引数まで含まれ、ヒット数がおおよそ 2 倍になります。また、リストが長いほど、より徹底的に見えるため、識別子をラップして、そのノイズが「修正」されてしまいます。測定結果はリファレンスを参照してください。
疑わしい理由のあるコードベースでスキャン結果がクリーンだった場合は、結果を信じる前に glob を確認してください。files パターンが src/**/*.ts では .tsx が lint の対象にならず、それでも実行は終了コード 0 で終わります。
Step 3 — 各ヒットを判定する
すべてのヒットに、ラップするか、理由を記録してスキップするかの処置を割り当てます。処置が決まらないまま、このステップを終えるヒットがあってはなりません。
スキップリストは、lingui-best-practicesスキルの「Don't Wrap Non-UI Strings」セクションにあります。ラップしないものを判断する際の基準となるため、処置を決める前にそこで確認してください。確認できない場合は、以下の役割テストに基づいて判断します。
処置を決めるのは文字列の形ではなく、文字列の役割です。データモジュール内の"Best seller"は、買い物客が読むバッジなのでラップします。同じオブジェクト内の"aero-press-go"はURLのセグメントなのでスキップします。判断が難しい場合は、次の2つの質問で決めます。
- 何かを壊すことなく、翻訳者がこの文字列を変更できますか?(ステータス値としての
"DRAFT"なら、できません。) - これが日本語で表示された場合、正しいですか、それともバグですか?(日本語の
skuはバグです。)
その行だけでは判断できない場合は、使用箇所を確認してください。文字列の役割は定義されている場所ではなく、使われている場所で決まります。
Step 4 — 範囲を絞って修正する
lingui-best-practicesのマクロ判定ツリーに従ってラップします。この監査で特によく見つかるケースは次のとおりです。
- JSXのコンテンツ →
Trans - コンポーネント内の文字列(属性、
alert、関数引数) →useLingui()+t`…` - コンポーネントの外にある文字列 — データモジュール、モジュールレベルのマップ、設定オブジェクト →
@lingui/core/macroのmsgディスクリプターを使用し、使用箇所で解決します。
この最後のケースこそ、この監査の目的であり、誤って修正されやすい箇所です。モジュールスコープでのtは、インポート時にその時点で有効だったロケールを使って一度だけ解決され、その後は変更されません。msgで定義し、表示される場所で解決してください。
// src/data/products.ts — definition
import { msg } from "@lingui/core/macro";
export const products = [
{ sku: "sku-1001", slug: "aero-press-go", name: msg`AeroPress Go` },
];
// consuming component — resolution
const { t } = useLingui();
<h2>{t(products[0].name)}</h2>;
フィールドの型がstringからMessageDescriptorに変わるため、TypeScriptがすべての使用箇所を示します。それに従って修正してください。レンダリングのタイミングがないReact以外のコードでは、i18n._(descriptor)が呼び出し時に解決されます。これはトーストヘルパーに適しています。
enhanced-message-contextに従ってラップする際に翻訳者向けコメントを追加してください。データモジュールのラベルと1語のバッジは、周囲のUIから意味を推測できない状態で渡されるため、必ずコメントを付けるケースです。
その後、ラウンドごとに1回、次を実行します。
npx lingui extract --clean
npx lingui compile
それからStep 2から再スキャンします。
上限:修正ラウンドは最大2回、または約40ファイルです。 どちらかに達したら停止して報告してください。人間なら監査を再実行できますが、200ファイルの差分をレビューすることはできません。
Step 5 — 終了する
次のいずれかに該当したら停止し、どれに該当したかを明記してください。
- スキャンで問題が見つからなかった。
- あるラウンドで新たにラップする文字列がゼロだった — 残りのヒットはすべて、誤検出であることが確認されています。該当する具体例をもとに、リファレンスの指針に従ってルールの設定済み除外項目を追加し、ほかのすべてを引き続き報告しながら、ガードレールが不要な警告を出さないようにします。重大度は
warnのまま、ルールは有効のままにし、ヒットはeslint-disableコメントではなく除外項目で処理します。 - 上限に達した(2ラウンド/約40ファイル)。
Step 6 — 報告する
すべてのスキャンで見つかった各ヒットを、file:lineを添えて次の3つの区分のいずれかに記載します。
- ラップ済み — 変更内容と使用したマクロ。
- スキップ — スキップリストにある理由(
slug、enum value、log output)。 - 未解決 — 未確認か確認済みかを問わず、未解決のまま残っているものを個別に列挙し、次回の開始地点として示します。
ガードレールの状態も報告してください。重大度と、追加した除外項目および各項目の根拠を記載します。
監査の完了を宣言する前に、すべてのヒットに対応してください。「16個の文字列をラップした」だけでは結果になりません。「16個をラップし、識別子とログ出力として16個をスキップし、未解決は0件で、ルールはwarnのままにした」ことが結果です。未報告の未解決項目をなくすことこそ、このスキルの目的です。そもそも見落としは、誰も記録しなかったために見えなくなっていたのです。
別のワークフローから呼び出されましたか?
オーケストレーター(セットアップスキルや TMS 接続スキル)から、対象範囲(src/features/checkout のみ、または移行で変更されたファイル)、プロジェクトのルールファイルに記載された追加のスキップリスト項目(ブランド名、製品コード)、既知の見落とし(「前回の確認ではデータ層を見落としていました」など)が渡されることがあります。
これら3つすべてに従ってください。パイプラインと報告の取り決めは変わりません。対象範囲によってスキャン範囲は絞られますが、報告内容は変わりません。指定された対象範囲外の検出事項は、省略せず対象範囲外として報告してください。
関連スキル
- lingui-best-practices — スキップリストと、ステップ3および4で実行するマクロの判断ツリーを管理します。
- enhanced-message-context — この監査で見つかる、短く文脈のないラベルに対する翻訳者向けコメントを提供します。