Internationalization (i18n) #
Glaze supports building multilingual static sites out of the box. When i18n is enabled, each language gets its own content directory, its own URL prefix, and a set of template helpers for language switching and string translation.
Single-language sites are unaffected — the i18n subsystem is completely inert
until i18n.defaultLanguage is set in glaze.neon.
Configuration #
Enable i18n by adding an i18n block to glaze.neon:
i18n:
defaultLanguage: en
languages:
en:
label: English
urlPrefix: ""
nl:
label: Nederlands
urlPrefix: /nl
contentDir: content/nl
fr:
label: Français
urlPrefix: /fr
contentDir: content/fr
defaultLanguage #
Required. The primary language code. Pages in this language live at the root of the site (no URL prefix by default). Setting this key activates the i18n subsystem.
languages #
A map of language code → language options. Each entry accepts:
| Key | Type | Description |
|---|---|---|
label |
string | Human-readable language name (used in language switchers) |
urlPrefix |
string | URL path prefix for this language ("" for root, "/nl" for /nl/...) |
contentDir |
string | Content directory for this language (relative to project root). Omit to use the project-level content/ directory |
The default language does not need a contentDir — it automatically uses the
project-level content directory. Non-default languages without a contentDir are
skipped during discovery.
Directory structure #
A typical two-language setup with English as the root language and Dutch under /nl/:
content/ <- English content (default language, no prefix)
index.dj -> /
about.dj -> /about/
blog/
hello.dj -> /blog/hello/
content/nl/ <- Dutch content (urlPrefix: /nl)
index.dj -> /nl/
about.dj -> /nl/about/
blog/
hallo.dj -> /nl/blog/hallo/
i18n/
en.neon <- English string translations
nl.neon <- Dutch string translations
The contentDir for Dutch is content/nl, so Glaze discovers files there and
automatically prefixes all NL routes with /nl.
URL routing #
Each language’s pages are prefixed with its urlPrefix. An empty urlPrefix
places the language at the site root (ideal for the default language):
| Language | urlPrefix |
File | Generated URL |
|---|---|---|---|
| English | "" |
content/index.dj |
/ |
| English | "" |
content/about.dj |
/about/ |
| Dutch | /nl |
content/nl/index.dj |
/nl/ |
| Dutch | /nl |
content/nl/about.dj |
/nl/about/ |
Translation linking #
Glaze automatically links matching pages across language trees so templates can offer language switchers. Two pages are considered translations of each other when their translation key is the same.
By default the translation key is the page’s relativePath (the source file path
relative to its content directory). Files with identical relative paths across
language directories are automatically linked:
content/about.dj key: about.dj
content/nl/about.dj key: about.dj <- linked as translations
When the file paths differ between languages, or when you want to manually control
linkage, set a translationKey in frontmatter:
---
title: Our story
translationKey: about
---
---
title: Ons verhaal
translationKey: about
---
Any page sharing translationKey: about across any language is treated as a
translation of that key.
String translations #
Place NEON files in the paths.translations directory (default: i18n/), one per
language:
# i18n/nl.neon
read_more: Lees meer
posted_on: "Geplaatst op {date}"
nav:
home: Start
about: Over ons
Keys may be flat or nested. Use dotted paths to address nested keys.
Use $this->t($key, $params, $fallback) in templates to look up a translation for
the current page’s language. When the key is not found in the current language or
the default language, the translation key itself is returned unless an explicit
$fallback string is provided:
<?= $this->t('read_more') ?>
<?= $this->t('posted_on', ['date' => $page->date->format('Y-m-d')]) ?>
<?= $this->t('nav.home') ?>
<?= $this->t('optional_promo', fallback: 'Check out our latest posts') ?>
Fallback resolution #
When a key is missing from the current language file, Glaze automatically falls
back to the default language file. If it is missing there too, the explicit
$fallback argument is returned, or the translation key itself when none is given.
Plural forms #
Keys whose translation requires two (or more) grammatical forms can be expressed
as a NEON list. Index 0 is used when count equals 1 (singular); index
1 is used for all other counts (zero, two, five, etc.).
# i18n/en.neon
items:
- one item
- "{count} items"
Pass a count key in the $params array to select the correct form:
<?= $this->t('items', ['count' => $n]) ?>
Output examples:
$n |
Result |
|---|---|
1 |
one item |
0 |
0 items |
5 |
5 items |
Languages with more than two forms #
Languages such as Polish or Russian need extra plural categories (zero, few, many). Add as many list entries as required — the form at the computed index is selected, falling back to the last entry when the index is out of range:
# i18n/pl.neon (simplified; real Polish needs custom index logic)
items:
- jeden element
- "{count} elementy"
- "{count} elementów"
Note: Glaze uses simple two-form rules (index 0 for count=1, index 1 for everything else). If your target language requires more than two forms, you can add extra entries as future-use placeholders; the selection logic extends naturally as custom
selectPluralForm()overrides are introduced.
Template helpers #
The following methods are available on $this (the SiteContext object) in every
Sugar template when i18n is enabled:
Current language #
$this->language() returns the language code of the current page (e.g. "nl"). Returns an empty
string on single-language sites.
<html lang="<?= $this->language() ?>">
All languages #
$this->languages() returns the full map of configured languages, keyed by language code. Each value
is a LanguageConfig object with code, label, and urlPrefix properties.
<a s:foreach="$this->languages() as $code => $lang"
href="<?= $this->languageUrl($code) ?? '#' ?>">
<?= $lang->label ?>
</a>
Language URL #
$this->languageUrl($code) returns the URL of the current page translated to the given language code, or
null when no translation exists.
<a s:notempty="$this->languageUrl('nl')"
href="<?= $this->languageUrl('nl') ?>">Nederlands</a>
All translations #
$this->translations() returns all translated versions of the current page as an array keyed by language
code. Useful for generating <link rel="alternate" hreflang="..."> tags.
<link s:foreach="$this->translations() as $lang => $translated"
rel="alternate"
hreflang="<?= $lang ?>"
href="<?= $this->url($translated->urlPath) ?>">
Single translation #
$this->translation($code) returns the translated ContentPage for a specific language code, or null when
not found.
<?php $nlPage = $this->translation('nl') ?>
Localized pages #
$this->localizedPages() returns a PageCollection of regular pages in the current page’s language.
On single-language sites this is equivalent to $this->regularPages().
<a s:foreach="$this->localizedPages() as $p"
href="<?= $this->url($p->urlPath) ?>"><?= $p->title ?></a>
Translate a string #
$this->t($key, $params = [], $fallback = '') translates a string key for the current page’s language. Supports {placeholder}
substitution via $params. Falls back to the default language, then returns $fallback
(or $key when $fallback is omitted) when no translation is found.
<?= $this->t('read_more') ?>
<?= $this->t('posted_on', ['date' => '2026-01']) ?>
<?= $this->t('promo', fallback: 'Check out the latest posts') ?>
The glaze routes command #
The routes command shows all discovered routes. For i18n sites, a Language
column is added automatically. Use --lang to filter by language code:
# show all routes (Language column appears automatically)
glaze routes
# show only Dutch routes
glaze routes --lang nl
# show English and Dutch routes
glaze routes --lang en,nl
See Commands → routes for the full option reference.
Sitemap hreflang #
The built-in sitemap extension automatically adds <xhtml:link rel="alternate">
entries for pages that have translations. No extra configuration is needed.
See Core Extensions for more about the sitemap extension.