Skip to main content

Multi-Language Setup

This guide covers configuring multiple languages and implementing i18n in your Unchained Engine storefront.

Overview​

Unchained Engine stores translations for entities like products, assortments, and filters using a locale-based system:

Product
└── texts: [
{ locale: 'en', title: 'T-Shirt', description: '...' },
{ locale: 'de', title: 'T-Shirt', description: '...' },
{ locale: 'fr', title: 'T-Shirt', description: '...' }
]

Configuration​

1. Set Up Languages​

Create languages in the system:

mutation CreateLanguage {
createLanguage(language: {
isoCode: "de"
}) {
_id
isoCode
isActive
}
}

Or seed languages at startup:

// seed/languages.ts
export const languages = [
{ isoCode: 'en', isActive: true },
{ isoCode: 'de', isActive: true },
{ isoCode: 'fr', isActive: true },
{ isoCode: 'it', isActive: false }, // Inactive
];

// In your boot script
for (const lang of languages) {
await modules.languages.create(lang);
}

2. Configure Default Language​

Set the default language via environment variable:

# .env
LANG=de # Default language

3. Set Up Countries​

Link countries to languages:

mutation CreateCountry {
createCountry(country: {
isoCode: "CH"
}) {
_id
isoCode
defaultCurrency {
isoCode
}
}
}

Adding Translations​

Product Translations​

mutation UpdateProductTexts {
updateProductTexts(productId: "product-123", texts: [
{
locale: "en"
title: "Organic Cotton T-Shirt"
subtitle: "Comfortable everyday wear"
description: "Made from 100% organic cotton..."
slug: "organic-cotton-t-shirt"
}
{
locale: "de"
title: "Bio-Baumwoll T-Shirt"
subtitle: "Bequeme Alltagskleidung"
description: "Hergestellt aus 100% Bio-Baumwolle..."
slug: "bio-baumwoll-t-shirt"
}
{
locale: "fr"
title: "T-Shirt en Coton Bio"
subtitle: "Vêtement de tous les jours confortable"
description: "Fabriqué à partir de 100% coton bio..."
slug: "t-shirt-coton-bio"
}
]) {
locale
title
slug
}
}

Assortment Translations​

mutation UpdateAssortmentTexts {
updateAssortmentTexts(assortmentId: "assortment-123", texts: [
{ locale: "en", title: "Men's Clothing", slug: "mens-clothing" }
{ locale: "de", title: "Herrenbekleidung", slug: "herrenbekleidung" }
{ locale: "fr", title: "Vêtements Homme", slug: "vetements-homme" }
]) {
locale
title
slug
}
}

Filter Translations​

mutation UpdateFilterTexts {
updateFilterTexts(filterId: "filter-123", filterOptionValue: null, texts: [
{ locale: "en", title: "Size" }
{ locale: "de", title: "Größe" }
{ locale: "fr", title: "Taille" }
]) {
locale
title
}
}

Querying Translations​

Automatic Locale Resolution​

Unchained automatically resolves the texts field based on the request locale:

# Request headers: Accept-Language: de
query {
product(productId: "...") {
texts {
title # Returns German title if available
description
}
}
}

Explicit Locale​

Query all translations:

query {
product(productId: "...") {
texts(forceLocale: "en") {
title
}
}
}

All Translations​

Get all available translations:

query {
product(productId: "...") {
# Default (resolved)
texts {
locale
title
}
# Specific locale
germanTexts: texts(forceLocale: "de") {
title
}
}
}

Frontend Implementation​

Language Switcher​

import { useQuery, useMutation } from '@apollo/client';

const LANGUAGES = gql`
query Languages {
languages(includeInactive: false) {
_id
isoCode
name
}
}
`;

function LanguageSwitcher() {
const { data } = useQuery(LANGUAGES);
const [currentLocale, setCurrentLocale] = useState('en');

const handleChange = (locale: string) => {
// Update cookie/localStorage
document.cookie = `locale=${locale}; path=/`;

// Update Apollo client headers
apolloClient.setLink(
authLink.concat(
createHttpLink({
uri: GRAPHQL_URL,
headers: {
'Accept-Language': locale,
},
})
)
);

// Refetch queries
apolloClient.resetStore();

setCurrentLocale(locale);
};

return (
<select value={currentLocale} onChange={(e) => handleChange(e.target.value)}>
{data?.languages.map((lang) => (
<option key={lang.isoCode} value={lang.isoCode}>
{lang.name || lang.isoCode.toUpperCase()}
</option>
))}
</select>
);
}

Next.js i18n Integration​

// next.config.js
module.exports = {
i18n: {
locales: ['en', 'de', 'fr'],
defaultLocale: 'en',
},
};
// pages/products/[slug].tsx
import { useRouter } from 'next/router';
import { useQuery } from '@apollo/client';

export default function ProductPage() {
const { locale } = useRouter();

const { data } = useQuery(PRODUCT_QUERY, {
context: {
headers: {
'Accept-Language': locale,
},
},
});

return (
<div>
<h1>{data?.product?.texts?.title}</h1>
<p>{data?.product?.texts?.description}</p>
</div>
);
}

Apollo Client Setup for i18n​

// lib/apollo-client.ts
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';

export function createApolloClient(locale: string) {
const httpLink = createHttpLink({
uri: process.env.NEXT_PUBLIC_GRAPHQL_URL,
});

const localeLink = setContext((_, { headers }) => ({
headers: {
...headers,
'Accept-Language': locale,
},
}));

return new ApolloClient({
link: localeLink.concat(httpLink),
cache: new InMemoryCache(),
});
}

Server-Side Language Resolution​

Unchained resolves the locale automatically from the Accept-Language HTTP header. The locale is available in the GraphQL context and affects how texts fields are resolved.

The resolution order is:

  1. Accept-Language header from the request
  2. Default language from the LANG environment variable
  3. Fallback to en

Bulk Import with Translations​

await modules.bulkImporter.prepare({
entity: 'PRODUCT',
data: {
_id: 'product-123',
type: 'SIMPLE',
texts: [
{ locale: 'en', title: 'T-Shirt', slug: 't-shirt' },
{ locale: 'de', title: 'T-Shirt', slug: 't-shirt-de' },
],
// ... other fields
},
});

Admin UI Translations​

The Admin UI supports language management:

  1. Go to Settings > Languages
  2. Add/edit languages
  3. Go to any entity (Products, Assortments)
  4. Use the locale switcher to edit translations

Best Practices​

1. Always Provide Fallback​

Ensure at least one language (typically English) has complete translations:

mutation {
updateProductTexts(productId: "...", texts: [
{ locale: "en", title: "Fallback Title" } # Always provide
{ locale: "de", title: "German Title" } # Optional
]) {
locale
title
}
}

2. Use Slugs Per Locale​

Different slugs allow for SEO-friendly URLs:

/en/products/organic-t-shirt
/de/products/bio-t-shirt
/fr/products/t-shirt-bio

3. Handle Missing Translations​

function ProductTitle({ product }) {
const title = product.texts?.title;

if (!title) {
// Fallback to product ID or show placeholder
return <span className="untranslated">{product._id}</span>;
}

return <h1>{title}</h1>;
}

4. Validate Translations​

Check for missing translations:

query ProductsWithMissingTranslations {
products {
_id
texts {
locale
title
}
}
}
// Find products missing German translations
const missingDE = products.filter(
(p) => !p.texts.some((t) => t.locale === 'de')
);