# 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.
