# General Translation Knowledge Base: Как найти захардкоженные строки, которые не переведены?
URL: https://generaltranslation.com/ru/kb/find-hardcoded-strings.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Запустите в CI линтер, который находит строковые литералы, видимые пользователю, — например, правило no-literal-string из eslint-plugin-i18next для JSX, проверку HardcodedText в Android Lint или линтер HardCodedString из erb_lint для Rails. Затем пройдитесь по приложению в псевдолокали, чтобы найти текст, который статические проверки отследить не могут.
Category: Интернационализация
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

Захардкоженная строка — это видимый пользователю текст, записанный прямо в коде, а не пропущенный через вашу библиотеку интернационализации. Поэтому в любой локали он отображается на языке-источнике. Добавление такой строки ничего не ломает: на английском приложение выглядит готовым, тесты проходят, и проблема всплывает, только когда кто-то открывает французскую сборку.

## Почему захардкоженные строки остаются незамеченными?

* **Язык-источник маскирует проблему.** Отсутствующий перевод выглядит точно так же, как корректный, пока вы не переключите локаль.
* **Текст находится вне дочерних элементов 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` — также обёрнутые строки и пропсы                                    | Строки вне 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-разметке; `TextView.setText` с литералами, конкатенацией или `Number.toString()`                   | Вызовы `Text("...")` в Jetpack Compose                                              |
| iOS и macOS                 | Извлечение строк в Xcode String Catalog и опция схемы **Show non-localized strings**                                                                                                                                           | Строки, отсутствующие в каталоге после сборки; нелокализованный текст, отображаемый заглавными буквами во время выполнения | Экраны, которые никто не открывает                                                  |
| Rails                       | [erb&#95;lint](https://github.com/Shopify/erb-lint) `HardCodedString`                                                                                                                                                          | Видимый текст в разметке ERB                                                                                               | Строки внутри тегов JavaScript                                                      |
| Любая кодовая база          | ripgrep                                                                                                                                                                                                                        | Текст между тегами в пределах одной строки                                                                                 | Многострочный текст; не отличает обёрнутый текст от необёрнутого                    |
| Любое запущенное приложение | [Псевдолокализация](/kb/pseudo-localization)                                                                                                                                                                                   | Любой отрендеренный текст, прошедший мимо слоя i18n                                                                        | Экраны, которые никто не открывает                                                  |

Используйте линтер в CI, если хотите, чтобы новые захардкоженные строки ломали сборку. Для разового аудита кодовой базы, которую вы собираетесь интернационализировать, подойдёт ripgrep. Перед релизом используйте [псевдолокализацию](/kb/pseudo-localization): текст, прошедший мимо слоя i18n, остаётся обычным, а всё остальное отображается с диакритикой — в том числе текст из API, констант и ответов сервера, который статический анализ отследить не может.

## Проверка JSX с помощью ESLint

В `eslint-plugin-i18next` есть одно правило — `no-literal-string`. В режиме по умолчанию (`jsx-text-only`) оно помечает только текст между JSX-тегами. Укажите `mode: 'jsx-only'`, чтобы помечать также строковые атрибуты и строковые выражения в JSX. Эта flat-конфигурация работает с 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' }],
    },
  },
];
```

Вот компонент, который мы проверили линтером:

```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" />`. Ни одна из проверок не отмечает вызовы `Text("...")` в Compose, поэтому ищите в файлах Compose строковые литералы, передаваемые в `Text(`.

**iOS и macOS.** Xcode подходит к задаче с другой стороны. При сборке Xcode добавляет в ваш String Catalog строки, переданные в [локализуемые API](https://developer.apple.com/documentation/xcode/preparing-your-apps-text-for-translation). Строковый литерал в SwiftUI-вызовах `Text("Title")`, `Button("Checkout")` или `String(localized:)` извлекается. Переменная типа `String`, переданная в `Text`, [отображается без локализации](https://developer.apple.com/documentation/swiftui/text) — как и `Text(verbatim:)`. Поэтому после сборки убедитесь, что в каталоге есть строки для нужных экранов: если записи нет, значит, текст не прошёл через локализуемый API — часто потому, что он хранился как обычный `String`, а не `LocalizedStringResource`. Чтобы обнаружить такой текст прямо на экране, включите **Show non-localized strings** в **Product &gt; Scheme &gt; Edit Scheme &gt; Run &gt; Options** — тогда при запуске приложения нелокализованный текст будет отображаться заглавными буквами ([Apple: Preparing your interface for localization](https://developer.apple.com/documentation/xcode/preparing-your-interface-for-localization)).

**Rails.** Линтер `HardCodedString` в erb&#95;lint по умолчанию отключён; включите его в разделе `linters:` файла `.erb_lint.yml`. 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), а не выводить сообщение «Translation missing».

**Любая кодовая база: 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]'
```

На примере компонента первый поиск нашёл заголовок, приветствие и кнопку, а второй — оба атрибута. Такой поиск пропускает текст, стоящий на отдельной строке, и при этом находит текст, который уже обёрнут для перевода, поэтому воспринимайте результаты как список для проверки, а не как окончательный вердикт.

## Поручите агенту для написания кода найти и обернуть строки

Cursor, Claude Code и похожие агенты хорошо справляются с механической частью работы: прочитать компонент, определить, какой текст видит пользователь, и переписать код с использованием вашего i18n API. Сообщите агенту решения, до которых он не может дойти сам, и сделайте прохождение линтера критерием готовности:

```text
Найди захардкоженные строки, видимые пользователю, в src/features/checkout и оберни их.

- Видимые пользователю — это: текст в JSX, значения placeholder/title/alt/aria-label,
  всплывающие уведомления и сообщения об ошибках валидации, а также подписи в константах, которые отображаются в UI.
- Не видимые пользователю: имена классов, тестовые ID, URL, сообщения логов, имена
  событий аналитики и название продукта «Acme».
- Оборачивай текст JSX в i18n-компонент проекта, а строковые пропсы и
  функции обратного вызова — в функцию, которую возвращает его i18n-хук. Вызывай хуки только на
  верхнем уровне React-компонента или пользовательского хука и никогда — из вспомогательной функции или
  обработчика событий.
- Для сообщений на уровне модуля и утилит вне React следуй документированному
  API соответствующего фреймворка. Используй src/features/cart/CartSummary.tsx как образец.
- Имена, цены и количества передавай как переменные, а не вписывай в текст.
- Не отключай правила линтера и не добавляй комментарии eslint-disable.
- По завершении выполни `npx eslint src/features/checkout` и исправь каждую
  ошибку i18next/no-literal-string, затем перечисли строки, которые ты оставил
  необёрнутыми, и объясни почему.
```

Затем проверьте результат самостоятельно: повторно запустите ESLint вместе с правилами React Hooks, просмотрите различия и найдите строки, которые были обёрнуты по ошибке (идентификаторы, значения enum, отправляемые в API), а затем откройте изменённые экраны в псевдолокали или на другом языке. Успешная проверка линтером гарантирует лишь отсутствие тех шаблонов, которые распознают включённые правила; константы и шаблонные литералы проверяйте отдельно. [Правила хуков](https://react.dev/reference/rules/rules-of-hooks) React распространяются и на i18n-хуки.

## Линтинг и оборачивание строк React с помощью General Translation

ESLint-плагин General Translation [`@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` как dev-зависимости:

```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` оборачивает динамические значения, например `{user.name}`, внутри [`<T>`](/docs/react/reference/components/t) в [`<Var>`](/docs/react/reference/components/var), а необёрнутый текст помечает, чтобы вы обернули его вручную. [Автоматическое внедрение JSX](/docs/cli/guides/using-auto-jsx) по умолчанию отключено — чтобы включить его, задайте `enableAutoJsxInjection` в `files.gt.parsingFlags` в `gt.config.json`. После этого при запуске компилятора 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). Чтобы обработать весь репозиторий, можно воспользоваться Locadex — AI-агентом General Translation: он запускает автоматизацию **Generate code**, которая оборачивает непереведённый контент UI и открывает пул-реквест на ревью. (См. [Настройка автоматизаций Locadex](/docs/platform/locadex/guides/configuring-automations).)

## Основные выводы

* На этом тестовом примере `no-literal-string` нашёл 4 проблемы в режиме по умолчанию и 7 — в режиме `jsx-only`. Шаблонный литерал и константа по-прежнему требуют дополнительной настройки или ручной проверки.
* Правило Android Lint `HardcodedText` охватывает XML-разметку, а `SetTextI18n` — вызовы `TextView.setText`; ни одно из них не помечает вызовы `Text` в Compose.
* Xcode извлекает строки, передаваемые в локализуемые API, поэтому если после сборки строки нет в String Catalog, значит, текст жёстко задан в коде.
* Определите для агента для написания кода область работы, критерии пользовательского текста и команду линтера, которую он должен успешно пройти, а затем проверьте приложение в работе.
* В коде на `gt-react` и `gt-next` пакет `@generaltranslation/react-core-linter` проверяет содержимое внутри [`<T>`](/docs/react/reference/components/t), поэтому используйте его вместе с `no-literal-string`, чтобы находить текст, который ещё не обёрнут.

## Related questions

- [Что такое псевдолокализация?](https://generaltranslation.com/ru/kb/pseudo-localization.mdx): Псевдолокализация — это тест, при котором исходный текст заменяется изменённой версией с диакритикой, удлинённой примерно на 40% и заключённой в скобки. Так жёстко закодированные строки, усечение, проблемы со шрифтами и склеенные из фрагментов предложения выявляются ещё до появления перевода. В Android для этого предусмотрены псевдолокали en-XA и ar-XB.
- [General Translation или i18n-tasks: что выбрать?](https://generaltranslation.com/ru/kb/general-translation-vs-i18n-tasks.mdx): i18n-tasks — это Ruby-gem, который находит отсутствующие и неиспользуемые ключи Rails I18N, наводит порядок в файлах локалей и умеет заполнять недостающие ключи с помощью Google Translate, DeepL, OpenAI, Yandex или watsonx. General Translation переводит файлы локалей целиком с учётом вашего Glossary и пользовательских промптов, а результат можно проверить в панели управления. Эти инструменты решают разные задачи, и многим командам пригодятся оба.
- [Чек-лист тестирования локализации для многоязычных приложений](https://generaltranslation.com/ru/kb/multilingual-app-launch-checklist.mdx): Проверьте девять аспектов в каждой поддерживаемой локали на сборке для рабочей среды: пользовательские сценарии целиком, включая состояния ошибок и письма; переключение языка; формы множественного числа; валюту и часовые пояса; увеличение длины текста; вёрстку для языков right-to-left; доступность; доставку переводов; индексируемые языковые URL. Задайте для каждой проверки явный критерий успешного прохождения.
- [Можно ли делать i18n без файлов перевода?](https://generaltranslation.com/ru/kb/i18n-without-translation-files.mdx): Да. Библиотеки gt-react и gt-next от General Translation обеспечивают i18n для React и Next.js без ключей перевода и без отдельного JSON-файла для каждого языка, который приходится поддерживать. Тексты остаются встроенными в JSX внутри компонента `<T>`, а команда gt translate генерирует переводы для каждого языка во время сборки, учитывая контекст из вашего кода. Locadex может поддерживать их в актуальном состоянии через пул-реквесты.
- [Как перевести условный текст в JSX?](https://generaltranslation.com/ru/kb/translate-conditional-jsx.mdx): Не разбивайте предложение и переводите каждый его вариант целиком, ведь тернарный оператор дробит его на фрагменты, которые теряют смысл и своё место в порядке слов каждого языка. В General Translation для этого используется `<Branch>` внутри `<T>`, который переводит предложение как единое целое.

## Previous and next questions

- Previous question: [Как называть ключи переводов i18n?](https://generaltranslation.com/ru/kb/i18n-key-naming.mdx)
- Next question: [Как определить язык браузера пользователя?](https://generaltranslation.com/ru/kb/detect-user-language.mdx)

## Sitemap

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