> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Auto-localize a template into per-locale carrier-approved variants

> Fan one source template out into per-locale variants with AI translation: the generate endpoint and the composer's translate-locales dialog for review, the commit endpoint that bulk-creates real (channel, name, language) template rows through carrier approval, placeholder preservation, limits and failure semantics, per-locale approval polling, and how each recipient's language picks a variant at send time.

# Auto-localize a template into per-locale carrier-approved variants

One source template, many languages: Orbit translates a template into up to 25 target locales in a single call, keeps your `{{token}}` placeholders intact, and (for WhatsApp and RCS) submits each translated variant for carrier approval exactly as if you had authored it by hand. This guide walks both halves of the feature, the dashboard and API flows, the limits and failure codes, and what happens at send time when a recipient's `language` is matched against your variants.

Auto-localization rewrites copy only. It never chooses a route or sends anything by itself; every variant still goes through your normal template, campaign, and approval flows.

## 1. The two halves: generate vs commit

The feature is two endpoints that solve different problems. Know which half you need before you call either.

| | **Generate** — `POST /api/v1/campaigns/templates/translate` | **Commit** — `POST /api/v1/messages/templates/{id}/localize` |
| - | - | - |
| Input | A body (+ optional subject) you provide in the request | A stored template, by id |
| Output | A `localeVariantSet`: one translated variant per locale, returned for review | Real template rows: one `(channel, name, language)` row created per locale |
| Persists anything? | No. Nothing is stored until you commit the variants yourself | Yes. Each variant becomes a template row in your library |
| Carrier approval | Not involved | Each WhatsApp/RCS variant is submitted through the standard carrier-approval path |
| Where you meet it | The **Translate locales** dialog in the campaign composer, or the API directly | The API (and template tooling built on it) |

**Generate** is for drafting: you hand it copy, it hands back translations for you to review, edit, and then commit onto a campaign or template through the regular write endpoints. The campaign composer's translate-locales dialog is a client of this endpoint.

**Commit** is for the template library: it reads one canonical template (its body and, for rich WhatsApp templates, its button labels), translates it, and bulk-creates one real template row per locale. Each created row is indistinguishable from a hand-authored variant. It shares the source template's `channel` and `name`, carries its own `language`, and is submitted to the carrier for approval on creation.

A useful rule of thumb: campaign copy that you review before a specific send → generate; a canonical WhatsApp/RCS template you want available in every market → commit.

## 2. Dashboard walkthrough: the translate-locales dialog

In the campaign composer (**Campaigns → Create**), the **Translate locales** dialog runs the generate half against your campaign's current message:

1. Open the dialog from the message step of the composer. It reads the body (and subject, for email) you have drafted so far as the source, with its locale as the source locale.
2. Pick your **target locales** (BCP-47 tags such as `fr`, `de`, `pt-BR`). Up to 25 per run.
3. Run the translation. The dialog shows one editable variant per locale, plus the source locale as the default variant. Edit any variant's body or subject in place if a translation needs a brand-specific tweak. The response also names any locale the model failed to return (`missingLocales`), so you author that one manually instead of silently shipping fewer variants.
4. Commit the variant set to the campaign. From then on, each recipient receives the variant matching their stored `language` (section 6).

Nothing is persisted server-side by the translation call itself. What the campaign sends is the variant set you reviewed and committed, so the review step is where quality control happens.

For per-channel authoring norms (SMS segment budget, WhatsApp positional tokens, RCS cards) that apply to every variant you review, see the [template + variable personalization walkthrough](/guides/campaign-template-variable-personalization-walkthrough).

## 3. API walkthrough

All calls are authenticated the same way as the rest of the API; the commit endpoint requires the `templates:write` scope and accepts an `Idempotency-Key` header.

### 3.1 Generate: translate a body for review

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/templates/translate" \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Hi {{first_name}}, your order {{order_id}} shipped. Track it: https://ex.co/t/{{order_id}}",
    "sourceLocale": "en",
    "targetLocales": ["fr", "de", "pt-BR", "es"],
    "channel": "sms",
    "tone": "friendly"
  }'
```

The response is one variant per locale, with the source locale included as the default so the set is directly usable:

```json theme={null}
{
  "data": {
    "localeVariantSet": {
      "defaultLocale": "en",
      "variants": [
        { "locale": "en", "body": "Hi {{first_name}}, your order {{order_id}} shipped. Track it: https://ex.co/t/{{order_id}}" },
        { "locale": "fr", "body": "Bonjour {{first_name}}, votre commande {{order_id}} a été expédiée. Suivi : https://ex.co/t/{{order_id}}" },
        { "locale": "de", "body": "Hallo {{first_name}}, deine Bestellung {{order_id}} wurde versandt. Sendungsverfolgung: https://ex.co/t/{{order_id}}" },
        { "locale": "pt-BR", "body": "Olá {{first_name}}, seu pedido {{order_id}} foi enviado. Acompanhe: https://ex.co/t/{{order_id}}" },
        { "locale": "es", "body": "Hola {{first_name}}, tu pedido {{order_id}} fue enviado. Seguimiento: https://ex.co/t/{{order_id}}" }
      ]
    },
    "model": "claude-sonnet-4-5",
    "missingLocales": []
  },
  "meta": { "request_id": "req_01J9Z8ABCDEF", "timestamp": "2026-08-12T09:30:00.000Z" }
}
```

If you pass a `subject`, each variant carries a translated `subject` too. `missingLocales` lists any requested locale the model did not return, so you can re-run just those or author them by hand.

The same call from JavaScript:

```js theme={null}
const res = await fetch("https://api.orbit.devotel.io/api/v1/campaigns/templates/translate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.ORBIT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    body: "Hi {{first_name}}, your order {{order_id}} shipped.",
    sourceLocale: "en",
    targetLocales: ["fr", "de", "pt-BR", "es"],
    channel: "whatsapp",
  }),
});
if (!res.ok) throw new Error(`translate failed: ${res.status}`);
const { data } = await res.json();
// data.localeVariantSet.variants — review, then commit onto your campaign/template
```

### 3.2 Commit: bulk-create per-locale template rows

Give the commit endpoint a stored template id and the locales you want. It translates the template's body (and button labels for rich WhatsApp templates) and creates one row per locale:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages/templates/tmpl_01J9Z8ABCDEF/localize" \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: localize-fall-launch" \
  -d '{
    "targetLocales": ["fr", "de", "pt-BR", "es"],
    "tone": "friendly"
  }'
```

`sourceLocale` is optional; when omitted, the source template row's own `language` is used. The response reports one outcome per locale, each with its own status:

```json theme={null}
{
  "data": {
    "source": { "id": "tmpl_01J9Z8ABCDEF", "channel": "whatsapp", "name": "order_shipped", "language": "en" },
    "model": "claude-sonnet-4-5",
    "localizations": [
      { "locale": "fr", "status": "created", "templateId": "tmpl_01J9ZFR00001", "templateStatus": "pending" },
      { "locale": "de", "status": "created", "templateId": "tmpl_01J9ZDE00002", "templateStatus": "pending" },
      { "locale": "pt-BR", "status": "created", "templateId": "tmpl_01J9ZPT00003", "templateStatus": "pending" },
      { "locale": "es", "status": "skipped", "reason": "variant_already_exists" }
    ],
    "missingLocales": []
  },
  "meta": { "request_id": "req_01J9ZGHIJKLM", "timestamp": "2026-08-12T09:31:00.000Z" }
}
```

One bad locale never aborts the batch: a locale the model dropped is `skipped` with `model_returned_no_variant`, a locale whose row already exists is `skipped` with `variant_already_exists` (re-running localize is safe), and a per-row create failure is `failed` with `create_failed` while the rest still land. `templateStatus` is the status at insert time; the carrier-approval state settles afterwards and is authoritative via the tracker endpoint in section 5.

## 4. Limits and behavior

* **1–25 target locales per call**, on both endpoints. For more markets, call again with the next batch; the commit endpoint's skip-if-exists behavior makes repeat runs harmless.
* **Source locale handling.** Generate always includes the source locale as the default variant and drops it from the translation targets; commit never re-translates the source row.
* **30 s translation ceiling.** The whole translation round-trip is capped at 30 seconds. If it expires you get `504` with `error.code: LLM_TIMEOUT`.
* **Upstream failure** of the translation provider returns `502` with `LLM_UPSTREAM_ERROR`; both are retryable.
* **Unparseable model output** returns `422` with `AI_PARSE_FAILED`. Retry, or retry with a smaller locale batch.
* **No AI provider configured** for your organization returns `503` (`LOCALIZATION_UNAVAILABLE` on commit). Author per-locale variants manually instead.
* **Validation errors** (empty body, more than 25 locales, malformed tags) return `400`.
* **Billing.** Translation runs on your organization's AI tier; usage is attributed to your organization, not to individual users.
* **Placeholder preservation.** The translation contract requires the model to copy every placeholder through exactly as written: named tags (`{{first_name}}`, `{{company}}`) and WhatsApp positional tokens (`{{1}}`, `{{2}}`) are never translated, their braces are never reordered, and no spaces are inserted inside them. URLs, email addresses, and phone numbers are likewise copied verbatim into every variant.
* **Channel length norms.** The model is instructed per channel: SMS bodies stay within 160 characters, push within 120, and WhatsApp/RCS bodies within the carrier body limit (1024 characters) with button labels within 25.

## 5. Per-locale carrier approval

Every WhatsApp/RCS row the commit endpoint creates goes through the same carrier-approval path as a hand-authored template: same submission, same queue, same status lifecycle on the row. There is no separate "localized" approval state; per-locale approval is just each row's `status`.

Poll all locales of one template with the tracker endpoint. The id may be **any** variant's id; it resolves the shared `(channel, name)` and returns every sibling variant with its approval status, carrier external id, and any submission error:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/messages/templates/tmpl_01J9Z8ABCDEF/localizations" \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

Expect one entry per language, each settling independently: `fr` approved while `pt-BR` is still pending or comes back rejected.

**When one locale is rejected and the others are approved:** treat the rejected row exactly like any rejected template. Edit and resubmit that one variant, or delete it; the approved siblings are unaffected and keep sending. Until the rejected locale is approved, recipients whose `language` points at it are served by the send-time fallback chain below rather than by an unapproved template. Re-running `localize` for that locale only recreates the row if you deleted it; an existing row reports `skipped` / `variant_already_exists`.

Approval authoring norms per channel live in [Create and approve WhatsApp templates](/guides/whatsapp/templates-create-approve) and the [RCS templates tab](/guides/rcs-templates-tab).

## 6. How a recipient's language picks a variant at send time

The input is the contact's `language` attribute: a BCP-47-ish tag (`en`, `fr`, `pt-BR`) stored on the contact, set through contact imports and the contacts API, defaulting to `en`.

At send time the resolver (the `resolveLocaleVariant` fallback chain in the template-localization module) picks a variant per recipient in this order, first hit wins:

1. **Exact tag** — a variant whose tag equals the recipient's, case-insensitively (`pt-br` matches `PT_br`).
2. **Primary subtag** — a variant sharing the language part. A bare-primary variant (`pt`) is preferred over a regional one (`pt-BR`), so a `pt-PT` recipient lands on neutral `pt` before an unrelated region.
3. **Your fallback chain** — the optional `fallback_languages` order you set on the template (for example `["es", "en"]`), consulted after the recipient's own language fails to match and before the default. This lets a multi-market sender say "no variant for this language? prefer Spanish, then English."
4. **The default locale** — the variant matching the set's declared source locale.
5. **First variant** — last resort, and a sign the default-locale variant was dropped from the set.

Matching is case-insensitive and treats `_` and `-` as equivalent, so messy CRM values like `"  PT_br "` still resolve. A recipient with no stored language falls through to the default locale.

For variable interpolation inside the chosen variant, see the [personalization walkthrough](/guides/campaign-template-variable-personalization-walkthrough); for reading translation quality back out of your traffic, see the [language quality guide](/guides/language-quality).

## 7. Troubleshooting

| Symptom | Meaning | What to do |
| - | - | - |
| `504` / `LLM_TIMEOUT` | The translation exceeded the 30 s ceiling. More locales and longer bodies make this likelier. | Retry once as-is; if it repeats, split the locale list into smaller batches and re-run. On commit, already-created rows report `skipped`, so splitting is safe. |
| `502` / `LLM_UPSTREAM_ERROR` | The translation provider failed transiently. | Retry with backoff. Nothing was persisted by a failed call. |
| `422` / `AI_PARSE_FAILED` | The model returned output that could not be parsed into variants. | Retry, or re-run with a smaller locale batch. If it persists, author the variants manually. |
| `503` / `LOCALIZATION_UNAVAILABLE` | No AI provider is configured for the organization. | Author per-locale variants manually through the regular template endpoints. |
| `400` | Request validation failed: empty body, more than 25 locales, or malformed locale tags. | Fix the request; check `targetLocales` length and tag shape. |
| `missingLocales` non-empty | The model returned no variant for some requested locales. | Re-run just those locales, or author them manually; the composer flags them in the review step. |
| A variant renders `{{first_name}}` differently or drops a token | Placeholder mangling, despite the verbatim-copy contract. | Diff each variant's `{{…}}` tokens, URLs, and phone numbers against the source before committing; edit the variant in the composer (or update the template row) to restore any altered token exactly as written. |
| Variant created, but WhatsApp sends fail with "Template name does not exist in the translation" | The variant was submitted against a different WABA than the one you send from (multi-WABA orgs). | Author the variant against the WABA you send from, using the same WABA selection (`phone_number_id`) you use when creating templates, and confirm that WABA shows the translation as approved. |
| A locale stays `pending` longer than expected | Carrier approval is asynchronous and per-row; one slow locale does not block the rest. | Poll `GET /api/v1/messages/templates/{id}/localizations`; send the approved locales now and let the pending one catch up. |

## 8. See also

* [Campaign template + variable personalization walkthrough](/guides/campaign-template-variable-personalization-walkthrough) — per-channel authoring norms and variable slots that apply to every variant.
* [Create and approve WhatsApp templates](/guides/whatsapp/templates-create-approve) — the approval path each localized WhatsApp variant joins.
* [RCS templates tab](/guides/rcs-templates-tab) — managing RCS template variants in the dashboard.
* [Language quality guide](/guides/language-quality) — measuring how your localized traffic actually reads.
* [Outbound templates](/guides/outbound-templates) — the template library these variants live in.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.