Domphy

Lazy Loading & Backend

Why lazy-load translations?

Shipping all translations upfront increases initial bundle size. Lazy loading splits translations by:

  • Locale — load only the active locale
  • Namespace — load only the translations needed for the current page/feature
  • Route — load translations on navigation

Locale-split with dynamic imports

Vite/Rollup tree-shakes dynamic import() with template literals:

import { createI18n } from "@domphy/i18n/domphy"

const { t, setLocale } = createI18n({
  locale: "en",
  messages: {
    en: () => import("./locales/en.json"),
    fr: () => import("./locales/fr.json"),
    de: () => import("./locales/de.json"),
    ja: () => import("./locales/ja.json"),
  },
})

Each locale is a separate chunk — only en.json downloads on first load. When the user switches to French, fr.json downloads and caches.

Namespace lazy loading per route

Load translations when a route mounts — not before:

import { createI18n } from "@domphy/i18n/domphy"

const { t, loadNamespace } = createI18n({
  locale: "en",
  namespaces: {
    common: () => import("./locales/en/common.json"),
    // "dashboard" namespace not preloaded — loaded on demand
  },
})

// In the dashboard route loader:
const dashboardRoute = createRoute({
  path: "/dashboard",
  loader: async () => {
    await loadNamespace("dashboard", () => import("./locales/en/dashboard.json"))
  },
  component: () => DashboardPage,
})

HTTP backend

Fetch translations from a server at runtime — useful for translations stored in a CMS or translation management system:

const { t } = createI18n({
  locale: "en",
  messages: {
    en: async () => {
      const response = await fetch("/api/translations/en")
      return response.json()
    },
    fr: async () => {
      const response = await fetch("/api/translations/fr")
      return response.json()
    },
  },
})

With a translation management service (Lokalise, Crowdin, Phrase):

const LOKALISE_CDN = "https://cdn.lokalise.com/my-project"

const { t } = createI18n({
  locale: navigator.language.split("-")[0] as Locale,
  messages: {
    en: () => fetch(`${LOKALISE_CDN}/en.json`).then(r => r.json()),
    fr: () => fetch(`${LOKALISE_CDN}/fr.json`).then(r => r.json()),
    de: () => fetch(`${LOKALISE_CDN}/de.json`).then(r => r.json()),
  },
})

Handling the loading state

Translations load asynchronously — show a loading state while the initial locale loads:

import { toState } from "@domphy/core"

const translationsReady = toState(false)

async function initApp() {
  const { t } = createI18n({ locale: "en", messages: { en: () => import("./en.json") } })
  await initI18n()   // load the initial locale
  translationsReady.set(true)
}

initApp()

const App = {
  div: (l) => translationsReady.get(l) ? MainApp : { div: "Loading…" },
}

Locale detection

Auto-detect locale from browser, user preference, or URL:

import { detectLocale } from "@domphy/i18n"

const supported: Locale[] = ["en", "fr", "de", "ja", "zh"]

function getInitialLocale(): Locale {
  // 1. Check URL param (?lang=fr)
  const urlParam = new URLSearchParams(location.search).get("lang")
  if (urlParam && supported.includes(urlParam as Locale)) return urlParam as Locale

  // 2. Check saved preference
  const saved = localStorage.getItem("locale")
  if (saved && supported.includes(saved as Locale)) return saved as Locale

  // 3. Browser language
  return detectLocale(navigator.languages, supported, "en") as Locale
}

const { t, setLocale } = createI18n({
  locale: getInitialLocale(),
  messages: { ... },
})

Preloading on hover

Preload the translated page before the user navigates to it:

const NavLink = (href: string, label: string, namespace: string) => ({
  a: label,
  href,
  onMouseenter: async () => {
    await loadNamespace(namespace)
  },
})

Caching loaded namespaces

Loaded namespaces are cached in memory — loadNamespace("dashboard") on re-navigation is instant:

// First call: fetches from network (~200ms)
await loadNamespace("dashboard", () => import("./locales/en/dashboard.json"))

// Subsequent calls: returns from memory cache (~0ms)
await loadNamespace("dashboard", () => import("./locales/en/dashboard.json"))

SSR considerations

On the server, translations must be loaded synchronously (or pre-awaited):

// Server-side
import en from "./locales/en.json"   // static import — synchronous
import fr from "./locales/fr.json"

const MESSAGES = { en, fr }

const { t } = createI18n({
  locale: userLocale,
  messages: {
    en: () => Promise.resolve(MESSAGES.en),
    fr: () => Promise.resolve(MESSAGES.fr),
  },
})

With @domphy/app SSR, the server can read the Accept-Language header or cookie to choose the locale, then hydrate the client with the same locale.