Multilingual & Localization
Build multilingual sites with locale routing, translations, and localized content
Nimbu provides comprehensive multilingual support with automatic locale routing, translation management, and localized content delivery.
Locale Configuration
Locales are configured in your theme's config/locales.yml:
default: en
available:
- en
- nl
- fr
- deLocale Variables
locale
Current active locale:
{{ locale }}
<!-- Output: "en", "nl", "fr", etc. -->
{% if locale == 'nl' %}
<!-- Dutch-specific content -->
{% endif %}locale_url_prefix
URL prefix for current locale:
{{ locale_url_prefix }}
<!-- Output: "/nl" for Dutch, "" for default locale -->
<a href="{{ locale_url_prefix }}/about">About</a>config.locales
All available locales:
{% for locale_pair in config.locales %}
{{ locale_pair[0] }}: {{ locale_pair[1] }}
{% endfor %}Translation Helpers
{% translate %}
Fetch translations from your translation files:
{% translate 'menu.home' %}
<!-- Output: "Home" (en), "Accueil" (fr), "Home" (nl) -->
{% translate 'menu.about', default: 'About Us' %}
{% translate 'cart.items_count', count: cart.items.size %}With Variables:
{% translate 'welcome.message', name: customer.firstname %}
<!-- Translation: "Welcome back, {{name}}!" -->
<!-- Output: "Welcome back, John!" -->{% localized_path %}
Generate URL for specific locale:
{% localized_path 'nl' %}
<!-- Current page in Dutch -->
{% localized_path 'fr' %}
<!-- Current page in French -->
<a href="{% localized_path 'de' %}">Deutsch</a>Working with Translations
Page Translations
Pages have built-in translation support:
{{ page.translations }}
<!-- Array of available translations -->
{% for translation in page.translations %}
<a href="{{ translation.url }}" hreflang="{{ translation.locale }}">
{{ translation.localized_language }}
</a>
{% endfor %}Translation Properties:
translation.locale- Locale code (en, nl, fr)translation.url- Full URL to translated pagetranslation.localized_language- Language name in that language
Practical Examples
Language Switcher
<div class="language-switcher">
{% if page.translations.size > 1 %}
<!-- Page-specific translations -->
{% for translation in page.translations %}
<a
href="{{ translation.url }}"
class="lang-link {% if locale == translation.locale %}active{% endif %}"
hreflang="{{ translation.locale }}">
{{ translation.locale | upcase }}
</a>
{% endfor %}
{% elsif config.locales.size > 1 %}
<!-- Site-wide locale switching -->
{% for locale_option in config.locales %}
<a
href="{% localized_path locale_option[1] %}"
class="lang-link {% if locale == locale_option[1] %}active{% endif %}">
{{ locale_option[1] | upcase }}
</a>
{% endfor %}
{% endif %}
</div>Dropdown Language Selector
<div class="language-dropdown">
<button class="current-language" aria-haspopup="true">
{{ locale | upcase }}
<span class="icon-chevron">▼</span>
</button>
<ul class="language-menu" aria-label="{% translate 'language.select' %}">
{% for translation in page.translations %}
{% unless locale == translation.locale %}
<li>
<a
href="{{ translation.url }}"
hreflang="{{ translation.locale }}">
{{ translation.localized_language }}
</a>
</li>
{% endunless %}
{% endfor %}
</ul>
</div>Multilingual Navigation
<nav class="main-nav" aria-label="{% translate 'nav.main' %}">
<a href="{{ locale_url_prefix }}/">
{% translate 'menu.home' %}
</a>
<a href="{{ locale_url_prefix }}/about">
{% translate 'menu.about' %}
</a>
<a href="{{ locale_url_prefix }}/products">
{% translate 'menu.products' %}
</a>
<a href="{{ locale_url_prefix }}/contact">
{% translate 'menu.contact' %}
</a>
</nav>Localized Date Formatting
<article>
<h1>{{ article.title }}</h1>
<time datetime="{{ article.published_at | date: '%Y-%m-%d' }}">
{{ article.published_at | localized_date: "long" }}
</time>
<!-- English: "October 7, 2025" -->
<!-- Dutch: "7 oktober 2025" -->
<!-- French: "7 octobre 2025" -->
<p class="byline">
{% translate 'article.published_by', author: article.author.name %}
</p>
{{ article.content }}
</article>Multilingual Search
A search form for templates/search.liquid that keeps the locale prefix. See Search for the results object.
<form action="{{ locale_url_prefix }}/search" method="get">
<input
type="search"
name="q"
placeholder="{% translate 'search.placeholder' %}"
value="{{ params.q | escape }}">
<button type="submit">
{% translate 'search.submit' %}
</button>
</form>
{% if params.q %}
{% assign safe_query = params.q | escape %}
<div class="search-results">
<h2>
{% translate 'search.results_for', query: safe_query %}
</h2>
{% if results.any? %}
{% for result in results.all %}
<div class="result">
<h3><a href="{{ result.url }}">{{ result.title }}</a></h3>
</div>
{% endfor %}
{% else %}
<p class="no-results">
{% translate 'search.no_results' %}
</p>
{% endif %}
</div>
{% endif %}Localized Product Catalog
<div class="product-list">
{% for product in products %}
<div class="product-card">
<a href="{{ locale_url_prefix }}{{ product.url }}">
<img
src="{{ product.images.first.url | filter, width: '400px' }}"
alt="{{ product.name }}">
</a>
<h3>
<a href="{{ locale_url_prefix }}{{ product.url }}">
{{ product.name }}
</a>
</h3>
<p class="price">
{{ product.price | money_with_currency }}
</p>
<a
href="{{ product.url }}"
class="btn">
{% translate 'product.view_details' %}
</a>
</div>
{% endfor %}
</div>Multilingual Contact Form
<div class="contact-page">
<h2>{% translate 'contact.title' %}</h2>
<p>{% translate 'contact.subtitle' %}</p>
{% form channels.contact, class: 'contact-form' %}
{% error_messages_for form_model %}
<div class="form-group">
{% input 'name', label: {% translate 'contact.name_label' %}, required: true %}
</div>
<div class="form-group">
{% input 'email', as: 'email', label: {% translate 'contact.email_label' %}, required: true %}
</div>
<div class="form-group">
{% text_area 'message', rows: 6, label: {% translate 'contact.message_label' %}, required: true %}
</div>
{% submit_tag {% translate 'contact.submit' %}, class: 'btn btn-primary' %}
{% endform %}
</div>Locale Selection
<div class="region-settings">
<div class="locale-selector">
<label>{% translate 'settings.language' %}:</label>
<select id="locale-select">
{% for translation in page.translations %}
<option
value="{{ translation.url }}"
{% if locale == translation.locale %}selected{% endif %}>
{{ translation.localized_language }}
</option>
{% endfor %}
</select>
</div>
</div>
<script>
document.getElementById('locale-select').addEventListener('change', function() {
window.location.href = this.value;
});
</script>A site has one shop currency, so there is no currency to switch.
SEO: Language Annotations
You don't write hreflang or canonical tags yourself. {{ content_for_header }} outputs, for the current page:
- a canonical link to the page in the current locale,
- a
<link rel="alternate" hreflang="...">for every locale the page is available in, - the same annotations as HTTP
Linkheaders.
Hidden locales, and locales where the page is not available, are left out. The themed 404 page gets no canonical or hreflang links.
Two settings in the Search Engines section of Settings → General change the output:
- Search language overrides: the language tag per locale, for example
nl-BEfornlto target Belgium only. Leave blank to use the locale code. - Fallback language (x-default): the locale that also gets
hreflang="x-default", the version suggested when no language matches. Visitors are not redirected.
Put the language on the <html> element, not in <head>:
<!DOCTYPE html>
<html lang="{{ locale }}">
<head>
<title>{{ page.title }} - {{ site.name }}</title>
{{ content_for_header }}
</head>If a layout does not output {{ content_for_header }}, none of these annotations are added. See SEO, robots.txt & llms.txt for more.
Translation File Structure
Translations are stored in config/translations/:
# config/translations/en.yml
en:
menu:
home: "Home"
about: "About Us"
products: "Products"
contact: "Contact"
cart:
title: "Shopping Cart"
items_count:
one: "1 item"
other: "{{count}} items"
empty: "Your cart is empty"
product:
view_details: "View Details"
add_to_cart: "Add to Cart"
out_of_stock: "Out of Stock"# config/translations/nl.yml
nl:
menu:
home: "Home"
about: "Over Ons"
products: "Producten"
contact: "Contact"
cart:
title: "Winkelwagen"
items_count:
one: "1 artikel"
other: "{{count}} artikelen"
empty: "Je winkelwagen is leeg"
product:
view_details: "Bekijk Details"
add_to_cart: "Toevoegen"
out_of_stock: "Niet op Voorraad"Best Practices
1. Use Translation Keys
<!-- ✅ Good: Use translation keys -->
<h1>{% translate 'page.title' %}</h1>
<!-- ❌ Bad: Hardcode text -->
<h1>Welcome</h1>2. Provide Defaults
<!-- ✅ Good: Fallback for missing translations -->
{% translate 'new.feature', default: 'New Feature' %}
<!-- ⚠️ Without default, shows key if missing -->
{% translate 'new.feature' %}3. Consistent URL Structure
<!-- ✅ Good: Use locale_url_prefix consistently -->
<a href="{{ locale_url_prefix }}/products">Products</a>
<!-- ❌ Bad: Hardcoded paths -->
<a href="/products">Products</a>4. Let Nimbu Handle Language Tags
Output {{ content_for_header }} in every layout and set <html lang="{{ locale }}">. Nimbu adds the hreflang alternates and canonical link; configure regional tags and x-default in the site settings instead of in the theme.
Next Steps
- Navigation - Language switchers
- Global Variables - Translation drops
- Filters - Dates - Localized dates
- Forms - Multilingual forms
Build truly global sites with Nimbu's multilingual features!