# Add i18n to a Vue 3 App from a Linear Issue

Yes. An agent reads the labelled Linear issue, adds vue-i18n, moves template strings into locale files, adds a switcher, keeps the e2e suite green, and opens a PR.

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

## What the agent does

The agent reads the issue and then the app's own conventions. In an app shaped like realworld-apps/vue-realworld-example-app, `package.json` declares Vue 3.5, Vue Router 5, Pinia 3 and Vite 8, with Bun as the package manager and `bun.lock` as the lockfile, and scripts for `serve`, `build`, `lint`, `format:check` and `test:e2e`, which runs the shared RealWorld Playwright suite out of the `realworld` git submodule against `bun run serve` on port 8080. `eslint.config.mjs` applies `eslint-plugin-vue`'s essential rules with Prettier, and `vite.config.js` resolves `@` to `src` and lets single-file components be imported without their `.vue` extension. `src/main.js` creates the app, installs Pinia and the router, checks auth in a `beforeEach` guard and exposes a `window.__conduit_debug__` object the end-to-end tests read.

The strings are in the templates. `src/components/TheHeader.vue` carries `Home`, `Sign in`, `Sign up` and `New Article`; `src/views/Login.vue` has the `Sign in` heading, `Need an account?` and the `Email` and `Password` placeholders; `src/views/Settings.vue` has `Your Settings` and its placeholders; `src/components/ArticleList.vue` has `Loading articles...` and `No articles are here... yet.`; and `src/views/Home.vue` carries the `conduit` banner, `A place to share your knowledge.` and `Popular Tags`. `src/common/format.js` formats dates with `date-fns`, which the agent reads so dates can follow the chosen locale as well.

The wiring is `vue-i18n`, whose current 11 line asks only for Vue 3. `src/i18n/index.js` calls `createI18n` in composition mode with `en` as the fallback, messages from `src/locales/en.json` and the second locale the issue names, and the starting locale from `localStorage`, then the browser language, then `en`. `main.js` installs it next to Pinia and the router. Each template string becomes a `$t()` call with a key, placeholders use `:placeholder="$t(...)"`, and a switcher in `TheFooter.vue` writes the choice to `localStorage` and to `<html lang>`. The English copy in `en.json` is kept character for character, because the shared Playwright specs assert that text and the selectors around it.

There is no unit test runner in this repository, so the proof is the suite it has. The agent runs `git submodule update --init` as the README instructs, `bun install`, `bun run lint`, `bun run format:check` and `bun run build`, then `bunx playwright install --with-deps chromium` and `bun run test:e2e`, which is what `.github/workflows/playwright.yml` runs, and reads the report. It then switches the locale in the sandbox browser and walks the login, settings and home pages to confirm every key resolves and no raw key shows.

## 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. Move the user-facing strings in the header, footer, login, register, settings, home and article list into locale files, add a second locale, add a language switcher in the footer that remembers the choice, and make dates follow the locale.\" Read package.json, eslint.config.mjs, vite.config.js, playwright.config.ts, src/main.js, src/common/format.js, src/components/TheHeader.vue, src/views/Login.vue, src/views/Settings.vue, src/components/ArticleList.vue and src/views/Home.vue first. Use vue-i18n in composition mode with English as the fallback and keep the English strings exactly as they are so the RealWorld Playwright suite still passes. Initialise the realworld submodule, run bun run lint, bun run format:check, bun run build and bun run test:e2e, check the second locale in the browser, 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. The diff adds `src/i18n/index.js` and `src/locales/` with one JSON file per locale, adds `vue-i18n` to `package.json` and `bun.lock`, installs the plugin in `src/main.js`, replaces literals with `$t()` calls in the header, footer, auth views, settings, home and article list, routes date formatting in `src/common/format.js` through the active locale, and adds the switcher. The description quotes the issue, lists each template whose strings moved, and names any string left in place because a selector or a test depends on it.

The checks are the proof. The `Format Check` workflow runs Prettier and ESLint, and the `Playwright Tests` workflow installs Chromium and runs the RealWorld suite against the dev server, uploading `playwright-report/` as an artifact. The session's trajectory at `GET /o/{org}/sessions/{id}/messages` keeps every `bun` command with its output and the browser pass through the second locale, so a reviewer sees the suite green and the translated pages rather than a claim.

Before merging, a reviewer checks that `en.json` matches the previous copy exactly, since the end-to-end specs read it, that the second locale has every key the first has, and that the `lang` attribute and the stored choice survive a reload. 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` and, if only some teams may hand work off, their keys in `config.teams`. The trigger adds the issue after the prompt and fills in nothing, so the agent reads 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 `https://linear.app/<workspace>/issue/ENG-42` in `resources`, which puts the issue text in the prompt through your Linear connection, with a `request_id` derived from the issue id so a redelivered webhook returns 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?

Moving every string in seven templates into keys is the kind of edit where one slip shows up as a raw key on the login page, and the only honest check here is the shared end-to-end suite, which needs the submodule, Chromium and the dev server. On a laptop that is a long loop beside other work. On Rush the agent runs that loop in its own sandbox with its own browser, after each pass, and pushes only when the suite and the lint are clean.

The result is a pull request with its own evidence: the diff shows every string that moved, the locale files show every key, the Playwright report shows the suite passing with English intact, and the trajectory shows the second locale rendered. Several labelled issues become several sandboxes and several pull requests at once.

## Frequently asked questions

### Will the RealWorld end-to-end suite still pass?

It should, because English stays the default and the fallback, and `en.json` carries the previous copy unchanged. The specs under `realworld/specs/e2e` assert that text and the selectors around it, and the agent runs `bun run test:e2e` before the pull request opens. Anything it had to leave untranslated to keep a selector stable is named in the description.

### Why composition mode?

Because the views and components use `<script setup>`, so `useI18n()` fits beside `useRouter()` and the Pinia stores, and `$t()` is still available in templates. `vue-i18n` 11 supports Vue 3 only, which this app is, and the legacy API would add an older style for no gain.

### How is the chosen language remembered?

In `localStorage`, read by `src/i18n/index.js` at startup before the browser language and the `en` fallback. The footer switcher writes the choice, updates the i18n locale and sets `<html lang>`, so a reload keeps it and screen readers get the right language. No server call is involved.

### Does this change the API or the stores?

No. `src/common/api.service.js`, the Pinia stores and the router are untouched; the change is in templates, one new plugin, the locale files and `src/common/format.js` for dates. Error messages that come from the RealWorld API are shown as the API returns them.
