@sonenta/astro
v0.3.0
Published
Official Astro integration for Sonenta i18n — build-time CDN translations, zero client JS (SSG).
Maintainers
Readme
@sonenta/astro
Official Astro integration for Sonenta i18n.
Your translations live in Sonenta; this integration pulls the released bundles from the CDN at build time and inlines them into your static HTML. There is zero client-side JavaScript — it's pure SSG, the way Astro content sites want i18n to work.
npm install @sonenta/astro
# or: pnpm add @sonenta/astro / yarn add @sonenta/astrov0.3 — standalone. This release does string resolution,
{{var}}/{var}interpolation, CLDR plurals (acountvar selects the plural form viaIntl.PluralRules), source-language fallback, and device-surface variants (responsive text via build-time overlays + CSS, zero JS). Accessibility surfaces arrive additively in a later release without any breaking change to the API below — see CONTRACT.md.
Quick start
// astro.config.mjs
import { defineConfig } from "astro/config";
import sonenta from "@sonenta/astro";
export default defineConfig({
// Astro's native i18n routing — the integration reads Astro.currentLocale.
i18n: { defaultLocale: "fr", locales: ["fr", "en", "es"] },
integrations: [
sonenta({
project: "your-project-uuid", // from your Sonenta dashboard
locales: ["fr", "en", "es"],
defaultLocale: "fr",
namespaces: ["common"],
}),
],
});---
// src/pages/index.astro
import { getT, defaultLocale } from "sonenta:i18n";
const t = getT(Astro.currentLocale ?? defaultLocale);
---
<html lang={Astro.currentLocale ?? defaultLocale}>
<head><title>{t("home.title")}</title></head>
<body>
<h1>{t("home.title")}</h1>
<p>{t("home.greeting", { name: "Ada" })}</p>
</body>
</html>That's it. astro build fetches common.json for every locale and freezes the
strings into the page. Nothing ships to the browser.
The sonenta:i18n virtual module
The integration exposes a virtual module you can import from any .astro file
(or .ts/.js run at build time):
| Export | Description |
| --- | --- |
| getT(locale) | Returns a t(key, vars?) bound to locale. |
| locales | The locales fetched into the build. |
| defaultLocale | The resolved source/default locale. |
| getCatalog(locale, ns?) | Raw fetched dictionary, or null if absent. |
TypeScript types for the virtual module are injected automatically (Astro's
injectTypes), so getT is fully typed with no extra config.
Key resolution
- Dotted keys walk nested objects:
t("home.cta.label"). - Namespaces use the i18next-style
ns:keysyntax:t("docs:intro"). Un-prefixed keys resolve against the first namespace (default"common"). - Interpolation:
t("greeting", { name: "Ada" })replaces{{name}}(i18next syntax, the shape the CDN emits) and the legacy{name}. - Plurals (CLDR):
t("items", { count: 3 })selects the plural form for the active locale viaIntl.PluralRules— reading the CDN's flat suffixeditems_one/items_other/… (or a nested{ one, other }dict), with an exactitems_0winning over the category form.{{count}}interpolates the number. Unknown placeholders are left intact. - Fallback: a missing key falls back through
fallbackLng(default: the source locale). A locale whose bundle is absent (e.g. plan-limit) falls back the same way. Still missing everywhere → the raw key is returned (i18next parity).
Surface variants (responsive text, 0 JS)
A surface lets one key carry different values per device — e.g. a CTA that
reads "Commencer gratuitement" on desktop and "Commencer" on mobile. Values
come from a sparse CDN overlay ({ns}.{surface}.json) layered over the base
bundle; nothing about your authoring changes except adding per-surface values
in the dashboard.
Because SSG can't know the viewport at build time, the integration fetches every surface and renders them all — a CSS media query reveals the right one. No client JavaScript.
Enable surfaces in the integration, then use the <SurfaceText> component:
// astro.config.mjs
sonenta({
project: "your-project-uuid",
locales: ["fr", "en", "es"],
surfaces: ["desktop", "mobile"], // also fetch these overlays
})---
import SurfaceText from "@sonenta/astro/SurfaceText.astro";
const locale = Astro.currentLocale ?? "fr";
---
<a href="/signup">
<SurfaceText key="cta.start" locale={locale} />
</a>
<!-- desktop → "Commencer gratuitement", mobile → "Commencer" --><SurfaceText> auto-collapses: when a key has no overlay (every surface
resolves equal) it renders a single element — no wrapper spans, no <style>,
no bloat. Props: key, locale, vars?, as? (wrapper tag, default
span), class?, breakpoints?.
Prefer your own markup (e.g. Tailwind hidden md:inline)? Use the primitives:
---
import { getSurfaces, getT } from "sonenta:i18n";
const { desktop, mobile } = getSurfaces(locale, "cta.start");
const oneSurface = getT(locale).surface("cta.start", "mobile");
---Breakpoint ladder (mirrors @sonenta/react-i18next): mobile < 640px,
tablet 640–1023px, desktop ≥ 1024px; configurable via surfaceBreakpoints
or the <SurfaceText breakpoints> prop.
Surface overlays must be published on the CDN for the key (dashboard / backend side). A key with no overlay simply renders its base value on every surface — safe by default.
Without the integration (explicit build-time fetch)
Prefer an explicit top-level await? Use the runtime directly — no virtual
module, no Vite plugin:
// src/i18n.ts
import { createSonentaI18n } from "@sonenta/astro/runtime";
export const i18n = await createSonentaI18n({
project: "your-project-uuid",
locales: ["fr", "en", "es"],
defaultLocale: "fr",
});
export const getT = i18n.getT;Configuration
| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| project | string | — | Required. Project UUID. |
| locales | string[] | — | Required. Locales to fetch + freeze. |
| version | string | "main" | Released version slug / pinned hash. |
| defaultLocale | string | locales[0] | Source / fallback locale. |
| fallbackLng | string \| string[] | [defaultLocale] | Missing-key fallback chain. |
| namespaces | string[] | ["common"] | Bundle files; first is the default ns. |
| surfaces | Surface[] | [] (off) | Device surfaces to fetch (desktop/mobile/tablet). |
| surfaceBreakpoints | {mobile,tablet} | {640,1024} | <SurfaceText> media-query ladder. |
| cdnBase | string | https://cdn.sonenta.com | CDN host (no /p). Env: SONENTA_CDN_BASE. |
| apiBase | string | https://api.sonenta.dev | Reserved (forward-compat). |
| fetchImpl | typeof fetch | global fetch | Custom fetch (runtime API only). |
Bundles are fetched from
{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json — the same
CDN layout as @sonenta/react-i18next.
Notes & limits (v0.3)
- CLDR plurals shipped (0.3).
t(key, { count })selects the plural form viaIntl.PluralRules. Still no a11y surfaces (t.aria/t.alt) and no ICU{{value, format}}formatters — a formatter placeholder passes through untouched. See CONTRACT.md. - Add-ons are island-only.
@sonenta/feedback,@sonenta/realtime, and@sonenta/in-contextare runtime/DOM SDKs — they run inside Astro client islands (reusing the React bindings), not in static.astrooutput. - SSR/hybrid is not yet wired; this is build-time SSG. A request-scoped resolver (fetch-cache + revalidate) arrives with the core-backed release.
License
MIT © Sonenta
