Internationalization (i18n)
@akashjs/i18n provides signal-based internationalization. Locale changes are reactive — when you switch languages, every translated string in your UI updates automatically.
Setup
Install the package and create an i18n instance with your messages:
import { createI18n } from '@akashjs/i18n';
const i18n = createI18n({
defaultLocale: 'en',
messages: {
en: {
greeting: 'Hello, {name}!',
nav: {
home: 'Home',
about: 'About',
contact: 'Contact',
},
},
es: {
greeting: '¡Hola, {name}!',
nav: {
home: 'Inicio',
about: 'Acerca de',
contact: 'Contacto',
},
},
},
});
const { t, te, locale, setLocale, availableLocales } = i18n;Translation with t()
The t() function looks up a key and returns the translated string.
t('nav.home'); // 'Home'
t('nav.about'); // 'About'If a key is not found, t() returns the key itself:
t('missing.key'); // 'missing.key'Interpolation
Use {placeholder} syntax in your messages. Pass values as the second argument:
t('greeting', { name: 'World' }); // 'Hello, World!'
t('greeting', { name: 'Alice' }); // 'Hello, Alice!'Params can be strings or numbers:
// messages: { items: 'You have {count} items' }
t('items', { count: 5 }); // 'You have 5 items'HTML Escaping
Interpolation values are HTML-escaped by default to prevent XSS. For example:
t('greeting', { name: '<script>alert(1)</script>' });
// 'Hello, <script>alert(1)</script>!'This ensures user-provided input is safe to render in the DOM without additional sanitization.
Nested Messages
Messages can be nested objects. Access them with dot notation:
const i18n = createI18n({
defaultLocale: 'en',
messages: {
en: {
user: {
profile: {
title: 'Profile',
settings: 'Settings',
},
},
},
},
});
i18n.t('user.profile.title'); // 'Profile'
i18n.t('user.profile.settings'); // 'Settings'Nested messages are flattened internally, so { user: { name: 'Name' } } and { 'user.name': 'Name' } are equivalent.
Locale Switching
Switch the active locale with setLocale(). This is async because it may need to load messages first.
import { defineComponent } from '@akashjs/runtime';
const LanguageSwitcher = defineComponent((ctx) => {
const { t, locale, setLocale, availableLocales } = i18n;
return () => (
<div>
<p>Current: {locale()}</p>
<p>{t('greeting', { name: 'World' })}</p>
<button on:click={() => setLocale('en')}>English</button>
<button on:click={() => setLocale('es')}>Español</button>
</div>
);
});Because locale is a signal, calling setLocale() triggers reactive updates everywhere t() is used in a render function.
Fallback Locale
If a key is missing in the current locale, the fallback locale is checked before returning the raw key:
const i18n = createI18n({
defaultLocale: 'fr',
fallbackLocale: 'en',
messages: {
en: {
greeting: 'Hello!',
farewell: 'Goodbye!',
},
fr: {
greeting: 'Bonjour!',
// 'farewell' is missing in French
},
},
});
i18n.setLocale('fr');
i18n.t('greeting'); // 'Bonjour!'
i18n.t('farewell'); // 'Goodbye!' (falls back to English)Lazy Loading Messages
For large apps, load translations on demand instead of bundling every locale upfront:
const i18n = createI18n({
defaultLocale: 'en',
messages: {
en: { greeting: 'Hello!' },
},
loadMessages: async (locale) => {
const mod = await import(`./locales/${locale}.json`);
return mod.default;
},
});
// When the user switches to Japanese, messages are fetched first
await i18n.setLocale('ja');
i18n.t('greeting'); // Uses the lazy-loaded Japanese messagesetLocale() returns a Promise that resolves once the messages are loaded. Messages are cached — switching back to a previously loaded locale does not re-fetch.
Pluralization
Pass a count or n param to use pluralization. Define plural forms as nested keys with one, other, zero, two, few, many:
const i18n = createI18n({
defaultLocale: 'en',
messages: {
en: {
items: {
one: 'You have {count} item',
other: 'You have {count} items',
},
},
},
});
i18n.t('items', { count: 1 }); // 'You have 1 item'
i18n.t('items', { count: 5 }); // 'You have 5 items'
i18n.t('items', { count: 0 }); // 'You have 0 items'You can also use n as the plural parameter:
// messages: { items: { one: '{n} item', other: '{n} items' } }
i18n.t('items', { n: 3 }); // '3 items'Pluralization works with fallback locales — if the key is missing in the current locale, the fallback locale's plural forms are used.
Built-in plural rules follow CLDR conventions for English, Spanish, French, and Arabic. You can provide custom rules:
const i18n = createI18n({
defaultLocale: 'ru',
pluralRules: {
ru: (count) => {
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return 'one';
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return 'few';
return 'many';
},
},
messages: {
ru: {
apples: {
one: '{count} яблоко',
few: '{count} яблока',
many: '{count} яблок',
},
},
},
});Checking Key Existence with te()
Use te() to check if a translation key exists before using it:
if (i18n.te('premium.banner')) {
// Show the premium banner with translated text
}
// Useful for conditional UI
const HelpLink = defineComponent((ctx) => {
const { t, te } = i18n;
return () => (
<div>
{te('help.tooltip') ? (
<span title={t('help.tooltip')}>{t('help.label')}</span>
) : (
<span>{t('help.label')}</span>
)}
</div>
);
});te() checks both the current locale and the fallback locale.