Skip to content
App Translations
Esc
↑↓navigate↵open⌘Jpreview
On this page

Delivery API

Fetch the text editors changed, or every message, for one namespace or all of them, and merge it into your application, with next-intl and i18next examples.

Your application fetches translations one language per request, from one of two endpoints. Settings > App translations builds the URL for each language and namespace.

Endpoint Returns Use it when
overrides Only the messages an editor changed. Your application bundles its own text, and merges the changes over it.
all Every message. An editor’s text where there is one, your source’s text everywhere else. Your application has no text of its own, and takes it all from Umbraco.

Overrides

GET /umbraco/delivery/api/v1/app-translations/overrides?locale=da-DK&namespace=website
{
  "greeting": "Hej {name}, godt at se dig igen!"
}

Only greeting is here, because it is the only message an editor changed. Merge it over the messages your application already has.

All messages

GET /umbraco/delivery/api/v1/app-translations/all?locale=da-DK&namespace=website
{
  "farewell": "Farvel",
  "greeting": "Hej {name}, godt at se dig igen!"
}

greeting is the editor’s text. farewell has not been changed, so it is the text Umbraco read from your source.

One namespace, several, or all of them

With one namespace, the response holds that namespace’s messages, without the namespace around them, as in the examples above.

Repeat namespace to fetch several at once, or leave it out to fetch every namespace in the language. Each namespace is then a top-level key:

GET /umbraco/delivery/api/v1/app-translations/all?locale=da-DK
{
  "checkout": { "pay": "Betal nu" },
  "website": { "farewell": "Farvel", "greeting": "Hej {name}, godt at se dig igen!" }
}

Naming both namespaces returns the same response, and leaves out any other namespace:

GET /umbraco/delivery/api/v1/app-translations/all?locale=da-DK&namespace=checkout&namespace=website

The order you name them in does not matter, and each namespace comes back under the name you used for it. A namespace that does not exist is left out of the response, and you can name up to 20 at once.

Repeat the parameter rather than separating names with commas: namespace=checkout,website asks for one namespace called checkout,website, and gets 400 when there is none.

A source that takes its namespaces from the first key of its file comes back in the same shape as that file. A source that puts every message in one namespace comes back under that namespace’s name, so ask for the namespace by name if you want the shape of the file. From overrides, a namespace no editor has changed is left out.

Message syntax

The JSON is the same whichever library reads it. What differs is the syntax inside a message: ICU’s {name} and {count, plural, ...}, or i18next’s {{name}} and _one/_other keys. Each source declares the syntax its messages are written in, and Umbraco serves them as written.

Add format to make sure a response holds nothing your application would misread. Umbraco then answers 409 rather than send a message in the other syntax.

format Syntax Read by, for example
icu ICU MessageFormat next-intl, react-intl and FormatJS, Lingui, messageformat
i18next i18next JSON v4 i18next, react-i18next, next-i18next

Plain-text messages go with either. Without format, one response cannot mix ICU and i18next messages, so ask for namespaces in different syntaxes separately.

Parameters

Both endpoints take locale, and optionally namespace, which you can repeat or leave out, and format. The API reference lists every parameter and response for custom text and all messages.

A locale or namespace that has no messages also returns {}, not 404, so an application keeps its own text rather than failing.

next-intl

In a next-intl application, merge the custom text in the request configuration. This example uses next-intl 4 with the App Router and one website namespace:

import { getRequestConfig } from "next-intl/server";
import { hasLocale } from "next-intl";
import { routing } from "./routing";

// Your application's locales, and the Umbraco language codes they are stored under.
const umbracoLocales: Record<string, string> = { en: "en-US", da: "da-DK" };

type Messages = { [key: string]: string | Messages };

const merge = (base: Messages, overrides: Messages): Messages => {
  const merged: Messages = { ...base };
  for (const [key, value] of Object.entries(overrides)) {
    const current = merged[key];
    merged[key] =
      typeof value === "object" && typeof current === "object" ? merge(current, value) : value;
  }
  return merged;
};

export default getRequestConfig(async ({ requestLocale }) => {
  const requested = await requestLocale;
  const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale;
  const messages: Messages = (await import(`../../messages/${locale}.json`)).default;

  const url = new URL("/umbraco/delivery/api/v1/app-translations/overrides", process.env.UMBRACO_URL);
  url.search = new URLSearchParams({
    locale: umbracoLocales[locale],
    namespace: "website",
    format: "icu",
  }).toString();

  // If Umbraco cannot be reached, the application still has its own text.
  const response = await fetch(url, { next: { revalidate: 60 } }).catch(() => undefined);
  const overrides: Messages = response?.ok ? await response.json() : {};

  return { locale, messages: merge(messages, overrides) };
});

i18next

When a source takes its namespaces from the first key of its file, they are the same as your i18next namespaces. Leave namespace out, and add each namespace in the response to i18next once it has loaded its own resources:

const url = new URL("/umbraco/delivery/api/v1/app-translations/overrides", UMBRACO_URL);
url.search = new URLSearchParams({ locale: "da-DK", format: "i18next" }).toString();

const response = await fetch(url);
if (response.ok) {
  const namespaces: Record<string, object> = await response.json();
  for (const [ns, messages] of Object.entries(namespaces)) {
    // Deep, and overwriting: the custom text replaces the bundled text key by key.
    i18next.addResourceBundle("da", ns, messages, true, true);
  }
}

With a source that puts every message in one namespace, ask for that namespace by name instead, and add the response to the i18next namespace your application uses, such as translation.

Use format=i18next and set the source’s message syntax to i18next JSON v4. The output keeps nested keys, {{variable}} interpolation, context keys and plural suffixes such as _one, _other and _ordinal_one.

Caching

Umbraco keeps each response in memory until the translations change: a sync that changed something, a saved or reverted message, or a deleted source. The change clears it on every server, so the next request reads the new text from the database. A response is also rebuilt after an hour, in case the database changed some other way, such as a restored backup.

Clients and CDNs may keep a response for 60 seconds by default, so an edit can take up to a minute to reach a client that caches. Set the lifetime with AppTranslations:Delivery:MaxAgeSeconds:

AppTranslations__Delivery__MaxAgeSeconds=0

With 0, clients keep the response but check it on every request, and see an edit as soon as it is saved. Every response carries an ETag, so a client that sends If-None-Match gets 304 Not Modified until something changes. Once Umbraco has built a response it answers those checks from memory; the first request after a change, and requests for a locale or namespace with no messages, still read the database. Umbraco keeps up to 100 responses for several namespaces at once. Past that, the one it has kept longest makes room, and is read from the database again the next time someone asks for it.

To drop a cached dictionary in your own application the moment it changes, subscribe to the App translations updated webhook.

Access

The endpoints are public, which suits most sites: the text ends up in the application’s bundle anyway. To keep them to your own applications, set a key:

AppTranslations__Delivery__ApiKey=replace-with-a-long-random-value

A request then needs the key in an Api-Key header, as Umbraco’s own Delivery API expects, or as a bearer token, and gets 401 without it. A response to a keyed request is marked private, so a CDN does not hand it to anyone else. Keep the key on the server side of your application, never in code that runs in the browser.

Was this page helpful?