Skip to content

i18next

An optional plugin that binds the i18next internationalization library to Alpine and Harmonia. It adds the x-h-translate directive, which renders a translation into the element's text content, and two magics: $t, which returns a translated string anywhere an Alpine expression runs (including Alpine.data objects), and $i18n, which exposes the current language reactively along with switching helpers. Everything re-renders automatically when the language changes or translation resources load, and languages switched through $i18n.changeLanguage propagate to other same-origin tabs and iframes the way the color mode does.

INFO

This plugin is not part of the default Harmonia bundle. You opt in by loading it, and i18next itself must be available as a global (window.i18next) before Alpine starts.

Usage

This plugin is opt-in, so you load it yourself. Load i18next and Harmonia first, then add the plugin.

CDN

The UMD build of i18next creates the window.i18next global itself:

html
<script src="https://unpkg.com/i18next@26/dist/umd/i18next.min.js"></script>
<script src="https://unpkg.com/@codbex/harmonia/dist/harmonia.min.js"></script>
<!-- opt in: registers x-h-translate, $t and $i18n on alpine:init -->
<script src="https://unpkg.com/@codbex/harmonia/dist/harmonia-i18next.min.js"></script>

ESM

The ESM build of i18next does not create the global, so assign it yourself before Alpine starts:

js
import { I18next } from '@codbex/harmonia';
import Alpine from 'alpinejs';
import i18next from 'i18next';

window.i18next = i18next; // the plugin reads the global as i18next is never bundled
Alpine.plugin(I18next);
Alpine.start();

Initializing i18next

Initialize i18next as usual. The plugin does not interfere with your configuration:

js
i18next.init({
  lng: 'en',
  fallbackLng: 'en',
  resources: {
    en: { translation: { greeting: 'Hello!' } },
    de: { translation: { greeting: 'Hallo!' } },
  },
});

Initialization is asynchronous: until it completes, translated elements show their fallback text (or their key when no fallback is provided), then re-render as soon as i18next reports initialized. Translations also re-render on languageChanged, loaded and on resource bundle changes (added / removed), including when you call i18next.changeLanguage(...) directly on the global.

Language synchronization across tabs and iframes

Languages switched through $i18n.changeLanguage are saved to localStorage (under the codbex.harmonia.language key by default) and propagate to every other same-origin document that uses the plugin, exactly like the color mode does. Embedded iframes and other browser tabs follow immediately. A document that loads later (an iframe added to the page, a new tab, a reload) adopts the stored language as soon as its own i18next instance initializes, overriding the configured lng. Calling i18next.changeLanguage(...) directly on the global updates only the current document.

The storage key is configurable through the Harmonia.plugins.i18next object, for example to keep the languages of two apps on one origin separate. Only documents using the same key stay in sync. Set it before Alpine starts:

html
<script src="https://unpkg.com/@codbex/harmonia/dist/harmonia-i18next.min.js"></script>
<script>
  Harmonia.plugins.i18next.setLanguageStorageKey('myapp.language');
</script>

With the ESM build, import the functions instead:

js
import { setLanguageStorageKey } from '@codbex/harmonia';

setLanguageStorageKey('myapp.language');

Because the stored language is applied after i18next initializes, a page briefly renders in its configured lng first. To start in the stored language directly, seed the init with it:

js
i18next.init({
  lng: localStorage.getItem(Harmonia.plugins.i18next.getLanguageStorageKey()) || 'en',
  // ...
});

API Reference

Besides the directive, the plugin registers two magic properties: $t for translating and $i18n for reading and switching the language.

Component attribute(s)

x-h-translate
  • x-h-translate

Renders the translation into the element's text content (always as plain text, never as HTML). The translation key comes from the expression. The element's initial text content is a fallback, shown while the key cannot be resolved (i18next not yet initialized, or the key missing from the loaded resources). Do not combine it with x-text on the same element as both write the text content.

Arguments

AttributeTypeRequiredDescription
expressionstring or arraytrueThe translation key, e.g. x-h-translate="'app.title'", or a [key, options] array to pass interpolation and plural parameters.

Attributes

AttributeTypeRequiredDescription
data-fallbackstringfalseFallback text shown while the key cannot be resolved, without pre-filling the element. Takes precedence over the element's initial text content. Passed to i18next as defaultValue, so a defaultValue in the expression options still wins.

When the key cannot be resolved and no fallback is provided, the key itself is rendered.

Magic properties

  • $t function

Returns the translated string for a key. It works inline (x-text="$t('app.title')") and inside Alpine.data objects (this.$t('app.title')), and any effect that uses it re-runs when the language or the loaded resources change. While i18next is not available or not yet initialized, it returns the key itself.

  • $i18n object
PropertyTypeDescription
languagestringThe current language. Reactive, e.g. :data-variant="$i18n.language === 'de' ? 'primary' : 'outline'".
languagesarrayThe language fallback chain, most specific first. Reactive.
isInitializedbooleanWhether i18next has finished initializing. Reactive.
changeLanguagefunctionSwitches the language, persists it, and propagates it to other same-origin tabs and iframes.
existsfunctionWhether a key resolves in the current language.
dirfunctionThe text direction of the current (or a given) language.

Arguments

  • $t function
ArgumentTypeRequiredDescription
keystringtrueThe translation key.
optionsobjectfalsePassed straight to i18next.t: interpolation values, count for plurals, ns, lng and so on.
js
this.$t('welcome', { name: 'Ada' });
  • changeLanguage function
ArgumentTypeRequiredDescription
lngstringtrueThe language to switch to. Returns the promise from i18next.changeLanguage.
js
this.$i18n.changeLanguage('de');
  • exists function
ArgumentTypeRequiredDescription
keystringtrueThe translation key to check.
optionsobjectfalsePassed straight to i18next.exists.
  • dir function
ArgumentTypeRequiredDescription
lngstringfalseThe language to get the direction for. Defaults to the current one. Returns 'ltr' or 'rtl'.

Configuration

The plugin adds an i18next object to Harmonia.plugins (the ESM build exports the same functions by name) for configuring the language synchronization:

FunctionDescription
setLanguageStorageKeySets the localStorage key under which the language is persisted and synchronized. Call it before Alpine starts.
getLanguageStorageKeyReturns the current storage key, codbex.harmonia.language by default.
  • setLanguageStorageKey function
ArgumentTypeRequiredDescription
keystringtrueThe localStorage key. Empty or non-string values are ignored.
js
Harmonia.plugins.i18next.setLanguageStorageKey('myapp.language');

Examples

INFO

The live examples on this page share the docs site's single i18next instance, so switching the language in one example updates every example on the page. The instance is initialized with these demo resources:

js
i18next.init({
  lng: 'en',
  fallbackLng: 'en',
  resources: {
    en: {
      translation: {
        greeting: 'Hello!',
        farewell: 'Goodbye!',
        welcome: 'Welcome, {{name}}!',
        items_one: '{{count}} item',
        items_other: '{{count}} items',
      },
    },
    de: {
      translation: {
        greeting: 'Hallo!',
        farewell: 'Auf Wiedersehen!',
        welcome: 'Willkommen, {{name}}!',
        items_one: '{{count}} Artikel',
        items_other: '{{count}} Artikel',
      },
    },
    bg: {
      translation: {
        greeting: 'Здравей!',
        farewell: 'Довиждане!',
        welcome: 'Добре дошли, {{name}}!',
        items_one: '{{count}} артикул',
        items_other: '{{count}} артикула',
      },
    },
  },
});

Basic translation

Point x-h-translate at a key with an expression:

html
<div x-data class="vbox items-start gap-1">
  <p x-h-translate="'greeting'"></p>
  <p x-h-translate="'farewell'"></p>
</div>

Fallback text

The element's initial text content is shown while the key cannot be resolved. It also keeps the element readable before i18next finishes initializing. The data-fallback attribute does the same without pre-filling the element and wins over the text content:

html
<div x-data class="vbox items-start gap-1">
  <p x-h-translate="'missing.key'">This translation does not exist yet</p>
  <p x-h-translate="'missing.key'" data-fallback="Neither does this one"></p>
  <p x-h-translate="'missing.key'"></p>
</div>

Without a fallback, the key itself is rendered (the third line above).

Translating with $t

The $t magic returns the translated string, so it can feed x-text, attribute bindings or any other expression:

html
<div x-data class="vbox items-start gap-1">
  <span x-text="$t('greeting')"></span>
</div>

It is also available as this.$t inside component objects:

js
Alpine.data('cart', () => ({
  get checkoutLabel() {
    return this.$t('farewell');
  },
}));

Interpolation and plurals

Pass options through the [key, options] expression form of the directive, or as the second argument of $t. Both stay reactive, so the text follows the bound values:

html
<div x-data="{ name: 'Ada', count: 1 }" class="vbox items-start gap-2">
  <input x-h-input type="text" x-model="name" aria-label="Name" />
  <span x-h-translate="['welcome', { name }]"></span>
  <span x-text="$t('items', { count })"></span>
  <div class="hbox gap-2">
    <button x-h-button data-size="sm" @click="count++">More</button>
    <button x-h-button data-size="sm" data-variant="outline" @click="count = Math.max(0, count - 1)">Fewer</button>
  </div>
</div>

Language switcher

$i18n.language is reactive, so the active language can drive styling, and $i18n.changeLanguage re-renders every translation on the page:

html
<div x-data class="vbox items-start gap-3">
  <div class="hbox gap-2">
    <button x-h-button :data-variant="$i18n.language === 'en' ? 'primary' : 'outline'" @click="$i18n.changeLanguage('en')">English</button>
    <button x-h-button :data-variant="$i18n.language === 'de' ? 'primary' : 'outline'" @click="$i18n.changeLanguage('de')">Deutsch</button>
    <button x-h-button :data-variant="$i18n.language === 'bg' ? 'primary' : 'outline'" @click="$i18n.changeLanguage('bg')">Български</button>
  </div>
  <p x-h-translate="'greeting'"></p>
</div>

Reactive key

The directive's expression is reactive, so switching the key re-renders the element:

html
<div x-data="{ key: 'greeting' }" class="vbox items-start gap-2">
  <div class="hbox gap-2">
    <button x-h-button data-size="sm" @click="key = 'greeting'">greeting</button>
    <button x-h-button data-size="sm" data-variant="outline" @click="key = 'farewell'">farewell</button>
  </div>
  <p x-h-translate="key"></p>
</div>

Cross frame synchronization

The frame below is a separate document with its own i18next instance, embedded in an iframe. Switching the language here updates the frame, switching it inside the frame updates this page (and every other example on it), and the frame comes up in the stored language when it loads:

html
<div x-data class="vbox items-start gap-3">
  <div class="hbox gap-2">
    <button x-h-button :data-variant="$i18n.language === 'en' ? 'primary' : 'outline'" @click="$i18n.changeLanguage('en')">English</button>
    <button x-h-button :data-variant="$i18n.language === 'de' ? 'primary' : 'outline'" @click="$i18n.changeLanguage('de')">Deutsch</button>
    <button x-h-button :data-variant="$i18n.language === 'bg' ? 'primary' : 'outline'" @click="$i18n.changeLanguage('bg')">Български</button>
  </div>
  <p x-h-translate="'greeting'"></p>
  <iframe src="/harmonia/i18next/i18next-frame.html" title="Harmonia page in an iframe" class="w-full rounded-md border border-border" style="height: 12rem"></iframe>
</div>