Handling Translated Content
The JSON:API Client offers a number of features to make it easy to work with localized content provided by Drupal. Let’s adapt our grid of recipes to make use of content translated in both English and Spanish from the Umami Demo profile.
The defaultLocale Option
Section titled “The defaultLocale Option”In our previous examples we have not specified a locale, which means that the default locale was used. If you want to work with content in a different locale by default, you can specify the defaultLocale option when creating an instance of the client.
---import { JsonApiClient } from "@drupal-api-client/json-api-client";import { Jsona } from "jsona";
const client = new JsonApiClient( "https://drupal-api-demo.party", { serializer: new Jsona(), defaultLocale: "es", },);const recipes = await client.getCollection("node--recipe", { queryString: "include=field_media_image.thumbnail",});---<style> .card-grid { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1rem; }
.card-grid .card { display: flex; flex-direction: column; justify-content: space-between; border: 2px solid var(--sl-color-text-accent); margin-top: 0; padding: 0.5rem; }</style>
<div> <h2>Umami Recetas</h2> <div class="card-grid"> {recipes.map((recipe) => ( <div class="card" key={recipe.id}> <h4>{recipe.title}</h4> <div> <p>Dificultad: {recipe.field_difficulty}</p> <img src=`${client.baseUrl}${recipe.field_media_image.thumbnail.uri.url}` alt={recipe.field_media_image.thumbnail.resourceIdObjMeta.alt} /> <a href={recipe.path.alias}>Ver Receta</a> </div> </div> ))} </div></div>By providing the defaultLocale option, we can now work with content in Spanish by default. As we’ll see next, it is still possible to specify a different locale when when making individual requests.
The locale Option
Section titled “The locale Option”Using our client defaulted to Spanish, we can still request content in English when necessary. By specifying the locale option with getResource we can override the default locale for individual requests.
---import { JsonApiClient } from "@drupal-api-client/json-api-client";import { Jsona } from "jsona";
const client = new JsonApiClient( "https://drupal-api-demo.party", { serializer: new Jsona(), defaultLocale: "es", },);const recipes = await client.getCollection("node--recipe", { queryString: "include=field_media_image.thumbnail", locale: "en",});---<style> .card-grid { display: grid; grid-template-columns: 1fr 1fr; grid-gap: 1rem; }
.card-grid .card { display: flex; flex-direction: column; justify-content: space-between; border: 2px solid var(--sl-color-text-accent); margin-top: 0; padding: 0.5rem; }</style>
<div> <h2>Umami Recipes</h2> <div class="card-grid"> {recipes.map((recipe) => ( <div class="card" key={recipe.id}> <h4>{recipe.title}</h4> <div> <p>Difficulty: {recipe.field_difficulty}</p> <img src=`${client.baseUrl}${recipe.field_media_image.thumbnail.uri.url}` alt={recipe.field_media_image.thumbnail.resourceIdObjMeta.alt} /> <a href={recipe.path.alias}>View Recipe</a> </div> </div> ))} </div></div>Language Selection on the Wire
Section titled “Language Selection on the Wire”A localized request sends the language twice: as the /{locale}/ URL path
prefix the examples above show (how stock Drupal negotiates), and as a
langCode query parameter (?langCode=es). Stock Drupal ignores the
parameter; a backend using
jsonapi_multilingual
selects the translation with only the parameter. The same call therefore
works on either kind of backend with no configuration or detection. Writes
target exactly one translation the same way: updateResource and
deleteResource send the prefix plus langCode (a DELETE with a locale
removes only that translation; without one it deletes the whole entity).
Strict Reads and 404
Section titled “Strict Reads and 404”A backend with jsonapi_multilingual treats langCode strictly: it serves
exactly the translation you asked for and never falls back silently. When
that translation does not exist, an individual read answers 404 Not Found
and a collection excludes the untranslated items, reporting them under
meta.omitted. The 404 advertises the translations that do exist (filtered
to those you may view), and getAvailableTranslations extracts them:
import { getAvailableTranslations } from "@drupal-api-client/json-api-client";
const { response, json } = await client.getResource("node--recipe", id, { locale: "fr", rawResponse: true,});if (response.status === 404) { // For example ["en", "es"]: retry with one, or render a language switcher. const available = getAvailableTranslations(json);}Server-Side Fallback
Section titled “Server-Side Fallback”To always get content instead of a strict miss, opt the read into the site’s
language fallback chain with includeFallback:
await client.getResource("node--recipe", id, { locale: "fr", includeFallback: true,});The backend resolves the best available translation through the same fallback
mechanism the rendered site uses (so fallback-policy modules like
language_hierarchy apply automatically): an individual read of an existing
entity always succeeds, and a collection includes every item, each in its
best available language. The resource’s langcode attribute reports the
language actually served. The site owns the fallback policy — the client only
opts in. On the wire this is ?langCode=fr&includeFallback=1; a backend
without jsonapi_multilingual ignores it.
Creating Translations
Section titled “Creating Translations”createTranslation adds a translation to an existing entity — distinct from
createResource, which creates a new entity. The payload’s langcode
attribute must match the targeted locale, and creating a translation that
already exists is rejected with a 409 conflict (use updateResource for
that):
await client.createTranslation("node--recipe", id, body, { locale: "es" });Turning Off the Path Prefix
Section titled “Turning Off the Path Prefix”The /{locale}/ prefix exists for backwards compatibility with URL
path-prefix negotiation. On a backend with jsonapi_multilingual, langCode
carries the language on the canonical URL, so the prefix is redundant there;
setting languagePathPrefix: false is recommended for those sites, and
required for any backend that does not serve /{locale}/ URLs at all.
const client = new JsonApiClient("https://drupal.example.com", { languagePathPrefix: false,});The option applies to every request the client makes, including the
decoupled_router hop of getResourceByPath, which then asks for
/router/translate-path instead of /{locale}/router/translate-path. A
translated path alias resolves from the path alone, and a backend with
jsonapi_multilingual reports the language it resolved as
entity.langcode; getResourceByPath passes that language to the follow-up
JSON:API read (unless you pass an explicit locale), so
getResourceByPath("/bonjour-le-monde") returns the French translation the
path names, with no URL prefix involved.
All of our examples thus far have dealt with collections of multiple resources. Let’s next see the ways we can use the client to retrieve only a single resource.