Because your i18n setup lives in your head and your config, not in the code the agent is looking at. Cursor regenerates a component from the context in front of it, and plain JSX text is the fastest thing that works. Nothing in the repo objects, so the literal ships. globalize.now is AI-powered localization infrastructure that closes the loop: it extracts the new strings, generates the keys, and syncs the translations on every Git push.

This is the failure mode nobody warns you about. Setting up i18n is a one-afternoon job. Keeping it set up while an agent rewrites your components twice a day is the actual problem.

Why does Cursor keep adding hardcoded strings after i18n is set up?

Three things are true at once, and together they guarantee the leak.

The agent sees a file, not a convention. When you ask Cursor to add a settings panel, it reads the component it is editing and maybe a couple of neighbours. Your locale directory, your namespace scheme, and the rule that every user-facing string goes through a translation function are not in that window unless you put them there.

Untranslated text is the statistical default. The overwhelming majority of React in any model's training data writes strings inline. When the agent is choosing the most probable next token for a button label, Save changes beats t('settings.save') on sheer frequency. You are asking it to pick the less common pattern every single time, with no feedback if it doesn't.

Nothing breaks. This is the decisive one. A hardcoded string type-checks. It builds. It renders. It passes your tests. There is no red mark anywhere in your pipeline that says a user in Germany is about to see an English toast. The defect is invisible until someone switches locale and scrolls.

Compare that with a missing import or a bad prop type, which the agent gets corrected on within seconds. Untranslated text is the one class of bug your toolchain silently accepts, so it is the one class of bug that accumulates.

Which strings leak most often?

The ones added last, under the least scrutiny.

Walk any AI-assisted codebase that started multilingual and the surviving English lives in predictable places:

  • Error and success toasts, especially ones added while fixing an unrelated bug
  • Empty states and loading text, which are written once and never reviewed
  • Form validation messages, often generated inline inside a schema
  • Accessibility attributes: aria-label, alt, title
  • Confirmation dialogs and destructive-action copy
  • Anything inside a catch block

The pattern is consistent. Your primary flows get translated because you look at them. The edges get hardcoded because nobody opens them in a second locale.

There is a second-order version of this too. When an agent adds a literal next to existing translated code, it sometimes "helpfully" invents a key that does not match your namespace scheme, so you end up with errors.saveFailed in one file and settings_save_error in another. Now you have both untranslated strings and a fragmented key space. If you are earlier in this journey, our guide on localizing an AI-generated app covers the initial cleanup.

Can .cursor/rules or AGENTS.md stop it?

They help, and you should write one, but they are guidance rather than enforcement.

The instruction-file layer standardised recently. AGENTS.md began as a vendor-neutral convention in 2025 and is now stewarded by the Agentic AI Foundation under the Linux Foundation. It is read natively by Cursor, Codex, Copilot, Gemini CLI, Aider, Windsurf and Zed, which means one file at your repo root reaches every agent your team uses. Cursor also supports scoped project rules as .mdc files under .cursor/rules/, with frontmatter controlling description, file globs, and whether the rule always applies. The practical setup in 2026 is an AGENTS.md at the root for portability plus scoped Cursor rules where auto-attach earns its keep.

A workable i18n block looks like this:

## Internationalization

- Never write user-facing text as a literal in JSX or in a string passed to a UI component.
- All user-facing text goes through the translation function from `next-intl`.
- Keys follow `feature.element.state` in lowerCamelCase, e.g. `settings.save.error`.
- Add the English value to `messages/en.json` only. Never hand-edit other locale files.
- This includes toasts, empty states, validation messages, `aria-label`, `alt` and `title`.

That will measurably cut the leak rate. It will not take it to zero, because rule files compete for attention with everything else in the context window, and long agent sessions drift. Treat this layer as reducing frequency, not as a guarantee.

How do you make an untranslated string fail the build?

Add a lint rule that treats a literal in JSX as an error, then run it in continuous integration.

The standard choice is the no-literal-string rule from eslint-plugin-i18next. By default it validates plain text in JSX markup rather than every literal in the file, which is what makes it usable on a real codebase instead of a wall of false positives. It supports ignore patterns by regular expression, callee exclusions for functions you know are safe, and a markup-only mode.

A starting configuration:

// eslint.config.js
import i18next from 'eslint-plugin-i18next';

export default [
  {
    files: ['**/*.{jsx,tsx}'],
    plugins: { i18next },
    rules: {
      'i18next/no-literal-string': [
        'error',
        {
          markupOnly: true,
          ignoreAttribute: ['data-testid', 'className'],
          ignore: ['^[0-9]+$', '^\\s*$'],
        },
      ],
    },
  },
];

Run it in your pipeline, not just in the editor:

npx eslint . --max-warnings=0

Now the invisible defect has a red mark. The agent gets the same feedback loop for an untranslated string that it already gets for a type error, which means it can fix its own mistake in the same session instead of leaving it for a user in another locale to find. If you want the wider picture of wiring i18n into a Cursor project from scratch, see our 15-minute Cursor and Next.js walkthrough and the Cursor integration page.

Why isn't a failing lint rule enough?

Because a red build tells you a string exists. It does not translate it.

Once the rule fires, someone still has to pick a key that matches your existing namespaces, add the English source value, and produce the other locales. On a solo project shipping several times a day, that work gets deferred. Deferred long enough and the honest fix becomes adding the file to an ignore list, at which point you are back where you started with extra config.

This is the gap that turns a solved problem into recurring debt. Layers one and two change how often the problem appears and how quickly you find out. Neither of them does the work.

What does the three-layer setup look like?

Instruction, enforcement, automation. Each layer catches what the one before it missed.

Layer one, instruction. Write the i18n block into AGENTS.md at your repo root, and add a scoped .cursor/rules/i18n.mdc with globs for your component directories. Cost: fifteen minutes, once. Effect: fewer literals written in the first place.

Layer two, enforcement. Turn on no-literal-string and fail continuous integration on it. Cost: an afternoon of tuning ignore patterns on an existing codebase. Effect: no literal reaches your main branch unnoticed, and the agent can self-correct.

Layer three, automation. Let the extraction, keying and translation happen without you. This is the layer globalize.now occupies. It sits above your i18n runtime rather than replacing it: next-intl or i18next still renders the strings. globalize.now detects the new hardcoded text, generates keys consistent with the namespaces already in your project, translates them across every configured locale, and commits the updated locale files back on every Git push. Set it up once — connect the repository at globalize.now — and there is no manual step to skip. You can also install it into your agent with:

npx skills add globalize-now/globalize-skills

Plans start at €5 a month; the €20 Starter includes €20 of translation credit (about 200,000 words) each month, with usage above the included volume billed at the same per-character rate. No per-seat and no per-language charges, and signup comes with a €5 translation credit, no card. More detail for engineers is on the developers page, and the shorter version for people shipping with AI tools is on the vibe coders page.

The point of the three layers is that none of them depends on your discipline holding across a hundred agent sessions. The rule file lowers the rate, the lint rule makes failures loud, and the sync makes the fix free. That combination is what keeps a multilingual app multilingual while an agent keeps rewriting it.

Your i18n setup is not the fragile part. The loop between the agent writing a string and that string reaching a locale file is. globalize.now closes that loop on every push.

globalize.now turns hardcoded app copy into translation-ready locale files and keeps them updated as you ship.

Try globalize.now free