Globalize guide
Set of skills for globalizing and localizing app
npx -y skills add globalize-now/globalize-skills --skill globalize-guideAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Drive the full internationalization journey for a project — detect the stack, recommend a library, set up the chosen library, wrap existing strings, and connect a translation platform (Globalize.now, default-on). Use when the user asks to add or configure i18n, internationalization, localization, multi-language support, or translations — including when they explicitly mention LinguiJS, Lingui, next-intl, "wrap strings", "find hardcoded text", "make my app translatable", or "set up translations". Triggers on generic phrasings like "add i18n", "internationalize my app", "add translations", "multi-language support", "localize my app", and on library-specific phrasings like "set up Lingui", "set up next-intl", "use Lingui in this project". Does not trigger for unrelated CSS-only RTL questions (those use css-i18n) or for managing an already-set-up Globalize.now project (those use globalize-now-cli-use).
SKILL.md
98.0 KB, as published. Nobody here has run it
i18n Orchestrator
This skill drives the full i18n journey through five stages:
| Stage | Goal |
|---|---|
| 1 — Inspect and decide | Detect stack, ask all user questions, generate an executable plan |
| 1.5 — Globalize account (upfront) | Install CLI + sign in + verify org via globalize-now-account-setup |
| 2 — Setup | Install + configure the chosen library |
| 3 — Convert | Wrap hardcoded strings, extract + compile catalogs |
| 4 — Globalize project (end) | Create project + connect repo + set patterns via globalize-now-project-setup |
Stages 1.5 and 4 only run when the user keeps Globalize connection in scope (default-on; see 1.6). The Globalize work is split deliberately: account creation is interactive and project-independent, so it runs upfront while the user is engaged; project + repo setup needs the converted catalog paths, so it runs at the end.
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. One carve-out: the interactive Globalize steps (account sign-in in 1.5, project + repo connection in Phase 4) run on the main thread by delegating to the globalize-now-account-setup / globalize-now-project-setup skills via the Skill tool — the orchestrator never inlines Globalize CLI commands itself (those skills own the authoritative command surface).
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 1.5/2/3/4
manifest-snapshot.json # frozen copy of the chosen manifest entry
globalize-inputs.json # resolved Globalize project inputs (written in Phase 4, read by globalize-now-project-setup)
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
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.
Globalize auth is global, not local. Sign-in state lives in ~/.globalize/config.json (or the GLOBALIZE_API_KEY env var), not in .globalize/, so never infer the account step's done-ness from a local marker — it may have been configured outside this run. On resume, just re-delegate: both Globalize skills are idempotent. The account skill self-skips when auth status already returns valid credentials; the project skill lists-before-creates (projects list, github installations, existing repo connections), so re-invoking it fast-paths whatever is already done. That idempotency is why Phase 4 needs no progress/globalize.json.
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 decision this skill collects lives in Phase 1 — Phases 1.5/2/3/4 are execution (the two Globalize stages delegate their own sign-in / browser interaction to the globalize-now-account-setup / globalize-now-project-setup skills).
User-facing message (orchestrator kickoff, before 1.1): "Hey — I'll walk you through internationalizing this project: inspect the stack, sign in to Globalize, set up the library, wrap your hardcoded strings, and finally create your Globalize project + connect the repo. 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 (including whether to connect Globalize — it's on by default, and you can opt out)."
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.
First decide the project language, applying these rules in order, first match wins (the order is load-bearing — a root
package.jsonmust beat native signals so hybrid/web wrappers route to JS):
- A
Gemfile/Gemfile.lock(containingrails),bin/rails, orconfig/application.rbis present → ruby (read the Ruby signals below instead of the JS ones).- A root
package.jsonis present → js-ts (read the JS signals; this deliberately includes hybrid/web-wrapper apps — React Native, Capacitor, Cordova, Ionic — which ship a nativeandroid/orios/folder but localize in the JS layer, so they belong on the JS path or its §1.2 stop, never a native path).- Native-Android Gradle signals are present (
AndroidManifest.xml, OR abuild.gradle/build.gradle.ktsapplyingcom.android.application/com.android.library, plusgradlew/settings.gradle) AND there is no rootpackage.jsonAND nopubspec.yaml→ android (read the Android signals below). (Apubspec.yamlmeans Flutter — it ships anandroid/Gradle subfolder but localizes via.arb, not nativestrings.xml; it is excluded here and stopped in §1.2.)*.xcodeproj/*.xcworkspace/Package.swift/*.swiftis present (and none of the above matched) → swift (read the Swift/Apple signals table below).- Otherwise → unknown.
For a js-ts project, 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:{ "language": "js-ts" | "ruby" | "android" | "swift" | "unknown", "framework": "next" | "vite" | "tanstack-start" | "remix" | "react-router-framework" | "nuxt" | "quasar" | "sveltekit" | "cra" | "rails" | "android" | "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" | "bundler" | "gradle", "platform": "ios" | "macos" | null, "buildSystem": "xcode" | "spm" | null, "uiFramework": "swiftui" | "uikit" | null, "version": string | null, "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" | "rails-i18n" | "android-strings" | "string-catalog" | "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 languageApply in order (first match wins): (1) Gemfile/Gemfile.lock(containingrails),bin/rails, orconfig/application.rb→ruby. (2) rootpackage.jsonpresent →js-ts(this beats native signals on purpose, so RN/Capacitor/Cordova/Ionic hybrids route to JS). (3) native-Android Gradle signals (AndroidManifest.xml, orbuild.gradle{,.kts}applyingcom.android.application/com.android.library, plusgradlew/settings.gradle) AND no rootpackage.jsonAND nopubspec.yaml→android. (4)*.xcodeproj/*.xcworkspace/Package.swift/*.swiftpresent (none of the above) →swift(read the Swift/Apple detection rules below, not these). (5) none →unknown. (The JS-path rules below apply only whenlanguage === "js-ts"; the Ruby table only whenlanguage === "ruby"; the Android table only whenlanguage === "android"; the Swift/Apple table only whenlanguage === "swift".)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, and display-copy string literals inside exported data/content modules — string values under object/array keys whose name matchesname,title,label,heading,subheading,description,summary,body,text,message,caption,placeholder,tooltip,alt,cta, orcontent(e.g.export const products = [{ name: '…', description: '…' }]). Deliberately narrow — do not treat identifier/config keys as display copy (id,slug,sku,key,href,src,type,variant,icon,role,className,testid); those are covered by thecode.mdskip-list and must not inflate the candidate set. This is a recall signal: a data module matching it becomes a wrap candidate so a Phase 3 subagent opens it; the subagent still applies the skip-list, so occasional over-match is harmless. 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.Ruby / Rails detection rules (apply only when
language === "ruby"; the JS rules above do not apply):
Field How to detect frameworkrailsgem inGemfile/Gemfile.lock, ORbin/rails, ORconfig/application.rb→rails. Otherwiseunknown(non-Rails Ruby — Sinatra, Hanami, plaini18ngem — is not supported; see 1.2).packageManagerbundler(Rails projects use Bundler +Gemfile).versionParse Gemfile.lock: the linerails (N.M.x)→ extractN.M(e.g."8.1"). Used for the soft EOL warning in 1.2 and therails-i18npin.nullif not resolvable.routernone(Rails routing is not modeled here; theversionfield is what the Rails path keys on).existing.libraryrails-i18ninGemfile/Gemfile.lock→rails-i18n; elsenone. (Rails' built-inI18nAPI is always present;rails-i18nadds CLDR plural data.)existing.configuredconfig.i18n.*keys (e.g.default_locale,available_locales) set inconfig/application.rbor aconfig/initializers/*.rb.existing.providerWiredAn around_action/switch_localeorI18n.with_localelocale switcher present inapp/controllers/application_controller.rb.existing.catalogsScaffoldedconfig/locales/*.{yml,rb}present with at least one populated, non-stub locale file (note split layouts —devise.en.yml, nested dirs — all auto-loaded by Rails).existing.stringsWrappedGlob app/views/**/*.erb,app/controllers/**/*.rb, sample up to 50 files, count files usingt(/l(helpers vs. files with bare user-visible text: > 80% using helpers → "yes", > 20% → "partial", else → "no".candidateFilesGlob app/views/**/*.erb,app/controllers/**/*.rb,app/mailers/**/*.rb,app/models/**/*.rb; grep for bare user-visible text and string literals not already wrapped int(/l(. Return files with ≥1 match, sorted by match count desc.localeSignalsList config/locales/files and the locale codes present;config.i18n.default_locale/available_localesvalues; README mentions of language names.Name-collision guardrail (Ruby): the gems
globalize,mobility, andtracotranslate DB/model content (per-row data like a product'sname), NOT UI strings, and are entirely unrelated to Globalize.now. Do not treatglobalize(the gem) as the Globalize.now platform. If any is present inGemfile/Gemfile.lock, record it inlocaleSignals.readmeHints(or a free-form note) as a detect-and-warn signal — it must never trigger UI-string i18n logic and is surfaced to the user in 1.2 but is non-blocking.Android detection rules (apply only when
language === "android"; the JS and Ruby rules above do not apply). "Moduleres/root" means the main module's resource dir — default toapp/src/main/res(note multi-module/flavor layouts:feature/src/main/res,src/<flavor>/res— surface them inlocaleSignalsbut target the app module in v1):
Field How to detect frameworkandroid(native Android — Gradle +AndroidManifest.xml).packageManagergradle.versionnull— Android emits no version-gated i18n code (string resources are stable across all API levels), so no version is captured.routernone(Android routing is not modeled here).existing.libraryandroid-stringsif<module>/res/values/strings.xmlexists; elsenone. (String resources are platform-built-in — there is no library to depend on.)existing.configuredAt least one target resource dir <module>/res/values-*/strings.xmlexists (a locale overlay is present).existing.providerWiredA locale-selection mechanism is wired: AppCompatDelegate.setApplicationLocales(in source, ORandroid:localeConfiginAndroidManifest.xmlwith ares/xml/locales_config.xml. (Optional — per-app language is an add-on;falseis normal.)existing.catalogsScaffolded<module>/res/values/strings.xml(the source catalog) exists.existing.stringsWrappedSample up to 50 files across <module>/res/layout/**/*.xmland**/*.{kt,java}; count files referencing resources (@string/,getString(,getText(,stringResource(,pluralStringResource() vs. files with bare user-visible literals: > 80% referencing → "yes", > 20% → "partial", else → "no".candidateFilesGlob <module>/res/layout/**/*.xml,<module>/res/menu/**/*.xml,<module>/res/xml/**/*.xml(grep bareandroid:text="literal",android:hint=,android:title=,android:contentDescription=with a non-@string/value), plus**/*.{kt,java}(string literals passed tosetText(,Text(,Toast, etc., not alreadygetString/stringResource). Return files with ≥1 match, sorted by match count desc.localeSignalsLocale codes parsed from <module>/res/values-*/dir qualifiers — normalize both legacy (values-pt-rBR) and BCP47 (values-b+sr+Latn) forms to BCP47; note any non-app-moduleres/dirs; README mentions of language names.Swift / Apple (iOS) detection rules (apply only when
language === "swift"; the JS, Ruby, and Android rules above do not apply):
Field How to detect language*.xcodeproj/*.xcworkspace,Package.swift, or*.swiftpresent →swift. (Only reached when there is no Gemfile-rails, no rootpackage.json, and no native-Android Gradle signals — a rootpackage.jsonroutes React Native / Capacitor / Flutter tojs-tsfirst.)framework/router/compiler/react/vue/svelte/typescriptNot meaningful for native Apple — set framework: "unknown",router: "none", and leavecompiler/react/vue/svelteasnull/false. No iOS manifest variant keys on any of them (iOS variants key onlanguage/buildSystem/platform/uiFramework).buildSystemPackage.swiftpresent and NO.xcodeproj/.xcworkspace→spm..xcodeproj/.xcworkspacepresent →xcode.uiFrameworkThe @mainentry point decides: a file withimport SwiftUI+ astruct …: App+@main→swiftui(even if it also uses aUIApplicationDelegateAdaptor).@UIApplicationMain/AppDelegate/SceneDelegate (a type conforming toUIApplicationDelegate/UIResponder) and/or.storyboard/.xibfiles with no SwiftUIApp→uikit. Genuine ambiguity → defaultswiftui. An SPM library with no app entry point →null. NEVER emit"mixed"(it is unroutable).platformiOS deployment target / IPHONEOS_DEPLOYMENT_TARGET/ iOS SDK →ios(the default for an app target). SPM library →null.packageManagernull(Swift has no npm-style package manager; SPM is the build system and is recorded underbuildSystem).existing.libraryA .xcstringspresent anywhere →string-catalog; elsenone.existing.configuredA .xcstringspresent.existing.catalogsScaffoldedA .xcstringspresent (same signal).existing.stringsWrappedSample *.swiftfiles: > 80% of user-visible literals usingString(localized:/Text("…")literals vs. bare user-visibleStringliterals → "yes", > 20% → "partial", else → "no".candidateFilesGlob **/*.swift(exclude tests andPackage.swift), grep each for user-visible string literals not already inString(localized:/Text(/NSLocalizedString(. Return files with ≥1 match, sorted by match count desc.localeSignals.lprojdirs; Info.plistCFBundleLocalizations/CFBundleDevelopmentRegion; existing.strings/.stringsdict/.xcstringsfiles; README language mentions.For a
swiftdetection,platform,buildSystem, anduiFrameworkMUST be populated (never left undefined) — the §1.3 matcher checks these structural keys by equality, and an undefined field never matches an iOS variant. Forjs-ts,ruby, andandroiddetections, all three arenull.Write the JSON file and exit. Do not engage in conversation.
User-facing message (after the inspect subagent returns and
detection.jsonis written): Forlanguage === "js-ts"(JS/TS): "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." Forlanguage === "ruby"(Rails —routeris "none" andcompileris not meaningful, so omit them): "Scan done. Detected: rails (bundler). 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." Forlanguage === "android"(native Android —routeris "none" andcompileris not meaningful, so omit them): "Scan done. Detected: android (gradle). Existing i18n: {existing.library} ({existing.configured ? 'string resources already present' : 'no string resources yet'}). Found {candidateFiles.length} files with hardcoded strings. Next, a few questions to shape the setup plan." Forlanguage === "swift"(iOS — router/compiler are not meaningful, so omit them): "Scan done. Detected: iOS ({uiFramework}, {buildSystem}). 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.
Evaluate first (signal stops): the three Android signal-stops in the "Android compatibility rules" block below — React Native, Capacitor/Cordova/Ionic, and Flutter — are checked before the generic rows in the table that follows, so a hybrid or Flutter project receives its specific, actionable message rather than the generic "React/Vue/Svelte only" stop (which would otherwise pre-empt it for a hybrid app that has no React/Vue/Svelte dependency).
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 |
|---|---|
language === "js-ts" AND react === false AND vue === false AND svelte === false | "globalize-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." |
language === "js-ts" AND 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." |
Ruby / Rails compatibility rules (apply only when language === "ruby"; the two JS rows above — React/Vue/Svelte and custom-build-pipeline — are guarded by language === "js-ts", so they apply only to JS projects and a Rails, Android, or Swift/iOS project does not falsely STOP there):
| Condition | Action |
|---|---|
language === "ruby" AND framework !== "rails" | STOP. "globalize-guide currently supports Ruby only through Rails (built-in I18n API + locale-rooted YAML). Non-Rails Ruby (Sinatra, Hanami, plain i18n gem) is not supported. Point me at a Rails app, or use the i18n gem docs directly." |
gettext_i18n_rails OR fast_gettext in Gemfile/Gemfile.lock | STOP. "This project uses gettext_i18n_rails — the catalog format is PO, not YAML. The v1 Rails path supports locale-rooted YAML only; the PO/gettext overlay for Rails is not yet supported. Proceed manually, or wait for the PO overlay." |
Name-collision warning (Ruby, non-blocking): if globalize, mobility, or traco was detected (the model/DB-content translation gems — unrelated to Globalize.now), surface but do not stop: "I found {gem} in your Gemfile. It translates DB/model content (per-row data), not UI strings, and is unrelated to Globalize.now. The i18n setup won't touch it, and its content won't be in the connected catalog. Proceeding with UI-string i18n." Never conflate the globalize gem with the Globalize.now platform.
Soft EOL warning (Ruby, non-blocking — NO emission gating): if language === "ruby" AND the detected Rails version is 7.1 or earlier: "This project is on Rails {version}, which reached end-of-life. The Rails path supports 6.1 → 8.1 at the same code level (no version-gated i18n branches) — the emitted code is identical — but running EOL Rails in production isn't recommended. Consider upgrading. Proceeding." The default target is Rails 8.1; support runs down to 6.1. There is no version-gated emission for Rails (clean contrast with the JS framework version branches above).
Android compatibility rules. The first three rows fire on signals (deps / files), independent of the detected language — the §1.1 rule already routes hybrid and Flutter projects away from language: "android", so these stops exist to give the user a clear, specific reason rather than a generic "no manifest entry matches":
| Condition | Action |
|---|---|
react-native in package.json AND an android/ folder present | STOP. "This is a React Native app. React Native localizes through JS i18n libraries (i18next, react-intl, Lingui), not native Android string resources. Native android-strings support targets native Kotlin/Java apps. Use the JS path, or wait for RN support." |
@capacitor/core, cordova, or any @ionic/* in package.json AND an android/ folder present | STOP. "This looks like a Capacitor/Cordova/Ionic hybrid app — its UI is localized in the web layer, not in native strings.xml. Run globalize-guide against the web UI (the JS path)." |
pubspec.yaml present (Flutter) | STOP. "This is a Flutter app. Flutter localizes via .arb files and gen_l10n, not native Android strings.xml. Flutter support isn't available yet." |
language === "android" AND a Kotlin/Compose Multiplatform layout (resources under commonMain / a compose.resources setup, no app/src/main/res) | Warn (non-blocking). "This looks like a Kotlin/Compose Multiplatform project, which uses a different resource mechanism (compose.resources) than standard Android res/values/strings.xml. v1 covers standard Android only — I'll proceed against the Android resource dirs I can find, but multiplatform resources won't be handled." |
No EOL / version warning for Android (clean contrast with the JS framework version branches and the Rails soft-EOL note): string resources are stable across all API levels, so there is no version to warn about and no version-gated emission. The only API-gated features — the b+ qualifier dir form (API 24+) and per-app language (API 33+) — are optional add-ons, never required.
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.
Swift / Apple (iOS) compatibility rules (apply only when language === "swift"; the two flipped JS rows above are exempted for Swift via their language === "js-ts" guard):
| Condition | Action |
|---|---|
language === "swift" AND a .xcstrings cannot be created and none exists (legacy .strings-only on a pre-Xcode-15 toolchain with no migration path) | Note (non-blocking where possible). Offer the Edit ▸ Convert to String Catalog migration; only STOP if there is genuinely no path to a catalog. |
language === "swift" AND a non-iOS Apple target out of v1 scope (macOS/watchOS/tvOS) with no iOS app and no Swift package | STOP (scope). "v1 of the iOS String Catalog path targets iOS apps and Swift packages. {platform} is out of scope — the shared catalog mechanics may still apply manually." |
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.
Matcher predicate (load-bearing). A match object mixes two kinds of keys, handled differently — every match key names a same-named detection field except library, which has no detection counterpart:
- Structural keys —
framework,router,compiler, and any other detection-state key an entry declares — must each equal the same-named field indetection. (Example:nextjs-app-router-*entries declareframework: "next",router: "app".) languageis structural but special-cased:- If
match.languageis present, it must equaldetection.language(sorails-yaml, which declaresmatch.language: "ruby", matches only whendetection.language === "ruby"). - If
match.languageis absent, treat it as"js-ts"— i.e. the entry matches only whendetection.language === "js-ts". All existing JS entries omitlanguage, so they keep matching exactly as before and are inert for Ruby projects.
- If
libraryis not a structural predicate. It is the identifier of the variant/option the entry offers — surfaced as a choice in §1.5. It is never matched againstdetection.existing.library(there is no top-leveldetection.library).detection.existing.librarydescribes prior setup state and is used only by the §1.2 stops and the §1.6 / Phase 2 already-configured handling — it never filters the candidate set. This is why two entries can share identical structural keys and differ only inlibrary(e.g.nextjs-app-router-linguiandnextjs-app-router-next-intl, bothframework: "next", router: "app"): a fresh Next app-router project (existing.library: "none") matches both, and the {lingui, next-intl} pair is surfaced as the §1.5 choice.platform,buildSystem,uiFrameworkare ordinary structural keys (each must equal the same-nameddetectionfield) that discriminate among the iOS variants — NOT a new special-cased matcher axis (onlylanguage's absent⇒js-tsdefault is special). Amatch.language: "swift"matches onlydetection.language === "swift", keeping the Swift, JS, and Ruby entry sets disjoint. No matcher-logic change beyond treating these three as plain structural keys.
This keeps the Ruby and JS entry sets disjoint by language: a Ruby detection can match only rails-yaml; a js-ts detection can match only the JS entries (never rails-yaml); a swift detection can match only the iOS variants.
Hand-trace (iOS). A {swift, xcode, ios, swiftui} detection selects only ios-swiftui-string-catalog: ios-uikit-string-catalog fails on uiFramework (uikit≠swiftui), ios-spm-string-catalog fails on buildSystem (spm≠xcode), every JS entry fails on absent⇒js-ts (≠swift), rails-yaml fails on language (ruby≠swift), and android-strings fails on language (android≠swift). A {swift, spm} detection (buildSystem spm, no app entry point) selects only ios-spm-string-catalog: the two app variants fail on buildSystem (xcode≠spm).
Net effect. A fresh Next app-router project yields {lingui, next-intl} candidates → §1.5 offers the choice. A fresh Rails project (existing.library: "none") yields exactly {rails-yaml} → §1.5 confirms "Rails built-in I18n (YAML)". A fresh native-Android project yields exactly {android-strings} (its match.language: "android" requires detection.language === "android") → §1.5 confirms "Android string resources (XML)". A fresh iOS project yields exactly one iOS variant → §1.5 confirms it (no multi-option prompt). A fresh js-ts detection never yields rails-yaml, android-strings, or an iOS variant (their match.language is ruby/android/swift, which fail).
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. |
framework === "rails" | Rails built-in I18n (YAML) | Rails ships a full I18n stack (t/l helpers, locale-rooted YAML at config/locales/, %{name} interpolation, CLDR plurals via rails-i18n); no third-party UI-string library needed. |
framework === "android" | Android string resources (XML) | Android ships localization built in — res/values/strings.xml + values-<qualifier> overlays, getString/stringResource accessors, positional %1$s args, native <plurals> (CLDR categories). No third-party library to install. |
language === "swift" | Apple String Catalog (built-in) | Apple ships a full localization stack (String Catalogs, String(localized:)/Text literals, .stringsdict/variations plurals via CLDR) built into the SDK — no third-party library or install needed. |
| 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. For language === "swift" exactly one iOS variant matches (see §1.3 hand-trace), so the choice is surfaced as a confirmation of the Apple String Catalog, not a multi-option prompt. The iOS variants are supportLevel: "experimental" — when confirming, tell the user so: e.g. "Setting up the Apple String Catalog path (built into the SDK — no install). Heads up: iOS/.xcstrings support is experimental — the live Globalize.now round-trip for this format hasn't been verified end-to-end yet, so double-check the Phase-4 connection."
1.6 Journey scope
User-facing message (before asking): "Which parts do you want to run? I've pre-checked the ones that make sense given what's already in the project. Connecting Globalize.now to translate your catalogs is included by default — you can uncheck it to skip the platform."
Ask which parts to run. Defaults derived from existing:
- If
existing.configured === false→ setup is suggested - If
existing.stringsWrapped !== "yes"ANDcandidateFiles.length > 0→ convert is suggested - Connecting Globalize is default-on — pre-check it. The user can uncheck to decline the platform; only then do stages 1.5 and 4 get skipped.
Use AskUserQuestion with three multi-select options (setup / convert / connect translation platform) and the inferred defaults pre-checked — including connect translation platform. Record the result under decisions.scope (decisions.scope.globalize is true unless the user unchecked it).
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 — for JS/TS stacks, only if file-based routing is detected; ask: prefix-based (
/en/...) or domain-based or none. For Rails (language === "ruby"): Rails detection setsrouter: "none", so the file-based gate would skip this question — ask it explicitly here instead. URL-locale routing embeds the locale in the path (scope "/:locale", e.g./en/books) and editsconfig/routes.rb+ addsApplicationController#default_url_options. Ask: URL-locale routing (prefix/:locale) or none. Unguided default = included (the Rails Guide's recommended locale-persistence approach). Record underdecisions.setupsorails.setup.mdStep 5 reads the decision instead of silently applying its own default. - 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 1.5 (when Globalize is in scope) or otherwise straight to Phase 2.
Phase 1.5 — Globalize account (upfront, interactive)
Gate: run this stage only if decisions.scope.globalize === true. If the user unchecked Globalize at 1.6, skip 1.5 and Phase 4 entirely and go to Phase 2.
This stage gets the user signed in to Globalize before any code changes, so the platform is ready the moment the catalogs are done. It is the one interactive Globalize step that has nothing to wait for — account creation needs no project data — so doing it now keeps the long Phase 2/3 work hands-off.
User-facing message (at stage start): "Before we touch your code, let's get you signed in to Globalize so it's ready when your catalogs are done. A browser window may open for device-flow sign-in. (Heads up: near the very end, when we connect your repo, you'll do one quick browser approval to authorize Globalize's GitHub/GitLab app — I'll remind you then.)"
Delegate to the account skill (main thread). Invoke the globalize-now-account-setup skill via the Skill tool. It runs install → sign-in → verify on the main thread (interactive device flow), and self-skips the login if auth status already reports valid credentials. Do not inline any CLI commands here — the skill owns them.
When it completes, record the authenticated org under decisions.md → ## Globalize-now → Authenticated org (a soft audit marker), and mark delegate_account_setup done in plan.md.
If globalize-now-account-setup is not installed (globalize-guide installed on its own), the Skill invocation fails. Tell the user:
"I couldn't load the
globalize-now-account-setupskill — install the Globalize skills withnpx skills add globalize-now/globalize-skills, then I'll continue." (The standard install co-installs all of them, so this is rare.)
If the user cancels sign-in (declines the browser step even though Globalize is in scope): don't block the run. Continue to Phase 2, and set a soft flag so Phase 4's auth status re-check re-offers account setup before the project step.
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. {for JS/TS:
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.; for Rails:I'll dispatch one background worker that installs the i18n gems with Bundler, wiresconfig/application.rb, scaffoldsconfig/locales/for your locales, sets up the locale switcher inApplicationController, and verifies by booting the app and parsing the source catalog.; for Android:Nothing to install — string resources are built into the platform. I'll dispatch one background worker that createsres/values/strings.xmlas your source catalog, scaffoldsres/values-<qualifier>/strings.xmlfor your target locales, optionally wires per-app language selection, and verifies by parsing the catalogs (and running Lint if the Android SDK is available).; for Swift:No packages to install — Apple's localization stack ships with the SDK. I'll dispatch one background worker that creates the String Catalog (Localizable.xcstrings), enables build-time string extraction (SWIFT_EMIT_LOC_STRINGS), registers your locales, and verifies the catalog is valid.} 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.
Lingui (v6+) requires Node ≥ 22.19 or ≥ 24 — it ships ESM-only and fails to load on older Node. If the resolved stack installs
@lingui/*, check the project's Node version (node -v, plus any.nvmrc/enginesfield) before installing. If it's older, tell the user and pause rather than installing a runtime they can't run.
| 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. For packageManager === "bundler" (Rails), both package lists are empty by design — gem installation is delegated to the setup subagent via rails.setup.md Step 2 (bundle install); the §2.0 install step is intentionally a no-op for Rails, not an error. For packageManager === "gradle" (Android), both lists are also empty by design — string resources are platform-built-in, so there is nothing to install; the §2.0 step is a no-op for Android too. For language === "swift" both package lists are likewise empty by design (native localization ships with the SDK — there is no third-party package to install), so §2.0 is a no-op for Swift too, not an error. 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.Rails (
language === "ruby"/framework === "rails"): Rails has no typecheck and no build step, so instead run a boot/smoke check that proves the app boots and the i18n stack loads. Runbin/rails runner 'I18n.t("site.title", default: "ok"); puts "i18n ok"'(orbin/rails aboutas a lighter boot probe) and confirm it exits 0 and printsi18n ok. Then confirm the source-locale catalog parses: loadconfig/locales/{default_locale}.ymland verify it is valid YAML (a malformed catalog is a setup failure). Map the result into the existingverificationResultshape: set the JS-onlytypecheckandbuildfields tonull(not applicable to Rails), and record the boot/smoke pass/fail and YAML-parse pass/fail. Setstatus: "succeeded"only if both the boot/smoke and the catalog parse pass; otherwise"failed".Android (
language === "android"/framework === "android"): Android has no typecheck and the full Gradle build needs the Android SDK, so verification has two tiers and degrades gracefully: (1) Always parse everyres/values/strings.xmlandres/values-*/strings.xmlfor XML well-formedness (a malformed resource file is a setup failure) and confirm a target dir exists for each configured locale. (2) If the Android SDK + Gradle are available (./gradlewpresent andANDROID_HOME/ANDROID_SDK_ROOTset, orlocal.propertieshassdk.dir), run./gradlew lintand capture theMissingTranslation/ExtraTranslationresults; if the SDK is absent, skip the Gradle step, do not fail, and record that lint was skipped for lack of a toolchain. Map intoverificationResult: set the JS-onlytypecheckandbuildfields tonull; record the XML-parse pass/fail and the lint result (or"skipped: no Android SDK"). Setstatus: "succeeded"if the XML parses and covers every locale (and lint is clean when it ran); otherwise"failed".Swift / Apple (
language === "swift"): Swift/Apple has notscand no JS build step, so instead run a catalog-integrity check using the headlessxcstringstoolthree-step form (NOT a pipe):DIR=$(mktemp -d) xcrun xcstringstool extract <sources> --SwiftUI --modern-localizable-strings [--legacy-localizable-strings] --output-directory "$DIR" xcrun xcstringstool sync <catalog> --stringsdata "$DIR"/*.stringsdata xcrun xcstringstool print <catalog>This confirms the catalog is valid JSON and covers the used keys. Map into the existing
verificationResultshape: set the JS-onlytypecheckandbuildfields tonull(not applicable to Swift), and record the catalog-integrity pass/fail. Graceful degradation: ifxcrun xcstringstoolis absent (no Xcode toolchain — e.g. a non-Apple CI machine), author + static-JSON-validate the catalog instead (parse the.xcstringsas JSON; check it hassourceLanguage,strings, andversion) and mark build-verify deferred (NOT failed) — the build-timeSWIFT_EMIT_LOC_STRINGS = YESextraction (or a laterxcstringstoolrun on macOS) will populate it. Setstatus: "succeeded"if the catalog-integrity check passes (or static-JSON-validate passes with build-verify deferred);"failed"only on a genuine integrity failure.
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 — {for JS/TS:
typecheck and build are clean; for Rails:the app boots and the i18n config loads; for Android:the string resources parse and cover every locale (Lint clean)or, when the SDK was absent,the string resources parse and cover every locale (Lint skipped — no Android SDK); for Swift:the String Catalog is valid and registered}. 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; Rails — loads config/locales/*.yml directly at runtime; and Android — the platform loads res/values*/strings.xml at runtime) loads message files directly with no compile step, so the step is verify_catalogs — confirm the existing catalog files parse and cover every locale (for Rails, also run bundle exec i18n-tasks missing -t used to confirm every used key has a source-locale entry — not health, which false-fails on incomplete target stubs; for Android, confirm every res/values-*/strings.xml is well-formed XML and covers the source keys, running ./gradlew lint for MissingTranslation when the SDK is available); 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 {for JS/TS:
runs extract + compile + build, then a recall self-check that catches any user-facing string the first pass missed (data modules, helpers) and self-heals it, so nothing ships unwrapped; for Rails:checks that every wrapped key has a matching source-locale entry, tidies any leftover scaffold keys, and runs your test suite if you have one; for Android:checks everystrings.xmlis well-formed and that target locales cover the source keys (running Lint forMissingTranslationif the Android SDK is available); for Swift:runs a catalog-integrity check — confirming the String Catalog is valid JSON and covers the keys your code uses (no extract/compile step; the build populates the catalog)}."
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. Android is also key-authored, with no macro and no automated extractor — author a<string name="key">value</string>(or<plurals>) entry intores/values/strings.xml, then replace the literal withgetString(R.string.key)/resources.getQuantityString(R.plurals.key, count, count)in Kotlin/Java,stringResource(R.string.key)/pluralStringResource(...)in Compose, or@string/keyin XML layouts. Android XML supports<!-- -->comments, so DO add a short translator comment above non-obvious keys, plus<xliff:g>for do-not-translate runs; followreferences/languages/android/native/android-strings.convert.md.) Update.globalize/progress/wrap-N.jsonafter each file (atomic write). Do NOT runextractorcompile— that runs once after all wrap subagents complete (Android has no extract/compile — the verify worker only validates).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).- Runtime-catalog, no compile (Rails): ensure_i18n_tasks, source_completeness_check, unused_cleanup, optional test_suite. There is no extract step and no compile step (Rails loads
config/locales/*.ymldirectly at runtime). The verify gate is base-locale completeness: confirm the source-locale YAML parses and that every key used in code has a base-locale entry (i18n-tasks missing -t usedis empty). Target-locale gaps are expected (stubs deferred to the connect phase) and never fail the gate; orphaned keys are cleaned, not gated. There is no normalize-drift gate — the catalog is left in its authored reading order (canonical ordering is a CI / connect-time concern, seesetup.add-ons.mdAdd-on 3).- Runtime-catalog, no compile (Android): xml_validity_check, locale_coverage_check, optional lint_check. There is no extract step and no compile step (the platform loads
res/values*/strings.xmlat runtime). The verify gate is catalog integrity: confirm everystrings.xmlis well-formed XML and that each targetres/values-*/strings.xmlcovers the source keys; if the Android SDK is available, run./gradlew lintand surfaceMissingTranslation/ExtraTranslation, otherwise skip lint without failing.- Single multi-locale catalog, build-time populated (Swift / Apple): catalog_integrity_check (no extract/compile codemod step). The wrap subagents make strings localizable per
string-catalog.convert.md; there is no extract or compile codemod — a normal Xcode build withSWIFT_EMIT_LOC_STRINGS = YESemits.stringsdatathat populatesLocalizable.xcstrings(or, headless,xcrun xcstringstool extract → syncdoes the same). The verify gate is catalog integrity: the.xcstringsis valid JSON and covers the used keys (see the Swift arm below).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.
- Recall self-check (backstop for detection misses). Following
references/languages/js-ts/convert.recall-self-check.md, scan the full source root for user-facing strings that were never wrapped — Lingui: install + runlingui/no-unlocalized-strings(per Add-on 2; guided-mode consent before installing, unguided installs directly; declined → grep scan); next-intl: tuned grep scan; vue-i18n:@intlify/eslint-plugin-vue-i18nno-raw-textor grep. Write findings toresult.recallViolations. If empty, continue to step 5. If non-empty, writestatus: "needs_cleanup"and stop — the orchestrator runs the cleanup loop (below), which self-heals viawrap-cleanupsubagents (each re-catalogs and re-scans itself) until the recall scan is clean or the budget is hit; once it settles, resume at step 5.- 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.- Recall self-check. Paraglide has no official
no-unlocalized-stringsrule (seeparaglide/setup.add-ons.md), so run the tuned grep scan fromreferences/languages/js-ts/convert.recall-self-check.mdoversrc/(adapted to.sveltetemplates). Writeresult.recallViolations; on non-empty,status: "needs_cleanup"and let the orchestrator run the cleanup loop (the cleanup subagent authors the missingm.key()entries per the Paraglide convert reference).For Rails: 0. Ensure
i18n-tasksis installed (this phase owns the install). Ifi18n-tasksis not inGemfile.lock, addgem "i18n-tasks", "~> 1.0"to the:development, :testgroup, runbundle install, and scaffoldconfig/i18n-tasks.ymlif absent (perrails.convert.mdStep 4). Ifbundle installfails, record the error, mark the gate skipped (not failed), and continue — do not block the run on missing audit tooling. (Rails installs gems inside subagents; Phase 2.0 is a no-op for bundler.)
- Base-locale completeness gate: run
bundle exec i18n-tasks missing -t used— the keys used in code that have no entry in the base locale. The gate passes when this reports no keys (every wrapped key has a source entry). Judge by the reported keys, not the process exit code:i18n-tasks missingis a report command and may exit0even with findings (onlyi18n-tasks healthis contracted to exit non-zero for CI). Inspect the output — any base-locale key listed under-t usedis a gate failure; for a machine-checkable form,bundle exec i18n-tasks missing -t used -f keysprints one key per line and an empty result means pass. Capture pass/fail. Atomically updateprogress/verify.jsonafter this step. Do not runi18n-tasks health— its all-localemissing+unusedwould false-fail on the empty target stubs and on setup seed keys (both by design). Target-locale gaps (bundle exec i18n-tasks missing -t diff) are expected — report them as informational, never as a failure. (Confirm the exact-t usedflag withbundle exec i18n-tasks missing --helpif the gem version differs.) There is no extract step and no compile step — Rails loadsconfig/locales/*.ymldirectly at runtime.- Unused cleanup (not a gate): run
bundle exec i18n-tasks unusedand delete genuinely-orphaned scaffold seed keys (e.g. thesite.titleseed the setup phase added that the app never references) from the source catalog, so the shipped catalog — and the later CIi18n-tasks healthgate — stays clean. This is a cleanup action, not a pass/fail gate. Do not runi18n-tasks normalizehere: leave the catalog in its authored reading order (canonical ordering is handled by CI Add-on 3). Confirm the catalogs parse and cover every configured locale.- If a test suite is present (
spec/with RSpec, ortest/for Minitest): run it. Withconfig.i18n.raise_on_missing_translations = trueset inconfig/environments/test.rb(configured by the setup phase), any missing key raises immediately, so a green suite confirms catalog coverage. Capture pass/fail. If no suite is present, skip this step.For Swift / Apple:
- Catalog-integrity gate (no extract/compile codemod step). The wrap subagents already made strings localizable per
string-catalog.convert.md; there is no separate extract or compile codemod (build-timeSWIFT_EMIT_LOC_STRINGS = YES/xcstringstoolpopulates the catalog). Run the headless three-step form (NOT a pipe) to confirm the catalog is valid and covers the used keys:Capture pass/fail. Atomically updateDIR=$(mktemp -d) xcrun xcstringstool extract <sources> --SwiftUI --modern-localizable-strings [--legacy-localizable-strings] --output-directory "$DIR" xcrun xcstringstool sync <catalog> --stringsdata "$DIR"/*.stringsdata xcrun xcstringstool print <catalog>progress/verify.jsonafter this step. Graceful degradation: ifxcrun xcstringstoolis absent (no Xcode toolchain), author + static-JSON-validate the catalog instead (parse the.xcstringsas JSON; checksourceLanguage,strings,version) and mark build-verify deferred (NOT failed) — a later macOS build /xcstringstoolrun populates it. Authoritative commands:references/languages/ios/native/string-catalog.convert.md.Authoritative commands:
references/languages/ruby/frameworks/rails/rails.convert.mdSteps 4-5 andreferences/languages/ruby/frameworks/rails/setup.add-ons.mdAdd-on 3.For Android:
- XML validity — parse every
res/values/strings.xmlandres/values-*/strings.xml; any malformed file (unescaped&/', broken tag) is a failure. Capture pass/fail. Atomically updateprogress/verify.jsonafter this step. (There is no extract step and no compile step — the platform loads resources at runtime.)- Locale coverage — for each configured target locale, confirm
res/values-<qualifier>/strings.xmlexists and defines everynamepresent in the sourceres/values/strings.xml(including each<plurals>group). Report any missing keys. Also confirm acomment_review_passover the sourcestrings.xml, adding<!-- -->translator comments and<xliff:g>annotations where the reference's heuristic says one should exist (Android XML carries comments — they DO apply, unlike Paraglide ICU-JSON).- Lint (if the Android SDK is available) — run
./gradlew lintand captureMissingTranslation/ExtraTranslation; this is the authoritative coverage gate when the toolchain is present. If the SDK/Gradle is absent, skip this step and do not fail — recordlint: "skipped: no Android SDK"and rely on steps 1-2.Authoritative commands:
references/languages/android/native/android-strings.convert.md(verify section) andreferences/languages/android/native/setup.add-ons.md(CI/lint add-on).Write
resultwith{ catalogPath, totalMessages, extractOk, compileOk, buildOk, commentsAdded, recallViolations, cleanupRounds, stringsWrappedInCleanup, residualViolations }— the last four are the recall self-check fields (recallViolations:[{file,line,text}]from the scan;cleanupRounds: how many cleanup rounds ran;stringsWrappedInCleanup: total strings the cleanup subagents wrapped;residualViolations: any left when the loop stopped, reported to the user). For libraries with no recall scan run (Rails/Android/Swift this pass), set all four tonull. For Paraglide, setextractOktonull; setcommentsAddedto the count added on the default PO format and tonullon ICU-JSON; report compile success undercompileOk. For Rails, setextractOktonull(no extraction — keys are hand-authored in YAML); report the base-locale completeness result (i18n-tasks missing -t usedempty) undercompileOk(Rails' catalog-integrity gate stands in for compile; a gate skipped becausebundle installfailed reports asnullwith the error noted inerrors); setbuildOkto the test-suite result when a suite ran, elsenull(Rails has no build step); setcommentsAddedtonull. For Rails,catalogPathisconfig/locales/{default_locale}.ymlandtotalMessagesis the key count in that file (Rails has no extraction step to count from). For Android, setextractOktonull(no extraction — keys are hand-authored in XML); report the combined XML-validity + locale-coverage result undercompileOk(Android's catalog-integrity gate stands in for compile); setbuildOkto the./gradlew lintresult when it ran, elsenull(lint skipped / no SDK); setcommentsAddedto the count of<!-- -->comments added (Android XML carries comments). For Android,catalogPathisapp/src/main/res/values/strings.xmlandtotalMessagesis the<string>+<plurals>count in that file. For Swift, setextractOktonull(no extraction codemod — keys land via build-timeSWIFT_EMIT_LOC_STRINGS/xcstringstool); report the catalog-integrity result undercompileOk(andnullwith build-verify-deferred noted inerrorswhenxcstringstoolis absent and only static-JSON-validate ran); setbuildOktonull(no JS build step); setcommentsAddedtonull. For Swift,catalogPathis the.xcstrings(e.g.Localizable.xcstrings) andtotalMessagesis the key count instrings.
3.5.1 Cleanup loop (JS/TS recall backstop)
If the verify subagent returns status: "needs_cleanup" with result.recallViolations, the orchestrator self-heals the missed strings — this is the backstop for any candidateFiles detection miss (data modules, toast/error helpers, config copy). Drive it per references/languages/js-ts/convert.recall-self-check.md:
For up to maxCleanupRounds (default 2) rounds:
- Collect the distinct files in
recallViolations. If more than 40, cap to the 40 highest-violation files and surface how many were dropped (no silent truncation). - Dispatch a
wrap-cleanupsubagent (background, same dispatch pattern as Phase 3.2) whose prompt mirrors the Phase 3.2 wrap prompt — samemanifest-snapshot.references.convert+references.code— with the file list = the violating files and the "these files were MISSED by detection; wrap percode.md, apply the skip-list, leave genuine non-translatables" note from the recall reference. Pre-createprogress/wrap-cleanup-{round}.json. - Before returning, the
wrap-cleanupsubagent itself re-runs the library's catalog step (Lingui:lingui extract --clean+compile; Paraglide:paraglide compile; next-intl/vue-i18n: none — runtime catalogs) and the recall scan, writing its round's residualrecallViolationsand the count it wrapped toprogress/wrap-cleanup-{round}.json(per the reference — there is no separate verify re-dispatch). The orchestrator reads that residual and aggregates the per-round counts into the verifyresult. - Stop when: the scan is clean; or a round wrapped zero new strings (
stringsWrappedInCleanup === 0); or the round budget is reached. Report anyresidualViolationsto the user with file+line (never drop silently).
Record result.cleanupRounds and surface a one-line summary: "Self-heal wrapped {stringsWrappedInCleanup} strings that detection missed{, N left for manual review if residual}." Then proceed to comment_review_pass and 3.6.
Rails/Android/Swift do not run this loop this pass — their verify gates (i18n-tasks missing -t used, ./gradlew lint, catalog integrity) already report coverage; extending the loop to them (erb_lint, gradlew HardcodedText, grep) is a follow-up.
3.6 Cost estimate (Phase 3 → 4 bridge)
After verify succeeds, parse the extracted catalog (JS) / the source-locale YAML (Rails) / the source res/values/strings.xml (Android) 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."
When result.stringsWrappedInCleanup > 0 (the 3.5.1 recall cleanup loop ran), prepend the recall self-heal summary to this message: "Self-heal wrapped {stringsWrappedInCleanup} strings that detection missed{, N left for manual review if residual}."
If decisions.scope.globalize === true, advance to Phase 4 with:
"Moving on to Phase 4 — creating your Globalize project and connecting the repo. You're already signed in from earlier, so this is mostly automated (one quick browser approval to authorize the GitHub/GitLab app)."
Otherwise (the user unchecked Globalize at 1.6), end with:
"Skipping the translation-platform step for now. Re-run
globalize-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 project (connect repo + patterns)
The gate already passed: Globalize is in scope and the user signed in back in Phase 1.5. This stage creates the project and connects the repo by delegating to globalize-now-project-setup on the main thread — the orchestrator assembles the inputs and invokes the skill; it does not inline Globalize CLI commands.
User-facing message (at Phase 4 start): "Phase 4 — creating your Globalize project and connecting your repo. You're already signed in, so I just hand the project details and your finished catalog paths to the project-setup skill. One quick browser approval authorizes Globalize's GitHub/GitLab app, then it creates the project, connects the repo, and wires the catalog file patterns."
4.1 Assemble .globalize/globalize-inputs.json
Build the inputs bundle the project skill consumes, from data already on hand — no re-detection:
- projectName, provider, sourceLocale, targetLocales ←
decisions.md(collected in 1.7 / 1.9). - owner, repo, gitUrl ←
detection.jsongit.remote(parse owner/repo; convert SSH → HTTPS for--git-url). - catalogPath ← Phase 3 verify
result.catalogPath. - localePathPattern + fileFormat ← derive from the library (the one piece of Globalize knowledge the orchestrator still owns, because it depends on the just-converted catalog):
- Lingui →
po, e.g.src/locales/{locale}/messages.po. - next-intl →
json-nested, e.g.messages/{locale}.json. - Paraglide →
poatmessages/{locale}.poby default; ifdecisions.setup.catalogFormat === "json",json-flatatmessages/{locale}.json. - Rails (
detection.language === "ruby",framework === "rails") →yaml-railsatconfig/locales/{locale}.yml; source = detecteddefault_locale(oren). Rails locale codes are already hyphenated (pt-BR,zh-TW) — pass through verbatim, no underscore normalization. - Android (
detection.language === "android") →android-strings; no{locale}segment — pointlocalePathPatternat the sourceapp/src/main/res/values/strings.xmland let the handler discover thevalues-*target overlays (it normalizes legacyvalues-pt-rBRand BCP47values-b+sr+Latn⇄ BCP47). Do not synthesize a{locale}token. - Swift / Apple (
detection.language === "swift") →xcstrings; single file, no{locale}segment —Localizable.xcstrings(or**/*.xcstringsfor multiple tables). The catalog holds every locale; source = the catalog'ssourceLanguage/CFBundleDevelopmentRegion.
- Lingui →
- importMode:
"ignore", importScope:"new_keys_only", mode:"orchestrated".
Write the file to .globalize/globalize-inputs.json and mark assemble_globalize_inputs done in plan.md.
4.2 Re-check auth
The account skill ran in Phase 1.5, but the token may be absent (user skipped sign-in) or expired. Delegate a quick auth status check. If not authenticated, invoke globalize-now-account-setup now (main thread) before proceeding; if authenticated, continue.
4.3 Delegate to globalize-now-project-setup (main thread)
Invoke the globalize-now-project-setup skill via the Skill tool, in orchestrated mode:
"Orchestrated mode. Read
.globalize/globalize-inputs.jsonfor all inputs; skip detection and the Setup Mode prompt; run unguided. Connect the git provider (one browser approval if the GitHub App / GitLab OAuth isn't installed yet), create the project, connect the repo, and set the catalog patterns from the suppliedlocalePathPattern+fileFormat."
The skill owns the entire CLI surface (auth precheck, projects create, provider connect, repositories create, etc.). The interactive GitHub-App / GitLab-OAuth approval happens here — the project skill front-loads it before the create/connect work. Mark delegate_project_setup done in plan.md when it returns.
If globalize-now-project-setup is not installed, the Skill invocation fails — tell the user to run npx skills add globalize-now/globalize-skills --skill globalize-now-project-setup and re-invoke. (The standard install co-installs all the Globalize skills.)
4.4 Surface and finish
Show the 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}
<!-- Include the Phase 1.5 section only when decisions.scope.globalize === true -->
## Phase 1.5 — Globalize account
Orchestrator-owned steps (main thread, delegated to `globalize-now-account-setup`):
- [ ] delegate_account_setup
## 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
- [ ] recall_self_check <!-- JS/TS backstop: scan for unwrapped strings, self-heal via wrap-cleanup subagents (see convert.recall-self-check.md); ≤2 rounds -->
- [ ] 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
- [ ] recall_self_check
-->
<!-- runtime-catalog, no compile (Rails) instead: no extract step, no compile step, no build step (Rails loads config/locales/*.yml directly at runtime). The gate is base-locale completeness (i18n-tasks missing -t used); there is no normalize-drift gate. test_suite is optional — include only if a spec/ or test/ dir exists (run with raise_on_missing_translations):
- [ ] ensure_i18n_tasks
- [ ] source_completeness_check
- [ ] unused_cleanup
- [ ] test_suite
-->
<!-- runtime-catalog, no compile (Android) instead: no extract step, no compile step (the platform loads res/values*/strings.xml at runtime). lint_check is optional — include only when the Android SDK/Gradle is available:
- [ ] xml_validity_check
- [ ] locale_coverage_check
- [ ] lint_check
-->
<!-- Include the Phase 4 section only when decisions.scope.globalize === true -->
## Phase 4 — Globalize project
Orchestrator-owned steps (main thread, delegated to `globalize-now-project-setup`):
- [ ] assemble_globalize_inputs
- [ ] delegate_project_setup
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 <!-- default-on; unchecked only if the user declined the platform at 1.6 -->
## 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
- Authenticated org: `{org}` <!-- recorded after Phase 1.5 sign-in completes -->
Progress file schema (per subagent)
{
"subagentId": "setup",
"phase": 2,
"status": "pending|running|succeeded|failed|needs_decision|needs_cleanup",
"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). 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.
- Phase 1.5 (account) and Phase 4 (project) are not subagents. They run on the main thread by delegating to the
globalize-now-account-setup/globalize-now-project-setupskills via the Skill tool (interactive sign-in and repo-connection browser steps need the main thread). No background dispatch, noprogress/*.jsonpolling for these two. - 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.