Runbook: Onboard a project to Polylang (i18n)
Step-by-step MCP runbook for an AI agent to fully onboard a project to ShipEasy Polylang — detect the framework, install the loader, codemod the source, push keys, validate, and publish. One human stop for browser auth.
This page is written for an AI coding assistant (Claude Code, Cursor, Windsurf, OpenCode) to follow autonomously. There is exactly one mandatory human stop: clicking the browser link that opens during shipeasy login. Every other step is a typed MCP tool call.
If you're a human reading this to decide whether to delegate — yes, hand it to your agent. The runbook is conservative (preview before apply, validate before publish) and it stops on ambiguity rather than guessing.
Required tool surface
Every step below references one of these MCP tools, all under the mcp__shipeasy__ prefix. If your client doesn't expose them, the user hasn't installed the MCP server — see the LLM index for setup.
CLAUDE.md.{ data_key, profile, loader_url, script_tag, next_step }.locale:env profile. Idempotent on name.confirm: true.{ pushed_count, skipped_count, failed_keys }./.well-known/i18n.json from a deployed URL to reconcile state.Step 1 — Verify the tool surface and authenticate
Confirm the tools are present
Before doing anything else, check that mcp__shipeasy__auth_check is callable. If it isn't, stop and tell the human:
"I don't see the ShipEasy MCP tools. Run
npm install -g @shipeasy/cli && shipeasy mcp install, restart this session, and try again."
Call auth_check
mcp__shipeasy__auth_check {}Expected shape: { authenticated: boolean, user?, project? }.
STOP — if not authenticated, ask the human to log in
If authenticated: false, you cannot proceed without a human. Ask them to run, in their terminal:
shipeasy loginThe CLI opens their default browser to a ShipEasy auth page. They sign in (GitHub / Google / magic link). The CLI prints Authenticated successfully and writes credentials to ~/.shipeasy/credentials. Do not attempt mcp__shipeasy__auth_login first — the device-code flow benefits from being attached to the user's terminal so they can see progress.
Once they confirm success, call auth_check again. If it still returns false, ask them to paste any error from the CLI output and stop.
Confirm the project binding
auth_check returns project when the cwd is bound to a ShipEasy project. If it's missing, ask the human:
"Which ShipEasy project should I onboard this codebase to? You can list yours with
shipeasy projects list."
Once they answer, run shipeasy link <project-id> (or have them run it) and re-check.
Step 2 — Identify the framework
Prefer declared facts over detection
If CLAUDE.md (or AGENTS.md) at the repo root contains a ## ShipEasy String Manager block with Framework:, Source root:, and Loader target: filled in, use those values and skip detect_project. Humans declare them precisely because detection guesses wrong on monorepos.
Otherwise, call detect_project
mcp__shipeasy__detect_project {}The response shape:
{
frameworks: string[], // e.g. ["nextjs-app"]
entry_points: string[], // e.g. ["src/app/layout.tsx"]
shipeasy: {
i18n_sdk: { installed: boolean, version?: string },
loader_script_tag: { present: boolean, file?: string }
},
status?: "ok" | "needs_clarification"
}Use entry_points[0] as the loader target. Infer the source root from it (e.g. src/app/layout.tsx → source root src/).
STOP — if status is needs_clarification, ask the human
Monorepos and unusual layouts return status: "needs_clarification". Ask:
"I see multiple candidate apps in this repo:
apps/web,apps/admin,packages/marketing. Which one should I onboard?"
Pass the chosen path back into detect_project via the optional cwd argument and continue.
Note the codemod capability
JSX/TSX/MDX frameworks (Next, Vite-React, CRA, Remix, React Native) support the automated codemod. Vue, Svelte, Astro, Angular, Rails, Django, Laravel, Express templates, Go templates support scan only — you discover candidates and the human (or you, with their approval) wraps them by hand. Record this; it changes Step 5.
Step 3 — Install the loader
Skip if already present
If detect_project returned shipeasy.loader_script_tag.present: true, skip this step entirely. The loader is already in the entry HTML.
Generate the script tag
mcp__shipeasy__i18n_install_loader {
"profile": "en:prod"
}If the source language isn't English, substitute the right BCP-47 code: fr:prod, de:prod, ja:prod, etc. The tool returns:
{
data_key: string, // public client key, scoped to read published bundles only
profile: "en:prod",
loader_url: string,
script_tag: string, // ready-to-paste <script src="…" data-key="…"></script>
next_step: string // hint for the CLI invocation below
}Inject the tag with the CLI
Run, in the project root:
shipeasy i18n install-loader --data-key <data_key> --profile en:prodThe CLI auto-detects the framework and patches the right file:
| Framework | Patches |
|---|---|
| Next.js App Router | src/app/layout.tsx (inside <head>) |
| Next.js Pages | pages/_document.tsx |
| React + Vite | index.html |
| Remix | app/root.tsx |
| Astro | src/layouts/*.astro |
| Anything else | Prints the tag, you paste it |
Pass --path <file> to override the target. Pass --print to dry-run (prints, doesn't write).
Failure handling
If the CLI says "unknown framework", take the script_tag from the tool response and ask the human to paste it inside the <head> of their root HTML. Then re-run detect_project to confirm loader_script_tag.present: true.
Step 4 — Create the source profile
Check if it already exists
Cheap read first:
mcp__shipeasy__list_resources { "kind": "profiles" }If a profile with name: "en:prod" (or your source locale) is already present, skip the create call.
Otherwise, create it
mcp__shipeasy__i18n_create_profile {
"name": "en:prod",
"locale": "en",
"environment": "prod"
}name is the public identifier you'll use everywhere downstream. Convention is <locale>:<environment>. Stick to it.
Step 5 — Discover and rewrite strings
The branch here depends on what Step 2 told you about codemod capability.
5a · JSX/TSX/MDX frameworks (auto codemod)
ALWAYS preview first
mcp__shipeasy__i18n_codemod_preview {
"files": ["<source root>"],
"profile": "en:prod"
}The response includes diffs and a list of proposed strings. Read the strings list before applying. Verify each is real UI copy — not a TypeScript identifier, an import path, a className token, a date format string, or an aria-label that's already a translation key.
STOP — show the human a sample for approval
For codebases over ~20 files, show the human the first 10–15 proposed rewrites and ask:
"Here are the first 12 strings the codemod wants to wrap. Anything that shouldn't be translated? (Brand names, code identifiers, technical strings.) I'll add them to the glossary or skip them."
For brand names and acronyms, edit shipeasy.config.ts's glossary (e.g. "ShipEasy": "ShipEasy" to lock the term).
Apply
mcp__shipeasy__i18n_codemod_apply {
"files": ["<source root>"],
"profile": "en:prod",
"confirm": true
}The tool refuses without confirm: true. After it runs, i18n-codemod-review.json is written at the project root listing every string that was skipped (dynamic interpolations, rich HTML, plurals, conditional fragments). Read it; surface anything non-trivial to the human.
Chunk large codebases
If the source root has more than ~300 source files, run preview/apply one subdirectory at a time (src/app/, then src/components/, then src/lib/). Diffs stay reviewable, failures stay isolated.
5b · Scan-only frameworks (Vue, Svelte, Rails, Django, Laravel, Astro, Angular, Go templates)
Scan for candidates
mcp__shipeasy__i18n_scan_code {
"paths": ["<source root>"]
}Returns a flat list of { file, line, snippet, suggested_key } candidates.
Wrap by hand or with framework helpers
For each candidate, edit the source to use the framework's i18n call. Examples:
- Vue:
{{ $t('hero.title') }} - Svelte:
{$_('hero.title')} - Rails (ERB):
<%= t('hero.title') %> - Django:
{% trans "Hero title" %} - Laravel Blade:
{{ __('hero.title') }}
Push the keys you create regardless of helper syntax — ShipEasy stores them by key name, not call site.
STOP — for ambiguous cases, ask
Server-rendered templates often interleave logic and copy. When you're not sure whether a string is user-facing, ask:
"In
views/orders/show.html.erbline 47, this string\"#{order.status.upcase}\"is interpolated. Is 'status' user-facing copy that should be translated, or a code-level enum value?"
Step 6 — Push keys to the source profile
Push
mcp__shipeasy__i18n_push_keys {
"profile": "en:prod",
"source": "codemod"
}Use "source": "scan" if you used the scan path in Step 5b. Response: { pushed_count, skipped_count, failed_keys }.
Inspect failures
If failed_keys is non-empty, the keys are usually malformed (whitespace, leading digits, reserved characters). Each entry includes a reason. Fix the key in source, re-run the codemod or edit by hand, and re-push.
Verify
mcp__shipeasy__list_resources { "kind": "keys" }Spot-check that the count matches pushed_count (allowing for keys that already existed).
Step 7 — Add descriptions
Translation quality drops sharply without context. For every key the codemod created, add a one-sentence description of where the string appears and what it does.
List keys without descriptions
mcp__shipeasy__list_resources { "kind": "keys", "filter": "no_description" }For each, call create_key with the description
i18n_create_key is upsert-style — call it on an existing key with new fields and they merge.
mcp__shipeasy__i18n_create_key {
"profile": "en:prod",
"key": "checkout.confirm_button",
"value": "Confirm and pay",
"description": "Primary CTA on the checkout summary page — submits the order for payment processing."
}Good descriptions cite where (page / component) and what (purpose / action). Bad descriptions repeat the value.
Don't block on this
For a first onboarding, descriptions on the top-50 highest-traffic keys is enough. Mark the rest as TODO in i18n-codemod-review.json and surface to the human.
Step 8 — Validate
Cross-check code references against the profile
mcp__shipeasy__i18n_validate_keys {
"profile": "en:prod",
"paths": ["<source root>"]
}Returns { missing_in_profile, unused_in_code, mismatched_values }. All three must be empty (or expected) before publishing.
Resolve mismatches
missing_in_profile: code callst("foo")butfooisn't in the profile. Push it: re-run Step 6, or calli18n_create_key.unused_in_code: profile hasbarbut no source file references it. Usually safe to leave (might be runtime-generated). Don't auto-delete.mismatched_values: code passes a different default than the profile's value. The code default is the source of truth — update the profile viai18n_create_keywith the new value.
Step 9 — Publish
STOP — confirm with the human
Publishing writes to production KV and purges the CDN — every visitor will see the new bundle within seconds. Confirm:
"Ready to publish
en:prod? This makes <N> keys live worldwide. Type 'yes' or tell me what to fix first."
Publish
mcp__shipeasy__i18n_publish_profile {
"profile": "en:prod"
}Response: { published_at, chunks, purge_urls }. Strings are live in seconds.
Verify in the wild
If the project has a deployed URL, optionally run:
mcp__shipeasy__i18n_discover_site { "url": "https://example.com" }This reads /.well-known/i18n.json and confirms the loader is fetching the bundle you just published.
Step 10 — Translate (optional, do per target locale)
Create the target profile
mcp__shipeasy__i18n_create_profile {
"name": "fr:prod",
"locale": "fr",
"environment": "prod"
}Generate the AI draft
mcp__shipeasy__i18n_translate_draft {
"profile": "fr:prod",
"source_profile": "en:prod"
}Lands as a draft in the dashboard — never auto-publishes. Anthropic Claude reads the project tone and glossary on every call.
STOP — hand off to a human reviewer
AI translations need a human pass for tone, brand terms, and locale-specific idioms. Tell the human:
"I've drafted
fr:prod. Review at https://app.shipeasy.ai/i18n/drafts/fr:prod and click 'Merge draft' when ready. Then I'll publish."
Publish the target profile
Once they confirm the draft is merged, repeat Step 9 with "profile": "fr:prod". Repeat per target locale.
Failure modes — quick reference
cat ~/.shipeasy/credentials — if missing, re-run shipeasy login; if present, check file permissions (should be 600).shipeasy.config.ts's files exclusion globs and re-preview. Don't apply.confirm: true. The refusal is intentional.Verification checklist
When you believe you're done, confirm each of these and report results to the human:
Loader present
detect_project returns shipeasy.loader_script_tag.present: true.
Source profile published
list_resources { "kind": "profiles" } shows en:prod (or your source locale) with a recent published_at.
Validation clean
i18n_validate_keys returns empty missing_in_profile and mismatched_values.
Codemod review handled
i18n-codemod-review.json either doesn't exist (clean run) or has been triaged with the human.
Site verified (if deployed)
i18n_discover_site against the deployed URL confirms the loader is fetching the bundle you published.
If all five pass, the project is onboarded. Tell the human:
"Polylang is fully wired.
en:prodis live with N keys. Add target locales any time withi18n_create_profile+i18n_translate_draft, or run/llms/i18n-stringsagain starting at Step 10."
Add a target locale next.
Same flow, but with a target profile. The agent creates the profile, drafts translations with Claude, hands off to a human for review, and publishes.