Translation Strings

7 views Markdown

The five endpoints that search, edit, remove and move translation strings.

Overview

Translation strings live in files rather than the database. Every string has a full key, and that key also says which file holds it, as in needs/button-save.

The head of the key places a string in one of three sets: the client side, the admin panel and the system text left over. A search result comes back grouped under those three.

There are two ways to write, and they differ on purpose. The bulk edit is strict: seeing one key it does not know, it writes nothing. The import is forgiving: it skips what it does not know and writes the rest.

Reference

get/api/v1/admin/languages/{key}/translations
Languages/GetLanguageTranslations admin 100 matches at most

Returns the translation strings carrying your text, in groups.

Query 1
searchstringreqThe text to look for. Three characters at the least, and the case matters.
Response fields data — 3 + meta — 3
clientobjectThe matches on the client side.
totalintHow many matched in the group.
dataobjectA map pairing the full key with its value.
adminobjectThe matches in the admin panel.
systemobjectEvery other match. Error messages and shared text.
totalintHow many were gathered. It stops at a hundred, and the true total can be larger.
searchstringThe text you looked for.
truncatedboolWhether the hundred limit was reached.
Errors 3
language_not_found404No such language.
search_too_short422The search text is under three characters.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/languages/de/translations?search=Save' \
  -H "Authorization: Bearer $API_KEY"
const url = new URL(`https://panel.example.com/api/v1/admin/languages/${key}/translations`);
url.searchParams.set('search', term);

const res  = await fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
const { data, meta } = await res.json();
if (meta.truncated) narrowTheSearch();
$qs = http_build_query(['search' => $term]);
$ch = curl_init('https://panel.example.com/api/v1/admin/languages/' . $key . '/translations?' . $qs);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The search is CASE SENSITIVE: 'save' and 'Save' do not give the same result.
$r = Api::Languages()->GetLanguageTranslations(['key' => $key], ['search' => 'Save']);
if ($r['meta']['truncated']) $logger->info('the result stopped at a hundred — narrow the search');

Editing Translations in Bulk

put/api/v1/admin/languages/{key}/translations
Languages/UpdateLanguageTranslations admin strict

Writes the values of the keys you give and leaves the rest alone.

Body 1
translationsobjectreqA map pairing the full key with its new value. One invalid key turns the whole request down.
Response fields data — 2
updatedintHow many keys were written.
languagestringThe language key.
Errors 4
language_not_found404No such language.
translations_required422The map is empty.
invalid_translation_key422A key points at a file that does not exist.
insufficient_scope403The key lacks the required scope.
Request
curl -X PUT 'https://panel.example.com/api/v1/admin/languages/de/translations' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"translations":{"needs/button-save":"Speichern"}}'
const res = await fetch(`https://panel.example.com/api/v1/admin/languages/${key}/translations`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    translations: {
      'needs/button-save': 'Speichern',
      'admin/services/page-list': 'Dienste',
    },
  }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/languages/' . $key . '/translations');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PUT',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'translations' => ['needs/button-save' => 'Speichern'],
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// ONE invalid key turns the whole batch down; pick the import endpoint for bulk loading.
Api::Languages()->UpdateLanguageTranslations([
    'key' => $key, 'translations' => ['needs/button-save' => 'Speichern'],
]);

Removing One Translation

delete/api/v1/admin/languages/{key}/translations
Languages/DeleteLanguageTranslation admin

Takes one translation string out of its file.

Body 1
keystringreqThe full translation key to remove. Do not mix it up with the language key in the address.
Response fields data — 3
deletedboolWhether the delete ran.
keystringThe translation key removed.
languagestringThe language key.
Errors 5
language_not_found404No such language.
key_required422No translation key was given.
invalid_translation_key422The translation key is not valid.
translation_file_not_found404The file the key points at does not exist.
insufficient_scope403The key lacks the required scope.
Request
curl -X DELETE 'https://panel.example.com/api/v1/admin/languages/de/translations' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"key":"needs/button-save"}'
const res = await fetch(`https://panel.example.com/api/v1/admin/languages/${lang}/translations`, {
  method: 'DELETE',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ key: 'needs/button-save' }),
});

const body = await res.json();
$ch = curl_init('https://panel.example.com/api/v1/admin/languages/' . $lang . '/translations');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['key' => 'needs/button-save']),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A screen asking for a removed key prints EMPTY text; consider correcting rather than clearing it.
Api::Languages()->UpdateLanguageTranslations([
    'key' => $lang, 'translations' => ['needs/button-save' => 'Speichern'],
]);

Exporting the Translations

get/api/v1/admin/languages/{key}/translations/export
Languages/ExportLanguageTranslations admin

Returns every translation string of a language as one flat map.

Response fields data + meta — 2
dataobjectA flat map pairing the full key with its value. It can go straight into the import endpoint.
countintHow many strings there are. It comes back under meta.
languagestringThe language key.
Errors 2
language_not_found404No such language.
insufficient_scope403The key lacks the required scope.
Request
curl 'https://panel.example.com/api/v1/admin/languages/de/translations/export' \
  -H "Authorization: Bearer $API_KEY" > de.json
const res = await fetch(`https://panel.example.com/api/v1/admin/languages/${key}/translations/export`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});

const { data, meta } = await res.json();
console.log(`${meta.count} dize`);
$ch = curl_init('https://panel.example.com/api/v1/admin/languages/' . $key . '/translations/export');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// The output is JSON and NOT a spreadsheet; the sheet format the panel offers is absent here.
$dump = Api::Languages()->ExportLanguageTranslations(['key' => $key])['data'];
file_put_contents("$key.json", json_encode($dump, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));

Importing the Translations

post/api/v1/admin/languages/{key}/translations/import
Languages/ImportLanguageTranslations admin forgiving

Writes a translation map in bulk and skips the keys it does not know.

Body 1
translationsobjectreqA map pairing the full key with its value. The same shape the export produces.
Response fields data — 4
languagestringThe language key.
translationsintHow many keys you sent.
applied_filesintHow many files were written.
skipped_filesarrayThe keys skipped for want of a target file. This is the one sign of a quiet loss.
Errors 3
language_not_found404No such language.
translations_required422The map is empty.
insufficient_scope403The key lacks the required scope.
Request
curl -X POST 'https://panel.example.com/api/v1/admin/languages/de/translations/import' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"translations":{"needs/button-save":"Speichern"}}'
const res = await fetch(`https://panel.example.com/api/v1/admin/languages/${key}/translations/import`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ translations: map }),
});

const { data } = await res.json();
if (data.skipped_files.length) reportSkipped(data.skipped_files);
$ch = curl_init('https://panel.example.com/api/v1/admin/languages/' . $key . '/translations/import');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['translations' => $map]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
// A key it does not know is SKIPPED quietly; without counting them a missing translation goes unseen.
$out = Api::Languages()->ImportLanguageTranslations(['key' => $key, 'translations' => $map])['data'];
if ($out['skipped_files']) $logger->warning('skipped key', $out['skipped_files']);

Pitfalls

Bulk edit and import meet one error differently

The bulk edit turns down the whole batch over one invalid key and writes nothing. The import skips that key quietly and writes the rest. Pick the first for careful editing and the second when loading an outside file.

Skipped keys show in the answer alone

The import lists what it skipped under skipped_files and says so nowhere else. A script that does not read the answer never learns it left part of the translations unwritten. Check that field on every run.

The search count is not a total

The search stops at the hundredth match and the total field gives what was gathered rather than what exists. A true truncated means there is more and how much stays unknown. Do not tie that number to a progress readout.

The search is case sensitive

The search text is matched as it stands: looking for save does not find the string Save. Search each spelling of a word on its own when replacing text across the installation.

Two different keys in one request

On the removal endpoint the address carries the language key and the body the translation key. Both go by the name key. Writing the language key into the body gives an invalid translation key error, which is not the language going missing.

The export does not give a spreadsheet

The spreadsheet output and the remote pull that the panel offers are absent here; the export returns a flat map and the import expects the same map. The two mirror each other, so a language exported from one installation loads into another as it stands.

Was this helpful?

Thanks for your feedback!

Still Need Help?

Our support team is here around the clock for anything you can't find above.