ShipEasy

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.

Runbook · for AI agentsOn this page · agent: 3–8 min · human: 1 clickUpdated · May 3, 2026Works with · Next · Vite · Remix · Astro · Vue · Svelte · Rails · Django · Laravel
Audience

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.

Field
Type
Description
auth_check
() → { authenticated, user?, project? }
Idempotent. Call first, and again after any login.
detect_project
() → { frameworks, entry_points, shipeasy }
Reads the cwd. Skip if the user already declared the framework in CLAUDE.md.
i18n_install_loaderrequired
{ profile }
Returns { data_key, profile, loader_url, script_tag, next_step }.
i18n_create_profilerequired
{ name, locale, environment }
Creates a locale:env profile. Idempotent on name.
i18n_scan_code
{ paths }
Returns candidate strings without rewriting. Use for non-JSX frameworks.
i18n_codemod_previewrequired
{ files, profile }
Returns proposed diffs. Always call before apply.
i18n_codemod_applyrequired
{ files, profile, confirm: true }
Writes files. Refuses without confirm: true.
i18n_push_keysrequired
{ profile, source: "codemod" | "scan" }
Returns { pushed_count, skipped_count, failed_keys }.
i18n_create_key
{ profile, key, value, description }
Use to add or refine a description. Same shape as discovered keys.
i18n_validate_keysrequired
{ profile, paths }
Cross-checks code references against the profile. Must return clean before publishing.
i18n_translate_draft
{ profile, source_profile? }
Generates AI translations for a target profile. Lands as a draft, never auto-published.
i18n_publish_profilerequired
{ profile }
Promotes the profile to KV + purges CDN. Live worldwide in seconds.
i18n_discover_site
{ url }
Optional. Reads /.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 login

The 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:prod

The CLI auto-detects the framework and patches the right file:

FrameworkPatches
Next.js App Routersrc/app/layout.tsx (inside <head>)
Next.js Pagespages/_document.tsx
React + Viteindex.html
Remixapp/root.tsx
Astrosrc/layouts/*.astro
Anything elsePrints 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.erb line 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 calls t("foo") but foo isn't in the profile. Push it: re-run Step 6, or call i18n_create_key.
  • unused_in_code: profile has bar but 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 via i18n_create_key with 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

Field
Type
Description
auth_check returns false after login
env
Credentials file unreadable. Ask the human to cat ~/.shipeasy/credentials — if missing, re-run shipeasy login; if present, check file permissions (should be 600).
detect_project returns needs_clarification
layout
Monorepo with multiple apps. Ask the human which subdirectory and pass it as cwd.
codemod_preview shows code identifiers as strings
false positive
Add the file or pattern to shipeasy.config.ts's files exclusion globs and re-preview. Don't apply.
codemod_apply refused
missing arg
You forgot confirm: true. The refusal is intentional.
push_keys reports failed_keys
naming
Key names with whitespace, leading digits, or reserved characters. Fix in source and re-run.
validate_keys shows missing_in_profile
drift
Re-run push_keys or create the missing keys explicitly. Never publish with this non-empty.
publish_profile fails with 403
auth
Token lacks publish permission for this project. Ask the human to verify role in the dashboard, or to re-login as a project owner.
publish_profile fails with 429
rate limit
Plan-level publish quota exceeded. Wait 60s and retry; if it persists, surface to the human (likely needs a plan upgrade).

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:prod is live with N keys. Add target locales any time with i18n_create_profile + i18n_translate_draft, or run /llms/i18n-strings again starting at Step 10."

DONE?

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.

New target profile
$mcp: i18n_create_profile { name: 'fr:prod', locale: 'fr', environment: 'prod' }
Draft translations
$mcp: i18n_translate_draft { profile: 'fr:prod' }
Drafts never auto-publish. A human always reviews before strings ship.
Was this page helpful?✎ Edit on GitHub

On this page