This skill drives the full i18n journey through four phases:
| Phase | Goal |
|---|---|
| 1 — Inspect and decide | Detect stack, ask all user questions, generate an executable plan |
| 2 — Setup | Install + configure the chosen library |
| 3 — Convert | Wrap hardcoded strings, extract + compile catalogs |
| 4 — Globalize-now (optional) | Connect a translation platform |
The orchestrator (this SKILL.md) is library-agnostic. Library- and stack-specific guidance lives in references/ and is loaded by subagents at dispatch time, driven by manifest.json.
Architectural rule: the orchestrator never executes work directly. It asks Phase 1 questions, generates the plan, dispatches subagents in the background, polls a shared progress workspace at .globalize/, and surfaces results. All file modifications, all bash commands, and all reference reading happen inside subagents.
Workspace: .globalize/
Created in the target project root (the project being internationalized). Every artifact the orchestrator and subagents share lives here.
.globalize/
detection.json # output of inspect subagent (Phase 1.1)
decisions.md # frozen user choices from Phase 1
plan.md # executable plan for phases 2/3/4
manifest-snapshot.json # frozen copy of the chosen manifest entry
progress/
setup.json # Phase 2 setup subagent
wrap-1.json…wrap-N.json # Phase 3 wrap subagents (one per partition)
verify.json # Phase 3 verify subagent
globalize.json # Phase 4 project subagent
archive/<ISO-timestamp>/ # archived after each phase completes
First-run setup
If .globalize/ does not exist in the target project, create it and append /.globalize/ to the project's .gitignore (create the file if missing). Confirm with the user only if .gitignore exists and already has rules — show the diff before appending.
User-facing message (when actually creating the folder): "Created
.globalize/in your project root and added it to.gitignore. This folder holds local progress files, decisions, and plans so we can resume mid-run and so you can audit what was decided. It stays out of git."
Resumability
If .globalize/ exists when the orchestrator starts, read plan.md and progress/*.json to determine state. If a plan is already in flight (some phases incomplete), tell the user:
"Found an in-progress i18n setup at
.globalize/. Last completed step:<step>. Want me to resume from there, or start over with a fresh plan?"
Proceed accordingly.
Phase 1 — Inspect and Decide
Phase 1 ends with a fully populated .globalize/ (detection, decisions, plan, manifest snapshot) and the user's "go" before any work happens. Every user prompt this skill ever asks lives in Phase 1 — Phases 2/3/4 are pure execution.
User-facing message (orchestrator kickoff, before 1.1): "Hey — I'll walk you through internationalizing this project in four phases: inspect, set up the library, wrap your hardcoded strings, and (optionally) connect a translation platform. First I'll do a read-only scan of your project — framework, router, existing i18n setup, files with translatable text. No changes yet. After that I'll ask a small set of questions to shape the plan."
1.1 Inspect subagent
Dispatch a subagent (foreground, blocking — small output, no progress polling needed) with this prompt:
You are inspecting a project to gather i18n setup context. Read-only — do not modify any files.
Read the project's
package.json, build config files (vite.config.*,next.config.*,.babelrc), and survey the source tree. Output only a single JSON object matching this schema, written to.globalize/detection.json:{ "framework": "next" | "vite" | "tanstack-start" | "remix" | "react-router-framework" | "nuxt" | "quasar" | "sveltekit" | "cra" | "unknown", "router": "app" | "pages" | "tanstack-router" | "tanstack-start" | "react-router" | "vue-router" | "sveltekit" | "none", "compiler": "swc" | "babel", "react": true | false, "vue": true | false, "svelte": true | false, "typescript": true | false, "packageManager": "npm" | "yarn" | "pnpm" | "bun", "sourceDir": "src" | "app" | string, "routeEntries": ["src/app/**/page.tsx", ...] | null, "git": { "isRepo": true | false, "branch": string | null, "remote": string | null }, "existing": { "library": "lingui" | "next-intl" | "react-intl" | "i18next" | "react-i18next" | "next-translate" | "typesafe-i18n" | "vue-i18n" | "@nuxtjs/i18n" | "i18next-vue" | "@tolgee/vue" | "fluent-vue" | "paraglide" | "none", "configured": true | false, "providerWired": true | false, "catalogsScaffolded": true | false, "stringsWrapped": "yes" | "partial" | "no" }, "candidateFiles": [ { "path": "src/components/Navbar.tsx", "matchCount": 8 } ], "localeSignals": { "existingLocaleDirs": ["src/locales/en", "src/locales/de"], "envHints": ["DEFAULT_LOCALE=en"], "readmeHints": ["mentions: English, German, French"] } }Detection rules:
Field How to detect frameworkEvaluate in this order, first match wins: nextin deps → next.nuxtin deps → nuxt.quasarin deps → quasar.@tanstack/react-startin deps → tanstack-start. Any@remix-run/*runtime package in deps → remix.react-routerin deps AND@react-router/devin devDeps AND areact-router.config.{ts,js}file at the repo root → react-router-framework.@sveltejs/kitin deps or devDeps → sveltekit.vitein devDeps (and none of the above) → vite.react-scriptsin deps → cra. (Order matters: Remix v2, React Router v7 framework mode, and SvelteKit all shipvitein devDeps, so they must be checked before thevitefallback. React Router v7 SPA mode —react-routerwithout@react-router/dev— correctly falls through tovitewithrouter: "react-router".)routerApp Router: app/orsrc/app/withlayout.tsx/layout.js. Pages Router:pages/with_app.tsx/_app.jsx. TanStack Start: deps include@tanstack/react-start. TanStack Router (client):@tanstack/react-routerwithoutreact-start. React Router:react-routerin deps (also the value reported forframework: "remix"andframework: "react-router-framework", since both use react-router internally; this is informational only, no matcher predicates on it for those frameworks). Vue Router:vue-routerin deps (Vite SPA / Quasar). SvelteKit:framework === "sveltekit"— file-based routing undersrc/routes/.compiler@vitejs/plugin-react-swc→ swc.@vitejs/plugin-react(no-swc) → babel. Next.js → swc unless.babelrcexists. TanStack Start → swc if@vitejs/plugin-react-swc(or@vitejs/plugin-react@6+) is in devDeps; babel otherwise. Remix v2 and React Router v7 framework mode → swc if@vitejs/plugin-react-swcis in devDeps; babel otherwise (both default to Babel via@vitejs/plugin-react). SvelteKit uses neither — the Svelte compiler runs through Vite (esbuild), and none of the rules above match — so the field is a don't-care for SvelteKit; report whatever the heuristic yields (it will not match any rule) and treat the value as not meaningful: the SvelteKit manifest entry does not key oncompiler.reactreactin deps or devDeps.vuevuein deps or devDeps.sveltesveltein deps or devDeps.packageManagerpackage-lock.json→ npm.yarn.lock→ yarn.pnpm-lock.yaml→ pnpm.bun.lock→ bun.routeEntriesApp Router: <root>/src/app/**/page.tsx. TanStack file-based:<root>/src/routes/**/*.tsx. Remix v2 or React Router v7 framework mode:<root>/app/routes/**/*.{tsx,jsx,ts,js}. SvelteKit:<root>/src/routes/**/*.svelte. None if no file-based routing detected.existing.libraryFirst match in deps/devDeps from the union of i18n libraries listed above. existing.configuredlingui.config.*present AND macro plugin wired in build config; ORnext-intlconfig present AND plugin wired; OR (Vue)createI18n(present insrc/i18n/index.*(Vite/Quasar) ordefineI18nConfig(ini18n.config.*(Nuxt) ANDmessageCompilerwired; OR (Paraglide)project.inlang/settings.jsonpresent ANDparaglideVitePlugininvite.config.*.existing.providerWiredLayout/main file imports and renders I18nProvider(Lingui) orNextIntlClientProvider(next-intl); OR (Vue)app.use(i18n)inmain.*(Vite) / boot file registered (Quasar) /@nuxtjs/i18nlisted inmodules(Nuxt); OR (Paraglide)paraglideMiddlewareinsrc/hooks.server.ts.existing.catalogsScaffoldedLocale directories with at least one message file exist. existing.stringsWrappedGlob source tree ( .{tsx,jsx,js,svelte}), sample up to 50 files, count files with bare markup text vs. files importing macros/message functions (for Paraglide,import { m } from '$lib/paraglide/messages.js'): > 80% imported → "yes", > 20% → "partial", else → "no".candidateFilesGlob src/**/*.{tsx,ts,jsx,js,svelte}, exclude tests/configs/.d.ts, grep each for: bare markup text (>Word<, including Svelte template text), user-visible attrs (placeholder=,aria-label=,title=,alt=), exported user-facing string literals. Return files with ≥1 match, sorted by match count desc.localeSignalsList existing locale dirs (e.g., src/locales/), env vars matching*LOCALE*, README mentions of language names.Write the JSON file and exit. Do not engage in conversation.
User-facing message (after the inspect subagent returns and
detection.jsonis written): "Scan done. Detected: {framework} + {router} ({compiler} compiler, {packageManager}). Existing i18n: {existing.library} ({existing.configured ? 'already configured' : 'not configured yet'}). Found {candidateFiles.length} files with hardcoded strings. Next, a few questions to shape the setup plan."If
existing.library !== "none", also surface: "Heads up — you already have{existing.library}in your dependencies. If it's compatible, we'll continue with it; if not, I'll flag it in the next step."
1.2 Apply compatibility hard-stops
Read detection.json. Apply these rules top-to-bottom. If any matches, STOP the orchestrator with the corresponding message — do not proceed to 1.3.
When stopping, prefix the message with Compatibility check — found a blocker: so the user sees a clear framing rather than an abrupt error.
| Condition | Stop message |
|---|---|
react === false AND vue === false AND svelte === false |
"i18n-guide currently supports React-based, Vue-based, and Svelte-based projects only. This project uses {framework}. No supported library available." |
framework === "cra" |
"Create React App is no longer supported by this skill. Migrate to Vite or Next.js, then re-run." |
existing.library is one of react-intl, i18next, react-i18next, next-translate, typesafe-i18n, i18next-vue, @tolgee/vue, fluent-vue |
"This project already uses {library}. Migrating between i18n libraries is out of scope for this skill. Either continue with {library} (use its native tooling), or remove it first and re-run." |
framework === "next" AND router === "pages" AND user wants Lingui |
(Surface only after library choice in 1.5) "Lingui setup does not currently cover the Next.js Pages Router. Use next-intl on Pages Router, or migrate to App Router." |
@remix-run/react in deps with major version < 2 (i.e. Remix v1) |
"Remix v1 is no longer supported by this skill. Upgrade to Remix v2 (@remix-run/* ≥ 2) or migrate to React Router v7 framework mode, then re-run." |
framework === "remix" AND (@remix-run/dev major.minor < 2.7 OR vite not in devDeps) |
"This Remix v2 project uses the classic compiler (pre-Vite). Lingui requires the Vite-based build. Upgrade to @remix-run/dev ≥ 2.7 and follow Remix's classic-compiler → Vite migration, then re-run." |
framework === "sveltekit" AND @sveltejs/kit major.minor < 2.3 |
"Paraglide's URL-based locale routing relies on SvelteKit's reroute hook, added in @sveltejs/kit 2.3.0 — your project is on an older version. Upgrade to SvelteKit ≥ 2.3, then re-run. (If you must stay below 2.3, a different, deprecated routing approach is required that this skill does not cover.)" |
svelte === true AND framework !== "sveltekit" |
"This skill currently supports Svelte only through SvelteKit (the Paraglide setup relies on SvelteKit's hooks and routing). A plain Vite + Svelte SPA is not yet supported. Adopt SvelteKit, or wait for SPA support, then re-run." |
Custom build pipeline (no vite.config, next.config, nuxt.config, quasar.config, or react-scripts) |
"This project uses an unsupported build pipeline. Lingui requires SWC or Babel; next-intl requires Next.js; vue-i18n requires Vite, Nuxt, or Quasar." |
These are not hard-stops, but note for the Paraglide path:
- If
existing.library === "paraglide"ANDexisting.configured === true, do not run a from-scratch setup — route Phase 2 to the collapse / already-configured case (see "Phase 2 collapse-case"). - If
@inlang/paraglide-sveltekit(the Paraglide 1.x SvelteKit adapter) is in deps, this is a migration, not a fresh setup: Paraglide 2.x replaced the dedicated adapter with the framework-agnosticreroute+handlemodel. Flag it to the user before proceeding; the setup reference covers the migration steps.
1.3 Resolve supported stacks from manifest
Read manifest.json. Filter stacks[] entries whose match predicate is satisfied by detection. The result is the set of (library, variant) options the user can choose from in 1.5.
If the filtered list is empty, surface a STOP with: "Your stack is supported in principle but no manifest entry currently matches. Detected: {summary}. File an issue or pick a different setup."
1.4 Branch recommendation
If git.isRepo === true AND git.branch is one of main, master, develop:
You're on
{branch}. Setup will modify several files, so I'd recommend a dedicated branch — easier to review or revert later:git checkout -b chore/i18n-setupWant me to create this branch when Phase 2 starts, stay on
{branch}, or use a different name? (No git commands run yet — just answering the question.)
Record the answer in decisions.
1.5 Library choice
User-facing message (before showing options): "Picking the i18n library. Based on {framework} + {router}, my recommendation is {recommended-library} — {one-line rationale from the table below}. You can override if you have a strong preference."
Show the user the list of supported variants from 1.3, with the recommendation marked. Recommendation rules (apply first match):
| Detection | Recommendation | Rationale |
|---|---|---|
framework === "next" |
next-intl | Purpose-built for Next.js; first-class App Router (RSC + middleware) and Pages Router support. Uses ICU MessageFormat. No compile step. |
framework === "next" AND user wants compile-time extraction |
Lingui (alternative) | Compile-time macros, zero-runtime-overhead translations. |
framework === "nuxt" |
vue-i18n via @nuxtjs/i18n | The Nuxt module wraps vue-i18n with SSR-aware routing, lazy-loaded locale catalogs, and locale meta via useLocaleHead. The canonical Nuxt choice. |
framework === "quasar" OR (vue === true AND framework === "vite") |
vue-i18n | The official Intlify library and de-facto standard across Vue 3 projects. Composition API + ICU via custom messageCompiler. |
framework === "remix" |
Lingui | Remix v2 (≥ 2.7, Vite-based) ships with no first-party i18n primitive. Lingui plugs in via @lingui/vite-plugin, gives compile-time extraction, and aligns with Remix's per-route loader pattern (dynamic catalog import per route). |
framework === "react-router-framework" |
Lingui | React Router v7 framework mode is the same shape: Vite + loader + root <html> rendering. Lingui's per-route catalogs map cleanly onto the routes config. |
framework === "sveltekit" |
Paraglide JS | First-party Svelte CLI add-on (sv add paraglide); compiler-based with tree-shaken messages; SSR-correct via AsyncLocalStorage; integrates through reroute + handle hooks. |
| anything else (vite + react, tanstack-start, etc.) | Lingui | The only library with reference support for non-Next.js React stacks today. |
Use AskUserQuestion if multiple variants apply. If only one variant matches, surface the choice as confirmation rather than a multi-option prompt.
1.6 Journey scope
User-facing message (before asking): "Which phases do you want to run? I've pre-checked the ones that make sense given what's already in the project."
Ask which phases to run. Defaults derived from existing:
- If
existing.configured === false→ setup is suggested - If
existing.stringsWrapped !== "yes"ANDcandidateFiles.length > 0→ convert is suggested - Globalize-now is opt-in only; default unchecked
Use AskUserQuestion with three multi-select options (setup / convert / connect translation platform) and the inferred defaults pre-checked.
1.7 Setup choices
User-facing message (before asking): "A few setup details. Locales drive which catalog folders we scaffold and which CLI flags we pass; the routing strategy determines how locale shows up in URLs; setup mode is just how chatty I should be while running Phase 2."
If setup is in scope, collect:
- Setup mode — guided (per-step explanations, consent gates on file modifications) vs. unguided (run end-to-end, summarize at end)
- Source locale — default to
localeSignalsfirst existing oren - Target locales — multi-input. Suggest from
localeSignals.existingLocaleDirsand README hints - Routing strategy — only if file-based routing is detected; ask: prefix-based (
/en/...) or domain-based or none - Catalog format (Paraglide only) — defaults to PO (gettext) and do not ask for a fresh setup. PO is the default because a
.pocatalog carries#.translator comments that flow to the Globalize platform (the single biggest quality lever for AI translation), which the ICU-JSON model cannot. Setdecisions.setup.catalogFormat = "po"silently. The only time to surface a choice is an already-configured Paraglide project on ICU-JSON (existingmessages/*.json+@inlang/plugin-icu1): ask whether to convert to PO (recommended — a lossless migration, since both formats use ICU bodies; see "Phase 2 collapse-case" → migration) or keep ICU-JSON (catalogFormat = "json"). Omit entirely for non-Paraglide libraries.
Record under decisions.setup.
1.8 Convert choices
User-facing message (before asking): "What's this app about? One-sentence answer is fine. I'll pass it to the wrapping workers in Phase 3 so the translator comments they add have the right context (e.g., '[Cart]', '[Onboarding tooltip]')."
If convert is in scope, ask the user to confirm the app domain. Infer from package.json description, README, route names, or component names. Default suggestion + freeform override.
The domain string flows into wrap-subagent prompts so they write better translator comments. For Paraglide on the default PO catalog format, .po entries carry #. translator comments, so the domain informs comments and key naming, exactly as for Lingui/next-intl. For Paraglide on the ICU-JSON catalog format (decisions.setup.catalogFormat === "json", which has no translator-comment field), the app domain instead informs key naming only — it helps the wrap subagent choose descriptive, context-encoding keys (e.g. cart_remove_button), the only disambiguation lever available there.
1.9 Globalize-now choices
User-facing message (before asking): "Translation-platform setup uses Globalize.now. I just need a project name and which git provider hosts your repo — both have sensible defaults from your
package.jsonand git remote."
If connect translation platform is in scope, collect:
- Project name — default = repo name (from
package.json). - Repo provider — auto-detect from
git.remote(github.com → GitHub, gitlab.com → GitLab); confirm.
1.10 Optional steps
User-facing message (before asking): "Optional add-ons. None of these are required, but the passive coding rules (an
@importline in yourCLAUDE.md) are recommended — they keep me from re-introducing hardcoded strings on future edits."
Multi-select for setup-time optionals: ESLint plugin, CI/CD integration (extract+compile in build), test setup wrapper, install passive coding rules (@import line in target CLAUDE.md).
1.11 Generate plan.md + manifest-snapshot.json + decisions.md
User-facing message (before writing): "Got everything I need. Writing your plan to
.globalize/plan.mdand your choices to.globalize/decisions.mdso this run is auditable and resumable."
Write the three artifacts to .globalize/. See "Plan and decisions formats" below for shape.
Copy the chosen manifest entry verbatim to .globalize/manifest-snapshot.json so subsequent runs and subagents read a stable snapshot, not the live manifest.
1.12 Render plan + final go
Show plan.md to the user as a checklist. Ask:
"Here's the plan. Ready to execute? (yes / cancel / edit) Once you say yes, I won't pause for more questions unless a subagent gets stuck or finishes a phase."
Cancel writes nothing further. Edit re-enters the relevant 1.x step. Yes proceeds to Phase 2.
Phase 2 — Setup
Single setup subagent. Orchestrator installs packages on the main thread first, then pre-creates the progress file and dispatches the subagent in the background.
User-facing message (at Phase 2 start): "Starting Phase 2 — setup. First I'll install the i18n packages on my main thread so your lockfile stays in sync, then I'll dispatch one background worker that wires your build config, sets up the provider, scaffolds catalog folders for your locales, and verifies with a typecheck and build. I'll show progress as a checklist that updates every ~30 seconds. If the worker hits something it can't decide on its own, it'll pause and ask."
2.0 Install packages (main thread)
Read manifest-snapshot.json's packages.runtime and packages.dev. Run the install commands in the foreground using the package manager from detection.json. Stream output to the user.
| Package manager | Runtime command | Dev command |
|---|---|---|
npm |
npm install <pkgs> |
npm install -D <pkgs> |
yarn |
yarn add <pkgs> |
yarn add -D <pkgs> |
pnpm |
pnpm add <pkgs> |
pnpm add -D <pkgs> |
bun |
bun add <pkgs> |
bun add -D <pkgs> |
Wrap each <pkgs> entry in single quotes when constructing the shell command — the manifest pins use ^ (e.g. next-intl@^4), and zsh interprets unquoted ^ as a glob negation operator under EXTENDED_GLOB (common on macOS via oh-my-zsh). Emit npm install 'next-intl@^4' rather than npm install next-intl@^4. The single quotes are inert under bash/dash and prevent zsh expansion.
Skip the runtime or dev command if its package list is empty. If the install command fails (network error, registry rejection, lockfile conflict), stop the run with the error — do not advance to 2.1.
Running on the main thread keeps the install outside the subagent sandbox, so the user's lockfile stays in sync. The setup subagent in 2.2 will not re-install these packages.
User-facing message (before running): "Installing the i18n packages on my main thread first so your lockfile stays in sync — I'll run
{install command}and stream the output."
2.1 Pre-create progress file
{
"subagentId": "setup", "phase": 2, "status": "pending",
"plan": [...steps from plan.md...],
"completed": [], "current": null,
"startedAt": null, "updatedAt": "<now>"
}
2.2 Dispatch setup subagent (background)
Subagent prompt skeleton:
You are executing Phase 2 (Setup) of an i18n journey. Read
.globalize/decisions.md,.globalize/detection.json,.globalize/plan.md, and.globalize/manifest-snapshot.jsonfor full context.Your plan steps are listed in
.globalize/progress/setup.jsonunderplan. Execute them in order.Read these reference files for variant-specific instructions:
- {paths from manifest-snapshot.references.setup, joined}
- Coding rules to install (if applicable): {manifest-snapshot.references.code joined}
Packages already installed. The orchestrator ran the package install on the main thread (Phase 2.0) for the manifest's
packages.runtimeandpackages.devbefore dispatching you. Do not runnpm install/yarn add/pnpm add/bun addfor those packages. If a reference's setup instructions list an install command for them, treat it as already done and move on. Only flag an extra install if the reference explicitly calls for a package that is not in the manifest'spackages(e.g., a pinned remediation version after a build failure or an opt-in extra) — in that case, writestatus: "needs_decision"with aneedsDecision: { step: "extra_install", question: "An extra package install is needed: <command>. Run it on the main thread?", options: ["yes", "skip"] }and exit so the orchestrator runs it on the main thread.Progress reporting: After each step transition, atomically update
.globalize/progress/setup.json(write<file>.tmpthenmv). Setstatus: "running"on first update; populatecompleted,current,currentDetail,filesCreated,filesModified,updatedAt.Ambiguity protocol: If you hit a case the references don't cover (e.g., two layout files, custom config shape), do NOT improvise. Write
status: "needs_decision"with aneedsDecision: { step, question, options }object and exit. The orchestrator will ask the user and re-dispatch you.Verification: After all steps, run the project's typecheck (
tsc --noEmitif TypeScript) and build command. Capture pass/fail inresult.verificationResult. Setstatus: "succeeded"or"failed"accordingly.
2.3 Poll progress
While progress/setup.json is in running state, wake every 30–60 seconds, read the file, update the user-visible todo list (one todo per plan step), and surface currentDetail as a transient status hint.
2.4 On completion
succeeded→ archiveprogress/setup.jsontoprogress/archive/<timestamp>/setup.json, render summary (files created, files modified, verification result), advance to Phase 3. User-facing wrap-up:"Setup verified — typecheck and build are clean. Files created: {N}, files modified: {M}. Moving on to Phase 3 — wrapping your hardcoded strings."
failed→ archive, render error, ask user how to proceed (retry, edit plan, abort). User-facing wrap-up:"Setup hit an error during
{step}: {one-line error summary}. Want me to retry, edit the plan, or stop here?"needs_decision→ surface the question to the user, capture answer, append todecisions.md, re-dispatch the same subagent (it reads existingprogress/setup.jsonand resumes fromcompleted). User-facing framing:"The setup worker paused — it needs you to decide on
{question}. Once you answer I'll send it back to finish from where it left off."
Phase 2 collapse-case
If existing.configured === true, plan.md reduces Phase 2 to a verify-and-complete plan (verify what exists, add only what's missing — no from-scratch create_config): verify_config, verify_provider, add_missing_locale_dirs, a library-appropriate catalog step, and build_verification. The catalog step follows the variant's catalog workflow (see its references.setup): a compile-time library (Lingui) re-runs extract_compile to regenerate runtime catalogs from source; a runtime-catalog library (next-intl, vue-i18n) loads message files directly with no compile step, so the step is verify_catalogs — confirm the existing catalog files parse and cover every locale; a compile-from-catalog library (Paraglide) re-runs paraglide_compile (npx '@inlang/paraglide-js@^2' compile --project ./project.inlang --outdir ./src/lib/paraglide) to regenerate src/lib/paraglide/ from the hand-authored messages/{locale}.{json,po} catalogs. Same dispatch pattern.
Paraglide ICU-JSON → PO migration collapse-case. If existing.library === "paraglide", existing.configured === true, the project currently has messages/*.json (ICU-JSON / @inlang/plugin-icu1), and the user chose decisions.setup.catalogFormat === "po", Phase 2 is a format migration, not a from-scratch setup. The plan steps are: migrate_settings (swap the icu1 module + key for the PO module + plugin.globalizeNow.po with "messageFormat": "icu"), migrate_catalogs (rewrite each messages/{locale}.json → messages/{locale}.po — lossless: "key": "ICU body" → msgid "key" / msgstr "ICU body", carrying interpolation/plural/select verbatim; add the msgid "" header block; delete the old .json), paraglide_compile, and build_verification. The migration is driven by the Migration: existing ICU-JSON → PO section of references/languages/js-ts/frameworks/sveltekit/paraglide.setup.md (the default PO setup file). The per-file migrate_catalogs rewrite is independent across locales, so dispatch it as parallel background subagents (one per messages/{locale}.json). After migrating, run the plural-render check from that file's Verify step — a missing "messageFormat": "icu" or a botched ICU escape fails silently (renders raw ICU), so the build passing is not sufficient.
Phase 3 — Convert
Multiple wrap subagents in parallel, then one verify subagent.
User-facing message (at Phase 3 start): "Starting Phase 3 — converting hardcoded strings. I'm splitting {file count} files across {N} workers that run in parallel — each one walks its assigned files, wraps user-visible strings with the right macro, and adds short translator comments where context isn't obvious. After they all finish, a final verify worker runs extract + compile + build to make sure everything still type-checks and the catalog is clean."
3.1 Pre-create progress files
For each wrap-N subagent declared in plan.md, write progress/wrap-N.json with status: "pending" and the planned per-file step list. Write progress/verify.json with status: "pending" and verify plan.
3.2 Dispatch wrap subagents in parallel
Send all wrap subagents in a single Agent tool message so they launch in parallel. Each wrap subagent prompt includes:
You are wrapping hardcoded UI strings with the project's i18n macros. Read
.globalize/decisions.mdand.globalize/detection.jsonfor context. App domain: {decisions.appDomain}.Your assigned files (process in order; layout/shell first, then shared, then pages, then utilities): {numbered list from plan.md for this partition}
Read these reference files for macro guidance: {paths from manifest-snapshot.references.convert}.
For each file: identify translatable strings, wrap with the correct macro, add translator comments inline per the rules in the reference. (Paraglide is key-authored with no macro — instead of wrapping, author a descriptive, context-encoding key plus its catalog entry and replace the string with the
m.key()call. On the default PO catalog format, author the entry intomessages/{baseLocale}.poas#.comment +msgid "key"+msgstr "ICU body"—.pocarries#.comments, so DO add them, followingreferences/languages/js-ts/frameworks/sveltekit/paraglide.convert.md. On the ICU-JSON catalog format (decisions.setup.catalogFormat === "json"), the inlang/ICU JSON model has no translator-comment field, so do NOT add comments — descriptive key naming is the only disambiguation lever; followreferences/languages/js-ts/libraries/paraglide/json-format.convert.mdinstead.) Update.globalize/progress/wrap-N.jsonafter each file (atomic write). Do NOT runextractorcompile— that runs once after all wrap subagents complete.Ambiguity protocol and progress schema as in Phase 2.
3.3 Poll all wrap subagents
Wake every 30–60s, read all wrap-N.json files, update the user-visible todo list (per-file todos under each subagent group). Surface aggregated progress: "wrap-1: 3/8 files, wrap-2: 2/5 files, wrap-3: 4/4 ✓".
3.4 Wait for all wrap subagents to terminate
If any returns needs_decision, pause polling, surface to user, re-dispatch as in Phase 2.
If any returns failed, surface error. The verify subagent should still run on whatever files were successfully wrapped — don't block extraction on a single partition failure unless catastrophic.
3.5 Dispatch verify subagent (background)
You are verifying the convert phase. Read
.globalize/decisions.mdfor catalog format and locales. Read manifest snapshot for library.Plan steps depend on the library's catalog model:
- Compile-time extraction (Lingui) and runtime-catalog (next-intl/vue-i18n): extract_clean, compile, build_check, comment_review_pass.
- Compile-from-catalog (Paraglide): paraglide_compile, build_check. There is no extract step (keys are authored by hand, not extracted). On the default PO format
.pocarries#.comments, so a comment_review_pass over the base.podoes apply, plus an ICU plural-render sanity check (see below). On the ICU-JSON format (catalogFormat === "json") there is no comment_review_pass (the inlang/ICU JSON model has no translator-comment field).For Lingui / next-intl / vue-i18n:
- Run
npx lingui extract --clean(Lingui) ornpx next-intl extractif applicable. Capture errors. Atomically updateprogress/verify.jsonafter this step.- Run
npx lingui compile(with--typescriptif TS). Capture errors.- Run the project's typecheck and build command. Capture pass/fail.
- Read the extracted catalog. For entries lacking translator comments where the heuristic in the reference says one should exist (single-/two-word phrases, action labels without object, domain-sensitive terms), edit the source file to add the missing comment.
For Paraglide:
- Run
npx '@inlang/paraglide-js@^2' compile --project ./project.inlang --outdir ./src/lib/paraglide(both flags; single-quoted pin so zsh'sEXTENDED_GLOBdoes not eat the caret). Capture errors. Atomically updateprogress/verify.jsonafter this step.- Run the project's typecheck and build command. Capture pass/fail. (No extract step.) Default PO format: (a) inspect a compiled plural message (
src/lib/paraglide/messages/<key>.js) to confirm it emits CLDRregistry.plural(...)branches and not the raw{count, plural, …}source as a literal — raw source means"messageFormat": "icu"is missing or anmsgstris malformed (both fail silently); (b) run a comment-review pass over the basemessages/{baseLocale}.po, adding#.comments where the reference's heuristic says one should exist. ICU-JSON format (catalogFormat === "json"): skip both — there is no comment field, and the ICU1 plugin already fails the build on malformed ICU.Write
resultwith{ catalogPath, totalMessages, extractOk, compileOk, buildOk, commentsAdded }. For Paraglide, setextractOktonull; setcommentsAddedto the count added on the default PO format and tonullon ICU-JSON; report compile success undercompileOk.
3.6 Cost estimate (Phase 3 → 4 bridge)
After verify succeeds, parse the extracted catalog to compute word count. Show the user:
"Phase 3 complete. Catalog: {totalMessages} messages, ~{wordCount} words. Translating into {N} target locales (
{targets}) would cost roughly ~${estimate} on Globalize.now."
If decisions.scope.globalize === true, advance to Phase 4 with:
"Moving on to Phase 4 — connecting Globalize.now."
Otherwise, end with:
"Skipping the translation-platform step for now. Re-run
i18n-guidewithconnect translation platformchecked when you're ready to wire it up."
Phase 3 collapse-cases
existing.stringsWrapped === "yes"→ skip wrap subagents; run only verify.existing.stringsWrapped === "partial"→ Phase 1 candidate list already excluded already-wrapped files.
Phase 4 — Globalize-now (optional)
Auth must stay on the main thread (interactive). Project + repo creation runs in a subagent.
User-facing message (at Phase 4 start): "Phase 4 — connecting Globalize.now. The first three steps run here in the foreground because they need you: installing the CLI, signing in (a browser window will open for device-flow auth), and confirming your org. After that, a background worker creates the project, configures languages, connects your repo, and sets the catalog file patterns."
4.1 Install CLI (main thread)
If @globalize-now/cli-client not installed, run npm install -g @globalize-now/cli-client (or npx form, depending on user preference). Show output.
4.2 Authenticate (main thread)
Run globalize login. This is interactive — opens browser device flow. Wait for completion.
4.3 Verify org (main thread)
Run globalize org current (or equivalent). Show org name. Confirm with user.
4.4 Pre-create progress/globalize.json
Plan: create_project, configure_languages, connect_repo, configure_patterns. (Glossaries and styleguide skipped in v1.)
4.5 Dispatch project subagent (background)
You are creating a Globalize.now project and connecting the repository. CLI is installed and authenticated. Read
.globalize/decisions.mdfor project name + locales. Read.globalize/detection.jsonfor repo identifier (git.remoteparsed).Run, in order:
globalize project create --name "{name}" --source {sourceLocale} --targets {targetLocales}— capture project ID and URL.globalize project languages list— verify all targets accepted.globalize repo connect --provider {gh|gl} --repo {owner/repo}— wire repo.globalize repo patterns set --source {catalogPath} --output {targetCatalogPattern}— configure paths from Phase 3 verify result. The file format follows the catalog: for Paraglide it ispowith catalog pathmessages/{locale}.poby default; ifdecisions.setup.catalogFormat === "json"it isjson-flatatmessages/{locale}.json. (Other libraries:pofor Lingui,json-nestedfor next-intl, etc.) Pass the format with the pattern if the CLI form in use takes an explicitfileFormat(seeglobalize-now-cli-use).Update
progress/globalize.jsonafter each step. Finalresultincludes{ projectId, projectUrl, repoConnected, patternsConfigured }.
4.6 Poll, surface, finish
Standard polling. On completion, show project URL and a one-line summary:
"All set. Project created: {projectUrl}. Repo connected, catalog patterns wired. From here on, use the
globalize-now-cli-useskill (or theglobalizeCLI directly) for ongoing translation work — pulling translations, tracking status, requesting new languages."
Plan and decisions formats
plan.md
Markdown with YAML frontmatter for metadata. Body uses strict checklist syntax (- [ ] step_id) so the orchestrator can parse it.
Skeleton:
---
version: 1
createdAt: <ISO timestamp>
manifestSnapshot: .globalize/manifest-snapshot.json
detection: .globalize/detection.json
decisions: .globalize/decisions.md
---
# i18n Setup Plan
Stack: **{framework + router}** + **{library}** (variant: `{variant-id}`)
Branch: {decision summary}
## Phase 2 — Setup
Subagent: `setup`
Progress: `.globalize/progress/setup.json`
References:
- {paths joined from manifest.references.setup}
- {paths joined from manifest.references.code}
Orchestrator-owned steps (main thread, before subagent dispatch):
- [ ] install_packages_main_thread
Subagent steps:
- [ ] checkout_branch
- [ ] create_config
- [ ] build_tool_integration
- [ ] provider_wiring
- [ ] language_switcher
- [ ] scaffold_catalogs
- [ ] extract_compile <!-- compile-time libraries only (Lingui); runtime-catalog libraries (next-intl, vue-i18n) consume the scaffolded catalogs directly — omit this step. Compile-from-catalog libraries (Paraglide) use `paraglide_compile` instead — see below. -->
- [ ] paraglide_compile <!-- compile-from-catalog libraries only (Paraglide): `npx '@inlang/paraglide-js@^2' compile --project ./project.inlang --outdir ./src/lib/paraglide`; replaces extract_compile, omit for all other libraries -->
- [ ] install_coding_rules
- [ ] {optional steps if opted in}
- [ ] build_verification
## Phase 3 — Convert
Partitions: {N} wrap subagents covering {file count} files
### wrap-1 ({subtree summary})
- [ ] {file path}
- [ ] {file path}
…
### verify
<!-- compile-time extraction (Lingui) and runtime-catalog (next-intl, vue-i18n): -->
- [ ] extract_clean
- [ ] compile
- [ ] build_check
- [ ] comment_review_pass
<!-- compile-from-catalog (Paraglide) instead: no extract step, no comment_review_pass (inlang/ICU has no comment field):
- [ ] paraglide_compile
- [ ] build_check
-->
## Phase 4 — Globalize-now
Steps:
- [ ] create_project
- [ ] configure_languages
- [ ] connect_repo
- [ ] configure_patterns
decisions.md
Markdown with YAML frontmatter, sections per category. Doubles as a project record if committed (note: .globalize/ is gitignored by default — see Workspace section).
---
createdAt: <ISO>
---
# i18n Setup Decisions
## Library
**{library}** (recommended; user accepted | user override)
## Scope
- [x] Setup
- [x] Convert existing strings
- [x] Connect Globalize.now
## Branch
Create new branch: `chore/i18n-setup`
## Setup mode
**Unguided**
## Locales
- Source: `en`
- Targets: `de`, `fr`, `es`
## Routing strategy
Prefix-based
## App domain
{user-confirmed domain}
## Optional setup steps
- [x] ESLint plugin
- [ ] CI/CD integration
- [ ] Test setup wrapper
- [x] Install passive coding rules (@import)
## Globalize-now
- Project name: `{name}`
- Repo provider: GitHub
Progress file schema (per subagent)
{
"subagentId": "setup",
"phase": 2,
"status": "pending|running|succeeded|failed|needs_decision",
"startedAt": "<ISO>", "updatedAt": "<ISO>",
"plan": ["step_id_1", "step_id_2"],
"completed": ["step_id_1"],
"current": "step_id_2",
"currentDetail": "modifying vite.config.ts",
"skipped": [],
"filesCreated": [],
"filesModified": [{ "path": "...", "summary": "..." }],
"errors": [],
"needsDecision": null,
"result": null
}
Reading the reference files (subagents)
Reference files under references/languages/.../*.md walk through their variant's setup or convert work linearly via section headings (e.g., "Packages", "Build Tool Integration", "Provider Setup", "Language Switcher"). Follow them in document order — section headings are the authoritative ordering, not the orchestrator's plan step IDs (those are higher-level phase markers used by the polling loop).
Some references include catalog-format sub-references for an alternate format (e.g., references/languages/js-ts/libraries/next-intl/po-format.setup.md, and for Paraglide references/languages/js-ts/libraries/paraglide/json-format.{setup,convert,code}.md). When the user is on the alternate format, substitute that reference's snippets in place of the default examples — the variant reference itself flags the substitution points. For Paraglide specifically: the default is PO, so the base files (frameworks/sveltekit/paraglide.{setup,convert}.md and libraries/paraglide/code.md) are the PO path and apply as-is. Only when decisions.setup.catalogFormat === "json" does the setup subagent apply json-format.setup.md over paraglide.setup.md, the wrap subagents apply json-format.convert.md over paraglide.convert.md, and the coding-rules add-on install json-format.code.md instead of code.md (see the add-ons reference).
If a reference's instructions appear to require user input that wasn't collected in Phase 1, do not improvise: write status: "needs_decision" to your progress file and exit so the orchestrator can ask.
Subagent dispatch mechanics
- Use the Agent tool with
run_in_background: truefor all phase subagents (Phase 2 setup, Phase 3 wrap and verify, Phase 4 project). The orchestrator polls progress files instead of waiting on the subagent's terminal output. - For Phase 1.1 (inspect), foreground (blocking) is fine — the subagent only writes one JSON file and returns.
- All wrap subagents in Phase 3 must be dispatched in a single tool-use message to launch in parallel.
- Subagents must use atomic file writes (write
<file>.tmp, thenmv) when updating progress files. - Each subagent reads the same set of inputs from
.globalize/: it does not need orchestrator-side state passed via prompt beyond pointers to those files.
Edge cases
- Multiple frameworks detected (e.g., both
nextandvitein deps): Next.js takes precedence — use Rule 1 in 1.5. - Monorepo: Detect from the closest
package.jsonto the working directory. Do not aggregate deps across workspace packages. - User overrides recommendation with a library that doesn't match a manifest entry: surface the supported list and ask them to pick one of those.
.globalize/already exists from a prior run: see Resumability section above.- No
gitrepo: skip 1.4 (branch recommendation) silently. decisions.mdis hand-edited between runs: re-validate againstmanifest-snapshot.jsonbefore re-dispatching subagents — fail loudly if invalid.