Translations
ElmapiCMS supports linking content entries across different locales as translations of each other. This feature allows you to build multilingual websites where content in different languages is connected, making it easy to implement language switchers and maintain content relationships.
Overview
When you link entries as translations:
- Entries share a
translation_group_idthat connects them - Each entry maintains its own locale, content, and status
- You can retrieve a translation of any entry via the API
Linking Translations in the Admin Panel
To link entries as translations:
- Open any content entry in edit mode
- Click the Translations button (visible when your project has multiple locales)
- For each locale:
- If no translation exists: Click Select to choose an existing entry
- If a translation exists: Click the entry number link to navigate to it, or Unlink to remove the connection
Getting Translations via API
Endpoint
[GET]
/{collection_slug}/{entry_uuid}?translation_locale={locale}Query Parameters
| Name | Type | Required | Description |
|---|---|---|---|
translation_locale | string | No | Get the translation of this entry in the specified locale. Returns the linked translation entry instead of the original. |
How It Works
- The API finds the entry by UUID
- Checks if the entry has a
translation_group_id - Searches for a linked entry with the requested locale
- Returns the translation entry if found
Example Requests
import { createClient } from '@elmapicms/js-sdk';
const client = createClient(
'https://your-domain.com/api',
'YOUR_API_TOKEN',
'YOUR_PROJECT_UUID'
);
// Get French translation of the entry
const translation = await client.getEntry('blog-posts', 'e3a4b1c2-d5e6-f7g8-h9i0-j1k2l3m4n5o6', {
translation_locale: 'fr'
});const collectionSlug = 'blog-posts';
const entryUuid = 'e3a4b1c2-d5e6-f7g8-h9i0-j1k2l3m4n5o6';
const targetLocale = 'fr';
// Get French translation of the entry
axios.get(`https://your-domain.com/api/${collectionSlug}/${entryUuid}`, {
params: {
translation_locale: targetLocale
},
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN', // Required for private APIs
'project-id': 'YOUR_PROJECT_UUID'
}
});use Illuminate\Support\Facades\Http;
$collectionSlug = 'blog-posts';
$entryUuid = 'e3a4b1c2-d5e6-f7g8-h9i0-j1k2l3m4n5o6';
$targetLocale = 'fr';
$response = Http::withHeaders([
'Accept' => 'application/json',
'Authorization' => 'Bearer YOUR_API_TOKEN',
'project-id' => 'YOUR_PROJECT_UUID'
])->get("https://your-domain.com/api/{$collectionSlug}/{$entryUuid}", [
'translation_locale' => $targetLocale
]);import React, { useState } from 'react';
function LanguageSwitcher({ currentEntryUuid, availableLocales, currentLocale }) {
const [loading, setLoading] = useState(false);
const switchLanguage = async (targetLocale) => {
if (targetLocale === currentLocale) return;
setLoading(true);
try {
const response = await fetch(
`https://your-domain.com/api/blog-posts/${currentEntryUuid}?translation_locale=${targetLocale}`,
{
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN',
'project-id': 'YOUR_PROJECT_UUID'
}
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Navigate to the translated entry
window.location.href = `/posts/${data.data.uuid}`;
} catch (error) {
console.error('Error fetching translation:', error);
alert('Translation not available for this language');
} finally {
setLoading(false);
}
};
return (
<div className="language-switcher">
{availableLocales.map(locale => (
<button
key={locale}
onClick={() => switchLanguage(locale)}
disabled={loading || locale === currentLocale}
className={locale === currentLocale ? 'active' : ''}
>
{locale.toUpperCase()}
</button>
))}
</div>
);
}<template>
<div class="language-switcher">
<button
v-for="locale in availableLocales"
:key="locale"
@click="switchLanguage(locale)"
:disabled="loading || locale === currentLocale"
:class="{ active: locale === currentLocale }"
>
{{ locale.toUpperCase() }}
</button>
</div>
</template>
<script setup>
import { ref } from 'vue';
const props = defineProps({
currentEntryUuid: { type: String, required: true },
availableLocales: { type: Array, required: true },
currentLocale: { type: String, required: true }
});
const loading = ref(false);
const switchLanguage = async (targetLocale) => {
if (targetLocale === props.currentLocale) return;
loading.value = true;
try {
const response = await fetch(
`https://your-domain.com/api/blog-posts/${props.currentEntryUuid}?translation_locale=${targetLocale}`,
{
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN',
'project-id': 'YOUR_PROJECT_UUID'
}
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Navigate to the translated entry
window.location.href = `/posts/${data.data.uuid}`;
} catch (error) {
console.error('Error fetching translation:', error);
alert('Translation not available for this language');
} finally {
loading.value = false;
}
};
</script>curl -X GET "https://your-domain.com/api/blog-posts/e3a4b1c2-d5e6-f7g8-h9i0-j1k2l3m4n5o6?translation_locale=fr" \
-H "Accept: application/json" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "project-id: YOUR_PROJECT_UUID"Responses
200: Success
Returns the translation entry in the requested locale.
{
"data": {
"uuid": "f4b5c2d3-e6f7-g8h9-i0j1-k2l3m4n5o6p7",
"locale": "fr",
"published_at": "2023-10-28T12:00:00Z",
"fields": {
"title": "Mon Premier Article de Blog",
"slug": "mon-premier-article-de-blog",
"content": "<p>Ceci est le contenu de l'article.</p>",
"author": "Jean Dupont"
}
}
}404: Translation Not Found
Returned if:
- The entry has no translation group
- No translation exists for the requested locale
{
"message": "Translation not found for locale 'fr'."
}{
"message": "This entry has no translations linked."
}Important Notes
- State Filtering: The translation respects the same
statefilter (published/draft) as the original entry request - Same Structure: Translation entries return the same data structure as regular entries
- UUID Navigation: Use the returned entry's UUID for routing/navigation in your frontend
- Fallback Handling: Always handle 404 responses gracefully when translations are not available
Use Cases
Language Switcher
Implement a language switcher on your detail pages:
import { createClient } from '@elmapicms/js-sdk';
const client = createClient(
'https://your-domain.com/api',
'YOUR_API_TOKEN',
'YOUR_PROJECT_UUID'
);
// When user clicks a language flag/button
async function switchLanguage(currentEntryUuid, targetLocale) {
try {
const translation = await client.getEntry('blog-posts', currentEntryUuid, {
translation_locale: targetLocale
});
// Update your page with the translated content
// Or navigate to: /posts/${translation.data.uuid}
return translation;
} catch (error) {
// Handle gracefully - maybe show a message or keep current page
console.log('Translation not available');
return null;
}
}// When user clicks a language flag/button
async function switchLanguage(currentEntryUuid, targetLocale) {
const response = await fetch(
`https://your-domain.com/api/blog-posts/${currentEntryUuid}?translation_locale=${targetLocale}`,
{
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN',
'project-id': 'YOUR_PROJECT_UUID'
}
}
);
if (response.ok) {
const translation = await response.json();
// Update your page with the translated content
// Or navigate to: /posts/${translation.data.uuid}
return translation;
} else {
// Handle gracefully - maybe show a message or keep current page
console.log('Translation not available');
return null;
}
}Multilingual Navigation
Build navigation menus that show content in the user's selected language:
import { createClient } from '@elmapicms/js-sdk';
const client = createClient(
'https://your-domain.com/api',
'YOUR_API_TOKEN',
'YOUR_PROJECT_UUID'
);
// Get all blog posts in French
const posts = await client.getEntries('blog-posts', {
locale: 'fr'
});
// When user clicks a post, get its English translation if needed
const englishPost = await client.getEntry('blog-posts', post.uuid, {
translation_locale: 'en'
});// Get all blog posts in French
const posts = await fetch('https://your-domain.com/api/blog-posts?locale=fr', {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN',
'project-id': 'YOUR_PROJECT_UUID'
}
});
// When user clicks a post, get its English translation if needed
const englishPost = await fetch(
`https://your-domain.com/api/blog-posts/${post.uuid}?translation_locale=en`,
{
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer YOUR_API_TOKEN',
'project-id': 'YOUR_PROJECT_UUID'
}
}
);Best Practices
- Always Check for Translations: Before showing a language switcher, verify that translations exist
- Handle Missing Translations: Provide a fallback (e.g., show original language or a message)
- Cache Translation UUIDs: Store translation UUIDs in your frontend state to avoid unnecessary API calls
- Use Consistent Locale Codes: Ensure your project locales match your frontend language codes