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
// 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="…">(anddirwhen configured); - emits
<link rel="alternate" hreflang="…">for every locale plusx-default; - passes
localeto 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:
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 withIntl.PluralRulesusingparams.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
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:
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 +
Intlplural 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.
hreflanguses the page's logical path; dynamic routes follow the currently requested params.