Translations API
A site has one main language, set by its language, and can be offered in
other languages too. Each other language serves the same storefront under a
path prefix, such as /fr/ for French, with your products, pages, and
checkout in that language. Shoppers who browse in it get their order email
in it too.
The storefront's own text, such as buttons and checkout labels, is already
translated. Your content is not: these endpoints list what there is to
translate into each language and save your translations. Until a field is
translated, the storefront shows it in the main language.
Related resources:
- Sites for each site's main
languageand its other
languages - Pages, Collections,
Products, Product Options,
Shipping Rates, and
Gift Options for the content being translated
Endpoints
- Set Site Languages -
PUT /api/v1/sites/{site_id}/languages/ - List Translations -
GET /api/v1/sites/{site_id}/translations/ - Summarize Translations -
GET /api/v1/sites/{site_id}/translations/summary/ - Save Translations -
PUT /api/v1/sites/{site_id}/translations/
What Can Be Translated
A translation is one field of one piece of content in one language. Its id
names the content: a public ID for anything that has one, a page type for an
info page, or home for the home page.
| Content | id |
Fields | Longest translation |
|---|---|---|---|
| Home page | home |
hero_content, seo_title, seo_description |
65,536, 255, and 1,023 characters |
| Info page | Page type, such as about or shipping |
content |
65,536 characters |
| Collection | col_... |
name, description |
256 and 2,047 characters |
| Product | pro_... |
name, description |
256 and 2,047 characters |
| Product image | img_... |
alt_text |
512 characters |
| Product option | pop_... |
name |
256 characters |
| Option value | pov_... |
value |
256 characters |
| Storefront filter | sfl_... |
name |
256 characters |
| Shipping rate | shr_... |
name |
100 characters |
| Gift option | gft_... |
name, description |
256 and 1,000 characters |
The info page types are about, shipping, returns, terms, privacy,
contact, order_confirmation, and announcement_bar. Only fields with
text in the main language can be translated: a product with no description
has no description to translate, and an image without
alt text has none.
Status
Every translation has a status:
| Status | Meaning |
|---|---|
missing |
Not translated. The storefront shows the main-language text |
outdated |
Translated, but you have since changed the main-language text. The storefront still shows the old translation |
done |
Translated from the current main-language text |
Each translation keeps the main-language text it was translated from, as
translated_from. Editing that text in the main language makes the
translation outdated until you save a new one; source is the text to
translate, and comparing it with translated_from shows what changed.
Turning a Language Off
Removing a language from a site stops serving the storefront in it and
hides its translations from these endpoints, but deletes none of them.
Adding the language back restores them, each with the status it would have
had: any you translated from text you have since changed come back
outdated.
Shoppers who used a language the site no longer offers see the main
language, and their email is sent in it.
Set Site Languages
Replace the languages a site is offered in besides its main one.
Endpoint: PUT /api/v1/sites/{site_id}/languages/
Permission Required: Write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
languages |
array | Yes | Language codes, such as ["de", "fr"]. An empty array offers only the main language |
Each language must be one the storefront's own text is translated into:
bg, cs, da, de, el, en, es, et, fi, fr, ga, hr, hu,
it, lt, lv, mt, nl, pl, pt, ro, sk, sl, or sv. A site
can offer at most 32 languages besides its main one, which can't be in the
list. Repeated languages are ignored, and the site lists them sorted.
Request
curl -X PUT "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/languages/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"languages": ["fr", "de"]}'
Response
Status: 204 No Content
Get Site then lists "languages": ["de", "fr"].
Error Responses
Missing languages
Status: 400 Bad Request
{
"error": "languages is required",
"code": "MISSING_FIELD"
}
Unsupported or main language
Status: 400 Bad Request
{
"error": "languages can't include the site's main language",
"code": "INVALID_FIELD"
}
A code that isn't a language, one the storefront isn't translated into, or
more than 32 languages are refused the same way, each with its own message.
Site not found
Status: 404 Not Found
List Translations
List a page of a site's translations. By default it lists the work left to
do, missing and outdated, in every one of the site's other languages, so a
translator can work down the list.
Translations are listed in the order the dashboard shows them: the home
page, info pages, collections, products with their options and option
values, shipping rates, then gift options.
Endpoint: GET /api/v1/sites/{site_id}/translations/
Permission Required: Read
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status |
string | todo (the default: missing and outdated), missing, outdated, done, or all |
language |
string | Only this language, which must be one of the site's other languages |
product |
string | Only this product's content: its name and description, its images' alt text, its options, and their values |
after |
string | The cursor of the last translation on the previous page |
before |
string | The cursor of the first translation on the next page, to page backward |
limit |
integer | Number of translations to return (default 10, max 100) |
Translations are paged by their cursor, not their id, since one piece of
content has a translation for each field and language. after and before
can't be used together. has_more says whether there are more past the page
in the direction it was read: after it, or before it with before.
A translation saved while you page through a todo list drops out of it, so
keep passing the cursor of the last translation you read, not of the last
one you saved.
Request
curl -X GET "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/translations/?language=fr&limit=2" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 200 OK
{
"data": [
{
"id": "pro_d0b6fmv28q6vn14peun0",
"field": "name",
"language": "fr",
"label": "Wool Blanket",
"source": "Wool Blanket",
"value": null,
"translated_from": null,
"translated_at": null,
"status": "missing",
"cursor": "pro_d0b6fmv28q6vn14peun0:name:fr"
},
{
"id": "pov_d0b6fmv28q6vn14peup0",
"field": "value",
"language": "fr",
"label": "Wool Blanket › Color",
"source": "Forest Green",
"value": "Vert",
"translated_from": "Green",
"translated_at": "2026-10-18T12:00:00Z",
"status": "outdated",
"cursor": "pov_d0b6fmv28q6vn14peup0:value:fr"
}
],
"has_more": true
}
Error Responses
Invalid query parameter
Status: 400 Bad Request
{
"error": "Invalid 'language' parameter: must be one of the site's other languages",
"code": "INVALID_REQUEST_ERROR"
}
An unknown status, a cursor for a translation the site no longer has,
both after and before, or a limit over 100 are refused the same way.
Site or product not found
Status: 404 Not Found
Summarize Translations
Count what is done and left to do in each of a site's other languages, in
the order the site lists them. It reads all of the site's content, so use it
to show progress, not before every request.
Endpoint: GET /api/v1/sites/{site_id}/translations/summary/
Permission Required: Read
Request
curl -X GET "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/translations/summary/" \
-H "Authorization: Bearer your_api_key_here"
Response
Status: 200 OK
{
"data": [
{
"language": "de",
"total": 148,
"done": 0,
"missing": 148,
"outdated": 0
},
{
"language": "fr",
"total": 148,
"done": 121,
"missing": 20,
"outdated": 7
}
]
}
done, missing, and outdated add up to total. A site with no other
languages returns an empty data. The list isn't paged, since a site offers
at most 32 other languages.
Save Translations
Save up to 100 translations at once, all or none. Each is saved as
translated from the field's current main-language text.
Endpoint: PUT /api/v1/sites/{site_id}/translations/
Permission Required: Write
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
data |
array | Yes | 1 to 100 translations to save |
Each translation in data:
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | The content, as in What Can Be Translated |
field |
string | Yes | The field to translate |
language |
string | Yes | One of the site's other languages |
value |
string | Yes | The translation. Surrounding whitespace is trimmed, and "" deletes the translation |
source |
string | No | The main-language text you translated. The translation is refused if it is no longer the current text |
value must be present even to delete, so a translation left out by mistake
never deletes one. Pass source, the source you listed, whenever the
translation may be saved after a delay, such as by a translator working
through a list: if you change the text meanwhile, the save is refused with
SOURCE_CHANGED instead of saving a translation of old text as done.
Request
curl -X PUT "https://studio.easel.engineering/api/v1/sites/sit_d0b6fmv28q6vn14peun0/translations/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-97531" \
-d '{
"data": [
{
"id": "pro_d0b6fmv28q6vn14peun0",
"field": "name",
"language": "fr",
"value": "Couverture en laine",
"source": "Wool Blanket"
},
{
"id": "pov_d0b6fmv28q6vn14peup0",
"field": "value",
"language": "fr",
"value": ""
}
]
}'
Response
Status: 200 OK
{
"saved": 1,
"deleted": 1
}
Error Responses
Translations refused
Status: 400 Bad Request
When any translation is refused, none are saved. errors lists every
refused one by its index in data, so you can fix them all and send the
batch again:
{
"error": "2 of 3 translations were refused, so none were saved",
"code": "TRANSLATIONS_REFUSED",
"errors": [
{
"index": 0,
"code": "SOURCE_CHANGED",
"message": "The main-language text changed after this was translated; translate the current source"
},
{
"index": 2,
"code": "TOO_LONG",
"message": "value must be at most 256 characters"
}
]
}
| Code | Meaning |
|---|---|
MISSING_FIELD |
id, field, or language is missing |
INVALID_FIELD |
value is missing |
INVALID_LANGUAGE |
language isn't a language code |
UNKNOWN_ID |
No content on this site has this id; it may have been deleted |
UNKNOWN_FIELD |
The content has no such field. The message lists the fields it has |
TOO_LONG |
value is longer than the field takes |
LANGUAGE_NOT_OFFERED |
The site isn't offered in language. Add it first |
NO_SOURCE |
The field is empty in the main language, so there is nothing to translate |
SOURCE_CHANGED |
source is no longer the field's main-language text |
Empty or too large a batch
Status: 400 Bad Request
{
"error": "Save at most 100 translations at a time",
"code": "INVALID_FIELD"
}
An empty data is refused with MISSING_FIELD.
Site not found
Status: 404 Not Found
Translation Object
| Field | Type | Description |
|---|---|---|
id |
string | The content translated: a public ID, a page type, or home |
field |
string | The field translated, such as name |
language |
string | The language translated into, such as fr |
label |
string | Context for a translator, such as the product and option an option value belongs to |
source |
string | The field's current text in the site's main language |
value |
string|null | The translation, or null when there is none |
translated_from |
string|null | The main-language text value was translated from. It differs from source when the translation is outdated |
translated_at |
string|null | ISO 8601 timestamp of when value was saved |
status |
string | missing, outdated, or done (see Status) |
cursor |
string | Names the translation for after and before |