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.
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.
getTranslation() in server-rendered outputThere'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
-
initvia server (nostorage) and client (withstorage) helpers. -
.astropage fetchesgetServerTranslations(lang)and passeslang+translationsto the island. - Island wraps its tree in
<I18nKeylessProvider>and runshydrateFromServer/initI18nClientin an effect. - Prefer
<T>over imperativegetTranslation()for server-rendered text.