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
/i18n/languages
{"data": {"default": "en", "languages": [{"code": "de", "has_overrides": false}, {"code": "en", "has_overrides": true}, {"code": "fr", "has_overrides": false}]}}
Response Codes
List Sources
/i18n/sources
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. |
{"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
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
/i18n/coverage
Parameters
| Name | Type | Description |
|---|---|---|
| source_lang optional | string | The reference language. An invalid or missing code falls back to the editor's default language. |
{"data": {"source_lang": "en", "coverage": [{"code": "fr", "total": 1868, "translated": 1702, "missing": 166, "overridden": 3}]}}
Response Codes
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
/i18n/keys
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). |
{"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
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
/i18n/keys/{key}
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. |
{"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
Get Overrides
/i18n/overrides/{lang}
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`. |
{"data": {"lang": "fr", "scoped": true, "namespace": "THEME_QUARK", "count": 1, "yaml": "THEME_QUARK:\n SEARCH: Rechercher\n"}}
Response Codes
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
/i18n/overrides/{lang}
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. |
{"set": {"THEME_QUARK.SEARCH": "Rechercher"}, "unset": ["PLUGIN_ADMIN.TITLE"]}
{"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
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
/i18n/overrides/{lang}
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. |
{"yaml": "THEME_QUARK:\n SEARCH: Rechercher\n", "namespace": "THEME_QUARK"}
{"data": {"count": 1, "dropped": [], "unknown": [], "removed": 0}}
Response Codes
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
/i18n/translate
{"data": {"available": false, "installed": true, "enabled": true, "reason": "not_configured", "max_keys": 200}}
Response Codes
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
/i18n/translate
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`. |
{"source_lang": "en", "target_lang": "fr", "keys": ["THEME_QUARK.SEARCH", "PLUGIN_ADMIN.TITLE"]}
{"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
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
/i18n/import/translation-strings
{"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
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
/i18n/import/translation-strings
{"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
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.