# Internationalization (i18n)

## Setup

```html
<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>
```

---

## External Locale Files

Instead of inlining all translations in JavaScript, load them from external JSON files.

### Flat Mode (one file per locale)

```
/locales/en.json
/locales/es.json
```

```html
<script>
  NoJS.i18n({
    defaultLocale: 'en',
    loadPath: '/locales/{locale}.json'
  });
</script>
```

### Namespace Mode (split by feature)

```
/locales/en/common.json
/locales/en/dashboard.json
```

```html
<script>
  NoJS.i18n({
    defaultLocale: 'en',
    loadPath: '/locales/{locale}/{ns}.json',
    ns: ['common']   // Loaded at init
  });
</script>
```

### Namespace per Route

```html
<template route="/dashboard" src="./pages/dashboard.tpl" i18n-ns="dashboard"></template>
```

### Namespace on Any Element

```html
<div i18n-ns="settings">
  <h2 t="settings.title"></h2>
  <p t="settings.desc"></p>
</div>
```

### Caching

Fetched JSON files are cached in memory by default. Disable in development:

```html
<script>
  NoJS.i18n({ loadPath: '/locales/{locale}.json', cache: false });
</script>
```

### Fallback Behavior

When a translation key is missing from the current locale, No.JS falls back to the `fallbackLocale`:

```html
<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.

### Browser Locale Detection

Enable automatic locale detection from the browser's `navigator.language`:

```html
<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.

#### Detection with lazy locale files (`supportedLocales`)

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.

```html
<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:

1. **Persisted `nojs-locale`** — a previously chosen locale in `localStorage` (when `persist: true`) always wins.
2. **Detected browser language** — `navigator.language` (or its base, e.g. `pt` from `pt-BR`), if it is listed in `supportedLocales`.
3. **`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.

---

## Usage

```html
<!-- 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>
```

---

## HTML Translations (`t-html`)

Add the `t-html` attribute to render a translation value as sanitized HTML instead of plain text.

```html
<!-- Translation: "Read our <a href='/terms'>terms</a>" -->
<div t="legal.notice" t-html></div>
```

The output is passed through `_sanitizeHtml()` to prevent XSS.

---

## Number & Date Formatting

```html
<!-- 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% -->
```

---

## Accessing Translations in Expressions (`$i18n.*`)

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.

### Syntax

```
$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:

```json
// /locales/en/common.json
{
  "buttons": { "save": "Save", "cancel": "Cancel" },
  "status": { "online": "Online", "offline": "Offline" }
}
```

You can access any value with `$i18n.common.buttons.save`.

### Usage in `state` and `store`

```html
<!-- 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>
```

### Usage in `bind`

```html
<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>
```

### Usage in `computed`

```html
<div state="{ role: 'admin' }">
  <span computed="roleLabel"
        expr="role === 'admin' ? $i18n.roles.admin : $i18n.roles.user"></span>
  <p bind="roleLabel"></p>
</div>
```

### Usage in conditionals

```html
<span if="$i18n.features.beta" bind="$i18n.features.beta"></span>
```

### Usage in `foreach`

```html
<!-- Translated headers for a table -->
<th foreach="col in ['name', 'email', 'role']"
    bind="$i18n.table.headers[col]"></th>
```

### Usage in attribute binding

```html
<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.*` vs `$i18n.t()` — When to Use Each

| | `$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 }`) |

```html
<!-- 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>
```

### Reserved Properties

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 |

### Reactivity

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.

### Missing Keys

When a key is not found in the current locale, the proxy returns `undefined`. Combined with the `default` filter, you can provide fallback text:

```html
<span bind="$i18n.missing.key | default:'Fallback text'"></span>
```

---

---

## See Also

- [Filters & Pipes](filters.md) — `currency`, `date`, `number`, `percent` filters
- [Routing](routing.md) — `i18n-ns` for per-route namespace loading
- [Configuration](configuration.md) — i18n config options

**Previous:** [Routing ←](routing.md) | **Next:** [Drag & Drop →](drag-and-drop.md)
