Localization (i18n)
@aioha/react-ui and @aioha/lit-ui share a single canonical message catalog published as @aioha/i18n-keys. The catalog exposes namespaced message IDs, an English default, a framework-neutral Messages adapter, ICU MessageFormat support for pluralization and interpolation, a pseudo-locale for testing, RTL direction detection, and bundled translations for 16 languages.
Installation
pnpm i @aioha/i18n-keys
Already installed transitively by @aioha/react-ui and @aioha/lit-ui — you only need to add it directly if you build your own adapter.
Usage
Switching language
<AiohaModal> (React) and <aioha-modal> (Lit) both accept a language prop/attribute. The default English catalog ships bundled; additional locales are loaded on demand.
import { AiohaModal, defaultMessages } from '@aioha/react-ui'
// Load a non-English catalog once at startup
await defaultMessages.loadLocale('fr', (lng) =>
import(`@aioha/i18n-keys/build/locales/${lng}.json`).then((m) => m.default)
)
<AiohaModal language="fr" displayed={modalDisplayed} loginOptions={...} onClose={setModalDisplayed} />
import { defaultMessages } from '@aioha/lit-ui'
await defaultMessages.loadLocale('fr', (lng) =>
import(`@aioha/i18n-keys/build/locales/${lng}.json`).then((m) => m.default)
)
// In your template
html`<aioha-modal language="fr" ...></aioha-modal>`
Bundled locales
The package ships translation JSON for the following locales. Load each with dynamic import — English is always available without loading.
| Locale | |
|---|---|
ar | Arabic (RTL) |
de | German |
en | English (bundled) |
es | Spanish |
fr | French |
hi | Hindi |
hr | Croatian |
it | Italian |
ja | Japanese |
ko | Korean |
ms | Malay |
pl | Polish |
pt | Portuguese |
ru | Russian |
zh-CN | Chinese (Simplified) |
zh-TW | Chinese (Traditional) |
Bring your own translations
Pass a Partial<Catalog> to defaultMessages.addCatalog(locale, catalog) to extend or replace the shipped strings. Any subset is acceptable — unspecified keys fall back to English.
import { defaultMessages } from '@aioha/react-ui'
defaultMessages.addCatalog('en', {
'wallet.connect': 'Sign in with Hive'
})
Right-to-left (RTL)
Both packages emit dir="rtl" on the modal root when the resolved direction is RTL, and use logical CSS properties (ms-*, me-*, ps-*, pe-*, inset-x-*) so layout mirrors automatically. Directional icons (back button, forward chevron) are flipped with rtl:-scale-x-100.
The direction is resolved in this order:
- Explicit
dirprop (React) ordirectionattribute (Lit):'ltr' | 'rtl' | 'auto'. - If
'auto':document.dirif set tortl. - Otherwise: the active locale's direction (Arabic, Hebrew, Persian, Urdu, and related languages resolve to
rtl).
<AiohaModal dir="rtl" language="ar" ... />
<aioha-modal direction="rtl" language="ar" ...></aioha-modal>
Pseudo-locale testing
A built-in en-XA locale wraps every English value with accented characters: [!! Çóññéçţ Ŵàľľéţ !!]. Use it to catch hardcoded strings, string-concatenation bugs, and layout truncation during development.
<AiohaModal language="en-XA" ... />
ICU placeholders ({user}, {count, plural, one {...} other {...}}) are preserved.
The Messages adapter
If your app already ships an i18n framework, implement the Messages interface and pass it as messages on the modal. This lets translators manage Aioha's strings alongside yours, and keeps only one i18n runtime in your bundle.
import type { Messages } from '@aioha/i18n-keys'
interface Messages {
t(id: MessageId, vars?: Record<string, string | number>): string
getLocale(): string
setLocale(locale: string): void | Promise<void>
getDir(): 'ltr' | 'rtl'
subscribe(listener: () => void): () => void
}
<AiohaModal messages={myMessages} ... />
<aioha-modal .messages=${myMessages} ...></aioha-modal>
The examples below show how to wire Aioha's 38 message IDs to a handful of popular frameworks. In every case, import the JSON catalogs from @aioha/i18n-keys/build/locales/<lng>.json — the framework becomes responsible only for formatting.
react-intl (FormatJS)
react-intl uses the same ICU MessageFormat syntax as Aioha's default adapter, so translations drop in unchanged.
import { createIntl, createIntlCache } from 'react-intl'
import type { Messages } from '@aioha/i18n-keys'
import en from '@aioha/i18n-keys/build/locales/en.json'
const cache = createIntlCache()
let intl = createIntl({ locale: 'en', messages: en }, cache)
const listeners = new Set<() => void>()
export const aiohaMessages: Messages = {
t: (id, vars) => intl.formatMessage({ id }, vars),
getLocale: () => intl.locale,
setLocale: async (lng) => {
const messages = (await import(`@aioha/i18n-keys/build/locales/${lng}.json`)).default
intl = createIntl({ locale: lng, messages }, cache)
listeners.forEach((l) => l())
},
getDir: () => (['ar', 'he', 'fa', 'ur'].includes(intl.locale.split('-')[0]) ? 'rtl' : 'ltr'),
subscribe: (cb) => {
listeners.add(cb)
return () => listeners.delete(cb)
}
}
import { AiohaModal } from '@aioha/react-ui'
import { aiohaMessages } from './aioha-messages'
<AiohaModal messages={aiohaMessages} language="fr" ... />
If you already have an <IntlProvider> in your tree, pass its intl object instead of creating a second one — then t simply becomes (id, vars) => providedIntl.formatMessage({ id }, vars).
i18next
i18next uses {{var}} interpolation by default, but Aioha's catalogs use ICU {var}. Install i18next-icu to parse them natively.
import i18next from 'i18next'
import ICU from 'i18next-icu'
import type { Messages } from '@aioha/i18n-keys'
import en from '@aioha/i18n-keys/build/locales/en.json'
const i18n = i18next.createInstance()
await i18n.use(ICU).init({
lng: 'en',
fallbackLng: 'en',
ns: ['aioha'],
defaultNS: 'aioha',
resources: { en: { aioha: en } }
})
export const aiohaMessages: Messages = {
t: (id, vars) => i18n.t(id, vars) as string,
getLocale: () => i18n.language,
setLocale: async (lng) => {
if (!i18n.hasResourceBundle(lng, 'aioha')) {
const res = (await import(`@aioha/i18n-keys/build/locales/${lng}.json`)).default
i18n.addResourceBundle(lng, 'aioha', res)
}
await i18n.changeLanguage(lng)
},
getDir: () => (i18n.dir() === 'rtl' ? 'rtl' : 'ltr'),
subscribe: (cb) => {
i18n.on('languageChanged', cb)
return () => i18n.off('languageChanged', cb)
}
}
LinguiJS
Lingui compiles ICU messages to a function map. Use the runtime i18n singleton and point it at Aioha's JSON:
import { i18n } from '@lingui/core'
import type { Messages } from '@aioha/i18n-keys'
import en from '@aioha/i18n-keys/build/locales/en.json'
i18n.load('en', en)
i18n.activate('en')
export const aiohaMessages: Messages = {
t: (id, vars) => i18n._(id, vars),
getLocale: () => i18n.locale,
setLocale: async (lng) => {
if (!i18n.messages[lng]) {
const res = (await import(`@aioha/i18n-keys/build/locales/${lng}.json`)).default
i18n.load(lng, res)
}
i18n.activate(lng)
},
getDir: () => (['ar', 'he', 'fa', 'ur'].includes(i18n.locale.split('-')[0]) ? 'rtl' : 'ltr'),
subscribe: (cb) => i18n.on('change', cb)
}
Vue I18n
For Vue apps, wrap an existing createI18n() instance:
import { watch } from 'vue'
import type { Messages } from '@aioha/i18n-keys'
import i18n from './i18n' // your existing Vue i18n instance
import en from '@aioha/i18n-keys/build/locales/en.json'
i18n.global.mergeLocaleMessage('en', { aioha: en })
export const aiohaMessages: Messages = {
t: (id, vars) => i18n.global.t(`aioha.${id}`, vars ?? {}),
getLocale: () => i18n.global.locale.value,
setLocale: async (lng) => {
if (!i18n.global.availableLocales.includes(lng)) {
const res = (await import(`@aioha/i18n-keys/build/locales/${lng}.json`)).default
i18n.global.mergeLocaleMessage(lng, { aioha: res })
}
i18n.global.locale.value = lng
},
getDir: () => (['ar', 'he', 'fa', 'ur'].includes(i18n.global.locale.value.split('-')[0]) ? 'rtl' : 'ltr'),
subscribe: (cb) => watch(i18n.global.locale, cb)
}
Vue I18n supports ICU via the @intlify/core engine — Aioha's {var} placeholders work out of the box.
@lit/localize
@lit/localize is a compile-time extraction system — it expects you to author msg() calls in source, uses positional (not named) placeholders, and does not parse ICU MessageFormat. Routing Aioha's pre-built ICU catalog through msg() fights the library.
The idiomatic integration is to share locale state only: let @lit/localize drive locale switching for your own strings, and keep Aioha's built-in ICU formatter for Aioha's strings. The adapter below forwards setLocale to @lit/localize and listens for lit-localize-status to stay in sync.
import { configureLocalization, getLocale, setLocale } from '@lit/localize'
import { createDefaultMessages, resolveDir } from '@aioha/i18n-keys'
import type { Messages } from '@aioha/i18n-keys'
// Your existing @lit/localize setup for your own strings
configureLocalization({
sourceLocale: 'en',
targetLocales: ['ar', 'de', 'es', 'fr', 'ja', 'zh-CN'],
loadLocale: (locale) => import(`./generated/locales/${locale}.js`)
})
// Aioha's built-in adapter does the ICU formatting for Aioha's catalog
const aioha = createDefaultMessages({ initialLocale: getLocale() })
// Bridge: mirror @lit/localize's active locale into Aioha
window.addEventListener('lit-localize-status', async (e: Event) => {
const ev = e as CustomEvent<{ status: string; readyLocale?: string }>
if (ev.detail.status !== 'ready' || !ev.detail.readyLocale) return
const lng = ev.detail.readyLocale
if (!aioha.getLocale || aioha.getLocale() === lng) return
try {
const cat = (await import(`@aioha/i18n-keys/build/locales/${lng}.json`)).default
aioha.addCatalog(lng, cat)
} catch {
// locale not shipped by @aioha/i18n-keys — falls back to English
}
aioha.setLocale(lng)
})
export const aiohaMessages: Messages = {
t: (id, vars) => aioha.t(id, vars),
getLocale: () => getLocale(),
setLocale: async (lng) => {
await setLocale(lng) // @lit/localize drives the change; the listener above syncs Aioha
},
getDir: () => resolveDir(getLocale()),
subscribe: aioha.subscribe
}
html`<aioha-modal .messages=${aiohaMessages} ...></aioha-modal>`
Now one call to setLocale('fr') switches both your msg() output and Aioha's modal.
Rolling your own
For any framework that isn't listed, the contract is only four responsibilities: format, read the current locale, switch it, and notify subscribers. Under the hood the built-in adapter uses intl-messageformat (FormatJS) — you can use it directly as a minimal formatter if your i18n layer doesn't ship an ICU parser.
Message IDs
See the @aioha/i18n-keys README for the full list of the 38 message IDs, their English defaults, and ICU variables.
Catalog versioning
The catalog schema is versioned. Consumers can guard against drift:
import { CATALOG_VERSION } from '@aioha/i18n-keys'
if (CATALOG_VERSION !== 1) console.warn('Aioha catalog changed; review translations')
The version bumps on any key addition, removal, or rename.