@sonenta/astro 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +42 -16
- package/README.md +69 -5
- package/SurfaceText.astro +93 -0
- package/dist/{chunk-CK2DAB6G.js → chunk-RG6KTECT.js} +90 -7
- package/dist/chunk-RG6KTECT.js.map +1 -0
- package/dist/index.cjs +113 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +30 -5
- package/dist/index.js.map +1 -1
- package/dist/runtime.cjs +96 -8
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +104 -16
- package/dist/runtime.d.ts +104 -16
- package/dist/runtime.js +13 -3
- package/package.json +4 -2
- package/dist/chunk-CK2DAB6G.js.map +0 -1
package/CONTRACT.md
CHANGED
|
@@ -10,9 +10,17 @@ engine); the contract here is frozen so the move onto `@sonenta/i18n-core`
|
|
|
10
10
|
| Version | What it is |
|
|
11
11
|
| --- | --- |
|
|
12
12
|
| **0.1.0** | Standalone. Build-time CDN fetch + string resolution + `{var}` interpolation + source-language fallback. Pure SSG, zero client JS. |
|
|
13
|
-
| **0.2.0** |
|
|
13
|
+
| **0.2.0** | Adds **device-surface variants** (#911) — sparse `{ns}.{surface}.json` overlays, `getSurfaces` / `t.surface` / `<SurfaceText>`, still standalone. A faithful, regression-locked subset of `@sonenta/react-i18next` #913 so the later core swap is behaviour-preserving. Additive — the 0.1 API is unchanged. |
|
|
14
|
+
| **0.3.0** | Internals swapped to `@sonenta/i18n-core`. Adds CLDR plurals, accessibility surfaces (`t.aria`/`t.alt`), BCP-47 variant→base inheritance, and optional SSR/hybrid — all **additive**, behaviour-preserving for the device-surface resolution above. |
|
|
14
15
|
| **1.0.0** | Promoted to stable once the demos / website have validated parity. |
|
|
15
16
|
|
|
17
|
+
> **Why surfaces landed in 0.2.0 standalone (not gated on i18n-core):** the
|
|
18
|
+
> surface resolution is the frozen #911 sparse-overlay merge — trivial and
|
|
19
|
+
> already verified in react-i18next #913. Implementing it faithfully now
|
|
20
|
+
> (regression-locked against #913's emission) unblocks real consumers without
|
|
21
|
+
> waiting on the multi-week core extraction; 0.3.0 then swaps the engine
|
|
22
|
+
> underneath with identical behaviour.
|
|
23
|
+
|
|
16
24
|
## Frozen public surface (will not break across 0.x → 1.0)
|
|
17
25
|
|
|
18
26
|
### Default export — the integration
|
|
@@ -35,18 +43,32 @@ type.
|
|
|
35
43
|
### Virtual module `sonenta:i18n`
|
|
36
44
|
|
|
37
45
|
```ts
|
|
38
|
-
export const getT: (locale: Locale) => TFn; // t(key, vars?)
|
|
46
|
+
export const getT: (locale: Locale) => TFn; // t(key, vars?) + t.surface(key, surface, vars?)
|
|
39
47
|
export const locales: Locale[];
|
|
40
48
|
export const defaultLocale: Locale;
|
|
49
|
+
export const surfaces: Surface[]; // configured device surfaces
|
|
50
|
+
export const surfaceBreakpoints: SurfaceBreakpoints;
|
|
51
|
+
export const getSurfaces: (locale, key, vars?) => Record<Surface, string>;
|
|
41
52
|
export const getCatalog: (locale, namespace?) => Record<string, unknown> | null;
|
|
42
53
|
export default i18n: SonentaI18n;
|
|
43
54
|
```
|
|
44
55
|
|
|
45
56
|
`getT(locale)` returns a `t()` whose **call signature is frozen**:
|
|
46
|
-
`t(key: string, vars?: Record<string, string | number>): string
|
|
47
|
-
|
|
48
|
-
`t.
|
|
49
|
-
bare `t(key)`
|
|
57
|
+
`t(key: string, vars?: Record<string, string | number>): string`, plus
|
|
58
|
+
`t.surface(key, surface, vars?)`. Later releases **add methods** to the
|
|
59
|
+
returned function (e.g. `t.aria(key)`, `t.alt(key)`, plural-aware
|
|
60
|
+
`t(key, { count })`) — additive, never breaking the bare `t(key)` /
|
|
61
|
+
`t.surface(...)` calls.
|
|
62
|
+
|
|
63
|
+
### Surface resolution (#911) — owned by backend, consumed here
|
|
64
|
+
|
|
65
|
+
Overlay URL: `{cdnBase}/p/{project}/{version}/latest/{locale}/{ns}.{surface}.json`
|
|
66
|
+
(the base path with `{ns}.{surface}.json`). **Sparse** — only overridden keys
|
|
67
|
+
present. Resolution = base ⊕ overlay, **overlay wins per key**, applied per
|
|
68
|
+
locale before the locale fallback. A leaf may be a plain string or the
|
|
69
|
+
envelope `{"$value": <string|pluralDict>, "$asset": {kind, ref}}`; v0.2 uses
|
|
70
|
+
`$value` (string) and ignores `$asset`. An overlay miss / error → base-only
|
|
71
|
+
(never throws). Surface ∈ `desktop | mobile | tablet`.
|
|
50
72
|
|
|
51
73
|
### Runtime entry `@sonenta/astro/runtime`
|
|
52
74
|
|
|
@@ -62,19 +84,23 @@ bare `t(key)` call.
|
|
|
62
84
|
- any other non-OK / transport error → **throws** (a misconfigured build fails
|
|
63
85
|
loudly rather than silently shipping empty pages).
|
|
64
86
|
|
|
65
|
-
##
|
|
87
|
+
## Explicit non-goals (deferred to the `@sonenta/i18n-core`-backed 0.3.0)
|
|
66
88
|
|
|
67
|
-
To guarantee the additive path above,
|
|
68
|
-
the following — there is therefore no
|
|
69
|
-
over their semantics:
|
|
89
|
+
To guarantee the additive path above, the package ships **no naive
|
|
90
|
+
implementation** of the following — there is therefore no behaviour to break
|
|
91
|
+
when core takes over their semantics:
|
|
70
92
|
|
|
71
|
-
- **CLDR plurals** (`t(key, { count })` pluralization).
|
|
93
|
+
- **CLDR plurals** (`t(key, { count })` pluralization). A `$value` that is a
|
|
94
|
+
plural dict resolves as "no string" (→ fallback / raw key) for now.
|
|
72
95
|
- **Accessibility surfaces** (`t.aria`, `t.alt`, screen-reader / plain-language
|
|
73
|
-
variants)
|
|
74
|
-
|
|
75
|
-
- **BCP-47 variant→base inheritance** (`fr-CA → fr`).
|
|
76
|
-
|
|
77
|
-
- **SSR / hybrid** request-scoped resolution.
|
|
96
|
+
variants) — these ride the SAME overlay engine but are out of scope until
|
|
97
|
+
0.3.0.
|
|
98
|
+
- **BCP-47 variant→base inheritance** (`fr-CA → fr`). The configured
|
|
99
|
+
`fallbackLng` list is applied literally.
|
|
100
|
+
- **SSR / hybrid** request-scoped resolution. This is build-time SSG only.
|
|
101
|
+
|
|
102
|
+
Device-surface variants (`desktop`/`mobile`/`tablet`) ARE supported as of
|
|
103
|
+
0.2.0 (standalone, frozen contract above).
|
|
78
104
|
|
|
79
105
|
## Boundaries
|
|
80
106
|
|
package/README.md
CHANGED
|
@@ -12,9 +12,10 @@ npm install @sonenta/astro
|
|
|
12
12
|
# or: pnpm add @sonenta/astro / yarn add @sonenta/astro
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
> **v0.
|
|
16
|
-
> interpolation,
|
|
17
|
-
>
|
|
15
|
+
> **v0.2 — standalone.** This release does string resolution, `{var}`
|
|
16
|
+
> interpolation, source-language fallback, and **device-surface variants**
|
|
17
|
+
> (responsive text via build-time overlays + CSS, zero JS). CLDR plurals and
|
|
18
|
+
> accessibility surfaces arrive **additively** in `0.3.0` (backed by
|
|
18
19
|
> `@sonenta/i18n-core`) **without any breaking change** to the API below — see
|
|
19
20
|
> [CONTRACT.md](./CONTRACT.md).
|
|
20
21
|
|
|
@@ -85,6 +86,63 @@ TypeScript types for the virtual module are injected automatically (Astro's
|
|
|
85
86
|
the same way. Still missing everywhere → the raw key is returned (i18next
|
|
86
87
|
parity).
|
|
87
88
|
|
|
89
|
+
## Surface variants (responsive text, 0 JS)
|
|
90
|
+
|
|
91
|
+
A **surface** lets one key carry different values per device — e.g. a CTA that
|
|
92
|
+
reads "Commencer gratuitement" on desktop and "Commencer" on mobile. Values
|
|
93
|
+
come from a sparse CDN overlay (`{ns}.{surface}.json`) layered over the base
|
|
94
|
+
bundle; nothing about your authoring changes except adding per-surface values
|
|
95
|
+
in the dashboard.
|
|
96
|
+
|
|
97
|
+
Because SSG can't know the viewport at build time, the integration fetches
|
|
98
|
+
**every** surface and renders them all — a CSS media query reveals the right
|
|
99
|
+
one. No client JavaScript.
|
|
100
|
+
|
|
101
|
+
Enable surfaces in the integration, then use the `<SurfaceText>` component:
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// astro.config.mjs
|
|
105
|
+
sonenta({
|
|
106
|
+
project: "your-project-uuid",
|
|
107
|
+
locales: ["fr", "en", "es"],
|
|
108
|
+
surfaces: ["desktop", "mobile"], // also fetch these overlays
|
|
109
|
+
})
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
```astro
|
|
113
|
+
---
|
|
114
|
+
import SurfaceText from "@sonenta/astro/SurfaceText.astro";
|
|
115
|
+
const locale = Astro.currentLocale ?? "fr";
|
|
116
|
+
---
|
|
117
|
+
<a href="/signup">
|
|
118
|
+
<SurfaceText key="cta.start" locale={locale} />
|
|
119
|
+
</a>
|
|
120
|
+
<!-- desktop → "Commencer gratuitement", mobile → "Commencer" -->
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`<SurfaceText>` **auto-collapses**: when a key has no overlay (every surface
|
|
124
|
+
resolves equal) it renders a single element — no wrapper spans, no `<style>`,
|
|
125
|
+
no bloat. Props: `key`, `locale`, `vars?`, `as?` (wrapper tag, default
|
|
126
|
+
`span`), `class?`, `breakpoints?`.
|
|
127
|
+
|
|
128
|
+
Prefer your own markup (e.g. Tailwind `hidden md:inline`)? Use the primitives:
|
|
129
|
+
|
|
130
|
+
```astro
|
|
131
|
+
---
|
|
132
|
+
import { getSurfaces, getT } from "sonenta:i18n";
|
|
133
|
+
const { desktop, mobile } = getSurfaces(locale, "cta.start");
|
|
134
|
+
const oneSurface = getT(locale).surface("cta.start", "mobile");
|
|
135
|
+
---
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Breakpoint ladder (mirrors `@sonenta/react-i18next`): `mobile < 640px`,
|
|
139
|
+
`tablet 640–1023px`, `desktop ≥ 1024px`; configurable via `surfaceBreakpoints`
|
|
140
|
+
or the `<SurfaceText breakpoints>` prop.
|
|
141
|
+
|
|
142
|
+
> Surface overlays must be published on the CDN for the key (dashboard /
|
|
143
|
+
> backend side). A key with no overlay simply renders its base value on every
|
|
144
|
+
> surface — safe by default.
|
|
145
|
+
|
|
88
146
|
## Without the integration (explicit build-time fetch)
|
|
89
147
|
|
|
90
148
|
Prefer an explicit top-level `await`? Use the runtime directly — no virtual
|
|
@@ -112,6 +170,8 @@ export const getT = i18n.getT;
|
|
|
112
170
|
| `defaultLocale` | `string` | `locales[0]` | Source / fallback locale. |
|
|
113
171
|
| `fallbackLng` | `string \| string[]` | `[defaultLocale]` | Missing-key fallback chain. |
|
|
114
172
|
| `namespaces` | `string[]` | `["common"]` | Bundle files; first is the default ns. |
|
|
173
|
+
| `surfaces` | `Surface[]` | `[]` (off) | Device surfaces to fetch (`desktop`/`mobile`/`tablet`). |
|
|
174
|
+
| `surfaceBreakpoints` | `{mobile,tablet}` | `{640,1024}` | `<SurfaceText>` media-query ladder. |
|
|
115
175
|
| `cdnBase` | `string` | `https://cdn.sonenta.com` | CDN host (no `/p`). Env: `SONENTA_CDN_BASE`. |
|
|
116
176
|
| `apiBase` | `string` | `https://api.sonenta.dev` | Reserved (forward-compat). |
|
|
117
177
|
| `fetchImpl` | `typeof fetch` | global `fetch` | Custom fetch (runtime API only). |
|
|
@@ -120,12 +180,16 @@ Bundles are fetched from
|
|
|
120
180
|
`{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json` — the same
|
|
121
181
|
CDN layout as `@sonenta/react-i18next`.
|
|
122
182
|
|
|
123
|
-
## Notes & limits (v0.
|
|
183
|
+
## Notes & limits (v0.2)
|
|
124
184
|
|
|
185
|
+
- **No CLDR plurals / a11y surfaces yet.** A `$value` that is a plural dict is
|
|
186
|
+
treated as "no string" (resolves to fallback/raw key); `t.aria`/`t.alt` and
|
|
187
|
+
pluralization arrive in `0.3.0` on `@sonenta/i18n-core`. See
|
|
188
|
+
[CONTRACT.md](./CONTRACT.md).
|
|
125
189
|
- **Add-ons are island-only.** `@sonenta/feedback`, `@sonenta/realtime`, and
|
|
126
190
|
`@sonenta/in-context` are runtime/DOM SDKs — they run inside Astro client
|
|
127
191
|
islands (reusing the React bindings), not in static `.astro` output.
|
|
128
|
-
- **SSR/hybrid** is not yet wired;
|
|
192
|
+
- **SSR/hybrid** is not yet wired; this is build-time SSG. A request-scoped
|
|
129
193
|
resolver (fetch-cache + revalidate) arrives with the core-backed release.
|
|
130
194
|
|
|
131
195
|
## License
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* <SurfaceText> — render a surface-variant string for SSG with zero client JS.
|
|
4
|
+
*
|
|
5
|
+
* The viewport is unknown at build time, so we render every distinct device
|
|
6
|
+
* surface and let a CSS media query reveal the right one. When a key has no
|
|
7
|
+
* surface overlay (every surface resolves equal) the component AUTO-COLLAPSES
|
|
8
|
+
* to a single plain render — no wrapper spans, no <style>, no HTML bloat.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ---
|
|
12
|
+
* import SurfaceText from "@sonenta/astro/SurfaceText.astro";
|
|
13
|
+
* ---
|
|
14
|
+
* <SurfaceText key="cta.start" locale={locale} />
|
|
15
|
+
* // desktop: "Commencer gratuitement" / mobile: "Commencer"
|
|
16
|
+
*
|
|
17
|
+
* Requires the `sonenta()` integration (provides the `sonenta:i18n` virtual
|
|
18
|
+
* module) with `surfaces` configured, e.g. surfaces: ["desktop", "mobile"].
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
getT,
|
|
22
|
+
getSurfaces,
|
|
23
|
+
surfaceBreakpoints as configuredBreakpoints,
|
|
24
|
+
} from "sonenta:i18n";
|
|
25
|
+
|
|
26
|
+
interface Props {
|
|
27
|
+
/** Translation key (supports `ns:key`). */
|
|
28
|
+
key: string;
|
|
29
|
+
/** Active locale (pass your own — works with any routing). */
|
|
30
|
+
locale: string;
|
|
31
|
+
/** Interpolation variables. */
|
|
32
|
+
vars?: Record<string, string | number>;
|
|
33
|
+
/** Wrapper element for each surface variant. Default `"span"`. */
|
|
34
|
+
as?: string;
|
|
35
|
+
/** Class applied to each rendered element. */
|
|
36
|
+
class?: string;
|
|
37
|
+
/** Override the breakpoint ladder; defaults to the integration's config. */
|
|
38
|
+
breakpoints?: { mobile: number; tablet: number };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
key,
|
|
43
|
+
locale,
|
|
44
|
+
vars,
|
|
45
|
+
as = "span",
|
|
46
|
+
class: className,
|
|
47
|
+
breakpoints,
|
|
48
|
+
} = Astro.props;
|
|
49
|
+
|
|
50
|
+
const bp = breakpoints ?? configuredBreakpoints ?? { mobile: 640, tablet: 1024 };
|
|
51
|
+
|
|
52
|
+
// Surface-agnostic base value, then the per-surface map (only configured
|
|
53
|
+
// surfaces). For each of the three device surfaces fall back to base so the
|
|
54
|
+
// CSS ladder always has a value to show — unconfigured surfaces == base.
|
|
55
|
+
const base = getT(locale)(key, vars);
|
|
56
|
+
const map = getSurfaces(locale, key, vars) as Partial<
|
|
57
|
+
Record<"desktop" | "mobile" | "tablet", string>
|
|
58
|
+
>;
|
|
59
|
+
const values = {
|
|
60
|
+
mobile: map.mobile ?? base,
|
|
61
|
+
tablet: map.tablet ?? base,
|
|
62
|
+
desktop: map.desktop ?? base,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// AUTO-COLLAPSE: no overlay differences → one plain render.
|
|
66
|
+
const collapsed =
|
|
67
|
+
values.mobile === values.tablet && values.tablet === values.desktop;
|
|
68
|
+
|
|
69
|
+
const Wrapper = as as any;
|
|
70
|
+
|
|
71
|
+
// Non-overlapping min/max ladder: < mobile → mobile; [mobile, tablet) →
|
|
72
|
+
// tablet; >= tablet → desktop. (.02px guard avoids fractional-width gaps.)
|
|
73
|
+
const css = collapsed
|
|
74
|
+
? ""
|
|
75
|
+
: [
|
|
76
|
+
`@media (max-width:${bp.mobile - 0.02}px){[data-sonenta-surface="tablet"],[data-sonenta-surface="desktop"]{display:none}}`,
|
|
77
|
+
`@media (min-width:${bp.mobile}px) and (max-width:${bp.tablet - 0.02}px){[data-sonenta-surface="mobile"],[data-sonenta-surface="desktop"]{display:none}}`,
|
|
78
|
+
`@media (min-width:${bp.tablet}px){[data-sonenta-surface="mobile"],[data-sonenta-surface="tablet"]{display:none}}`,
|
|
79
|
+
].join("");
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
{
|
|
83
|
+
collapsed ? (
|
|
84
|
+
<Wrapper class={className}>{base}</Wrapper>
|
|
85
|
+
) : (
|
|
86
|
+
<>
|
|
87
|
+
<Wrapper class={className} data-sonenta-surface="mobile">{values.mobile}</Wrapper>
|
|
88
|
+
<Wrapper class={className} data-sonenta-surface="tablet">{values.tablet}</Wrapper>
|
|
89
|
+
<Wrapper class={className} data-sonenta-surface="desktop">{values.desktop}</Wrapper>
|
|
90
|
+
<style set:html={css} />
|
|
91
|
+
</>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
var DEFAULT_CDN_BASE = "https://cdn.sonenta.com";
|
|
3
3
|
var DEFAULT_VERSION = "main";
|
|
4
4
|
var DEFAULT_NAMESPACE = "common";
|
|
5
|
+
var DEFAULT_SURFACE_BREAKPOINTS = {
|
|
6
|
+
mobile: 640,
|
|
7
|
+
tablet: 1024
|
|
8
|
+
};
|
|
9
|
+
function surfaceForWidth(width, breakpoints = DEFAULT_SURFACE_BREAKPOINTS) {
|
|
10
|
+
if (width < breakpoints.mobile) return "mobile";
|
|
11
|
+
if (width < breakpoints.tablet) return "tablet";
|
|
12
|
+
return "desktop";
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
// src/cdn.ts
|
|
7
16
|
function trimSlashes(s) {
|
|
@@ -16,6 +25,11 @@ function buildBundleUrl(opts, locale, namespace) {
|
|
|
16
25
|
const version = opts.version ?? DEFAULT_VERSION;
|
|
17
26
|
return `${base}/p/${opts.project}/${version}/latest/${locale}/${namespace}.json`;
|
|
18
27
|
}
|
|
28
|
+
function buildOverlayUrl(opts, locale, namespace, surface) {
|
|
29
|
+
const base = resolveCdnBase(opts);
|
|
30
|
+
const version = opts.version ?? DEFAULT_VERSION;
|
|
31
|
+
return `${base}/p/${opts.project}/${version}/latest/${locale}/${namespace}.${surface}.json`;
|
|
32
|
+
}
|
|
19
33
|
async function fetchBundle(opts, locale, namespace) {
|
|
20
34
|
const url = buildBundleUrl(opts, locale, namespace);
|
|
21
35
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
@@ -53,6 +67,35 @@ async function fetchAll(opts, locales, namespaces) {
|
|
|
53
67
|
);
|
|
54
68
|
return out;
|
|
55
69
|
}
|
|
70
|
+
async function fetchOverlay(opts, locale, namespace, surface) {
|
|
71
|
+
const url = buildOverlayUrl(opts, locale, namespace, surface);
|
|
72
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
73
|
+
try {
|
|
74
|
+
const res = await doFetch(url, { method: "GET" });
|
|
75
|
+
if (!res.ok) return null;
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
return data && typeof data === "object" ? data : null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function fetchAllOverlays(opts, locales, namespaces, surfaces) {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const l of locales) {
|
|
85
|
+
out[l] = {};
|
|
86
|
+
for (const ns of namespaces) out[l][ns] = {};
|
|
87
|
+
}
|
|
88
|
+
await Promise.all(
|
|
89
|
+
locales.flatMap(
|
|
90
|
+
(l) => namespaces.flatMap(
|
|
91
|
+
(ns) => surfaces.map(async (s) => {
|
|
92
|
+
out[l][ns][s] = await fetchOverlay(opts, l, ns, s);
|
|
93
|
+
})
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
56
99
|
|
|
57
100
|
// src/resolver.ts
|
|
58
101
|
function normalizeFallback(fallbackLng, defaultLocale) {
|
|
@@ -76,6 +119,16 @@ function walk(bundle, path) {
|
|
|
76
119
|
}
|
|
77
120
|
return cursor;
|
|
78
121
|
}
|
|
122
|
+
function unwrapValue(node) {
|
|
123
|
+
if (node && typeof node === "object" && Object.prototype.hasOwnProperty.call(node, "$value")) {
|
|
124
|
+
return node.$value;
|
|
125
|
+
}
|
|
126
|
+
return node;
|
|
127
|
+
}
|
|
128
|
+
function resolveLeaf(bundle, path) {
|
|
129
|
+
const v = unwrapValue(walk(bundle, path));
|
|
130
|
+
return typeof v === "string" ? v : void 0;
|
|
131
|
+
}
|
|
79
132
|
function interpolate(template, vars) {
|
|
80
133
|
if (!vars) return template;
|
|
81
134
|
return template.replace(
|
|
@@ -83,16 +136,30 @@ function interpolate(template, vars) {
|
|
|
83
136
|
(_, name) => name in vars ? String(vars[name]) : `{${name}}`
|
|
84
137
|
);
|
|
85
138
|
}
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
139
|
+
function resolutionOrder(locale, fallbackChain) {
|
|
140
|
+
return [locale, ...fallbackChain.filter((l) => l !== locale)];
|
|
141
|
+
}
|
|
142
|
+
function createT(data, overlays, locale, fallbackChain, defaultNamespace) {
|
|
143
|
+
const order = resolutionOrder(locale, fallbackChain);
|
|
144
|
+
const t = function t2(key, vars) {
|
|
89
145
|
const [ns, rest] = splitKey(key, defaultNamespace);
|
|
90
146
|
for (const l of order) {
|
|
91
|
-
const hit =
|
|
92
|
-
if (
|
|
147
|
+
const hit = resolveLeaf(data[l]?.[ns], rest);
|
|
148
|
+
if (hit !== void 0) return interpolate(hit, vars);
|
|
93
149
|
}
|
|
94
150
|
return key;
|
|
95
151
|
};
|
|
152
|
+
t.surface = function surface(key, surface, vars) {
|
|
153
|
+
const [ns, rest] = splitKey(key, defaultNamespace);
|
|
154
|
+
for (const l of order) {
|
|
155
|
+
const overlayHit = resolveLeaf(overlays[l]?.[ns]?.[surface], rest);
|
|
156
|
+
if (overlayHit !== void 0) return interpolate(overlayHit, vars);
|
|
157
|
+
const baseHit = resolveLeaf(data[l]?.[ns], rest);
|
|
158
|
+
if (baseHit !== void 0) return interpolate(baseHit, vars);
|
|
159
|
+
}
|
|
160
|
+
return key;
|
|
161
|
+
};
|
|
162
|
+
return t;
|
|
96
163
|
}
|
|
97
164
|
async function createSonentaI18n(options) {
|
|
98
165
|
if (!options.project) {
|
|
@@ -106,12 +173,23 @@ async function createSonentaI18n(options) {
|
|
|
106
173
|
const namespaces = options.namespaces?.length ? options.namespaces : [DEFAULT_NAMESPACE];
|
|
107
174
|
const defaultNamespace = namespaces[0];
|
|
108
175
|
const fallbackChain = normalizeFallback(options.fallbackLng, defaultLocale);
|
|
176
|
+
const surfaces = options.surfaces ?? [];
|
|
177
|
+
const surfaceBreakpoints = options.surfaceBreakpoints ?? DEFAULT_SURFACE_BREAKPOINTS;
|
|
109
178
|
const data = await fetchAll(options, locales, namespaces);
|
|
179
|
+
const overlays = surfaces.length ? await fetchAllOverlays(options, locales, namespaces, surfaces) : {};
|
|
110
180
|
return {
|
|
111
181
|
locales,
|
|
112
182
|
defaultLocale,
|
|
183
|
+
surfaces,
|
|
184
|
+
surfaceBreakpoints,
|
|
113
185
|
getT(locale) {
|
|
114
|
-
return createT(data, locale, fallbackChain, defaultNamespace);
|
|
186
|
+
return createT(data, overlays, locale, fallbackChain, defaultNamespace);
|
|
187
|
+
},
|
|
188
|
+
getSurfaces(locale, key, vars) {
|
|
189
|
+
const t = createT(data, overlays, locale, fallbackChain, defaultNamespace);
|
|
190
|
+
const out = {};
|
|
191
|
+
for (const s of surfaces) out[s] = t.surface(key, s, vars);
|
|
192
|
+
return out;
|
|
115
193
|
},
|
|
116
194
|
getCatalog(locale, namespace) {
|
|
117
195
|
return data[locale]?.[namespace ?? defaultNamespace] ?? null;
|
|
@@ -120,12 +198,17 @@ async function createSonentaI18n(options) {
|
|
|
120
198
|
}
|
|
121
199
|
|
|
122
200
|
export {
|
|
201
|
+
DEFAULT_SURFACE_BREAKPOINTS,
|
|
202
|
+
surfaceForWidth,
|
|
123
203
|
resolveCdnBase,
|
|
124
204
|
buildBundleUrl,
|
|
205
|
+
buildOverlayUrl,
|
|
125
206
|
fetchBundle,
|
|
126
207
|
fetchNamespace,
|
|
127
208
|
fetchAll,
|
|
209
|
+
fetchOverlay,
|
|
210
|
+
fetchAllOverlays,
|
|
128
211
|
createT,
|
|
129
212
|
createSonentaI18n
|
|
130
213
|
};
|
|
131
|
-
//# sourceMappingURL=chunk-
|
|
214
|
+
//# sourceMappingURL=chunk-RG6KTECT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/cdn.ts","../src/resolver.ts"],"sourcesContent":["/**\n * Public types for `@sonenta/astro` v0.1 (standalone, pre-core).\n *\n * FORWARD-COMPATIBILITY CONTRACT (frozen day-one — see CONTRACT.md): every\n * option key here is the SAME key the future `@sonenta/i18n-core`-backed\n * release will consume, so the later refactor (0.2.0) is a non-breaking,\n * additive internal swap. New capabilities (surfaces, a11y accessors, CLDR\n * plurals, variants) arrive ADDITIVELY; nothing here changes meaning.\n */\n\n/** A BCP-47 locale code, e.g. `\"fr\"`, `\"en\"`, `\"fr-CA\"`. */\nexport type Locale = string;\n\n/** A bundle namespace (the `{ns}.json` file on the CDN), e.g. `\"common\"`. */\nexport type Namespace = string;\n\n/** Interpolation variables for a `t()` call: `t(\"greeting\", { name: \"Ada\" })`. */\nexport type Vars = Record<string, string | number>;\n\n/**\n * A device surface — the second resolution dimension layered on top of the\n * locale chain (#911). The base bundle applies to every surface; a sparse\n * `{ns}.{surface}.json` overlay overrides individual keys for that surface.\n */\nexport type Surface = \"desktop\" | \"mobile\" | \"tablet\";\n\n/**\n * Min-width (px) thresholds mapping a viewport to a surface (mobile-first\n * ladder, mirrors `@sonenta/react-i18next` #913): width `< mobile` → mobile;\n * `< tablet` → tablet; otherwise desktop. Drives the `<SurfaceText>` media\n * queries (SSG renders every surface; CSS reveals the right one).\n */\nexport interface SurfaceBreakpoints {\n /** Upper bound (exclusive) of the `mobile` surface, px. Default 640. */\n mobile: number;\n /** Upper bound (exclusive) of the `tablet` surface, px. Default 1024. */\n tablet: number;\n}\n\n/**\n * A resolved, per-locale translation function. Calling `t(key, vars?)` returns\n * the surface-agnostic base value. `t.surface(key, surface, vars?)` returns the\n * value for a specific device surface (overlay ?? base).\n */\nexport interface TFn {\n (key: string, vars?: Vars): string;\n /** Resolve `key` for a specific device `surface` (overlay wins over base). */\n surface(key: string, surface: Surface, vars?: Vars): string;\n}\n\n/**\n * Configuration for the Sonenta Astro integration / runtime.\n *\n * Bundles are fetched at BUILD time from\n * `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`\n * (the canonical Sonenta CDN layout, identical to `@sonenta/react-i18next`).\n */\nexport interface SonentaI18nOptions {\n /** Project UUID from your Sonenta dashboard. Required. */\n project: string;\n\n /**\n * Released version slug or pinned content hash. Default `\"main\"`.\n * The CDN serves the latest release of this version under `/latest/`.\n */\n version?: string;\n\n /**\n * Locales to fetch and freeze into the static build. Required, non-empty.\n * A locale whose bundle 404s (e.g. a plan-limit-blocked language) is kept\n * as `null` and transparently falls back to the source language.\n */\n locales: Locale[];\n\n /**\n * Source / default locale. Default = `locales[0]`. Used as the implicit\n * final fallback target and as Astro's source language.\n */\n defaultLocale?: Locale;\n\n /**\n * Ordered fallback chain applied when a key is missing in the active\n * locale. Default = `[defaultLocale]`. v0.1 applies these locales in\n * order; BCP-47 variant→base inheritance (`fr-CA → fr`) is deliberately\n * left to the core-backed release.\n */\n fallbackLng?: Locale | Locale[];\n\n /**\n * Namespaces (bundle files) to fetch. Default `[\"common\"]`. The first\n * entry is the default namespace for un-prefixed keys; address others with\n * the i18next-style `\"ns:key\"` syntax.\n */\n namespaces?: Namespace[];\n\n /**\n * CDN host root for translation bundles, WITHOUT the `/p` segment.\n * Default `\"https://cdn.sonenta.com\"`. Overridable via the\n * `SONENTA_CDN_BASE` env var (also a bare host; for local dev point it at\n * your translation CDN). The `/p/{project}/{version}/latest/...` path is\n * appended by the loader.\n */\n cdnBase?: string;\n\n /**\n * API host root. Reserved for forward-compatibility (the core-backed\n * release uses it for the public language manifest and dev-mode runtime\n * fetch). Default `\"https://api.sonenta.dev\"`. Unused by v0.1's prod\n * build-time path.\n */\n apiBase?: string;\n\n /**\n * Device surfaces to fetch + expose (#911). When set (e.g.\n * `[\"desktop\", \"mobile\"]`), the build also pulls each sparse overlay\n * `{ns}.{surface}.json` and `getSurfaces` / `t.surface` / `<SurfaceText>`\n * resolve per-surface values. Omit to disable surfaces entirely (no overlay\n * fetch, identical to v0.1 behaviour).\n */\n surfaces?: Surface[];\n\n /**\n * Breakpoint ladder for `<SurfaceText>`'s responsive CSS. Default\n * `{ mobile: 640, tablet: 1024 }`. Resolution itself renders every surface;\n * these only decide which one CSS reveals at each viewport width.\n */\n surfaceBreakpoints?: SurfaceBreakpoints;\n\n /**\n * Injectable `fetch` implementation (testing / proxies / custom agents).\n * Defaults to the global `fetch`.\n */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * The resolved Sonenta i18n handle returned by `createSonentaI18n` and\n * exposed through the `sonenta:i18n` virtual module.\n */\nexport interface SonentaI18n {\n /** Build a translation function bound to `locale`. */\n getT(locale: Locale): TFn;\n /** The locales that were fetched (the configured `locales`). */\n readonly locales: Locale[];\n /** The resolved source/default locale. */\n readonly defaultLocale: Locale;\n /** The configured device surfaces (empty when surfaces are disabled). */\n readonly surfaces: Surface[];\n /** The resolved surface breakpoint ladder. */\n readonly surfaceBreakpoints: SurfaceBreakpoints;\n /**\n * Resolve `key` for every configured surface at `locale`, e.g.\n * `{ desktop: \"Commencer gratuitement\", mobile: \"Commencer\" }`. A surface\n * without an overlay for the key resolves to the base value. Empty when\n * surfaces are disabled.\n */\n getSurfaces(locale: Locale, key: string, vars?: Vars): Record<Surface, string>;\n /**\n * Raw fetched dictionary for `locale` / `namespace` (default namespace when\n * omitted), or `null` when that bundle was absent (404 / plan-limit).\n */\n getCatalog(locale: Locale, namespace?: Namespace): Record<string, unknown> | null;\n}\n\n/** Default CDN host (no `/p`). */\nexport const DEFAULT_CDN_BASE = \"https://cdn.sonenta.com\";\n/** Default API host. */\nexport const DEFAULT_API_BASE = \"https://api.sonenta.dev\";\n/** Default version slug. */\nexport const DEFAULT_VERSION = \"main\";\n/** Default namespace. */\nexport const DEFAULT_NAMESPACE = \"common\";\n/** Default surface breakpoint ladder (mirrors `@sonenta/react-i18next` #913). */\nexport const DEFAULT_SURFACE_BREAKPOINTS: SurfaceBreakpoints = {\n mobile: 640,\n tablet: 1024,\n};\n\n/**\n * Map a viewport width (px) to a {@link Surface} using `breakpoints`. Pure —\n * shared with the `<SurfaceText>` CSS generation so the runtime mapping and\n * the media queries agree. `< mobile` → mobile, `< tablet` → tablet, else\n * desktop.\n */\nexport function surfaceForWidth(\n width: number,\n breakpoints: SurfaceBreakpoints = DEFAULT_SURFACE_BREAKPOINTS,\n): Surface {\n if (width < breakpoints.mobile) return \"mobile\";\n if (width < breakpoints.tablet) return \"tablet\";\n return \"desktop\";\n}\n","/**\n * Build-time CDN loader for Sonenta translation bundles.\n *\n * Mirrors the canonical Sonenta CDN layout used by `@sonenta/react-i18next`:\n * `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`\n *\n * Pure data fetch — no DOM, no React, no runtime. Called during `astro build`\n * so the strings inline into the static HTML (zero client JS).\n */\n\nimport {\n DEFAULT_CDN_BASE,\n DEFAULT_VERSION,\n type Locale,\n type Namespace,\n type SonentaI18nOptions,\n type Surface,\n} from \"./types\";\n\n/** A fetched bundle (a flat or nested dictionary of message strings). */\nexport type Bundle = Record<string, unknown>;\n\n/** Strip trailing slashes so URL joins never double up. */\nfunction trimSlashes(s: string): string {\n return s.replace(/\\/+$/, \"\");\n}\n\n/**\n * Resolve the effective CDN host (no `/p`): explicit option wins, then the\n * `SONENTA_CDN_BASE` env var, then the production default.\n */\nexport function resolveCdnBase(opts: Pick<SonentaI18nOptions, \"cdnBase\">): string {\n const env =\n typeof process !== \"undefined\" ? process.env?.SONENTA_CDN_BASE : undefined;\n return trimSlashes(opts.cdnBase ?? env ?? DEFAULT_CDN_BASE);\n}\n\n/**\n * Build the bundle URL for one `(locale, namespace)` pair.\n * `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`\n */\nexport function buildBundleUrl(\n opts: Pick<SonentaI18nOptions, \"project\" | \"version\" | \"cdnBase\">,\n locale: Locale,\n namespace: Namespace,\n): string {\n const base = resolveCdnBase(opts);\n const version = opts.version ?? DEFAULT_VERSION;\n return `${base}/p/${opts.project}/${version}/latest/${locale}/${namespace}.json`;\n}\n\n/**\n * Build the sparse surface-overlay URL for one `(locale, namespace, surface)`,\n * mirroring `@sonenta/react-i18next` #913:\n * `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.{surface}.json`\n */\nexport function buildOverlayUrl(\n opts: Pick<SonentaI18nOptions, \"project\" | \"version\" | \"cdnBase\">,\n locale: Locale,\n namespace: Namespace,\n surface: Surface,\n): string {\n const base = resolveCdnBase(opts);\n const version = opts.version ?? DEFAULT_VERSION;\n return `${base}/p/${opts.project}/${version}/latest/${locale}/${namespace}.${surface}.json`;\n}\n\n/**\n * Fetch a single bundle. Resolves to `null` on 404 (locale absent from the\n * project — e.g. a plan-limit-blocked language) so callers fall back to the\n * source language. Any other non-OK status or transport error throws so a\n * misconfigured build fails loudly rather than silently shipping empty pages.\n */\nexport async function fetchBundle(\n opts: Pick<SonentaI18nOptions, \"project\" | \"version\" | \"cdnBase\" | \"fetchImpl\">,\n locale: Locale,\n namespace: Namespace,\n): Promise<Bundle | null> {\n const url = buildBundleUrl(opts, locale, namespace);\n const doFetch = opts.fetchImpl ?? fetch;\n let res: Response;\n try {\n res = await doFetch(url, { method: \"GET\" });\n } catch (e) {\n throw new Error(\n `[@sonenta/astro] CDN fetch failed for ${locale}/${namespace}: ${\n (e as Error).message\n }`,\n );\n }\n if (!res.ok) {\n if (res.status === 404) return null;\n throw new Error(`[@sonenta/astro] CDN ${res.status} on ${url}`);\n }\n return (await res.json()) as Bundle;\n}\n\n/**\n * Fetch one namespace across every locale, in parallel. Absent locales map to\n * `null`. Returns `Record<Locale, Bundle | null>`.\n */\nexport async function fetchNamespace(\n opts: Pick<\n SonentaI18nOptions,\n \"project\" | \"version\" | \"cdnBase\" | \"fetchImpl\"\n >,\n locales: Locale[],\n namespace: Namespace,\n): Promise<Record<Locale, Bundle | null>> {\n const entries = await Promise.all(\n locales.map(\n async (l) => [l, await fetchBundle(opts, l, namespace)] as const,\n ),\n );\n return Object.fromEntries(entries) as Record<Locale, Bundle | null>;\n}\n\n/**\n * Fetch every `(locale, namespace)` pair. Returns a nested map keyed by\n * locale then namespace: `data[locale][namespace] = Bundle | null`.\n */\nexport async function fetchAll(\n opts: Pick<\n SonentaI18nOptions,\n \"project\" | \"version\" | \"cdnBase\" | \"fetchImpl\"\n >,\n locales: Locale[],\n namespaces: Namespace[],\n): Promise<Record<Locale, Record<Namespace, Bundle | null>>> {\n const out: Record<Locale, Record<Namespace, Bundle | null>> = {};\n for (const l of locales) out[l] = {};\n await Promise.all(\n locales.flatMap((l) =>\n namespaces.map(async (ns) => {\n out[l]![ns] = await fetchBundle(opts, l, ns);\n }),\n ),\n );\n return out;\n}\n\n/**\n * Fetch a single sparse surface overlay. Unlike {@link fetchBundle}, an\n * overlay is OPTIONAL: any miss (404), non-OK status, or transport error\n * resolves to `null` (base-only) and NEVER throws — a missing/erroring overlay\n * must never break the build or downgrade the base render. Mirrors\n * `@sonenta/react-i18next` #913's `{}`-on-miss behaviour.\n */\nexport async function fetchOverlay(\n opts: Pick<SonentaI18nOptions, \"project\" | \"version\" | \"cdnBase\" | \"fetchImpl\">,\n locale: Locale,\n namespace: Namespace,\n surface: Surface,\n): Promise<Bundle | null> {\n const url = buildOverlayUrl(opts, locale, namespace, surface);\n const doFetch = opts.fetchImpl ?? fetch;\n try {\n const res = await doFetch(url, { method: \"GET\" });\n if (!res.ok) return null;\n const data = (await res.json()) as Bundle;\n return data && typeof data === \"object\" ? data : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Fetch every `(locale, namespace, surface)` overlay. Returns a nested map:\n * `overlays[locale][namespace][surface] = Bundle | null`. Empty when\n * `surfaces` is empty.\n */\nexport async function fetchAllOverlays(\n opts: Pick<\n SonentaI18nOptions,\n \"project\" | \"version\" | \"cdnBase\" | \"fetchImpl\"\n >,\n locales: Locale[],\n namespaces: Namespace[],\n surfaces: Surface[],\n): Promise<Record<Locale, Record<Namespace, Record<Surface, Bundle | null>>>> {\n const out: Record<Locale, Record<Namespace, Record<Surface, Bundle | null>>> =\n {};\n for (const l of locales) {\n out[l] = {};\n for (const ns of namespaces) out[l]![ns] = {} as Record<Surface, Bundle | null>;\n }\n await Promise.all(\n locales.flatMap((l) =>\n namespaces.flatMap((ns) =>\n surfaces.map(async (s) => {\n out[l]![ns]![s] = await fetchOverlay(opts, l, ns, s);\n }),\n ),\n ),\n );\n return out;\n}\n","/**\n * Build-time translation resolver.\n *\n * v0.2 SCOPE: string resolution + `{var}` interpolation + source-language\n * fallback (v0.1) PLUS device-surface variants (#911) — a sparse\n * `{ns}.{surface}.json` overlay over the base bundle, overlay-wins-per-key,\n * resolved per locale. Still NO CLDR plurals and NO a11y surfaces: a `$value`\n * that is a plural dict (object) is treated as \"no string here\" (deferred to\n * the `@sonenta/i18n-core`-backed 0.3.0). The `$asset` half of an envelope is\n * ignored for text resolution. This keeps the surface semantics a faithful,\n * regression-locked subset of react-i18next #913 so the later core swap is\n * behaviour-preserving.\n */\n\nimport { fetchAll, fetchAllOverlays, type Bundle } from \"./cdn\";\nimport {\n DEFAULT_NAMESPACE,\n DEFAULT_SURFACE_BREAKPOINTS,\n type Locale,\n type Namespace,\n type SonentaI18n,\n type SonentaI18nOptions,\n type Surface,\n type SurfaceBreakpoints,\n type TFn,\n type Vars,\n} from \"./types\";\n\n/** Nested base data: `data[locale][namespace] = Bundle | null`. */\ntype BaseData = Record<Locale, Record<Namespace, Bundle | null>>;\n/** Nested overlays: `overlays[locale][namespace][surface] = Bundle | null`. */\ntype OverlayData = Record<\n Locale,\n Record<Namespace, Record<Surface, Bundle | null>>\n>;\n\n/** Normalize `fallbackLng` (scalar | array | undefined) into a locale list. */\nfunction normalizeFallback(\n fallbackLng: SonentaI18nOptions[\"fallbackLng\"],\n defaultLocale: Locale,\n): Locale[] {\n if (fallbackLng == null) return [defaultLocale];\n return Array.isArray(fallbackLng) ? fallbackLng : [fallbackLng];\n}\n\n/** Split an i18next-style `\"ns:key\"` into `[namespace, key]`. */\nfunction splitKey(key: string, defaultNamespace: Namespace): [Namespace, string] {\n const i = key.indexOf(\":\");\n if (i === -1) return [defaultNamespace, key];\n return [key.slice(0, i), key.slice(i + 1)];\n}\n\n/** Walk a dotted path (`\"home.title\"`) through a bundle; `undefined` on miss. */\nfunction walk(bundle: Bundle | null | undefined, path: string): unknown {\n if (!bundle) return undefined;\n let cursor: unknown = bundle;\n for (const part of path.split(\".\")) {\n if (cursor && typeof cursor === \"object\" && part in cursor) {\n cursor = (cursor as Record<string, unknown>)[part];\n } else {\n return undefined;\n }\n }\n return cursor;\n}\n\n/**\n * Strip the `{ \"$value\", \"$asset\" }` envelope (#911): a node owning `$value`\n * resolves to that `$value` (the `$asset` half is ignored for text). Plain\n * nodes pass through unchanged.\n */\nfunction unwrapValue(node: unknown): unknown {\n if (\n node &&\n typeof node === \"object\" &&\n Object.prototype.hasOwnProperty.call(node, \"$value\")\n ) {\n return (node as Record<string, unknown>).$value;\n }\n return node;\n}\n\n/** Walk + unwrap to a leaf string, or `undefined` if absent / not a string. */\nfunction resolveLeaf(bundle: Bundle | null | undefined, path: string): string | undefined {\n const v = unwrapValue(walk(bundle, path));\n return typeof v === \"string\" ? v : undefined;\n}\n\n/** Substitute `{var}` placeholders; unknown placeholders are left intact. */\nfunction interpolate(template: string, vars?: Vars): string {\n if (!vars) return template;\n return template.replace(/\\{(\\w+)\\}/g, (_, name: string) =>\n name in vars ? String(vars[name]) : `{${name}}`,\n );\n}\n\n/** De-duped resolution order: active locale first, then configured fallbacks. */\nfunction resolutionOrder(locale: Locale, fallbackChain: Locale[]): Locale[] {\n return [locale, ...fallbackChain.filter((l) => l !== locale)];\n}\n\n/**\n * Build a `t()` (with `.surface`) bound to `locale`. Base lookup: active\n * locale's namespace dict → each fallback locale's same dict → raw key.\n * Surface lookup: per locale, the overlay wins over base, then the same\n * locale-fallback walk (mirrors react-i18next #913 compose-then-fallback).\n */\nexport function createT(\n data: BaseData,\n overlays: OverlayData,\n locale: Locale,\n fallbackChain: Locale[],\n defaultNamespace: Namespace,\n): TFn {\n const order = resolutionOrder(locale, fallbackChain);\n\n const t = function t(key: string, vars?: Vars): string {\n const [ns, rest] = splitKey(key, defaultNamespace);\n for (const l of order) {\n const hit = resolveLeaf(data[l]?.[ns], rest);\n if (hit !== undefined) return interpolate(hit, vars);\n }\n return key;\n } as TFn;\n\n t.surface = function surface(key: string, surface: Surface, vars?: Vars): string {\n const [ns, rest] = splitKey(key, defaultNamespace);\n for (const l of order) {\n // Overlay wins over base WITHIN a locale; then fall to the next locale.\n const overlayHit = resolveLeaf(overlays[l]?.[ns]?.[surface], rest);\n if (overlayHit !== undefined) return interpolate(overlayHit, vars);\n const baseHit = resolveLeaf(data[l]?.[ns], rest);\n if (baseHit !== undefined) return interpolate(baseHit, vars);\n }\n return key;\n };\n\n return t;\n}\n\n/**\n * Fetch every configured bundle (and surface overlay) at build time and return\n * a resolved {@link SonentaI18n} handle. Call once (top-level `await`) and\n * reuse its `getT(locale)` / `getSurfaces(locale, key)` across pages.\n */\nexport async function createSonentaI18n(\n options: SonentaI18nOptions,\n): Promise<SonentaI18n> {\n if (!options.project) {\n throw new Error(\"[@sonenta/astro] `project` (UUID) is required.\");\n }\n if (!options.locales?.length) {\n throw new Error(\"[@sonenta/astro] `locales` must be a non-empty array.\");\n }\n const locales = options.locales;\n const defaultLocale = options.defaultLocale ?? locales[0]!;\n const namespaces =\n options.namespaces?.length ? options.namespaces : [DEFAULT_NAMESPACE];\n const defaultNamespace = namespaces[0]!;\n const fallbackChain = normalizeFallback(options.fallbackLng, defaultLocale);\n const surfaces = options.surfaces ?? [];\n const surfaceBreakpoints: SurfaceBreakpoints =\n options.surfaceBreakpoints ?? DEFAULT_SURFACE_BREAKPOINTS;\n\n const data = await fetchAll(options, locales, namespaces);\n const overlays: OverlayData = surfaces.length\n ? await fetchAllOverlays(options, locales, namespaces, surfaces)\n : ({} as OverlayData);\n\n return {\n locales,\n defaultLocale,\n surfaces,\n surfaceBreakpoints,\n getT(locale: Locale): TFn {\n return createT(data, overlays, locale, fallbackChain, defaultNamespace);\n },\n getSurfaces(locale: Locale, key: string, vars?: Vars): Record<Surface, string> {\n const t = createT(data, overlays, locale, fallbackChain, defaultNamespace);\n const out = {} as Record<Surface, string>;\n for (const s of surfaces) out[s] = t.surface(key, s, vars);\n return out;\n },\n getCatalog(locale: Locale, namespace?: Namespace): Bundle | null {\n return data[locale]?.[namespace ?? defaultNamespace] ?? null;\n },\n };\n}\n"],"mappings":";AAqKO,IAAM,mBAAmB;AAIzB,IAAM,kBAAkB;AAExB,IAAM,oBAAoB;AAE1B,IAAM,8BAAkD;AAAA,EAC7D,QAAQ;AAAA,EACR,QAAQ;AACV;AAQO,SAAS,gBACd,OACA,cAAkC,6BACzB;AACT,MAAI,QAAQ,YAAY,OAAQ,QAAO;AACvC,MAAI,QAAQ,YAAY,OAAQ,QAAO;AACvC,SAAO;AACT;;;ACxKA,SAAS,YAAY,GAAmB;AACtC,SAAO,EAAE,QAAQ,QAAQ,EAAE;AAC7B;AAMO,SAAS,eAAe,MAAmD;AAChF,QAAM,MACJ,OAAO,YAAY,cAAc,QAAQ,KAAK,mBAAmB;AACnE,SAAO,YAAY,KAAK,WAAW,OAAO,gBAAgB;AAC5D;AAMO,SAAS,eACd,MACA,QACA,WACQ;AACR,QAAM,OAAO,eAAe,IAAI;AAChC,QAAM,UAAU,KAAK,WAAW;AAChC,SAAO,GAAG,IAAI,MAAM,KAAK,OAAO,IAAI,OAAO,WAAW,MAAM,IAAI,SAAS;AAC3E;AAOO,SAAS,gBACd,MACA,QACA,WACA,SACQ;AACR,QAAM,OAAO,eAAe,IAAI;AAChC,QAAM,UAAU,KAAK,WAAW;AAChC,SAAO,GAAG,IAAI,MAAM,KAAK,OAAO,IAAI,OAAO,WAAW,MAAM,IAAI,SAAS,IAAI,OAAO;AACtF;AAQA,eAAsB,YACpB,MACA,QACA,WACwB;AACxB,QAAM,MAAM,eAAe,MAAM,QAAQ,SAAS;AAClD,QAAM,UAAU,KAAK,aAAa;AAClC,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,QAAQ,KAAK,EAAE,QAAQ,MAAM,CAAC;AAAA,EAC5C,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,yCAAyC,MAAM,IAAI,SAAS,KACzD,EAAY,OACf;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,IAAI,IAAI;AACX,QAAI,IAAI,WAAW,IAAK,QAAO;AAC/B,UAAM,IAAI,MAAM,wBAAwB,IAAI,MAAM,OAAO,GAAG,EAAE;AAAA,EAChE;AACA,SAAQ,MAAM,IAAI,KAAK;AACzB;AAMA,eAAsB,eACpB,MAIA,SACA,WACwC;AACxC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ;AAAA,MACN,OAAO,MAAM,CAAC,GAAG,MAAM,YAAY,MAAM,GAAG,SAAS,CAAC;AAAA,IACxD;AAAA,EACF;AACA,SAAO,OAAO,YAAY,OAAO;AACnC;AAMA,eAAsB,SACpB,MAIA,SACA,YAC2D;AAC3D,QAAM,MAAwD,CAAC;AAC/D,aAAW,KAAK,QAAS,KAAI,CAAC,IAAI,CAAC;AACnC,QAAM,QAAQ;AAAA,IACZ,QAAQ;AAAA,MAAQ,CAAC,MACf,WAAW,IAAI,OAAO,OAAO;AAC3B,YAAI,CAAC,EAAG,EAAE,IAAI,MAAM,YAAY,MAAM,GAAG,EAAE;AAAA,MAC7C,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;AASA,eAAsB,aACpB,MACA,QACA,WACA,SACwB;AACxB,QAAM,MAAM,gBAAgB,MAAM,QAAQ,WAAW,OAAO;AAC5D,QAAM,UAAU,KAAK,aAAa;AAClC,MAAI;AACF,UAAM,MAAM,MAAM,QAAQ,KAAK,EAAE,QAAQ,MAAM,CAAC;AAChD,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,WAAO,QAAQ,OAAO,SAAS,WAAW,OAAO;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,iBACpB,MAIA,SACA,YACA,UAC4E;AAC5E,QAAM,MACJ,CAAC;AACH,aAAW,KAAK,SAAS;AACvB,QAAI,CAAC,IAAI,CAAC;AACV,eAAW,MAAM,WAAY,KAAI,CAAC,EAAG,EAAE,IAAI,CAAC;AAAA,EAC9C;AACA,QAAM,QAAQ;AAAA,IACZ,QAAQ;AAAA,MAAQ,CAAC,MACf,WAAW;AAAA,QAAQ,CAAC,OAClB,SAAS,IAAI,OAAO,MAAM;AACxB,cAAI,CAAC,EAAG,EAAE,EAAG,CAAC,IAAI,MAAM,aAAa,MAAM,GAAG,IAAI,CAAC;AAAA,QACrD,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;AC/JA,SAAS,kBACP,aACA,eACU;AACV,MAAI,eAAe,KAAM,QAAO,CAAC,aAAa;AAC9C,SAAO,MAAM,QAAQ,WAAW,IAAI,cAAc,CAAC,WAAW;AAChE;AAGA,SAAS,SAAS,KAAa,kBAAkD;AAC/E,QAAM,IAAI,IAAI,QAAQ,GAAG;AACzB,MAAI,MAAM,GAAI,QAAO,CAAC,kBAAkB,GAAG;AAC3C,SAAO,CAAC,IAAI,MAAM,GAAG,CAAC,GAAG,IAAI,MAAM,IAAI,CAAC,CAAC;AAC3C;AAGA,SAAS,KAAK,QAAmC,MAAuB;AACtE,MAAI,CAAC,OAAQ,QAAO;AACpB,MAAI,SAAkB;AACtB,aAAW,QAAQ,KAAK,MAAM,GAAG,GAAG;AAClC,QAAI,UAAU,OAAO,WAAW,YAAY,QAAQ,QAAQ;AAC1D,eAAU,OAAmC,IAAI;AAAA,IACnD,OAAO;AACL,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAOA,SAAS,YAAY,MAAwB;AAC3C,MACE,QACA,OAAO,SAAS,YAChB,OAAO,UAAU,eAAe,KAAK,MAAM,QAAQ,GACnD;AACA,WAAQ,KAAiC;AAAA,EAC3C;AACA,SAAO;AACT;AAGA,SAAS,YAAY,QAAmC,MAAkC;AACxF,QAAM,IAAI,YAAY,KAAK,QAAQ,IAAI,CAAC;AACxC,SAAO,OAAO,MAAM,WAAW,IAAI;AACrC;AAGA,SAAS,YAAY,UAAkB,MAAqB;AAC1D,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,SAAS;AAAA,IAAQ;AAAA,IAAc,CAAC,GAAG,SACxC,QAAQ,OAAO,OAAO,KAAK,IAAI,CAAC,IAAI,IAAI,IAAI;AAAA,EAC9C;AACF;AAGA,SAAS,gBAAgB,QAAgB,eAAmC;AAC1E,SAAO,CAAC,QAAQ,GAAG,cAAc,OAAO,CAAC,MAAM,MAAM,MAAM,CAAC;AAC9D;AAQO,SAAS,QACd,MACA,UACA,QACA,eACA,kBACK;AACL,QAAM,QAAQ,gBAAgB,QAAQ,aAAa;AAEnD,QAAM,IAAI,SAASA,GAAE,KAAa,MAAqB;AACrD,UAAM,CAAC,IAAI,IAAI,IAAI,SAAS,KAAK,gBAAgB;AACjD,eAAW,KAAK,OAAO;AACrB,YAAM,MAAM,YAAY,KAAK,CAAC,IAAI,EAAE,GAAG,IAAI;AAC3C,UAAI,QAAQ,OAAW,QAAO,YAAY,KAAK,IAAI;AAAA,IACrD;AACA,WAAO;AAAA,EACT;AAEA,IAAE,UAAU,SAAS,QAAQ,KAAa,SAAkB,MAAqB;AAC/E,UAAM,CAAC,IAAI,IAAI,IAAI,SAAS,KAAK,gBAAgB;AACjD,eAAW,KAAK,OAAO;AAErB,YAAM,aAAa,YAAY,SAAS,CAAC,IAAI,EAAE,IAAI,OAAO,GAAG,IAAI;AACjE,UAAI,eAAe,OAAW,QAAO,YAAY,YAAY,IAAI;AACjE,YAAM,UAAU,YAAY,KAAK,CAAC,IAAI,EAAE,GAAG,IAAI;AAC/C,UAAI,YAAY,OAAW,QAAO,YAAY,SAAS,IAAI;AAAA,IAC7D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAOA,eAAsB,kBACpB,SACsB;AACtB,MAAI,CAAC,QAAQ,SAAS;AACpB,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AACA,MAAI,CAAC,QAAQ,SAAS,QAAQ;AAC5B,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AACA,QAAM,UAAU,QAAQ;AACxB,QAAM,gBAAgB,QAAQ,iBAAiB,QAAQ,CAAC;AACxD,QAAM,aACJ,QAAQ,YAAY,SAAS,QAAQ,aAAa,CAAC,iBAAiB;AACtE,QAAM,mBAAmB,WAAW,CAAC;AACrC,QAAM,gBAAgB,kBAAkB,QAAQ,aAAa,aAAa;AAC1E,QAAM,WAAW,QAAQ,YAAY,CAAC;AACtC,QAAM,qBACJ,QAAQ,sBAAsB;AAEhC,QAAM,OAAO,MAAM,SAAS,SAAS,SAAS,UAAU;AACxD,QAAM,WAAwB,SAAS,SACnC,MAAM,iBAAiB,SAAS,SAAS,YAAY,QAAQ,IAC5D,CAAC;AAEN,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,QAAqB;AACxB,aAAO,QAAQ,MAAM,UAAU,QAAQ,eAAe,gBAAgB;AAAA,IACxE;AAAA,IACA,YAAY,QAAgB,KAAa,MAAsC;AAC7E,YAAM,IAAI,QAAQ,MAAM,UAAU,QAAQ,eAAe,gBAAgB;AACzE,YAAM,MAAM,CAAC;AACb,iBAAW,KAAK,SAAU,KAAI,CAAC,IAAI,EAAE,QAAQ,KAAK,GAAG,IAAI;AACzD,aAAO;AAAA,IACT;AAAA,IACA,WAAW,QAAgB,WAAsC;AAC/D,aAAO,KAAK,MAAM,IAAI,aAAa,gBAAgB,KAAK;AAAA,IAC1D;AAAA,EACF;AACF;","names":["t"]}
|