# General Translation Knowledge Base: ¿Cómo encuentro cadenas codificadas de forma fija que no están traducidas?
URL: https://generaltranslation.com/es/kb/find-hardcoded-strings.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Ejecuta en CI un linter que marque los literales de cadena visibles para el usuario, como la regla no-literal-string de eslint-plugin-i18next para JSX, la comprobación HardcodedText de Android Lint o el linter HardCodedString de erb_lint para Rails. Después, recorre la app con una pseudoconfiguración regional para detectar el texto que las comprobaciones estáticas no pueden rastrear.
Category: Internacionalización
Last updated: 2026-10-20
Authors: Kevin Wu, Jackie Chen
Published: 2026-10-20

Una cadena codificada de forma fija es texto visible para el usuario que se escribe directamente en el código en lugar de pasar por tu biblioteca de i18n, por lo que aparece en el idioma de origen en todas las configuraciones regionales. Añadir una no rompe nada: la app parece terminada en inglés, las pruebas pasan y el problema solo sale a la luz cuando alguien abre la compilación en francés.

## ¿Por qué se cuelan las cadenas codificadas de forma fija?

* **El idioma de origen lo oculta.** Una traducción que falta se ve exactamente igual que una correcta hasta que cambias de configuración regional.
* **El texto está fuera de los hijos de JSX.** Los atributos `placeholder`, `title`, `alt` y `aria-label`, los mensajes de notificaciones (toasts) y de validación generados en JavaScript, y las etiquetas guardadas en objetos de configuración se pasan por alto fácilmente en la revisión.
* **Cada funcionalidad añade texto.** Lo más rápido para escribir un botón es usar un literal, y nadie lo envuelve después a menos que falle alguna comprobación.

Por eso, la solución es una comprobación que falle: un linter en CI para el código y una revisión visual para todo lo que el linter no pueda detectar.

## ¿Cómo se detectan las cadenas codificadas de forma fija en cada plataforma?

| Plataforma                        | Comprobación                                                                                                                                                                                                                   | Qué detecta                                                                                                                  | Qué pasa por alto                                                                   |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| React y JSX                       | [`eslint-plugin-i18next`](https://github.com/edvardchen/eslint-plugin-i18next) `no-literal-string`                                                                                                                             | Texto JSX de forma predeterminada; atributos e hijos `{'...'}` en modo `jsx-only`; todos los literales en modo `all`         | Texto que llega a la UI desde constantes, salvo que aceptes el ruido del modo `all` |
| React y JSX                       | [`eslint-plugin-react`](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-literals.md) `jsx-no-literals`                                                                                         | Texto JSX sin envolver de forma predeterminada; cadenas envueltas y props con `noStrings`                                    | Cadenas fuera de JSX                                                                |
| Android                           | Android Lint [`HardcodedText`](https://googlesamples.github.io/android-custom-lint-rules/checks/HardcodedText.md.html) y [`SetTextI18n`](https://googlesamples.github.io/android-custom-lint-rules/checks/SetTextI18n.md.html) | Texto literal en el XML de diseño; `TextView.setText` con literales, concatenaciones o `Number.toString()`                   | Llamadas `Text("...")` de Jetpack Compose                                           |
| iOS y macOS                       | Extracción de String Catalog de Xcode y la opción de esquema **Show non-localized strings**                                                                                                                                    | Cadenas que faltan en el catálogo tras una compilación; texto no localizado que aparece en mayúsculas en tiempo de ejecución | Pantallas que nadie abre                                                            |
| Rails                             | [erb&#95;lint](https://github.com/Shopify/erb-lint) `HardCodedString`                                                                                                                                                          | Texto visible en el marcado ERB                                                                                              | Cadenas dentro de etiquetas JavaScript                                              |
| Cualquier base de código          | ripgrep                                                                                                                                                                                                                        | Texto entre etiquetas en una sola línea                                                                                      | Texto de varias líneas; no distingue lo envuelto de lo que no lo está               |
| Cualquier aplicación en ejecución | [Pseudolocalización](/kb/pseudo-localization)                                                                                                                                                                                  | Cualquier texto renderizado que se haya saltado la capa de i18n                                                              | Pantallas que nadie abre                                                            |

Usa un linter en CI si quieres que las nuevas cadenas codificadas de forma fija hagan fallar la compilación. Usa ripgrep para hacer una auditoría puntual de una base de código que vayas a internacionalizar. Usa la [pseudolocalización](/kb/pseudo-localization) antes de un lanzamiento: el texto que se saltó la capa de i18n se queda tal cual, mientras que todo lo demás aparece con acentos, incluido el texto procedente de API, constantes y respuestas del servidor que el análisis estático no puede rastrear.

## Analizar JSX con ESLint

`eslint-plugin-i18next` tiene una sola regla: `no-literal-string`. Su modo predeterminado, `jsx-text-only`, solo detecta el texto entre etiquetas JSX. Establece `mode: 'jsx-only'` para detectar también los atributos de tipo cadena y las expresiones de cadena en JSX. Esta configuración plana (flat config) funciona con ESLint 9 y 10, e incluye los archivos 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: {
      // Detecta texto JSX, atributos de tipo cadena e hijos {'string'}
      'i18next/no-literal-string': ['error', { mode: 'jsx-only' }],
    },
  },
];
```

Este es el componente que analizamos con el linter:

```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)
```

Lo que muestra la ejecución:

* **El modo predeterminado informa de 4 problemas.** Con la configuración `flat/recommended` del plugin, los atributos y el hijo `{'...'}` pasan la comprobación; `jsx-only` añade esos 3 hallazgos.
* **Algunos literales siguen pasando.** `className` y `type` se excluyen de forma predeterminada, que es justo lo que quieres. El literal de plantilla de la línea 10 pasa a menos que añadas `'should-validate-template': true`, y la constante `'Payment failed'` pasa a menos que uses `mode: 'all'`, que además marca todas las cadenas del archivo que no son de UI.
* **Cuenta con falsos positivos.** `+ 2` se marca porque los patrones de exclusión predeterminados solo cubren cadenas compuestas únicamente por símbolos y sin espacios. Añade un patrón en `words.exclude` y copia en él los valores predeterminados de `lib/options/defaults.js` del plugin: cualquier opción que pases sustituye los valores predeterminados de esa opción.

Si ya usas `eslint-plugin-react`, su regla `jsx-no-literals` es la alternativa. Sin opciones, solo exige que el texto JSX esté dentro de un contenedor de expresión, de modo que `<h2>Order summary</h2>` falla pero `<h2>{'Order summary'}</h2>` pasa. Establece `{ noStrings: true, ignoreProps: true }` para marcar también las cadenas envueltas y los literales de plantilla en los hijos; si quitas `ignoreProps`, marca además todas las props de tipo cadena, incluida `className="checkout"`.

## Otras plataformas

**Android.** Ejecuta `./gradlew lint` o, en Android Studio, elige **Code &gt; Inspect Code** y revisa la categoría Internationalization. Ambas comprobaciones son advertencias de forma predeterminada; con [`lint.xml`](https://developer.android.com/studio/write/lint) puedes convertirlas en errores mediante `<issue id="HardcodedText" severity="error" />`. Ninguna de las dos comprobaciones detecta las llamadas `Text("...")` de Compose, así que busca en los archivos de Compose literales de cadena pasados a `Text(`.

**iOS y macOS.** Xcode funciona en sentido inverso. Al compilar, Xcode añade a tu String Catalog las cadenas pasadas a [API localizables](https://developer.apple.com/documentation/xcode/preparing-your-apps-text-for-translation). Los literales de cadena en `Text("Title")`, `Button("Checkout")` o `String(localized:)` de SwiftUI se extraen. Una variable `String` pasada a `Text` se [muestra sin localizar](https://developer.apple.com/documentation/swiftui/text), igual que `Text(verbatim:)`. Por eso, después de compilar, comprueba que el catálogo incluya las pantallas que esperas: si falta una entrada, el texto nunca pasó por una API localizable, a menudo porque se almacenó como un `String` simple en lugar de como `LocalizedStringResource`. Para detectar ese mismo texto en pantalla, activa **Show non-localized strings** en **Product &gt; Scheme &gt; Edit Scheme &gt; Run &gt; Options**; así, el texto no localizado se renderiza en mayúsculas al ejecutar la app ([Apple: Preparing your interface for localization](https://developer.apple.com/documentation/xcode/preparing-your-interface-for-localization)).

**Rails.** El linter `HardCodedString` de erb&#95;lint está desactivado de forma predeterminada; actívalo en `linters:` dentro de `.erb_lint.yml`. i18n-tasks cubre otro hueco: su comando `missing` informa de las claves que usa tu código y que faltan en un archivo de configuración regional, pero no detecta texto literal en las vistas. (Consulta [General Translation vs. i18n-tasks](/kb/general-translation-vs-i18n-tasks) para ver cómo encaja en un flujo de traducción). Si estableces `config.i18n.raise_on_missing_translations = true` en el entorno de pruebas, las claves que faltan [lanzarán errores](https://guides.rubyonrails.org/i18n.html) en lugar de renderizar el mensaje &quot;Translation missing&quot;.

**Cualquier base de código, con ripgrep.** Para hacer una auditoría rápida antes de adoptar un linter, busca texto entre etiquetas y atributos que contengan texto:

```bash
# Texto que empieza por una letra entre > y < en una sola línea
rg -n --glob '*.{jsx,tsx}' '>\s*[A-Za-z][^<>]*<'

# Atributos que contienen texto con valores literales
rg -n --glob '*.{jsx,tsx}' '\b(placeholder|title|alt|aria-label)="[^"]*[A-Za-z]'
```

En el componente de ejemplo, la primera búsqueda encontró el encabezado, el saludo y el botón, y la segunda encontró ambos atributos. No detecta el texto que ocupa una línea propia y, además, detecta texto que ya está envuelto para traducción, así que toma los resultados como una lista que revisar, no como un filtro definitivo.

## Pide a un agente de programación que encuentre y envuelva las cadenas

Cursor, Claude Code y otros agentes similares se desenvuelven bien en la parte mecánica: leer un componente, decidir qué texto ve el usuario y reescribirlo con tu API de i18n. Dale al agente las decisiones que no puede deducir por sí solo y haz que el linter marque cuándo el trabajo está terminado:

```text
Busca las cadenas visibles para el usuario que estén codificadas de forma fija (hardcoded) en src/features/checkout y envuélvelas.

- Visible para el usuario significa: texto JSX, valores de placeholder/title/alt/aria-label,
  mensajes de notificación y de validación, y etiquetas en constantes que se renderizan en la UI.
- No visibles para el usuario: nombres de clases, IDs de prueba, URLs, mensajes de log, nombres
  de eventos de analítica y el nombre del producto «Acme».
- Envuelve el texto JSX con el componente i18n del proyecto, y las props de cadena y los
  callbacks con la función que devuelve su Hook i18n. Llama a los Hooks solo en el
  nivel superior de un componente de React o de un Hook personalizado, nunca desde una función auxiliar o un
  controlador de eventos.
- Para mensajes a nivel de módulo o utilidades que no sean de React, sigue la API
  documentada para ese framework. Usa src/features/cart/CartSummary.tsx como referencia.
- Mantén los nombres, precios y cantidades como variables, no como parte del texto.
- No desactives reglas de lint ni añadas comentarios eslint-disable.
- Al terminar, ejecuta `npx eslint src/features/checkout` y corrige todos los
  errores de i18next/no-literal-string; después, enumera las cadenas que dejaste
  sin envolver y explica por qué.
```

Después, compruébalo tú mismo: vuelve a ejecutar ESLint, incluidas tus reglas de React Hooks; revisa el diff en busca de cadenas que se hayan envuelto sin deber (identificadores, valores de enum que se envían a una API) y abre las pantallas modificadas en una pseudoconfiguración regional o en un segundo idioma. Que lint no muestre errores solo garantiza los patrones que detectan las reglas habilitadas; revisa por separado las constantes y los literales de plantilla. Las [Rules of Hooks](https://react.dev/reference/rules/rules-of-hooks) de React también se aplican a los hooks de i18n.

## Analizar con linter y envolver cadenas de React con General Translation

El plugin de ESLint de General Translation, [`@generaltranslation/react-core-linter`](/docs/react/guides/linting-your-code), revisa el código de `gt-react`, `gt-next` y `gt-react-native` que ya está envuelto: [`static-jsx`](/docs/react/reference/lint-rules#static-jsx) exige que el contenido dinámico dentro de [`<T>`](/docs/react/reference/components/t) se encuentre en un componente variable como [`<Var>`](/docs/react/reference/components/var), y [`static-string`](/docs/react/reference/lint-rules#static-string) exige cadenas estáticas en las funciones de traducción. No detecta texto sin envolver, así que úsalo junto con `no-literal-string` y excluye [`<T>`](/docs/react/reference/components/t) de esa regla. Las peer dependencies del plugin admiten ESLint 8 o 9, así que instala `eslint@9` junto con `eslint-plugin-i18next`, `@generaltranslation/react-core-linter`, `typescript` y `typescript-eslint` como dependencias de desarrollo:

```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: {
      // Texto que aún no está dentro de <T>
      'i18next/no-literal-string': [
        'error',
        { mode: 'jsx-only', 'jsx-components': { exclude: ['T'] } },
      ],
    },
  },
  // Contenido que ya está dentro de <T>
  gtLint.configs.recommended,
];
```

`npx eslint src --fix` envuelve en [`<Var>`](/docs/react/reference/components/var) los valores dinámicos como `{user.name}` que estén dentro de [`<T>`](/docs/react/reference/components/t) y marca el texto sin envolver para que lo envuelvas tú. La [inyección automática de JSX](/docs/cli/guides/using-auto-jsx), que permanece desactivada hasta que configures `enableAutoJsxInjection` en `files.gt.parsingFlags` dentro de `gt.config.json`, envuelve el texto JSX traducible en [`<T>`](/docs/react/reference/components/t) cuando se ejecuta el Compilador de GT. Sigue las instrucciones de configuración de compilación enlazadas: Next.js usa el compilador de Babel con `next dev --webpack` y `next build --webpack`, mientras que las aplicaciones React configuran el compilador para su empaquetador. Las props de tipo cadena se siguen gestionando con [`useGT`](/docs/react/reference/hooks/use-gt). Si quieres abarcar un repositorio completo, Locadex, el Agente de IA de General Translation, ejecuta una automatización **Generate code** que envuelve el contenido de UI sin traducir y abre un pull request para que lo revises. (Consulta [Configurar automatizaciones de Locadex](/docs/platform/locadex/guides/configuring-automations)).

## Conclusiones clave

* En este caso de prueba, `no-literal-string` detectó 4 problemas en su modo predeterminado y 7 en el modo `jsx-only`. El literal de plantilla y la constante aún requieren configuración adicional o una revisión manual.
* `HardcodedText` de Android Lint cubre el XML de diseño y `SetTextI18n` cubre `TextView.setText`; ninguna de las dos detecta las llamadas a `Text` de Compose.
* Xcode extrae las cadenas que se pasan a las API localizables, así que si una cadena no aparece en el String Catalog después de una compilación, es señal de que hay texto codificado de forma fija.
* Dale a tu agente de programación un ámbito, una definición de qué se considera visible para el usuario y el comando de lint que debe superar; después, revisa la aplicación en ejecución.
* En código con `gt-react` y `gt-next`, `@generaltranslation/react-core-linter` revisa el contenido dentro de [`<T>`](/docs/react/reference/components/t), así que combínalo con `no-literal-string` para detectar el texto que todavía no está envuelto.

## Related questions

- [¿Qué es la pseudolocalización?](https://generaltranslation.com/es/kb/pseudo-localization.mdx): La pseudolocalización es una prueba que sustituye tu texto de origen por una versión alterada (con acentos, alrededor de un 40 % más larga y entre corchetes) para que las cadenas codificadas de forma fija, el truncamiento, las fuentes rotas y las oraciones concatenadas salgan a la luz antes de que exista ninguna traducción. Android incluye para ello las pseudoconfiguraciones regionales en-XA y ar-XB.
- [General Translation vs. i18n-tasks: ¿cuál deberías usar?](https://generaltranslation.com/es/kb/general-translation-vs-i18n-tasks.mdx): i18n-tasks es una gema de Ruby que detecta claves de Rails I18n faltantes y sin usar, ordena los archivos de configuración regional y puede completar las claves faltantes mediante Google Translate, DeepL, OpenAI, Yandex o watsonx. General Translation traduce archivos de configuración regional completos con tu glosario, tus prompts personalizados y la revisión en el panel de control. Cumplen funciones distintas, y muchos equipos pueden usar ambas.
- [Lista de comprobación de pruebas de localización para aplicaciones multilingües](https://generaltranslation.com/es/kb/multilingual-app-launch-checklist.mdx): Prueba nueve aspectos en cada configuración regional compatible en una compilación de producción: recorridos completos, incluidos los estados de error y los correos electrónicos; cambio de idioma; plurales; moneda y zonas horarias; expansión del texto; diseño right-to-left; accesibilidad; entrega de traducciones; y URL de idioma rastreables. Asigna a cada comprobación un criterio de aprobación explícito.
- [¿Puedo hacer i18n sin archivos de traducción?](https://generaltranslation.com/es/kb/i18n-without-translation-files.mdx): Sí. Las bibliotecas gt-react y gt-next de General Translation permiten hacer i18n en React y Next.js sin claves de traducción y sin tener que mantener un archivo JSON por idioma. El texto permanece inline en JSX dentro del componente `<T>`, y el comando gt translate genera las traducciones de cada idioma durante la compilación, con contexto extraído de tu código. Locadex puede mantenerlas al día mediante pull requests.
- [¿Cómo traduzco texto condicional en JSX?](https://generaltranslation.com/es/kb/translate-conditional-jsx.mdx): Mantén la oración completa y traduce cada una de sus versiones, porque un ternario la divide en fragmentos que pierden su significado y su lugar en el orden de palabras de cada idioma. General Translation lo resuelve con `<Branch>` dentro de `<T>`, que traduce la oración como una sola unidad.

## Previous and next questions

- Previous question: [¿Cómo doy soporte a idiomas RTL (right-to-left) en CSS?](https://generaltranslation.com/es/kb/right-to-left-css.mdx)
- Next question: [¿Cómo hago que un sitio web sea multilingüe?](https://generaltranslation.com/es/kb/make-website-multilingual.mdx)

## Sitemap

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