Profiles
Versioned, named manifests of label keys for one locale — the unit you publish, diff, and roll back.
A profile is a named, versioned manifest of label keys for a specific locale combination. It is the unit you publish, version, diff, and roll back. Everything else in Shipeasy Translations — drafts, AI translations, the loader script, validation in CI — pivots around the profile.
If gates and configs are "one row per setting", profiles are "one snapshot per locale per publish". Every publish is immutable; revert is just "republish version N".
Naming
Profiles use locale:
| Profile | Meaning |
|---|---|
en | English, production |
fr | French, production |
de:staging | German, staging |
ja:dev | Japanese, dev |
pt-BR:prod | Brazilian Portuguese, production |
The locale is BCP-47 (en, fr, pt-BR, zh-Hant). The optional :suffix (:dev, :staging, :prod) is just part of the profile name — a convention teams use to keep an in-review copy separate from the live one.
Translations have no built-in environment axis. Unlike a config or killswitch — which
stores a distinct dev / staging / prod value behind one name — a profile is a single named
snapshot. "Staging" vs "prod" copy means two separate profiles (fr:staging and fr:prod)
that you pin and promote by name (see below), not one profile with per-env values.
Use a region tag (pt-BR, es-MX, zh-Hant) whenever register, vocabulary, or script differs
from the base language. The AI translator switches its system prompt based on the region tag —
es-MX produces different copy than es-ES, and zh-Hant writes traditional characters where
zh-Hans writes simplified.
Anatomy of a profile
Every profile is a row in the project's database plus a published
bundle in KV. The dashboard's Profiles list surfaces it with rough
shape { id, name, locale, role, current_version, key_count, missing_count, stale_count, published_at } — see
packages/core/src/db/schema.ts (labelProfiles) for the canonical
fields.
The runtime fetches the strings via the i18n loader script (see The
loader script below), which talks to
https://api.shipeasy.ai/sdk/i18n/strings?profile=<name> with the
client key. The published bundle is served from KV through the CDN
with long TTL and explicitly purged on every publish.
Lifecycle
Create
# name is positional; --locales is comma-separated (defaults to "en")
shipeasy i18n profiles create fr --locales frOn creation the profile is empty (key_count: 0, no version). It is registered in your project but nothing is written to KV until the first publish.
Populate
Populate the source profile via codemod (shipeasy i18n scan --rewrite then i18n push). For target locales, drive the AI translation from the dashboard Drafts panel or the i18n_translate_draft MCP tool — the CLI subcommand for AI drafts isn't shipped today. See Drafts for the full review flow.
# Source profile — pushed from a code scan
shipeasy i18n push --profile en --source codemodPublish
Each publish creates a new immutable version. The current version is what the loader serves from KV. Old versions stay in the dashboard for rollback.
shipeasy i18n publish --profile frPublishing is two writes and one purge: write the chunked bundle to KV, write a new row to i18n_profile_versions, purge the CDN URL for the locale's index.json. End-to-end latency is typically under three seconds globally.
Diff
The version-to-version diff lives in the dashboard's Versions tab — added, removed, and changed keys with their old and new values. CLI diff / rollback subcommands aren't shipped today; both flows are dashboard-driven.
Roll back
From Profiles → fr → Versions, click any prior version → Restore. The selected version is republished as the new current version (so v17 → restore v15 → produces v18 with v15's contents). This keeps history append-only and roll-forward-only.
Rollback typically goes live in under five seconds — same path as a normal publish.
Source vs target
Shipeasy Translations does not hard-code English as "the" source. The first profile you create on a project is implicitly the source: it is where the codemod pushes discovered keys, and it is the basis the AI translates from.
Other profiles are targets: they hold translations of the source's keys. You can switch the source profile in Project → Shipeasy Translations → Settings, but in practice every team picks one and sticks with it.
Source profile→
Holds the canonical key list. Keys are added by shipeasy i18n push from a code
scan. Values are the strings developers wrote in code.
Target profile→
One per locale you ship. Values come from AI drafts, human translators, or imported
XLIFF. Keys are read-only — they mirror the source.
Profile inheritance and fallback
When a key is missing from a target profile the loader falls back to the source profile :
loader requests fr label "checkout.cta"
↓ fr has no value
↓ falls back to source profile (en)
↓ returns "Pay"This means an in-progress French translation never breaks the page — visitors see English for the missing strings until you publish translations. Fallbacks are also visible in the dashboard so you can target what to translate next; the Coverage column shows 1272 / 1284 (99.1%) per target profile.
The fallback only crosses locale. fr does not fall back to fr — staging is meant to be the place where bad translations are caught.
Per-locale profiles
Why not just one profile per locale? Because:
- Translators can work in
frwhilefrkeeps serving last week's strings. - New keys land in
en:devfirst; you do not need to translate them until they are promoted to staging or prod. - An emergency copy fix in
endoes not require redoing translation review for development branches. - Preview deployments (Vercel, Cloudflare Pages branch previews) can pin themselves to
:devor:stagingso QA sees the unreviewed copy without it leaking to users.
Promotion (fr:staging → fr:prod) is a dashboard operation today —
open Translations → Profiles → fr:staging → Promote and pick the
destination. A CLI i18n promote subcommand is on the roadmap.
The recommended flow: write and translate in staging, validate
(shipeasy i18n validate ./src --profile fr:staging), then promote to
prod from the dashboard. Promotion creates a draft on the
destination so you still get one final review before it ships.
Pinning a build to a version
Drop the loader into your page once; it reads data-key (your client
SDK key) and data-profile (the locale:env profile, e.g. fr:prod)
and installs window.i18n with t(key, vars):
<script
src="https://api.shipeasy.ai/sdk/i18n/loader.js"
data-key="sdk_client_..."
data-profile="fr:prod"
></script>The loader always serves the current published version. Per-page
version pinning (snapshot deploys, locked landing pages) isn't a
shipped feature today — the workaround for "freeze the copy for this
signed contract" is to publish into a separate, no-longer-edited
profile (e.g. fr:contract-2026-q2) and point that page's script tag
at it.
API
The same operations are available over HTTPS for programmatic use; the CLI is a thin wrapper over these.
{ name, locale, role? }.{ version }.?from=v15&to=v17.All endpoints take a personal access token or a CI key. The runtime SDK key cannot create or publish profiles — it is read-only against the loader URL.
Limits
| Plan | Profiles per project | Keys per profile | Versions kept |
|---|---|---|---|
| Free | 5 | 1,000 | 10 |
| Pro | 50 | 50,000 | 100 |
| Enterprise | Unlimited | Unlimited | Unlimited |
Hitting a limit does not break published profiles — it blocks new publishes until you upgrade or prune. Old versions past the retention window are deleted from KV; the metadata row stays so you can see what was deleted.
Deleting a profile removes its KV bundle, all versions, and all drafts. The CDN purge is instant but in-flight loader requests may still receive a stale 404 for up to a minute. Always rename or stop traffic before deleting.
Now fill them with keys.
Profiles are empty until you push keys. The codemod finds them automatically — and the schema decides how the AI translates each one.