# General Translation Knowledge Base: 翻訳されていないハードコードされた文字列を見つけるにはどうすればよいですか？
URL: https://generaltranslation.com/ja/kb/find-hardcoded-strings.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: CI でリンターを実行し、ユーザー向けの文字列リテラルを検出します。たとえば JSX なら eslint-plugin-i18next の no-literal-string ルール、Android なら Android Lint の HardcodedText チェック、Rails なら erb_lint の HardCodedString リンターが使えます。さらに、疑似ロケールでアプリを一通り操作し、静的チェックでは追跡できないテキストを洗い出します。
Category: 国際化
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

ハードコードされた文字列とは、i18nライブラリを通さずにコードへ直接書かれたユーザー向けテキストのことです。そのため、どのロケールでもソース言語のまま表示されてしまいます。しかも、追加したところで何かが壊れるわけではありません。英語ではアプリは完成しているように見え、テストも通るため、誰かがフランス語版のビルドを開いて初めて抜け漏れに気づくことになります。

## ハードコードされた文字列が見落とされるのはなぜか？

* **ソース言語では問題が見えない。** 翻訳が抜けていても、ロケールを切り替えるまでは正しく翻訳されている場合とまったく見分けがつきません。
* **テキストが JSX の子要素の外にある。** `placeholder`、`title`、`alt`、`aria-label` などの属性、JavaScript で組み立てるトーストやバリデーションメッセージ、設定オブジェクトに格納されたラベルは、レビューで見落とされがちです。
* **機能を追加するたびに文言が増える。** ボタンを手早く書くならリテラルを使うのが一番で、チェックで失敗しない限り、後からわざわざラップする人はいません。

つまり必要なのは「失敗するチェック」です。コードには CI でリンターをかけ、リンターで検出できない部分は目視で確認します。

## 各スタックでハードコードされた文字列を見つけるには？

| スタック        | チェック方法                                                                                                                                                                                                                           | 検出できるもの                                                                    | 検出できないもの                                      |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | --------------------------------------------- |
| React と JSX | [`eslint-plugin-i18next`](https://github.com/edvardchen/eslint-plugin-i18next) `no-literal-string`                                                                                                                               | デフォルトでは JSX テキスト。`jsx-only` モードでは属性と `{'...'}` 形式の子要素。`all` モードではすべてのリテラル  | 定数経由で UI に渡されるテキスト (`all` モードのノイズを許容する場合を除く)  |
| React と JSX | [`eslint-plugin-react`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-literals.md) `jsx-no-literals`                                                                                           | デフォルトではラップされていない JSX テキスト。`noStrings` を指定すると、ラップされた文字列と props も対象          | JSX 外の文字列                                     |
| Android     | Android Lint の [`HardcodedText`](https://googlesamples.github.io/android-custom-lint-rules/checks/HardcodedText.md.html) と [`SetTextI18n`](https://googlesamples.github.io/android-custom-lint-rules/checks/SetTextI18n.md.html) | レイアウト XML 内のリテラルテキスト。リテラル、文字列連結、`Number.toString()` を渡す `TextView.setText` | Jetpack Compose の `Text("...")` 呼び出し          |
| iOS と macOS | Xcode String Catalog による抽出と、スキームオプション **Show non-localized strings**                                                                                                                                                             | ビルド後にカタログに含まれていない文字列。実行時にすべて大文字で表示される未ローカライズのテキスト                          | 誰も開かない画面                                      |
| Rails       | [erb&#95;lint](https://github.com/Shopify/erb-lint) `HardCodedString`                                                                                                                                                            | ERB マークアップ内の表示テキスト                                                         | JavaScript タグ内の文字列                            |
| 任意のコードベース   | ripgrep                                                                                                                                                                                                                          | 1 行内でタグに挟まれたテキスト                                                           | 複数行にわたるテキスト。ラップ済みかどうかは判別できない                  |
| 実行中のあらゆるアプリ | [疑似ローカライゼーション](/kb/pseudo-localization)                                                                                                                                                                                          | i18n レイヤーを経由せずにレンダリングされたすべてのテキスト                                           | 誰も開かない画面                                      |

新たなハードコード文字列でビルドを失敗させたい場合は、CI でリンターを実行しましょう。これから国際化するコードベースを一度だけ監査するなら、ripgrep が適しています。リリース前には [疑似ローカライゼーション](/kb/pseudo-localization) を使いましょう。i18n レイヤーを経由していないテキストだけが元のまま表示され、それ以外はすべてアクセント付きになるため、静的解析では追跡できない API、定数、サーバーレスポンス由来のテキストも見つけられます。

## ESLint で JSX を lint する

`eslint-plugin-i18next` が提供するルールは `no-literal-string` の 1 つだけです。デフォルトのモードである `jsx-text-only` では、JSX タグの間にあるテキストのみが検出されます。JSX 内の文字列属性や文字列式も検出するには、`mode: 'jsx-only'` を設定してください。以下のフラット設定は ESLint 9 および 10 で動作し、TypeScript ファイルにも対応しています。

```bash
npm install --save-dev eslint eslint-plugin-i18next typescript typescript-eslint
```

```js title="eslint.config.mjs"
import i18next from 'eslint-plugin-i18next';
import tseslint from 'typescript-eslint';

export default [
  {
    files: ['src/**/*.{js,jsx,ts,tsx}'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
    plugins: { i18next },
    rules: {
      // JSX テキスト、文字列属性、{'string'} 形式の子要素を検出する
      'i18next/no-literal-string': ['error', { mode: 'jsx-only' }],
    },
  },
];
```

lintを実行したコンポーネントは次のとおりです。

```jsx title="src/Checkout.jsx"
export function Checkout({ total, user }) {
  const error = 'Payment failed';
  return (
    <section className="checkout">
      <h2>Order summary</h2>
      <p>Hello, {user.name}</p>
      <input placeholder="Promo code" type="text" />
      <button title="Pay now" onClick={() => alert(error)}>Pay</button>
      <p>{'Free shipping on orders over $50'}</p>
      <p>{`Total: ${total}`}</p>
      <span>+ 2</span>
    </section>
  );
}
```

```text title="npx eslint src (eslint-plugin-i18next 6.1.5, ESLint 10.13.0)"
src/Checkout.jsx
   5:11  error  disallow literal string: <h2>Order summary</h2>                                             i18next/no-literal-string
   6:10  error  disallow literal string: <p>Hello, {user.name}</p>                                          i18next/no-literal-string
   7:26  error  disallow literal string: placeholder="Promo code"                                           i18next/no-literal-string
   8:21  error  disallow literal string: title="Pay now"                                                    i18next/no-literal-string
   8:60  error  disallow literal string: <button title="Pay now" onClick={() => alert(error)}>Pay</button>  i18next/no-literal-string
   9:11  error  disallow literal string: {'Free shipping on orders over $50'}                               i18next/no-literal-string
  11:13  error  disallow literal string: <span>+ 2</span>                                                   i18next/no-literal-string

✖ 7 problems (7 errors, 0 warnings)
```

実行結果からわかること:

* **デフォルトモードでは 4 件の問題が報告されます。** プラグインの `flat/recommended` 設定では、属性と `{'...'}` の子要素は検出されません。`jsx-only` では、これら 3 件が追加で検出されます。
* **検出されないリテラルも残ります。** `className` と `type` はデフォルトで除外されますが、これは意図どおりの動作です。10 行目のテンプレートリテラルは `'should-validate-template': true` を追加しない限り検出されません。また、`'Payment failed'` 定数は `mode: 'all'` を使わない限り検出されませんが、このモードではファイル内の UI 以外の文字列もすべて検出されます。
* **誤検出は避けられません。** `+ 2` が検出されるのは、デフォルトの除外パターンがスペースを含まない記号のみの文字列しか対象にしていないためです。`words.exclude` にパターンを追加し、プラグインの `lib/options/defaults.js` にあるデフォルト値もそこにコピーしてください。オプションを指定すると、そのオプションのデフォルト値は上書きされるためです。

すでに `eslint-plugin-react` を使っている場合は、代わりにその `jsx-no-literals` ルールを利用できます。オプションなしの場合、JSX テキストを式コンテナで囲むことしか求めないため、`<h2>Order summary</h2>` はエラーになりますが、`<h2>{'Order summary'}</h2>` は通過します。`{ noStrings: true, ignoreProps: true }` を設定すると、子要素内のラップされた文字列やテンプレートリテラルも検出されます。`ignoreProps` を外すと、`className="checkout"` を含むすべての文字列 prop も検出対象になります。

## その他のスタック

**Android.** `./gradlew lint` を実行するか、Android Studio で **Code &gt; Inspect Code** を選択し、Internationalization カテゴリを確認します。どちらのチェックもデフォルトでは警告扱いですが、[`lint.xml`](https://developer.android.com/studio/write/lint) で `<issue id="HardcodedText" severity="error" />` と指定すればエラーにできます。ただし、どちらのチェックも Compose の `Text("...")` 呼び出しは検出しないため、Compose のファイルでは `Text(` に渡されている文字列リテラルを検索してください。

**iOS と macOS.** Xcode のアプローチはこれとは逆です。Xcode はビルド時に、[ローカライズ可能な API](https://developer.apple.com/documentation/xcode/preparing-your-apps-text-for-translation) に渡された文字列を String Catalog に追加します。SwiftUI の `Text("Title")`、`Button("Checkout")`、`String(localized:)` に渡した文字列リテラルは抽出されます。一方、`Text` に渡した `String` 変数は[ローカライズされずに表示され](https://developer.apple.com/documentation/swiftui/text)、`Text(verbatim:)` も同様です。そのため、ビルド後は対象の画面についてカタログを確認してください。エントリが見当たらない場合、そのテキストはローカライズ可能な API を経由していません。原因の多くは、`LocalizedStringResource` ではなく通常の `String` として保持されていることです。同様のテキストを画面上で見つけるには、**Product &gt; Scheme &gt; Edit Scheme &gt; Run &gt; Options** で **Show non-localized strings** を有効にします。これにより、アプリの実行時にローカライズされていないテキストがすべて大文字で表示されます ([Apple: Preparing your interface for localization](https://developer.apple.com/documentation/xcode/preparing-your-interface-for-localization)) 。

**Rails.** erb&#95;lint の `HardCodedString` リンターはデフォルトで無効になっているため、`.erb_lint.yml` の `linters:` 配下で有効にしてください。i18n-tasks はこれとは別の部分をカバーします。`missing` コマンドは、コードから呼び出されているのにロケールファイルに存在しないキーを報告しますが、ビュー内のリテラルテキストは検出しません (翻訳パイプラインへの組み込み方については [General Translation vs. i18n-tasks](/kb/general-translation-vs-i18n-tasks) を参照してください) 。テスト環境で `config.i18n.raise_on_missing_translations = true` を設定すると、キーが欠落している場合に「Translation missing」というメッセージをレンダリングする代わりに[エラーが発生する](https://guides.rubyonrails.org/i18n.html)ようになります。

**ripgrep を使えばどのコードベースでも.** リンターを導入する前に手早く監査したい場合は、タグで囲まれたテキストと、テキストを持つ属性を検索します。

```bash
# 同じ行の > と < の間にある、英字で始まるテキスト
rg -n --glob '*.{jsx,tsx}' '>\s*[A-Za-z][^<>]*<'

# リテラル値が指定された、テキストを持つ属性
rg -n --glob '*.{jsx,tsx}' '\b(placeholder|title|alt|aria-label)="[^"]*[A-Za-z]'
```

サンプルのコンポーネントでは、1 つ目の検索で見出し、挨拶文、ボタンが、2 つ目の検索で両方の属性が見つかりました。ただし、この方法では単独の行にあるテキストは検出できず、すでに翻訳用にラップされているテキストにもマッチしてしまいます。そのため、結果は合否の判定基準ではなく、確認すべき候補のリストとして扱ってください。

## コーディングエージェントに文字列の検出とラップを依頼する

Cursor や Claude Code などのエージェントは、機械的な作業を得意としています。コンポーネントを読み込み、ユーザーに表示されるテキストを見極め、i18n API を使って書き換えるといった作業です。エージェントが自分では判断できない方針はあらかじめ伝え、リンターのチェックを通過することを完了の条件にしましょう:

```text
src/features/checkout 内でハードコードされたユーザー向けの文字列を見つけ、ラップしてください。

- ユーザー向けとは：JSX テキスト、placeholder/title/alt/aria-label の値、
  トーストやバリデーションメッセージ、UI にレンダリングされる定数内のラベルを指します。
- ユーザー向けではないもの：クラス名、テスト ID、URL、ログメッセージ、分析用の
  イベント名、製品名「Acme」。
- JSX テキストはプロジェクトの i18n コンポーネントでラップし、文字列の props や
  コールバックはその i18n フックが返す関数でラップしてください。フックは React コンポーネントまたは
  カスタムフックのトップレベルでのみ呼び出し、ヘルパーや
  イベントハンドラ内では絶対に呼び出さないでください。
- モジュールレベルのメッセージや React 以外のユーティリティについては、そのフレームワークの
  ドキュメントに記載された API に従ってください。参考として src/features/cart/CartSummary.tsx を使用してください。
- 名前、価格、件数はテキストの一部にせず、変数として保持してください。
- lint ルールを無効化したり、eslint-disable コメントを追加したりしないでください。
- 完了したら `npx eslint src/features/checkout` を実行してすべての
  i18next/no-literal-string エラーを修正し、ラップせずに残した文字列と
  その理由を一覧にしてください。
```

その後、自分でも検証してください。React Hooks のルールを含めて ESLint を再実行し、差分を確認して、ラップすべきでない文字列 (識別子や API に送信される enum 値など) がラップされていないかチェックします。さらに、変更された画面を疑似ロケールまたは別の言語で開いて確認してください。lint がエラーなしで通っても、検出されるのは有効化されたルールが対象とするパターンだけです。定数とテンプレートリテラルは別途確認してください。React の [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) は i18n のフックにも適用されます。

## General Translation で React の文字列を lint してラップする

General Translation の ESLint プラグイン [`@generaltranslation/react-core-linter`](/docs/react/guides/linting-your-code) は、すでにラップ済みの `gt-react`、`gt-next`、`gt-react-native` のコードをチェックします。[`static-jsx`](/docs/react/reference/lint-rules#static-jsx) は [`<T>`](/docs/react/reference/components/t) 内の動的コンテンツを [`<Var>`](/docs/react/reference/components/var) などの変数コンポーネントで囲むことを、[`static-string`](/docs/react/reference/lint-rules#static-string) は翻訳関数に静的な文字列を渡すことを必須とします。このプラグインはラップされていないテキストを検出しないため、`no-literal-string` と併用し、同ルールの対象から [`<T>`](/docs/react/reference/components/t) を除外してください。プラグインの peer dependency の対応範囲は ESLint 8 または 9 なので、`eslint@9` を `eslint-plugin-i18next`、`@generaltranslation/react-core-linter`、`typescript`、`typescript-eslint` とあわせて dev dependencies としてインストールします:

```js title="eslint.config.mjs"
import i18next from 'eslint-plugin-i18next';
import gtLint from '@generaltranslation/react-core-linter';
import tseslint from 'typescript-eslint';

export default [
  {
    files: ['src/**/*.{js,jsx,ts,tsx}'],
    languageOptions: {
      parser: tseslint.parser,
      parserOptions: { ecmaFeatures: { jsx: true } },
    },
    plugins: { i18next },
    rules: {
      // まだ <T> で囲まれていないテキスト
      'i18next/no-literal-string': [
        'error',
        { mode: 'jsx-only', 'jsx-components': { exclude: ['T'] } },
      ],
    },
  },
  // すでに <T> で囲まれているコンテンツ
  gtLint.configs.recommended,
];
```

`npx eslint src --fix` は、[`<T>`](/docs/react/reference/components/t) 内にある `{user.name}` などの動的な値を [`<Var>`](/docs/react/reference/components/var) でラップします。ラップされていないテキストは警告として残るため、手動でラップしてください。[自動 JSX インジェクション](/docs/cli/guides/using-auto-jsx)は、`gt.config.json` の `files.gt.parsingFlags` で `enableAutoJsxInjection` を設定するまで無効です。有効にすると、GT コンパイラの実行時に翻訳可能な JSX テキストが [`<T>`](/docs/react/reference/components/t) で自動的にラップされます。ビルドのセットアップはリンク先の手順に従ってください。Next.js では `next dev --webpack` と `next build --webpack` で Babel コンパイラを使用し、React アプリでは使用するバンドラーに応じてコンパイラを設定します。文字列の props には、引き続き [`useGT`](/docs/react/reference/hooks/use-gt) を使用します。リポジトリ全体に適用したい場合は、General Translation の AIエージェントである Locadex の **Generate code** 自動化を使用できます。この自動化は未翻訳の UI コンテンツをラップし、レビュー用のプルリクエストを作成します ([Locadex の自動化の設定](/docs/platform/locadex/guides/configuring-automations)を参照してください) 。

## 重要なポイント

* このフィクスチャでは、`no-literal-string` はデフォルトモードで 4 件、`jsx-only` モードで 7 件の問題を報告しました。テンプレートリテラルと定数については、引き続き追加の設定かレビューが必要です。
* Android Lint の `HardcodedText` はレイアウト XML を、`SetTextI18n` は `TextView.setText` を対象としますが、どちらも Compose の `Text` 呼び出しは検出しません。
* Xcode はローカライズ可能な API に渡された文字列を抽出します。そのため、ビルド後に String Catalog に含まれていない文字列があれば、それはハードコードされたテキストである可能性が高いと言えます。
* コーディングエージェントには、対象スコープ、「ユーザー向け」の定義、パスすべき lint コマンドを指示したうえで、実際にアプリを動かして確認してください。
* `gt-react` および `gt-next` のコードでは、`@generaltranslation/react-core-linter` が [`<T>`](/docs/react/reference/components/t) 内のコンテンツをチェックします。まだラップされていないテキストを検出するには、`no-literal-string` と併用してください。

## Related questions

- [疑似ローカライゼーションとは？](https://generaltranslation.com/ja/kb/pseudo-localization.mdx): 疑似ローカライゼーションは、原文をアクセント記号付きで約40%長く水増しし、括弧で囲んだ改変版に置き換えるテストです。これにより、ハードコードされた文字列、切り詰め、フォントの表示崩れ、文の連結といった問題を、翻訳を用意する前に発見できます。Android にはこの用途向けに en-XA と ar-XB の疑似ロケールが用意されています。
- [General Translation と i18n-tasks：どちらを使うべきか？](https://generaltranslation.com/ja/kb/general-translation-vs-i18n-tasks.mdx): i18n-tasks は、Rails I18n の不足キーや未使用キーを検出し、ロケールファイルを整理し、Google Translate、DeepL、OpenAI、Yandex、watsonx を使って不足キーを補完できる Ruby gem です。General Translation は、用語集、カスタムプロンプト、ダッシュボードでのレビューを活用してロケールファイル全体を翻訳します。両者は役割が異なるため、多くのチームにとっては併用も有効です。
- [多言語アプリのローカライゼーションテスト チェックリスト](https://generaltranslation.com/ja/kb/multilingual-app-launch-checklist.mdx): 本番環境のビルドで、サポート対象のすべてのロケールについて次の9項目をテストしてください：エラー状態やメールを含むジャーニー全体、言語の切り替え、複数形、通貨とタイムゾーン、テキストの伸長、右から左のレイアウト、アクセシビリティ、翻訳の配信、クロール可能な言語別URL。各項目には明確な合格条件を設けてください。
- [翻訳ファイルなしでi18nは実現できますか？](https://generaltranslation.com/ja/kb/i18n-without-translation-files.mdx): はい。General Translationの gt-react と gt-next ライブラリを使えば、翻訳キーも、言語ごとに管理するJSONファイルもなしで、ReactとNext.jsのi18nを実現できます。文言は `<T>` コンポーネント内のJSXにインラインで記述したままで、gt translate コマンドがコードから得た文脈をもとに、ビルド時に各言語の翻訳を生成します。さらにLocadexを使えば、プルリクエストを通じて翻訳を常に最新の状態に保てます。
- [JSX の条件付きテキストはどのように翻訳すればよいですか？](https://generaltranslation.com/ja/kb/translate-conditional-jsx.mdx): 文を分割せずに、すべてのバージョンを翻訳します。三項演算子を使うと文が断片に分かれ、断片は意味を失ううえ、各言語の語順に沿った位置にも置けなくなります。General Translation では `<T>` の中で `<Branch>` を使い、文をひとつの単位として翻訳します。

## Previous and next questions

- Previous question: [疑似ローカライゼーションとは？](https://generaltranslation.com/ja/kb/pseudo-localization.mdx)
- Next question: [翻訳管理システム（TMS）とは？](https://generaltranslation.com/ja/kb/what-is-a-translation-management-system.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/ja/kb/sitemap.md) for every article.
