# Add i18n to a React App from a Linear Issue

Yes. An agent reads the labelled Linear issue, wires i18next into a Vite React app, moves strings into locale files, adds a switcher and tests, and opens a PR.

[Run this on Rush](https://cloud.getrush.ai/run/add-i18n/react/), in a Project that has your repository.

## What the agent does

The agent reads the issue and then the repository before it changes anything. In a repository shaped like alan2207/bulletproof-react there are three apps, and the issue points at `apps/react-vite`. Its `package.json` declares React 18.3.1, `react-router` 7, Vite 5, Vitest 2, Zod 3 and TypeScript 5.4, with Yarn scripts for `test`, `lint`, `check-types`, `build` and `test-e2e`. The ESLint config in `.eslintrc.cjs` enforces kebab-case file and folder names through `eslint-plugin-check-file`, and `src/config/env.ts` reads only `VITE_APP_` variables through a Zod schema, so the locale default is read there rather than from a loose `import.meta.env` call.

The strings live where the app composes itself. `src/app/provider.tsx` wraps the tree in Suspense, an error boundary, `HelmetProvider` and the React Query client; `src/components/layouts/dashboard-layout.tsx` names the side navigation `Dashboard`, `Discussions` and `Users` and the menu items `Your Profile` and `Sign Out`; `src/features/auth/components/login-form.tsx` labels its inputs `Email Address` and `Password`; and `src/app/routes/landing.tsx` carries the `Bulletproof React`, `Get started` and `Github Repo` copy. The agent installs `i18next`, `react-i18next` and `i18next-browser-languagedetector`, after checking that `react-i18next` 17 asks for React 16.8 or newer and TypeScript 5, 6 or 7, both of which this app satisfies.

The wiring is one module and one directory. `src/lib/i18n.ts` initialises i18next with the browser language detector, English as `fallbackLng`, and resources imported from `src/locales/en/translation.json` and the second locale the issue names, with the kebab-case paths the lint rule requires. `provider.tsx` imports that module once so every route renders through it, and a `useTranslation` hook replaces the literal strings in the layouts, the auth forms and the landing page with keys. A language menu is added to the user dropdown in `dashboard-layout.tsx`, and the choice persists through the detector's `localStorage` cache so a reload keeps it. English stays the default, which matters because the Playwright specs under `e2e/tests` and the Vitest tests under `src/app/routes/app/discussions/__tests__` find elements by their English text.

Verification uses the repository's own tools. `src/testing/test-utils.tsx` renders under `AppProvider` with a memory router, so a new test renders the dashboard layout, changes the language through i18next, and asserts the navigation reads in the second locale. The agent runs `yarn test`, `yarn lint`, `yarn check-types` and `yarn build`, which is the same sequence `.github/workflows/react-vite-ci.yml` runs in its `all-cli-checks` job, and it runs `yarn test-e2e` against the mock server the `e2e` job uses, so the smoke, profile and auth setup specs still pass with English as the default.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add internationalisation to the React Vite app. Move the visible strings in the layouts, the auth forms and the landing page into locale files, add a second locale, and add a language switcher in the user menu that remembers the choice.\" Work in apps/react-vite. Read package.json, .eslintrc.cjs, src/config/env.ts, src/app/provider.tsx, src/components/layouts/dashboard-layout.tsx, src/features/auth/components/login-form.tsx and src/app/routes/landing.tsx first. Use i18next with react-i18next and the browser language detector, keep English as the fallback so the existing Vitest and Playwright tests still pass, keep file and folder names in kebab-case, add a Vitest test that renders in the second locale, run yarn test, yarn lint, yarn check-types, yarn build and yarn test-e2e, and open a pull request that links the issue.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch, scoped to `apps/react-vite`. The diff adds `src/lib/i18n.ts` and the `src/locales/` directory with one JSON file per locale, adds three dependencies to `package.json` and `yarn.lock`, imports the i18n module in `src/app/provider.tsx`, replaces literals with `t()` calls in the layout, auth and landing files, and adds the language menu and its test. The description quotes the issue, lists every file whose strings moved, and names any string the agent left in place because a test or an accessibility label depends on it.

The checks are the proof. `react-vite-ci.yml` runs on the pull request: the `all-cli-checks` job builds, tests, lints and type-checks, and the `e2e` job installs Playwright browsers and runs the specs against the mock API, uploading `playwright-report/` as an artifact. The session's trajectory at `GET /o/{org}/sessions/{id}/messages` keeps every `yarn` command with its output, so a reviewer sees the suites pass rather than reading a claim.

Before merging, a reviewer checks three things: that no key is missing from the second locale file, which the new test exercises for the navigation but not for every string; that `src/config/env.ts` still validates the environment with the new optional default-locale variable; and that the English copy in the locale file matches what the Playwright specs look for. The two Next.js apps in the monorepo are untouched, and nothing reaches main until the pull request merges.

## Run it on every Linear issue labelled agent

Sign in to Linear once with `POST /me/connections {"type": "app", "name": "linear"}` and finish the authorize URL in the console. Then add an automation to the Project with this prompt and a `linear` trigger on `linear.issue.labeled`, naming the label in `config.labels`. The trigger adds the issue after the prompt and fills in nothing, so the agent reads which app and which locales the issue asks for from the event. The automation's `max_runs_per_hour`, between 1 and 30, caps how many labelled issues it takes on in an hour, each in its own sandbox with its own pull request.

From your own server instead, a handler that receives Linear's webhook for the label sends `POST /o/{org}/sessions` with the repository and the issue URL in `resources`: `https://linear.app/<workspace>/issue/ENG-42` puts the issue text in the prompt through your Linear connection. A `request_id` derived from the issue id makes a redelivered webhook return the run it already started. Give that key a `webhook_url` and Rush POSTs `run.completed` when the turn finishes, with `data.session_url` to read for the pull request link.

## Why run it on Rush?

Internationalising an app is mostly a long, careful edit: every literal in a dozen files becomes a key, every key needs an entry in every locale, and one missed string shows up as a raw key in production. On a laptop that work competes with everything else open in the editor. On Rush the agent has its own sandbox and its own clone, runs `yarn test`, `yarn lint` and `yarn check-types` after each pass, and pushes only when the sequence the CI workflow runs is clean.

The result is a pull request you can judge, not a branch you have to finish. The diff shows every string that moved, the locale files show every key, the new Vitest test shows the switch working, and the repository's own CI runs the unit and Playwright suites on it. Several labelled issues mean several sandboxes at once, and the trajectory keeps every command and its output for each.

## Frequently asked questions

### Will the existing tests break when strings become keys?

They should not, because English stays the default and the fallback. The Vitest tests in `src/app/routes/app/discussions/__tests__` and the Playwright specs under `e2e/tests` find elements by English text, and that text now comes from `src/locales/en/translation.json` unchanged. The agent runs both suites before opening the pull request, and any string it had to leave alone is named in the description.

### Which library does it use?

`i18next` with `react-i18next` and `i18next-browser-languagedetector`. The agent checks the peer requirements first: `react-i18next` 17 asks for React 16.8 or newer, `i18next` 26.2 or newer and TypeScript 5, 6 or 7, and this app has React 18.3.1 and TypeScript 5.4. If your issue names another library, say so and the agent follows it.

### Does it touch the Next.js apps too?

No. The issue names `apps/react-vite`, and the pull request stays inside it. The Next.js App Router and Pages Router apps have their own CI workflows and would need their own routing-aware setup, so they are a separate issue rather than a side effect of this one.

### How is the chosen language remembered?

Through the browser language detector's `localStorage` cache, which is its default order: a stored choice wins, then the browser language, then English. The switcher in the user dropdown writes the choice through i18next's `changeLanguage`, so the detector stores it and a reload keeps it. No server round trip is involved.

### Will the ESLint file-name rule reject the new files?

No. `.eslintrc.cjs` requires kebab-case names for files and folders under `src/`, and the agent names the new module `src/lib/i18n.ts` and the locale directory `src/locales/en/` to match. `yarn lint` runs before the pull request opens, and the CI workflow runs it again.
