Skip to main content

i18n-keyless with Astro (React islands)

Astro renders .astro files on the server and hydrates interactive React islands on the client. Like the Next.js App Router, there's no single React render hook to wrap — so you drive the component path with <I18nKeylessProvider>, fed by props the .astro page serializes into the island, and seed the client store with hydrateFromServer.

tip

Read the SSR overview first. Astro islands behave like the Next.js App Router: <T> is SSR-correct via the provider; prefer it over imperative getTranslation() for zero flash. This guide mirrors the runnable examples/astro app, which renders the whole app as one island.

1. Initialize once (server + client helpers)

// src/i18n.ts
import { init, type Lang } from "i18n-keyless-react";

export const SUPPORTED_LANGUAGES = ["fr", "en", "es"] as const;
const config = {
API_KEY: import.meta.env.PUBLIC_I18N_KEYLESS_API_KEY,
languages: { primary: "fr", supported: [...SUPPORTED_LANGUAGES] },
};

export const initI18nServer = () => init({ ...config }); // no storage
export const initI18nClient = () => init({ ...config, storage: window.localStorage });

export const normalizeLang = (value?: string | null): Lang =>
(SUPPORTED_LANGUAGES as readonly string[]).includes(value ?? "") ? (value as Lang) : "fr";

2. Fetch translations in the .astro page, pass them to the island

---
// src/pages/[lang]/index.astro (runs on the server)
import { getServerTranslations } from "i18n-keyless-react";
import { initI18nServer, normalizeLang } from "../../i18n";
import App from "../../components/App";

const lang = normalizeLang(Astro.params.lang);
await initI18nServer();
const translations = await getServerTranslations(lang);
---

<html lang={lang}>
<body>
<!-- Astro serializes these props into the island for flash-free hydration -->
<App client:load lang={lang} translations={translations} page="home" />
</body>
</html>

3. The island: Provider + hydrateFromServer

// src/components/App.tsx
import { useEffect } from "react";
import { I18nKeylessProvider, hydrateFromServer, type Translations } from "i18n-keyless-react";
import { initI18nClient } from "../i18n";

export default function App({
lang,
translations,
page,
}: {
lang: string;
translations: Translations;
page: "home" | "about";
}) {
// <I18nKeylessProvider> makes <I18nKeylessText> SSR-correct; hydrateFromServer +
// initI18nClient run in an effect for the client store / background fetch.
useEffect(() => {
hydrateFromServer({ lang: lang as never, translations });
initI18nClient();
}, [lang, translations]);

return (
<I18nKeylessProvider lang={lang as never} translations={translations}>
{/* every <T> / <I18nKeylessText> here is SSR-correct and hydrates without a flash */}
<YourUI page={page} />
</I18nKeylessProvider>
);
}

Because Astro passes the same lang + translations to the island on the server and replays them on the client, the provider mounts identically on both sides — no mismatch.

Imperative getTranslation() in server-rendered output

There's no ALS render hook to wrap an Astro island, so getTranslation() evaluated during the server render returns the primary language and resolves to the target language only after the client effect. Prefer <I18nKeylessText> / <T> for server-rendered text.

Checklist

  • init via server (no storage) and client (with storage) helpers.
  • .astro page fetches getServerTranslations(lang) and passes lang + translations to the island.
  • Island wraps its tree in <I18nKeylessProvider> and runs hydrateFromServer / initI18nClient in an effect.
  • Prefer <T> over imperative getTranslation() for server-rendered text.