Integration Guide
App setup
A complete @domphy/i18n setup involves three pieces: the i18n instance, a router hook that loads the right namespace for each route, and a locale switcher.
import i18next from "@domphy/i18n"
import { initReactI18next } from "i18next-react-dom" // or native adapter
await i18next.init({
lng: detectLocale(), // see detectLocale() below
fallbackLng: "en",
ns: ["common"], // always-loaded namespaces
defaultNS: "common",
resources: {
en: {
common: {
"nav.home": "Home",
"nav.about": "About",
"actions.save": "Save",
"actions.cancel": "Cancel",
},
},
vi: {
common: {
"nav.home": "Trang chủ",
"nav.about": "Về chúng tôi",
"actions.save": "Lưu",
"actions.cancel": "Hủy",
},
},
},
})Locale detection
Auto-detect the user's preferred locale:
function detectLocale(): string {
// 1. URL param (?lng=vi)
const url = new URLSearchParams(window.location.search)
const urlLng = url.get("lng")
if (urlLng) return urlLng
// 2. localStorage
const stored = localStorage.getItem("lng")
if (stored) return stored
// 3. Browser preference
const [browserLng] = navigator.language.split("-")
return browserLng
// 4. Fallback handled by i18next.init fallbackLng
}Reactive locale state
Expose the current locale as Domphy state so the UI updates when it changes:
import { toState } from "@domphy/core"
import i18next from "@domphy/i18n"
export const currentLocale = toState(i18next.language)
// Keep in sync when i18next changes locale
i18next.on("languageChanged", (lng) => {
currentLocale.set(lng)
localStorage.setItem("lng", lng)
})
export function t(key: string, options?: object): string {
return i18next.t(key, options)
}Locale switcher component
const LOCALES = [
{ code: "en", label: "English" },
{ code: "vi", label: "Tiếng Việt" },
{ code: "ja", label: "日本語" },
]
const LocaleSwitcher = {
div: [
{
select: LOCALES.map(({ code, label }) => ({
option: label,
value: code,
})),
value: (l) => currentLocale.get(l),
onChange: (e: Event) => {
const lng = (e.target as HTMLSelectElement).value
i18next.changeLanguage(lng)
},
"aria-label": "Select language",
},
],
}Router integration — load namespaces per route
Load only the translations needed for the current route:
import { createRoute } from "@domphy/router"
const adminRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/admin",
beforeLoad: async () => {
// Load "admin" namespace before the route renders
await i18next.loadNamespaces(["admin"])
},
component: () => ({
div: [
{ h1: i18next.t("admin:dashboard.title") },
],
}),
})Using translations in elements
import { computed } from "@domphy/core"
// Reactive translation — updates on locale change
const Header = {
header: [
{
nav: [
{ a: (l) => i18next.t("nav.home"), href: "/" },
{ a: (l) => i18next.t("nav.about"), href: "/about" },
],
},
LocaleSwitcher,
],
}Note: i18next.t() is synchronous but not reactive — wrap in a listener function (l) => i18next.t(key) and subscribe to currentLocale for auto-updates:
import { effect } from "@domphy/core"
// Force re-render on locale change by reading currentLocale in each listener
const PageTitle = {
h1: (l) => {
currentLocale.get(l) // subscribe to locale changes
return i18next.t("page.title")
},
}TypeScript: typed translation keys
Use i18next-typescript plugin for typed keys:
// i18n.d.ts
import "i18next"
declare module "i18next" {
interface CustomTypeOptions {
defaultNS: "common"
resources: {
common: typeof import("./locales/en/common.json")
admin: typeof import("./locales/en/admin.json")
}
}
}
// Now t() is typed:
i18next.t("nav.home") // OK
i18next.t("nav.nonexistent") // ✗ TypeScript error
i18next.t("admin:dashboard.title") // OK (with ns prefix)RTL support
Handle right-to-left languages (Arabic, Hebrew, Persian):
const RTL_LANGUAGES = new Set(["ar", "he", "fa", "ur"])
effect(() => {
const lng = currentLocale.get()
document.documentElement.setAttribute("dir", RTL_LANGUAGES.has(lng) ? "rtl" : "ltr")
document.documentElement.setAttribute("lang", lng)
})Domphy respects dir on the root element — CSS logical properties (margin-inline-start, padding-inline) automatically flip for RTL.