Internationalization (i18n)
Build multilingual content with first-class i18n support
Overview
Helix has internationalization built into its core architecture. Content can be localized at the field level, relation level, or entire Entity level, giving you maximum flexibility for multilingual projects.
The @Localized() Decorator
The @Localized() decorator marks fields that should have different values per locale. Without this decorator, fields are "base" fields - shared across all locales.
export class Article {
// Base fields (same for all languages)
slug!: string;
publishedAt!: Date;
// Localized fields (different per language)
@Localized()
title!: string;
@Localized()
@Widget("richtext")
content!: string;
}
How Localization Works
Localized data is stored in the attrs JSONB column with a map structure from locale code to field values:
{
"base": {
"slug": "hello-world",
"publishedAt": "2024-01-15"
},
"attrs": {
"en": {
"title": "Hello World",
"content": "<p>Welcome...</p>"
},
"sv": {
"title": "Hej Världen",
"content": "<p>Välkommen...</p>"
}
}
}
Locale Codes
Helix uses standard IETF BCP 47 language tags (e.g., en, en-US, sv-SE).
Localized Components
Entire Components can be localized:
class Hero {
@Localized()
title!: string;
backgroundImage?: string; // Not localized
}
export class LandingPage {
@Localized()
hero!: Hero; // Different Hero per locale
}
Localized Relations
Entity relations can also be localized:
export class LandingPage {
@Localized()
features!: Feature[]; // Different features per locale
}
Querying Localized Content
// Query content in English using the Document API
const articles = await client.entities(articles).list({
locale: 'en',
depth: 2
});
// Query content in Swedish using the Query API
const artiklar = await client
.select()
.from(articles)
.execute({ locale: 'sv' });
Fallback Behavior
When content doesn't exist for a requested locale:
- Try the requested locale (e.g.,
en-US) - Try the base language (e.g.,
en) - Try the default locale configured for the Space
- Return
nullor empty if no content exists
Translation Workflows
A common pattern is to use branches for translation workflows:
- Create a branch
translate-to-german - Add German translations to content
- Review and approve translations
- Merge the branch back to
main
Best Practices
- Mark fields
@Localized()upfront - easier to add locales than migrate later - Keep base fields truly global (dates, IDs, flags, etc.)
- Use locale-specific relations sparingly
- Leverage branches for translation workflows
- Set up fallback locales to improve UX