Skip to content

Powered by Grav + Helios

Translations Editor

Translations Editor

The endpoints behind the Admin Next translations editor. They let you browse every translation string that Grav core, plugins and themes ship, see how complete each language is, and override any string for this site. Overrides are stored per language in user/languages/.yaml. These routes l...

The endpoints behind the Admin Next translations editor. They let you browse every translation string that Grav core, plugins and themes ship, see how complete each language is, and override any string for this site. Overrides are stored per language in user/languages/<lang>.yaml.

These routes live under /i18n, not /translations, and they require authentication. The public Get Translations endpoint (GET /translations/{lang}) is a different thing: it is the admin's own dictionary, served without authentication and filled in from English, so it can't tell a missing string from one translated with the same words. The editor reads the sources directly so that a missing string reads as missing.

Permissions: api.translations.read for the reads, api.translations.write for saving overrides, machine translation and importing. Demo accounts get 403 on every write.

Cell states

Each key in the editor has a cell per language, with a value, the shipped value (what the source provides, or null) and a state:

State Meaning
shipped The value comes from a plugin, theme or core language file
overridden This site has its own value in user/languages/<lang>.yaml
missing No source ships the key in this language and there is no override, so visitors see a fallback

A row's known is false when no source ships the key at all and only a site override names it, for example a typo or a string from a plugin that was removed. Those rows are listed so they can be found and fixed.

Language codes

Language codes must look like en, fr, pt-BR or zh-Hant. On the /i18n/overrides/{lang} routes a code that doesn't match is a 422. Query parameters such as lang and source_lang fall back to the editor's default language instead: the site's default language when a source ships it, else en, else the first language any source ships.

Migrating from translation-strings

Sites that used the translation-strings plugin can bring those overrides in with Import Status and Import Translation Strings. While that plugin is enabled its values still win over user/languages, so import first, check the result, then disable the plugin.

List Languages

GET /i18n/languages
Every language code that any source ships, each flagged with whether this site has an override file for it, plus the editor's default language. Requires `api.translations.read`.
JSON
{"data": {"default": "en", "languages": [{"code": "de", "has_overrides": false}, {"code": "en", "has_overrides": true}, {"code": "fr", "has_overrides": false}]}}

Response Codes

200 Languages returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.

List Sources

GET /i18n/sources
Every source (Grav core, plugins, themes and this site's own overrides) that ships at least one string in the requested language, with the key namespaces it contributes. This drives the editor's browse pane. Requires `api.translations.read`.

Parameters

Name Type Description
lang optional string Language to count keys in. An invalid or missing code falls back to the editor's default language.
JSON
{"data": {"lang": "en", "providers": [{"id": "theme:quark", "kind": "theme", "slug": "quark", "label": "Quark", "enabled": true, "key_count": 42, "namespaces": [{"name": "THEME_QUARK", "key_count": 42}]}]}}

Response Codes

200 Sources returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.

A source id is system:core, plugin:<slug>, theme:<slug> or user:overrides, and kind is system, plugin, theme or user. enabled says whether a plugin is enabled or a theme is the active one; it is always true for core and the site's overrides. Pass an id as provider to List Keys to see that source's strings.

Get Coverage

GET /i18n/coverage
For every language, how many of the source language's keys it has (shipped or overridden) and how many are missing. Requires `api.translations.read`.

Parameters

Name Type Description
source_lang optional string The reference language. An invalid or missing code falls back to the editor's default language.
JSON
{"data": {"source_lang": "en", "coverage": [{"code": "fr", "total": 1868, "translated": 1702, "missing": 166, "overridden": 3}]}}

Response Codes

200 Coverage returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.

total is the number of keys in the source language, and translated and missing are counted against that key set only, so a stale key that only an old translation still carries doesn't count. overridden is the number of site overrides in the language, for any key.

List Keys

GET /i18n/keys
The editor matrix: one row per key, sorted by key, with the source-language value and a cell for each requested language. Keys that only exist as site overrides are included so they can be fixed. Requires `api.translations.read`.

Parameters

Name Type Description
source_lang optional string The reference language. An invalid or missing code falls back to the editor's default language.
langs optional string Comma-separated language codes to include as cells, e.g. `en,fr,de`. Codes no source ships are ignored; defaults to the source language alone.
q optional string Search text, matched case-insensitively against the key and the source-language value.
provider optional string Only keys shipped by this source id, e.g. `theme:quark` (see [List Sources](/2/api/endpoints/translations/list-sources)).
namespace optional string Only keys equal to or under this dotted prefix, e.g. `PLUGIN_ADMIN`.
status optional string `all` (default), `shipped`, `overridden`, `missing` or `unknown`. A row matches when any requested language's cell has that state; `unknown` matches keys no source ships.
page optional integer Page number (default 1).
per_page optional integer Rows per page (default 20, max 500).
JSON
{"data": [{"key": "THEME_QUARK.SEARCH", "namespace": "THEME_QUARK", "source_value": "Search", "providers": ["theme:quark"], "owner": "theme:quark", "known": true, "values": {"en": {"value": "Search", "state": "shipped", "shipped": "Search"}, "fr": {"value": "Rechercher", "state": "overridden", "shipped": "Recherche"}}}], "meta": {"pagination": {"page": 1, "per_page": 20, "total": 1, "total_pages": 1}}, "links": {"self": "/api/v1/i18n/keys?page=1&per_page=20"}}

Response Codes

200 Rows returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.

q matches the value as well as the key because people know the words on screen, not the key behind them. source_value is the site's override in the source language when one exists, otherwise the shipped value. providers lists every source that ships the key and owner is the one whose value wins at runtime. See the collection introduction for the cell states.

The pagination links keep every query parameter you sent (search, filters, languages), so following one stays within the same filtered view.

Get Key

GET /i18n/keys/{key}
One key across every language that any source ships. An unknown key still returns 200, with `known: false` and `missing` cells. Requires `api.translations.read`.

Parameters

Name Type Description
key required string Dotted translation key (path parameter), e.g. `THEME_QUARK.SEARCH`. Only letters, digits, `_`, `.` and `-` are routed; anything else is a 404.
source_lang optional string The reference language for `source_value`. An invalid or missing code falls back to the editor's default language.
JSON
{"data": {"key": "THEME_QUARK.SEARCH", "namespace": "THEME_QUARK", "source_value": "Search", "providers": ["theme:quark"], "owner": "theme:quark", "known": true, "values": {"de": {"value": "Suche", "state": "shipped", "shipped": "Suche"}, "en": {"value": "Search", "state": "shipped", "shipped": "Search"}, "fr": {"value": "Rechercher", "state": "overridden", "shipped": "Recherche"}}}}

Response Codes

200 Key returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.
404 The key contains characters outside the routed set.

Get Overrides

GET /i18n/overrides/{lang}
The site's override file for one language (`user/languages/.yaml`) as YAML, for the advanced editor. Without `namespace` this is the raw file text, or an empty string when there is no file. With `namespace`, only the overrides under that prefix are returned, re-dumped as nested YAML, so "edit as YAML" covers only what is on screen. Requires `api.translations.read`.

Parameters

Name Type Description
lang required string Language code (path parameter), e.g. `fr`.
namespace optional string Dotted key prefix to limit the output to, e.g. `THEME_QUARK`.
JSON
{"data": {"lang": "fr", "scoped": true, "namespace": "THEME_QUARK", "count": 1, "yaml": "THEME_QUARK:\n  SEARCH: Rechercher\n"}}

Response Codes

200 Overrides returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.
422 Invalid language code.

scoped is true when namespace was given, and count is the number of overrides included. Save the edited text back with Replace Overrides, passing the same namespace.

Update Overrides

PATCH /i18n/overrides/{lang}
Inline edits to one language's overrides. `set` writes values and `unset` removes overrides so the shipped value shows again. Sends `X-Invalidates: i18n:update, translations:update`. Requires `api.translations.write`; demo accounts get 403.

Parameters

Name Type Description
lang required string Language code (path parameter), e.g. `fr`.
source_lang optional string Query parameter: the reference language for `source_value` in the returned rows. An invalid or missing code falls back to the editor's default language.
set optional object Object of dotted key to new value. It must be an object: a JSON array is a 422. Numeric keys inside an object are ignored.
unset optional array Keys whose override should be removed. Non-string entries are ignored.
JSON
{"set": {"THEME_QUARK.SEARCH": "Rechercher"}, "unset": ["PLUGIN_ADMIN.TITLE"]}
JSON
{"data": {"written": {"THEME_QUARK.SEARCH": "Rechercher"}, "removed": ["PLUGIN_ADMIN.TITLE"], "reverted": [], "unknown": [], "rows": [{"key": "THEME_QUARK.SEARCH", "namespace": "THEME_QUARK", "source_value": "Search", "providers": ["theme:quark"], "owner": "theme:quark", "known": true, "values": {"fr": {"value": "Rechercher", "state": "overridden", "shipped": "Recherche"}}}, {"key": "PLUGIN_ADMIN.TITLE", "namespace": "PLUGIN_ADMIN", "source_value": "Title", "providers": ["plugin:admin"], "owner": "plugin:admin", "known": true, "values": {"fr": {"value": "Titre", "state": "shipped", "shipped": "Titre"}}}]}}

Response Codes

200 Overrides saved.
401 Unauthorized.
403 Missing `api.translations.write` permission, or a demo account.
422 Invalid language code, `set` that is not an object or `unset` that is not an array, or both empty.

A set whose value equals what the source ships is stored as a removal rather than an override, so the file only holds real differences and keeps following the source's future wording. Those keys are listed in reverted. Keys no source ships are still written, and listed in unknown.

written holds the keys stored as overrides with their values, and removed the keys whose existing override was deleted. rows has a fresh matrix row for every key the request touched, with a cell for this language only, so the editor can update in place.

Replace Overrides

PUT /i18n/overrides/{lang}
Advanced-mode save. Parses `yaml` (a mapping, nested or with dotted keys) and replaces the language's overrides with it. Sends `X-Invalidates: i18n:update, translations:update`. Requires `api.translations.write`; demo accounts get 403.

Parameters

Name Type Description
lang required string Language code (path parameter), e.g. `fr`.
yaml required string YAML mapping of keys to strings. A YAML list is a 422. An empty string deletes every override in scope.
namespace optional string Dotted key prefix to limit the replacement to. Omit it or send an empty string to replace the whole file.
JSON
{"yaml": "THEME_QUARK:\n  SEARCH: Rechercher\n", "namespace": "THEME_QUARK"}
JSON
{"data": {"count": 1, "dropped": [], "unknown": [], "removed": 0}}

Response Codes

200 Overrides saved.
401 Unauthorized.
403 Missing `api.translations.write` permission, or a demo account.
422 Invalid language code, missing `yaml`, YAML that does not parse or is not a mapping, or a scoped save that names keys outside the namespace.

With namespace, only the overrides under that prefix are replaced and everything else in the file is kept. That is what makes saving a filtered view safe: an unscoped save of one theme's strings would delete every other override. A key outside the namespace fails the whole save with a 422 that names the out-of-scope keys (up to five), rather than a parse error.

Values equal to the shipped value are not stored and are listed in dropped. Keys no source ships are saved and listed in unknown rather than rejected, so a typo is reported instead of doing nothing. count is the number of overrides stored from this YAML, and removed how many fewer in-scope overrides exist than before.

Translate Status

GET /i18n/translate
Whether machine translation is available through the AI Translate plugin, and if not, why. The editor hides its translate actions when it is unavailable. Requires `api.translations.read`.
JSON
{"data": {"available": false, "installed": true, "enabled": true, "reason": "not_configured", "max_keys": 200}}

Response Codes

200 Status returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.

reason is null when available is true, otherwise not_installed, not_enabled or not_configured (the plugin has no configured provider), so a client can tell the site owner what to do. max_keys is the most keys one Translate Keys request accepts.

Translate Keys

POST /i18n/translate
Send up to 200 keys' source values to the AI Translate plugin and get proposed translations back. Nothing is written: review the proposals and save the ones you want with [Update Overrides](/2/api/endpoints/translations/update-overrides). Requires `api.translations.write`, since it spends provider credits; demo accounts get 403.

Parameters

Name Type Description
target_lang required string Language to translate into, e.g. `fr`.
keys required array Keys to translate, 1 to 200. Non-string entries are ignored.
source_lang optional string Language to translate from. Defaults to the editor's default language, and must differ from `target_lang`.
JSON
{"source_lang": "en", "target_lang": "fr", "keys": ["THEME_QUARK.SEARCH", "PLUGIN_ADMIN.TITLE"]}
JSON
{"data": {"source_lang": "en", "target_lang": "fr", "proposals": [{"key": "THEME_QUARK.SEARCH", "source": "Search", "value": "Rechercher", "ok": true, "reason": null}, {"key": "PLUGIN_ADMIN.TITLE", "source": "Title", "value": "Titre", "ok": true, "reason": null}]}}

Response Codes

200 Proposals returned.
401 Unauthorized.
403 Missing `api.translations.write` permission, or a demo account.
422 Machine translation unavailable, missing `target_lang` or `keys`, no keys or more than 200, source and target the same, or the provider call failed.

Placeholders are masked before the text is sent and checked when it comes back. Proposals come back in the order the keys were sent. A proposal with ok: false has a null value and one of these reason values:

Reason Meaning
no_source The key has no value in the source language
icu_needs_human An ICU message that a person has to translate
nothing_to_translate The value has no translatable text, only placeholders or markup
provider_returned_nothing The provider returned an empty result
placeholders_mangled The provider altered a placeholder, so the proposal was refused

The first three are decided before the provider is called, so they cost nothing.

Import Status

GET /i18n/import/translation-strings
What the old translation-strings plugin has configured, and what importing it would do for each language. Read-only. Requires `api.translations.read`.
JSON
{"data": {"present": true, "plugin_enabled": true, "config_path": "/var/www/grav/user/config/plugins/translation-strings.yaml", "pending": 4, "total": 6, "languages": [{"code": "fr", "total": 6, "new": 3, "already": 1, "conflict": 1, "shipped": 1, "unknown": 0, "keys": [{"key": "THEME_QUARK.SEARCH", "status": "new", "unknown": false, "current": null, "value": "Rechercher"}]}]}}

Response Codes

200 Import preview returned.
401 Unauthorized.
403 Missing `api.translations.read` permission.
422 The plugin's config has a language code the editor cannot store.

present is true when the plugin has at least one override configured, and plugin_enabled matters because while the plugin is enabled its values still win over user/languages. pending counts the keys an import would write (new plus conflict). config_path is the absolute path of the plugin's config file; demo accounts get (hidden in demo mode) instead.

Per language, new keys have no override yet, already keys are overridden with the same value, conflict keys are overridden with a different value that importing replaces with the plugin's, and shipped keys equal the shipped value so importing stores nothing. The counts are exact, but keys lists at most 50 keys per language. In each listed key, current is the site's existing override (or null) and value is the plugin's value.

Import Translation Strings

POST /i18n/import/translation-strings
Merge every override configured in the translation-strings plugin into `user/languages/.yaml`. Overrides for other keys are kept; where both name the same key, the plugin's value wins, because that is what the site shows today. Sends `X-Invalidates: translations:update`. Requires `api.translations.write`; demo accounts get 403.
JSON
{"data": {"imported": 4, "reverted": 1, "unknown": [], "languages": [{"code": "fr", "written": 4, "reverted": 1, "unknown": 0, "path": "/var/www/grav/user/languages/fr.yaml"}], "plugin_enabled": true}}

Response Codes

200 Import completed.
401 Unauthorized.
403 Missing `api.translations.write` permission, or a demo account.
422 The plugin has no overrides configured, or its config has a language code the editor cannot store.

Every language code in the plugin's config is checked before anything is written, so a bad code fails the whole import with a 422 naming it, and no language is left half imported.

The plugin is not disabled for you. Turning it off is a config change with its own permission, so do it afterwards through Update Config; plugin_enabled reports its current state. imported counts the overrides written across all languages, reverted the values skipped because they matched the shipped value, and unknown lists imported keys that no source ships.