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:
Accept-Languageheader from the request- Default language from the
LANGenvironment variable - 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:
- Go to Settings > Languages
- Add/edit languages
- Go to any entity (Products, Assortments)
- 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')
);
Related
- Languages Module - Language configuration
- Multi-Currency Setup - Currency configuration
- Bulk Import - Importing translations