---
name: localize-io-translation
description: Translate and proofread strings, files and screens through the Localize.io API — read the client's configuration, supply glossaries, translation memory, style guides and screenshots, build each string's context, submit a job, check what arrived, wait for it, collect the result and keep the translation memory current. Use when asked to localize, translate or proofread product copy with Localize.io, or to integrate a build or CI step with it.
---

# Translating with Localize.io

Localize.io is a translation API. You send an order — strings with ids, a file in your
own format, or one line of text — and collect the finished work. Your company's
configuration decides which languages you may order and which models do the work;
your glossary, translation memory, style guides and screenshots shape it. You read
it and order within it. The one thing an order may choose for itself is its models,
from those the service offers (section 5).

Base URL: `https://t.localize.io`. Every `/v1` call takes the key:

```
authorization: Bearer $LOCALIZE_IO_TOKEN
```

Keep the key in an environment variable. Never print it, commit it or paste it into
an order.

## How it works

A few ideas explain most of what the API does, and most of what it refuses.

- **Your configuration is the contract.** You order the languages, pairs and content
  types it declares, and nothing else. Nothing is filled in with a sensible default:
  an order for something that is not configured is refused straight away, with what
  is configured in the message, rather than translated somehow and billed.
- **Your material is read once, and every job draws on it.** A glossary, a memory, a
  style guide or a screenshot is understood when you upload it, down to every
  attribute it carries — not re-read, or skimmed, per request.
- **Each string gets what applies to it.** Before anything is translated, the
  service works out which of your terms bear on each string, and which of your
  screenshots it appears on. The translator sees those, not your whole glossary —
  so a rule meant for one word is not stretched over a sentence it has nothing to
  do with.
- **Everything is translated, and memory is evidence.** Each string goes to the
  translator with its exact matches from your memory, and the ten earlier
  translations nearest to it in meaning, each with how near it is. Even a string
  your memory already has is translated with that in front of it, and the result
  says whether it was reused as is, corrected, or written fresh. Nothing is copied
  through unseen.
- **The memory is yours to keep.** Nothing a job delivers is added to it: your
  memory here is exactly the files you last uploaded. Approved translations go into
  your own memory, and the memory is uploaded again before the next order (section
  9).
- **What can be checked is checked.** Every string must come back, placeholders
  intact, inside its length, with its line breaks; your terminology is reviewed on
  its own. What fails a check is sent back and redone within the job, before you
  see it.
- **Your identifiers are yours.** Ids and language codes come back exactly as you
  sent them.
- **`failed` means it was stopped on purpose.** When a job cannot be done properly,
  it stops and says why, rather than delivering something that looks right and is
  not.
- **Every job can be explained.** What it was made from, and what it cost, is kept
  with it.

## 1. Read the configuration before the first order

What an order may say is exactly what the configuration declares. Read it rather
than guessing — an order outside it is refused, with nothing spent.

```bash
curl -s https://t.localize.io/v1/config -H "authorization: Bearer $LOCALIZE_IO_TOKEN"            # your config.json, as it is
curl -s https://t.localize.io/v1/projects -H "authorization: Bearer $LOCALIZE_IO_TOKEN"          # which projects exist
curl -s https://t.localize.io/v1/projects/PROJECT/config -H "authorization: Bearer $LOCALIZE_IO_TOKEN"
```

Each is the file itself, byte for byte. Read it for:

- **`languages`** — the targets you may order. `{"pt-BR": {"from": "en"}}` means
  Brazilian Portuguese out of English only; no `from` means from any source. A language written
  inside a content type or a pass counts too.
- **`content_types`** — every order must name one of these.
- **`aliases`** — what your codes mean, on orders and inside your files
  (`"zh_Hans": "zh-CN"`, `"en-001": "en"`). Send your own code; it comes back as
  you sent it. **Your assets' codes are matched exactly or through this map, never
  guessed**: a memory tagged `zh-CN` serves `zh-Hans` jobs only once
  `"zh-CN": "zh-Hans"` is written. Case and `_` need no alias. After uploading, read
  `read[].problems` in `GET /v1/assets`: a code nothing maps is listed there with how
  much material sits under it, and a job whose own language gets nothing from that
  material carries a warning saying so until it is fixed.
- **models** — `model` (translates), `annotator` (reads screenshots), `qa` (checks
  terminology), `selector` (chooses the terms and screenshots each string is shown)
  and `embedder` (searches your translation memory by meaning), at the top, in
  `defaults`, under a content type, under a language, or under `job_types` for a
  pass. This is what will run unless an order chooses its own `models` (see
  "Choosing models for one order" below). Where nothing names an `annotator`, `qa`,
  `selector` or `embedder`, ours are used; the translating `model` is never chosen
  for you. `GET /v1/models` lists what each may be.

A project that declares languages or content types speaks for itself; one that
declares none uses the client's.

**Changing it.** Send the whole file back with `PUT`, carrying the `ETag` you read
it with as `If-Match`. It needs a key minted with `config:write`, or the account's
own token:

```bash
curl -s -D- https://t.localize.io/v1/config -H "authorization: Bearer $LOCALIZE_IO_TOKEN" -o config.json   # note the ETag
# edit config.json, then:
curl -s -X PUT https://t.localize.io/v1/config -H "authorization: Bearer $LOCALIZE_IO_TOKEN" \
  -H 'If-Match: "…the ETag…"' -H 'content-type: application/json' --data-binary @config.json
# → {"etag": "…", "changed": ["languages"]}
```

- Checked before it replaces anything, and refused whole with every problem named
  (`config_invalid`, `model_not_listed`, `model_not_allowed`, `engine_unknown`). A
  model it names must be one `GET /v1/models` lists for that purpose.
- `412` means it changed since you read it: read it again and apply your change to
  that. `428` means you sent no `If-Match`.
- It takes effect on the next order; nothing is read again.
- Every replaced version is kept: `GET /v1/config/history`, and
  `POST /v1/config/restore {"version": "…"}` puts one back. Projects the same, under
  `/v1/projects/PROJECT/config`. `POST /v1/projects` creates a project, and needs
  `config:write` too.

## 2. Supply your material

What shapes a translation is material you supply once and every job uses: a
glossary, a translation memory, style guides, screenshots of the product. Upload it
as files; there is nothing to declare and nothing to run afterwards. An upload or a
removal has your material read straight away, and a job whose material has not
been read yet waits for it, up to five minutes, then goes ahead on a first look at
the files with a warning on its result saying so.

It lives at **two levels**:

```bash
# the client's — applies to every project, and to jobs that name no project
curl -s -X PUT "https://t.localize.io/v1/assets/glossary/company terms.tbx" \
  -H "authorization: Bearer $LOCALIZE_IO_TOKEN" --data-binary @"company terms.tbx"

# one project's — applies to that project alone
curl -s -X PUT "https://t.localize.io/v1/projects/PROJECT/assets/tm/app.tmx" \
  -H "authorization: Bearer $LOCALIZE_IO_TOKEN" --data-binary @app.tmx
```

The body is the file's raw bytes; the path is yours to arrange. **What a file is
is worked out from what is inside it**, not from its name or folder. Which languages
it is for is read from its contents, its name and its folders, and kept exactly as
written — `EN-UK` stays `EN-UK`. Folders scope nothing: everything at a level
applies to every job there, whatever its content type.

| what you upload | read as |
|---|---|
| csv, tsv, xlsx, json, bilingual tbx | a **glossary** — terms with their translations, applied to jobs translated *from* the language its terms are written in (read from the file, never assumed to be English) |
| monolingual tbx | **terminology** — approved terms for one language |
| tmx, xliff, po, or a spreadsheet with a column per language | your **translation memory** |
| md, txt, rst, html, docx; a pdf is read by a model | a **style guide** |
| png, jpg, webp, gif | a **screenshot** of the product, described once and shown to the translator with the strings that are on it |

Nothing is dropped from a glossary: every attribute the file fills in — definition,
part of speech, usage, status, a forbidden variant, an extra column such as `Notes`
— reaches the translator and the terminology check, under the name the file gives
it. A translation memory keeps every translation of a string with its key, context,
review state and dates. It is **never copied past the model** — the model is shown
it and says what it did (`memory_use` in the result). Besides exact matches, each
string is shown the ten remembered strings nearest to it in meaning, scored; to find
them, a memory is made searchable once, by the first job that needs it — seconds
for most, about two minutes for one of 100,000 units — and reused after that, only
new strings being added. If your memory's units carry
the same keys as your segment ids (`x-context_seg_key`), the unit for this very
segment is recognised as such.

**Check what was made of each file** after uploading:

```bash
curl -s "https://t.localize.io/v1/assets" -H "authorization: Bearer $LOCALIZE_IO_TOKEN"
curl -s "https://t.localize.io/v1/projects/PROJECT/assets" -H "authorization: Bearer $LOCALIZE_IO_TOKEN"
# → {"assets": [{"path": "glossary/terms.tbx", "kind": "glossary", "read_as": "glossary",
#                "size": 1062901, "why": "TBX-Basic, one langSet per language…",
#                "by": "session", "languages": ["en", "nl-NL"], "read": {…},
#                "feeds": ["glossary.nl-NL"], "counts": {…}, "problems": []}, …]}
```

`kind` is what the file is, `read_as` how it is used, `why` what was seen in it.
**A fresh upload has no classification yet**: the answer to the `PUT` says only
`"reading": true`, and the listing shows `kind: null` with `by: "unread"` until a
model has read the file, usually about a minute. Do not wait on it before ordering
(the job waits); list again later if you want to see what it was taken for.
`by` says whether it has been read (`session`), is still a first look (`guess`),
or was passed over when the rest was read (`unanswered`); `languages` are as the
file writes them; `read` is how it was read, `feeds` what it went into and `counts`
what came out. These appear once the file
has been used for a job. **`problems` are what to act on** — a file that could not
be placed, a glossary with no entries, a memory in a format that is not read, memory
units with text in one language only. Codes your configuration does not name are
reported per level, in `read[].problems`. A
file whose kind is `unknown` is stored but not used — `why` says what could not be
read. Take a file back byte for byte with `GET` on its path, and remove it with
`DELETE`.

Where the two levels disagree, **the project wins the thing it overrides and
inherits the rest**: a project's term replaces the client's for that term and every
other term stays; style guides add up, the client's voice first; memories merge,
the project's entry winning a conflict.

## 3. Build each string's context

Context is the strongest thing you can give the translator, and it is the part that
is easy to skip. Write it **while you extract the strings**, with the component or
template in front of you, not afterwards from the key. The strings next to it tell
you the section, the component tells you the element, and the rendered screen tells
you how much space it has.

The test, for every string: **could someone who has never seen your code find this
text on the screen from your context alone?**

| | context |
|---|---|
| yes | `Footer, on every page. Brand tagline under the company logo.` |
| yes | `Checkout, empty basket. Heading above the "Continue shopping" button. Max ~30 chars.` |
| no, too vague | `Shared\|cta_text`, `Homepage\|Button`, `Footer link` |
| no, machine paths and ids: a cleaner path is still a path | `COMMON -> MENU -> ITEMS -> TITLE`, `/tile/body/ul/li[2]/text()`, `NFDBB2FA9-tu56` |

Say, in one or two plain sentences:

1. **Where:** the page or screen, or that it is a shared component.
2. **Where on it:** the section or component ("hero", "FAQ", "footer, social links").
3. **What it is:** heading, button, tab label, alt text for the logo, error message.
4. **What it does:** "opens the terms of service", "shows when the item was published".
5. **How much space**, for short interface strings. Better still, put the limit in
   `max_length`, which is enforced.

Context describes the string; it is not part of it. Do not put tone, audience or
style rules in it: those belong in your style guide, once, rather than differently
on every string. Keep ids stable from order to order: a memory entry carrying
the same id as its key is recognised as this very string's, beside the ones found
by similarity. An id taken from the string's place in the batch (`seg-0`, `seg-1`,
an array index) is not stable: it looks fine within one order and names a different
string in the next. Use a key your code or content already gives the string. For anything on a screen, **attach a screenshot**
(section 4): it shows the space, the neighbours and the look better than any
sentence.

## 4. Attach screenshots to an order, if you have them

A screenshot that belongs to one order rather than to the product — this string,
this screen, now — is uploaded against the order instead. It is kept until you
delete it, and one upload can be named by any number of orders.

It tells the translator what a string is — a button of fixed width, a heading, a
label in a crowded row. Upload it first, then name it in the order.

```bash
curl -s -X POST https://t.localize.io/v1/uploads -H "authorization: Bearer $LOCALIZE_IO_TOKEN" \
  -F 'file=@checkout.png;type=image/png' \
  -F 'meta={"note": "the empty state of the checkout screen"};type=application/json'
# → {"id": "up_9f2c8d1e4a7b", "name": "checkout.png", ...}
```

- `file` is the image: PNG, JPEG, WebP or GIF, up to 32 MB. Video is refused.
- `meta` is optional JSON with `note` (what the picture is — up to 2,000 characters,
  line breaks allowed) and `name`. Any other key is refused, so check the spelling.
- Do **not** send the note as an `x-note` header; that is refused.
- A `503` means no model can read pictures for this account right now: order
  without the attachment, and put what the picture shows in the segment's `context`.

The upload returns at once; the picture is read in the background, and an order
naming it waits for that.

## 5. Submit the order

```bash
curl -s -X POST https://t.localize.io/v1/jobs -H "authorization: Bearer $LOCALIZE_IO_TOKEN" \
  -H 'content-type: application/json' -d '{
    "requester": "mobile-ci",
    "content_type": "ui",
    "project": "mobile",
    "source_lang": "en",
    "target_langs": ["de", "nl"],
    "instructions": "Mobile app. Terse, imperative.",
    "references": ["up_9f2c8d1e4a7b"],
    "segments": [
      {"id": "checkout.empty.title", "text": "Your basket is empty",
       "context": "heading on the empty checkout screen", "max_length": 32,
       "references": ["up_9f2c8d1e4a7b"]}
    ]}'
# → {"job_id": "job_ab12cd34ef56", "status": "queued", ...}
```

**Required:** `requester` (who asked — a person, a team or a system name; your side's
answer to "who ordered this"), `content_type`, `source_lang`, `target_langs`, and
`segments` with a unique `id` and the `text` for each.

**Optional:** `project`; `instructions` (a brief for the whole order);
per segment `context` (where the string appears — worth writing), `max_length` (a hard
character limit the space it sits in imposes) and `references`; `references` on the
order for pictures about the whole job; `steps: ["translate", "proofread"]` to have
the draft reviewed as a second pass; `models` to choose the models for this order
instead of the configured ones (below).

**Choosing models for one order.** `"models": {"translation": "…", "annotation":
"…", "qa": "…", "selection": "…"}` — any of the four. `translation` translates it,
`annotation` reads its screenshots, `qa` checks its terminology, `selection`
chooses which of your terms and memory each string is shown. Each must be one of
the models `GET https://t.localize.io/v1/models` lists for it; anything else is refused. `qa`
cannot be the model that translates. A file job names them on the query:
`translation_model=`, `annotation_model=`, `qa_model=`, `selection_model=`. Your
configuration can set the same four as defaults: `model`, `annotator`, `qa`,
`selector`.

Keep ids stable across orders — they are how your strings are recognised, and a
translation memory keyed by them is matched to this very segment.

**A file in your own format** goes as the body, as it is, with `requester`,
`content_type` and `project` on the query
(`POST /v1/jobs?project=mobile&content_type=ui&requester=mobile-ci`); collect it back
filled in from `GET /v1/jobs/{job_id}/file`.

**Refused at intake** is `422` with a list of problems, each with a `code` and a
message that names what is configured:

```json
{"detail": {"message": "the job did not pass the intake checks",
            "problems": [{"code": "pair_not_configured",
                          "message": "de -> pt-BR is not configured for this client — pt-BR is configured from en (…)"}]}}
```

Fix the order from the message and send it again. Do not retry it unchanged, and do
not "fix" it by dropping the language or inventing a content type — tell whoever
asked. Common codes: `requester_required`, `content_type_required`,
`content_type_not_configured`, `language_not_configured`, `pair_not_configured`,
`unknown_reference`, `unknown_project`, `model_not_listed`, `model_not_allowed`,
`qa_is_translator`. A project is a folder name, exactly as
`GET /v1/projects` lists it.

## 6. Check that your context arrived

Do this on the first order from any new script, and after any change to how you
build orders:

```bash
curl -s https://t.localize.io/v1/jobs/JOB_ID/trace -H "authorization: Bearer $LOCALIZE_IO_TOKEN"
```

`order.segments[].context` is the order as it arrived. **If it is empty where you
wrote context, stop**: your own code dropped it on the way out (a wrapper that sends
only `id` and `text` is the usual cause), and the translation was made blind,
however good it reads. The job has run and is billed; do not use its output, and
treat anything concluded from it, such as a quality comparison, as void. Fix the
code that builds the order, then order again. Check `max_length` and `references`
the same way.

## 7. Wait for it

```bash
curl -s https://t.localize.io/v1/jobs/JOB_ID -H "authorization: Bearer $LOCALIZE_IO_TOKEN"
```

`status` goes `queued` → `processing` → `done`, or `failed`. Poll every five to ten
seconds; small jobs take seconds, large ones minutes. There is no callback.

**`failed` is final.** The job was stopped on purpose and says why in `error`. Do
not resubmit it automatically: read the error, report it, and resubmit only once
the cause is dealt with.

## 8. Collect the result

```bash
curl -s https://t.localize.io/v1/jobs/JOB_ID/result -H "authorization: Bearer $LOCALIZE_IO_TOKEN"
```

```json
{"status": "done", "result": {
   "segments": [
     {"id": "checkout.empty.title", "lang": "de", "text": "Dein Warenkorb ist leer",
      "notes": null, "memory_use": "fixed"}
   ],
   "warnings": [], "usage": [{"model": "…", "cost_usd": 0.0012, "…": "…"}],
   "error": null}}
```

- One entry per segment **per target language**, keyed by your `id` and your
  language code, spelled as you sent them.
- `memory_use` is what the model says it did with your translation memory:
  `as_is` (a translation you already had, unchanged), `fixed` (one of yours,
  corrected or adapted) or `new` (translated from scratch). `fixed` is worth a look —
  it changes wording that shipped before.
- `warnings` are worth passing on: an asset that could not be read, a glossary with
  no entries for a language. The work still completed.
- `usage` is what each model call for the job cost — the translation, and
  every other step, named by `purpose`: `select` and `select-screens` (choosing
  terms and screenshots), `qa` (the terminology check), `embed` (your job's
  strings made searchable against your memory); `GET /v1/jobs` shows the total per
  job as `cost_usd`.
- What a job is **charged** is on `GET /v1/jobs`: `price_usd` — its `characters`
  (your source text, once per target language) at your plan's rate, plus its model
  usage at cost (`cost_usd`, passed through one to one), itemised in `billing` and
  fixed when the job was delivered. A failed job is not charged.
- With your own OpenRouter key in place (bring your own key — we put it in on
  request), every model call is made on your account, billed to you by OpenRouter,
  and your prices are characters only. `GET /v1/openrouter-key` says whether it is
  in use and what went through it.
- `PUT /v1/budget {"monthly_usd": 500}` caps a month; an order that would pass it
  is refused with `402` and `"code": "budget_exceeded"` — not a mistake in the
  order, so do not retry it; raise the budget or wait for the next month.
- **Zero data retention is on by default:** providers keep none of the text. Where
  the account runs on a key kept for it alone, it may switch that off
  (`PUT /v1/openrouter-key/zdr {"enabled": false}`), and models whose providers may
  keep text become available; `GET /v1/models` answers for the account as it is.
- `GET /v1/costs?month=YYYY-MM` is a month of that, by project, by API key and by
  kind of model work; `GET /v1/costs.csv` the same as a spreadsheet.
- `409` means not finished yet; `422` means it failed, with `{"status": "failed",
  "error": "…"}` — read `error`.

`GET /v1/jobs/JOB_ID/trace` shows how the job was made — which model, what it was
sent, what it cost — for when a result needs explaining.

## 9. Keep your translation memory

Nothing a job delivers is remembered here: the memory used is exactly the file you
last uploaded. So the memory is yours to keep.

Keep every translation as three things together: the **source** text, the
**target** text, and its **context**.

- **Source:** the original string, exactly as it appears in your product.
- **Target:** the approved translation, under the language code you order with.
- **Context:** where the string appears and what it does, for example "Checkout
  page, button that confirms payment". Without it, the same word in a menu and in a
  heading looks identical.

When a translation is approved or corrected, add it to your memory, and upload the
memory again before your next order:

```bash
curl -s -X PUT "https://t.localize.io/v1/assets/tm/memory.tmx" \
  -H "authorization: Bearer $LOCALIZE_IO_TOKEN" --data-binary @memory.tmx
```

## Everything else

- `GET /v1/jobs?status=failed&project=mobile&limit=20` — recent jobs, newest first.
- `POST /v1/keys {"name": "CI"}` — a key for one integration, shown once. Use a key
  per integration so one can be revoked alone. Every key may order and read; add
  `"scopes": ["config:write"]` for one that may change configuration. A key can
  grant only what it holds itself.
- Full reference: https://t.localize.io/docs · machine-readable: https://t.localize.io/openapi.json ·
  summary: https://t.localize.io/llms.txt
