<script>
NoJS.i18n({
defaultLocale: 'en',
fallbackLocale: 'en',
locales: {
en: {
greeting: 'Hello, {name}!',
items: '{count} item | {count} items', // Pluralization
nav: {
home: 'Home',
about: 'About'
}
},
'pt-BR': {
greeting: 'Olá, {name}!',
items: '{count} item | {count} itens',
nav: {
home: 'Início',
about: 'Sobre'
}
}
}
});
</script>Instead of inlining all translations in JavaScript, load them from external JSON files.
/locales/en.json
/locales/es.json
<script>
NoJS.i18n({
defaultLocale: 'en',
loadPath: '/locales/{locale}.json'
});
</script>/locales/en/common.json
/locales/en/dashboard.json
<script>
NoJS.i18n({
defaultLocale: 'en',
loadPath: '/locales/{locale}/{ns}.json',
ns: ['common'] // Loaded at init
});
</script><template route="/dashboard" src="./pages/dashboard.tpl" i18n-ns="dashboard"></template><div i18n-ns="settings">
<h2 t="settings.title"></h2>
<p t="settings.desc"></p>
</div>Fetched JSON files are cached in memory by default. Disable in development:
<script>
NoJS.i18n({ loadPath: '/locales/{locale}.json', cache: false });
</script>When a translation key is missing from the current locale, No.JS falls back to the fallbackLocale:
<script>
NoJS.i18n({
defaultLocale: 'pt-BR',
fallbackLocale: 'en',
locales: {
en: { greeting: 'Hello', farewell: 'Goodbye' },
'pt-BR': { greeting: 'Olá' }
// farewell missing → falls back to English "Goodbye"
}
});
</script>If the key is missing from both the current locale and the fallback, the raw key string is displayed.
Enable automatic locale detection from the browser's navigator.language:
<script>
NoJS.i18n({
detectBrowser: true,
defaultLocale: 'en',
fallbackLocale: 'en'
});
</script>When detectBrowser is true, No.JS checks navigator.language and uses the matching locale if available. Falls back to defaultLocale if no match is found.
With inline locales, detection matches navigator.language against the loaded bundle keys. But with loadPath (lazy JSON locales), no bundles exist yet when detection runs — so you must declare the locales you ship in supportedLocales. The browser language is adopted on the first visit only when it appears in that list.
<script>
NoJS.i18n({
loadPath: '/locales/{locale}/{ns}.json',
ns: ['common'],
supportedLocales: ['en', 'es', 'pt', 'it', 'fr'],
detectBrowser: true,
persist: true,
defaultLocale: 'en'
});
</script>On the first load, the active locale is resolved in this priority order:
- Persisted
nojs-locale— a previously chosen locale inlocalStorage(whenpersist: true) always wins. - Detected browser language —
navigator.language(or its base, e.g.ptfrompt-BR), if it is listed insupportedLocales. defaultLocale— the fallback when nothing else matches.
This means a returning visitor who picked a language keeps it, while a first-time visitor lands in their browser's language when you support it.
<!-- Simple translation -->
<h1 t="greeting" t-name="user.name"></h1>
<!-- Output: "Hello, John!" or "Olá, John!" -->
<!-- Nested keys -->
<a route="/" t="nav.home"></a>
<!-- Pluralization -->
<span t="items" t-count="cart.items.length"></span>
<!-- Output: "1 item" or "5 items" -->
<!-- Switch locale -->
<button on:click="$i18n.locale = 'pt-BR'">Português</button>
<button on:click="$i18n.locale = 'en'">English</button>
<!-- Current locale -->
<span bind="$i18n.locale"></span>Add the t-html attribute to render a translation value as sanitized HTML instead of plain text.
<!-- Translation: "Read our <a href='/terms'>terms</a>" -->
<div t="legal.notice" t-html></div>The output is passed through _sanitizeHtml() to prevent XSS.
<!-- Currency -->
<span bind="price | currency"></span> <!-- $1,234.56 -->
<span bind="price | currency:'BRL'"></span> <!-- R$ 1.234,56 -->
<!-- Date -->
<span bind="createdAt | date"></span> <!-- 02/25/2026 -->
<span bind="createdAt | date:'long'"></span> <!-- February 25, 2026 -->
<span bind="createdAt | datetime"></span> <!-- 02/25/2026 3:45 PM -->
<span bind="createdAt | relative"></span> <!-- 2 hours ago -->
<!-- Number -->
<span bind="value | number"></span> <!-- 1,234.56 -->
<span bind="value | number:0"></span> <!-- 1,235 -->
<span bind="value | percent"></span> <!-- 45% -->The $i18n reactive proxy lets you access translation data as dot-notation properties directly in any expression context. This is the preferred approach for simple lookups — no directive or function call needed.
$i18n.[namespace].[key].[subkey]
The proxy resolves dot-path properties against the loaded translation data for the current locale. For example, given this locale file:
// /locales/en/common.json
{
"buttons": { "save": "Save", "cancel": "Cancel" },
"status": { "online": "Online", "offline": "Offline" }
}You can access any value with $i18n.common.buttons.save.
<!-- Initialize state with translated labels -->
<div state="{ label: $i18n.common.buttons.save }">
<button bind="label"></button>
</div>
<!-- Use in store definitions -->
<div store="ui" value="{
title: $i18n.shell.sidebar.introduction,
placeholder: $i18n.common.search.placeholder
}"></div><span bind="$i18n.common.buttons.save"></span>
<h1 bind="$i18n.home.hero.title"></h1>
<!-- Combine with other expressions -->
<span bind="$i18n.common.status.online + ' (' + users.length + ')'"></span><div state="{ role: 'admin' }">
<span computed="roleLabel"
expr="role === 'admin' ? $i18n.roles.admin : $i18n.roles.user"></span>
<p bind="roleLabel"></p>
</div><span if="$i18n.features.beta" bind="$i18n.features.beta"></span><!-- Translated headers for a table -->
<th foreach="col in ['name', 'email', 'role']"
bind="$i18n.table.headers[col]"></th><input bind-placeholder="$i18n.common.search.placeholder">
<img bind-alt="$i18n.common.logo.alt" src="/logo.png">
<button bind-title="$i18n.common.buttons.save" bind-aria-label="$i18n.common.buttons.save">$i18n.key.path |
$i18n.t('key.path', opts) |
|
|---|---|---|
| Use for | Simple string lookups | Interpolation, pluralization |
| Syntax | Dot-notation property access | Function call with options |
| Interpolation | No | Yes ({ name: user.name }) |
| Pluralization | No | Yes ({ count: n }) |
<!-- Simple lookup — use $i18n.* -->
<span bind="$i18n.common.buttons.save"></span>
<!-- Interpolation — use $i18n.t() -->
<span bind="$i18n.t('greeting', { name: user.name })"></span>
<!-- Pluralization — use $i18n.t() -->
<span bind="$i18n.t('items', { count: cart.length })"></span>The following properties on $i18n always resolve to methods or configuration values, not translation keys:
| Property | Type | Description |
|---|---|---|
locale |
string |
Current locale (get/set) |
locales |
object |
All loaded locale data |
t |
function |
Translation function for interpolation/pluralization |
setLocale |
function |
Change the active locale |
All $i18n.* bindings are fully reactive. When the locale changes (via $i18n.locale = 'pt' or $i18n.setLocale('pt')), every element that references $i18n.* in any directive automatically re-evaluates with the new locale's data.
When a key is not found in the current locale, the proxy returns undefined. Combined with the default filter, you can provide fallback text:
<span bind="$i18n.missing.key | default:'Fallback text'"></span>- Filters & Pipes —
currency,date,number,percentfilters - Routing —
i18n-nsfor per-route namespace loading - Configuration — i18n config options
Previous: Routing ← | Next: Drag & Drop →