# General Translation Knowledge Base: 如何找出未翻译的硬编码字符串？
URL: https://generaltranslation.com/zh/kb/find-hardcoded-strings.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 在 CI 中运行 linter，标记面向用户的字符串字面量，例如 JSX 可使用 eslint-plugin-i18next 的 no-literal-string 规则，Android 可使用 Android Lint 的 HardcodedText 检查，Rails 可使用 erb_lint 的 HardCodedString linter。然后切换到伪区域设置，逐一浏览应用，找出静态检查无法追踪到的文本。
Category: 国际化
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

硬编码字符串是指直接写在代码里、未经国际化库处理的面向用户的文本，因此无论在哪个区域设置下，它都会以源语言显示。添加这类字符串时不会有任何报错：应用在英文环境下看起来一切正常，测试全部通过，直到有人打开法语构建版本，问题才会暴露出来。

## 为什么硬编码字符串会成为漏网之鱼？

* **源语言会掩盖问题。** 在切换区域设置之前，缺失的翻译看起来与正确的翻译毫无二致。
* **文本位于 JSX 子元素之外。** `placeholder`、`title`、`alt` 和 `aria-label` 属性，在 JavaScript 中构建的 toast 消息和验证消息，以及存储在配置对象中的标签，在代码审查时都很容易被忽略。
* **每个新功能都会带来新文案。** 写按钮最快的方式就是直接写字面量，而只要检查不报错，之后就没人会再去包裹它。

因此，解决办法就是加一项会报错的检查：在 CI 中用 linter 检查代码，再对 linter 覆盖不到的部分做一轮视觉检查。

## 如何在各个技术栈中查找硬编码字符串？

| 技术栈         | 检查工具                                                                                                                                                                                                                           | 能检测到的内容                                                                | 会遗漏的内容                              |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | ----------------------------------- |
| 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` 后还检测已包裹的字符串和属性                          | 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** scheme 选项                                                                                                                                                           | 构建后目录中缺失的字符串；运行时以全大写显示的未本地化文本                                          | 没人打开过的页面                            |
| Rails       | [erb&#95;lint](https://github.com/Shopify/erb-lint) `HardCodedString`                                                                                                                                                          | ERB 标记中的可见文本                                                           | JavaScript 标签内的字符串                  |
| 任意代码库       | ripgrep                                                                                                                                                                                                                        | 同一行内标签之间的文本                                                            | 多行文本；无法区分已包裹与未包裹的文本                 |
| 任意运行中的应用    | [伪本地化](/kb/pseudo-localization)                                                                                                                                                                                                | 所有绕过 i18n 层直接渲染的文本                                                     | 没人打开过的页面                            |

如果希望新增的硬编码字符串导致构建失败，请在 CI 中使用 linter。如果要对即将国际化的代码库做一次性排查，请使用 ripgrep。发布前请使用[伪本地化](/kb/pseudo-localization)：其他文本都会带上重音符号，唯独绕过 i18n 层的文本保持原样，就连静态分析无法追踪的、来自 API、常量和服务器响应的文本也能一并发现。

## 使用 ESLint 检查 JSX

`eslint-plugin-i18next` 只有一条规则：`no-literal-string`。其默认模式 `jsx-text-only` 仅标记 JSX 标签之间的文本。设置 `mode: 'jsx-only'` 后，还会标记 JSX 中的字符串属性和字符串表达式。以下 flat config 适用于 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"`。

## 其他技术栈

**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，通常是因为它被存储为普通的 `String`，而非 `LocalizedStringResource`。若想直接在屏幕上找出这类文本，请在 **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` linter 默认关闭，需在 `.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`，即可让缺失的键直接[抛出错误](https://guides.rubyonrails.org/i18n.html)，而不是渲染一条 &quot;Translation missing&quot; 消息。

**任意代码库：使用 ripgrep。** 如果想在引入 linter 之前快速排查一遍，可以搜索标签之间的文本以及包含文本的属性：

```bash
# 匹配同一行内 > 与 < 之间以字母开头的文本
rg -n --glob '*.{jsx,tsx}' '>\s*[A-Za-z][^<>]*<'

# 匹配值为字面量的文本类属性
rg -n --glob '*.{jsx,tsx}' '\b(placeholder|title|alt|aria-label)="[^"]*[A-Za-z]'
```

在示例组件中，第一次搜索找到了标题、问候语和按钮，第二次搜索找到了两个属性。这种搜索会漏掉单独成行的文本，也会匹配到已被包裹以供翻译的文本，因此请把结果当作待审查的清单，而不是通过与否的检查标准。

## 让编程智能体查找并包裹字符串

Cursor、Claude Code 等智能体擅长处理机械性的工作：读取组件、判断哪些内容面向用户，再用你的 i18n API 改写这些内容。对于智能体无法自行推断的决策，由你来告诉它；同时以 linter 检查通过作为完成标准：

```text
找出 src/features/checkout 中硬编码的面向用户的字符串，并用 i18n 包裹它们。

- 面向用户的内容包括：JSX 文本、placeholder/title/alt/aria-label 的值、
  toast 和验证消息，以及常量中会在 UI 中渲染的标签。
- 非面向用户的内容：类名、测试 ID、URL、日志消息、埋点
  事件名称，以及产品名称“Acme”。
- 用项目的 i18n 组件包裹 JSX 文本；用其 i18n 钩子返回的函数包裹字符串属性和
  回调。只能在 React 组件或自定义钩子的顶层调用 Hooks，
  切勿在辅助函数或事件处理函数中调用。
- 对于模块级消息或非 React 工具函数，请按照该框架文档中说明的
  API 处理。可参考 src/features/cart/CartSummary.tsx。
- 名称、价格和数量应作为变量传入，不要写进文本中。
- 不要禁用 lint 规则，也不要添加 eslint-disable 注释。
- 完成后，运行 `npx eslint src/features/checkout` 并修复每个
  i18next/no-literal-string 错误，然后列出所有未包裹的
  字符串及原因。
```

然后亲自验证：重新运行 ESLint (包括 React Hooks 规则) ，检查差异中是否有不该包裹却被包裹的字符串 (如内部标识符、发送给 API 的枚举值) ，并在伪区域设置或第二种语言下打开改动过的页面。lint 通过只能说明已启用规则能检测到的模式没有问题；常量和模板字面量仍需单独检查。React 的 [Hooks 规则](https://react.dev/reference/rules/rules-of-hooks)同样适用于 i18n hooks。

## 使用 General Translation 检查并包裹 React 字符串

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 依赖范围为 ESLint 8 或 9，因此请将 `eslint@9`、`eslint-plugin-i18next`、`@generaltranslation/react-core-linter`、`typescript` 和 `typescript-eslint` 一并安装为开发依赖：

```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 使用 Babel 编译器，并配合 `next dev --webpack` 和 `next build --webpack` 使用；React 应用则需针对所用的打包工具配置编译器。字符串属性仍需通过 [`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/zh/kb/pseudo-localization.mdx): 伪本地化是一种测试方法：将源文本替换为改写后的版本——加上重音符号、长度扩充约 40%，并用方括号包裹——这样在还没有任何翻译之前，硬编码字符串、截断、字体显示异常和句子拼接等问题就会暴露出来。Android 为此内置了 en-XA 和 ar-XB 两个伪区域设置。
- [General Translation 与 i18n-tasks：应该选择哪一个？](https://generaltranslation.com/zh/kb/general-translation-vs-i18n-tasks.mdx): i18n-tasks 是一个 Ruby gem，用于查找缺失和未使用的 Rails I18n 键、整理区域设置文件，还能借助 Google Translate、DeepL、OpenAI、Yandex 或 watsonx 补全缺失的键。General Translation 则结合你的词汇表、自定义提示词和仪表板审校，翻译完整的区域设置文件。两者分工不同，许多团队完全可以搭配使用。
- [多语言应用本地化测试清单](https://generaltranslation.com/zh/kb/multilingual-app-launch-checklist.mdx): 在生产环境构建中，于每个支持的区域设置下测试九项内容：包括错误状态和邮件在内的完整流程、语言切换、复数形式、货币与时区、文本扩展、从右到左布局、无障碍访问、译文交付，以及可抓取的语言 URL。为每项检查设定明确的通过标准。
- [不使用翻译文件能实现 i18n 吗？](https://generaltranslation.com/zh/kb/i18n-without-translation-files.mdx): 可以。借助 General Translation 的 gt-react 和 gt-next 库，你可以为 React 和 Next.js 实现 i18n，既无需翻译键，也不用为每种语言维护 JSON 文件。文案以内联方式写在 JSX 的 `<T>` 组件中，gt translate 命令会在构建时结合代码中的上下文，为每种语言生成译文。Locadex 还能通过拉取请求让译文保持最新。
- [如何翻译 JSX 中的条件文本？](https://generaltranslation.com/zh/kb/translate-conditional-jsx.mdx): 保持句子完整，并翻译它的每个版本。三元表达式会把句子拆成碎片，使其失去含义，也无法适应各语言的语序。General Translation 通过在 `<T>` 中使用 `<Branch>` 解决这一问题，将整个句子作为一个整体进行翻译。

## Previous and next questions

- Previous question: [如何在不破坏语法的前提下翻译 Markdown 和 MDX？](https://generaltranslation.com/zh/kb/translate-markdown-files.mdx)
- Next question: [如何检测用户的浏览器语言？](https://generaltranslation.com/zh/kb/detect-user-language.mdx)

## Sitemap

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