Skip to content

DocsGuidesInternationalization

On this page

Internationalization

Locale routing and translation helpers are built in — no module, no dependency. Enable routing, translate in loaders/components, and let the framework handle <html lang>, hreflang, and per-locale caching.

Enable locale routing

ts
// src/app.ts
createPageRoutes({
  pages,
  renderer: createReactRenderer(),
  i18n: {
    locales: ["en", "id"],
    defaultLocale: "en",
    // prefixDefaultLocale: true,     // every locale gets a prefix (incl. default)
    // directions: { ar: "rtl" },     // <html dir="rtl"> for specific locales
  },
});
Request Result
/about Served as the default locale (no prefix)
/id/about Served as id
/en · /en/about 308 → / · /about (when the default locale is unprefixed)
prefixDefaultLocale: true The inverse: /about → 308 /en/about

The framework also:

  • sets <html lang="…"> (and dir when configured);
  • emits <link rel="alternate" hreflang="…"> for every locale plus x-default;
  • passes locale to loaders, actions, page props (PageProps.locale), and the hydration payload;
  • includes the locale in loader cache keys, so revalidated pages cannot leak data across languages;
  • leaves API routes untouched.

Translating

Catalogs are plain nested objects (JSON-friendly). Load them where the data lives and pass them to createTranslator:

tsx
import { createTranslator, type Messages } from "ramonyx";

const catalog: Record<string, Messages> = {
  en: { hello: "Hello {name}!", items: { one: "{count} item", other: "{count} items" } },
  id: { hello: "Halo {name}!", items: { one: "{count} item", other: "{count} item" } },
};

export async function loader({ locale }: { locale?: string }) {
  const active = locale ?? "en";
  const { t, formatNumber, formatDate } = createTranslator({
    locale: active,
    messages: catalog[active] ?? {},
    fallbackMessages: catalog.en,        // optional
    // onMissing: (key) => console.warn("i18n:", key),
  });
  return {
    greeting: t("hello", { name: "Rina" }),
    count: t("items", { count: 3 }),      // plural via Intl.PluralRules
    price: formatNumber(1234.5),
    date: formatDate(new Date(), { dateStyle: "long" }),
  };
}
  • Interpolation: {name} is replaced from the params object; unknown params stay as written.
  • Plural: object values (one/other/…) are selected with Intl.PluralRules using params.count.
  • Fallback: missing key → fallbackMessages → onMissing(key) → the key itself.
  • The helper is pure and edge-safe; it can also be used in middleware or client components.

Language switcher

tsx
import { localePath } from "ramonyx";

// From any page, build the same page in another locale (existing prefix is stripped).
<a href={localePath(currentPath, "id", { locales: ["en", "id"], defaultLocale: "en" })}>
  Bahasa Indonesia
</a>

Accept-Language negotiation (middleware)

The framework does not auto-detect or auto-redirect: explicit behavior is cache- and SEO-friendly. Use the helper in pre-routing middleware with a cookie:

ts
import { negotiateLocale, localePath } from "ramonyx";
import { parseCookies, serializeCookie } from "@ramonyx/security";

const i18n = { locales: ["en", "id"], defaultLocale: "en" } as const;

export async function localeMiddleware(context, next) {
  const url = new URL(context.request.url);
  if (url.pathname !== "/") return next();
  const saved = parseCookies(context.request.headers.get("cookie") ?? "").locale;
  const locale = saved ?? negotiateLocale(context.request.headers.get("accept-language"), i18n);
  return new Response(null, {
    status: 302,
    headers: {
      location: localePath("/", locale, i18n),
      "set-cookie": serializeCookie("locale", locale, { path: "/", httpOnly: false, sameSite: "lax" }),
    },
  });
}

Limitations (honest)

  • No ICU MessageFormat (interpolation + Intl plural categories only).
  • No automatic detection/redirect; use the middleware pattern above.
  • Catalogs are app-provided (JSON/data store) — the framework does not load files for you.
  • One route per locale (linear in the number of locales); per-route localization is not supported.
  • hreflang uses the page's logical path; dynamic routes follow the currently requested params.
Edit this page on GitHub ↗