@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/dist/runtime.cjs
CHANGED
|
@@ -20,13 +20,18 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/runtime.ts
|
|
21
21
|
var runtime_exports = {};
|
|
22
22
|
__export(runtime_exports, {
|
|
23
|
+
DEFAULT_SURFACE_BREAKPOINTS: () => DEFAULT_SURFACE_BREAKPOINTS,
|
|
23
24
|
buildBundleUrl: () => buildBundleUrl,
|
|
25
|
+
buildOverlayUrl: () => buildOverlayUrl,
|
|
24
26
|
createSonentaI18n: () => createSonentaI18n,
|
|
25
27
|
createT: () => createT,
|
|
26
28
|
fetchAll: () => fetchAll,
|
|
29
|
+
fetchAllOverlays: () => fetchAllOverlays,
|
|
27
30
|
fetchBundle: () => fetchBundle,
|
|
28
31
|
fetchNamespace: () => fetchNamespace,
|
|
29
|
-
|
|
32
|
+
fetchOverlay: () => fetchOverlay,
|
|
33
|
+
resolveCdnBase: () => resolveCdnBase,
|
|
34
|
+
surfaceForWidth: () => surfaceForWidth
|
|
30
35
|
});
|
|
31
36
|
module.exports = __toCommonJS(runtime_exports);
|
|
32
37
|
|
|
@@ -34,6 +39,15 @@ module.exports = __toCommonJS(runtime_exports);
|
|
|
34
39
|
var DEFAULT_CDN_BASE = "https://cdn.sonenta.com";
|
|
35
40
|
var DEFAULT_VERSION = "main";
|
|
36
41
|
var DEFAULT_NAMESPACE = "common";
|
|
42
|
+
var DEFAULT_SURFACE_BREAKPOINTS = {
|
|
43
|
+
mobile: 640,
|
|
44
|
+
tablet: 1024
|
|
45
|
+
};
|
|
46
|
+
function surfaceForWidth(width, breakpoints = DEFAULT_SURFACE_BREAKPOINTS) {
|
|
47
|
+
if (width < breakpoints.mobile) return "mobile";
|
|
48
|
+
if (width < breakpoints.tablet) return "tablet";
|
|
49
|
+
return "desktop";
|
|
50
|
+
}
|
|
37
51
|
|
|
38
52
|
// src/cdn.ts
|
|
39
53
|
function trimSlashes(s) {
|
|
@@ -48,6 +62,11 @@ function buildBundleUrl(opts, locale, namespace) {
|
|
|
48
62
|
const version = opts.version ?? DEFAULT_VERSION;
|
|
49
63
|
return `${base}/p/${opts.project}/${version}/latest/${locale}/${namespace}.json`;
|
|
50
64
|
}
|
|
65
|
+
function buildOverlayUrl(opts, locale, namespace, surface) {
|
|
66
|
+
const base = resolveCdnBase(opts);
|
|
67
|
+
const version = opts.version ?? DEFAULT_VERSION;
|
|
68
|
+
return `${base}/p/${opts.project}/${version}/latest/${locale}/${namespace}.${surface}.json`;
|
|
69
|
+
}
|
|
51
70
|
async function fetchBundle(opts, locale, namespace) {
|
|
52
71
|
const url = buildBundleUrl(opts, locale, namespace);
|
|
53
72
|
const doFetch = opts.fetchImpl ?? fetch;
|
|
@@ -85,6 +104,35 @@ async function fetchAll(opts, locales, namespaces) {
|
|
|
85
104
|
);
|
|
86
105
|
return out;
|
|
87
106
|
}
|
|
107
|
+
async function fetchOverlay(opts, locale, namespace, surface) {
|
|
108
|
+
const url = buildOverlayUrl(opts, locale, namespace, surface);
|
|
109
|
+
const doFetch = opts.fetchImpl ?? fetch;
|
|
110
|
+
try {
|
|
111
|
+
const res = await doFetch(url, { method: "GET" });
|
|
112
|
+
if (!res.ok) return null;
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
return data && typeof data === "object" ? data : null;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function fetchAllOverlays(opts, locales, namespaces, surfaces) {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const l of locales) {
|
|
122
|
+
out[l] = {};
|
|
123
|
+
for (const ns of namespaces) out[l][ns] = {};
|
|
124
|
+
}
|
|
125
|
+
await Promise.all(
|
|
126
|
+
locales.flatMap(
|
|
127
|
+
(l) => namespaces.flatMap(
|
|
128
|
+
(ns) => surfaces.map(async (s) => {
|
|
129
|
+
out[l][ns][s] = await fetchOverlay(opts, l, ns, s);
|
|
130
|
+
})
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
88
136
|
|
|
89
137
|
// src/resolver.ts
|
|
90
138
|
function normalizeFallback(fallbackLng, defaultLocale) {
|
|
@@ -108,6 +156,16 @@ function walk(bundle, path) {
|
|
|
108
156
|
}
|
|
109
157
|
return cursor;
|
|
110
158
|
}
|
|
159
|
+
function unwrapValue(node) {
|
|
160
|
+
if (node && typeof node === "object" && Object.prototype.hasOwnProperty.call(node, "$value")) {
|
|
161
|
+
return node.$value;
|
|
162
|
+
}
|
|
163
|
+
return node;
|
|
164
|
+
}
|
|
165
|
+
function resolveLeaf(bundle, path) {
|
|
166
|
+
const v = unwrapValue(walk(bundle, path));
|
|
167
|
+
return typeof v === "string" ? v : void 0;
|
|
168
|
+
}
|
|
111
169
|
function interpolate(template, vars) {
|
|
112
170
|
if (!vars) return template;
|
|
113
171
|
return template.replace(
|
|
@@ -115,16 +173,30 @@ function interpolate(template, vars) {
|
|
|
115
173
|
(_, name) => name in vars ? String(vars[name]) : `{${name}}`
|
|
116
174
|
);
|
|
117
175
|
}
|
|
118
|
-
function
|
|
119
|
-
|
|
120
|
-
|
|
176
|
+
function resolutionOrder(locale, fallbackChain) {
|
|
177
|
+
return [locale, ...fallbackChain.filter((l) => l !== locale)];
|
|
178
|
+
}
|
|
179
|
+
function createT(data, overlays, locale, fallbackChain, defaultNamespace) {
|
|
180
|
+
const order = resolutionOrder(locale, fallbackChain);
|
|
181
|
+
const t = function t2(key, vars) {
|
|
121
182
|
const [ns, rest] = splitKey(key, defaultNamespace);
|
|
122
183
|
for (const l of order) {
|
|
123
|
-
const hit =
|
|
124
|
-
if (
|
|
184
|
+
const hit = resolveLeaf(data[l]?.[ns], rest);
|
|
185
|
+
if (hit !== void 0) return interpolate(hit, vars);
|
|
125
186
|
}
|
|
126
187
|
return key;
|
|
127
188
|
};
|
|
189
|
+
t.surface = function surface(key, surface, vars) {
|
|
190
|
+
const [ns, rest] = splitKey(key, defaultNamespace);
|
|
191
|
+
for (const l of order) {
|
|
192
|
+
const overlayHit = resolveLeaf(overlays[l]?.[ns]?.[surface], rest);
|
|
193
|
+
if (overlayHit !== void 0) return interpolate(overlayHit, vars);
|
|
194
|
+
const baseHit = resolveLeaf(data[l]?.[ns], rest);
|
|
195
|
+
if (baseHit !== void 0) return interpolate(baseHit, vars);
|
|
196
|
+
}
|
|
197
|
+
return key;
|
|
198
|
+
};
|
|
199
|
+
return t;
|
|
128
200
|
}
|
|
129
201
|
async function createSonentaI18n(options) {
|
|
130
202
|
if (!options.project) {
|
|
@@ -138,12 +210,23 @@ async function createSonentaI18n(options) {
|
|
|
138
210
|
const namespaces = options.namespaces?.length ? options.namespaces : [DEFAULT_NAMESPACE];
|
|
139
211
|
const defaultNamespace = namespaces[0];
|
|
140
212
|
const fallbackChain = normalizeFallback(options.fallbackLng, defaultLocale);
|
|
213
|
+
const surfaces = options.surfaces ?? [];
|
|
214
|
+
const surfaceBreakpoints = options.surfaceBreakpoints ?? DEFAULT_SURFACE_BREAKPOINTS;
|
|
141
215
|
const data = await fetchAll(options, locales, namespaces);
|
|
216
|
+
const overlays = surfaces.length ? await fetchAllOverlays(options, locales, namespaces, surfaces) : {};
|
|
142
217
|
return {
|
|
143
218
|
locales,
|
|
144
219
|
defaultLocale,
|
|
220
|
+
surfaces,
|
|
221
|
+
surfaceBreakpoints,
|
|
145
222
|
getT(locale) {
|
|
146
|
-
return createT(data, locale, fallbackChain, defaultNamespace);
|
|
223
|
+
return createT(data, overlays, locale, fallbackChain, defaultNamespace);
|
|
224
|
+
},
|
|
225
|
+
getSurfaces(locale, key, vars) {
|
|
226
|
+
const t = createT(data, overlays, locale, fallbackChain, defaultNamespace);
|
|
227
|
+
const out = {};
|
|
228
|
+
for (const s of surfaces) out[s] = t.surface(key, s, vars);
|
|
229
|
+
return out;
|
|
147
230
|
},
|
|
148
231
|
getCatalog(locale, namespace) {
|
|
149
232
|
return data[locale]?.[namespace ?? defaultNamespace] ?? null;
|
|
@@ -152,12 +235,17 @@ async function createSonentaI18n(options) {
|
|
|
152
235
|
}
|
|
153
236
|
// Annotate the CommonJS export names for ESM import in node:
|
|
154
237
|
0 && (module.exports = {
|
|
238
|
+
DEFAULT_SURFACE_BREAKPOINTS,
|
|
155
239
|
buildBundleUrl,
|
|
240
|
+
buildOverlayUrl,
|
|
156
241
|
createSonentaI18n,
|
|
157
242
|
createT,
|
|
158
243
|
fetchAll,
|
|
244
|
+
fetchAllOverlays,
|
|
159
245
|
fetchBundle,
|
|
160
246
|
fetchNamespace,
|
|
161
|
-
|
|
247
|
+
fetchOverlay,
|
|
248
|
+
resolveCdnBase,
|
|
249
|
+
surfaceForWidth
|
|
162
250
|
});
|
|
163
251
|
//# sourceMappingURL=runtime.cjs.map
|
package/dist/runtime.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime.ts","../src/types.ts","../src/cdn.ts","../src/resolver.ts"],"sourcesContent":["/**\n * `@sonenta/astro/runtime` — the framework-agnostic build-time resolver.\n *\n * Two ways to use it:\n * 1. Via the integration (default export of `@sonenta/astro`): the generated\n * `sonenta:i18n` virtual module imports `createSonentaI18n` from here.\n * 2. Directly, if you prefer the explicit website-style top-level `await`\n * without the integration:\n *\n * // src/i18n.ts\n * import { createSonentaI18n } from \"@sonenta/astro/runtime\";\n * export const i18n = await createSonentaI18n({ project, locales });\n * export const getT = i18n.getT;\n */\n\nexport {\n createSonentaI18n,\n createT,\n} from \"./resolver\";\n\nexport {\n buildBundleUrl,\n fetchBundle,\n fetchNamespace,\n fetchAll,\n resolveCdnBase,\n type Bundle,\n} from \"./cdn\";\n\nexport type {\n Locale,\n Namespace,\n Vars,\n TFn,\n SonentaI18n,\n SonentaI18nOptions,\n} from \"./types\";\n","/**\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/** A resolved, per-locale translation function. */\nexport type TFn = (key: string, vars?: Vars) => string;\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 * 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 /**\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","/**\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} 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 * 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 * Build-time translation resolver.\n *\n * v0.1 SCOPE (frozen — see CONTRACT.md): string resolution + `{var}`\n * interpolation + source-language fallback ONLY. Deliberately NO CLDR\n * plurals, NO surfaces, NO a11y accessors, NO BCP-47 variant→base\n * inheritance — those carry real i18n SEMANTICS owned by `@sonenta/i18n-core`\n * and arrive additively in 0.2.0. Keeping them out of v0.1 means there is no\n * naive semantics to break when the core swaps in underneath this same API.\n */\n\nimport { fetchAll, type Bundle } from \"./cdn\";\nimport {\n DEFAULT_NAMESPACE,\n type Locale,\n type Namespace,\n type SonentaI18n,\n type SonentaI18nOptions,\n type TFn,\n type Vars,\n} from \"./types\";\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/** 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/**\n * Build a `t()` bound to `locale`, given the fetched data and resolution\n * order. Lookup: active locale's namespace dict → each fallback locale's same\n * namespace dict → the raw key (i18next-parity missing-key behaviour).\n */\nexport function createT(\n data: Record<Locale, Record<Namespace, Bundle | null>>,\n locale: Locale,\n fallbackChain: Locale[],\n defaultNamespace: Namespace,\n): TFn {\n // De-duped resolution order: active locale first, then configured\n // fallbacks (skipping the active one if it reappears).\n const order = [locale, ...fallbackChain.filter((l) => l !== locale)];\n return function t(key: string, vars?: Vars): string {\n const [ns, rest] = splitKey(key, defaultNamespace);\n for (const l of order) {\n const hit = walk(data[l]?.[ns], rest);\n if (typeof hit === \"string\") return interpolate(hit, vars);\n }\n // Missing everywhere → return the raw key (unchanged), i18next-style.\n return key;\n };\n}\n\n/**\n * Fetch every configured bundle at build time and return a resolved\n * {@link SonentaI18n} handle. Call once (top-level `await`) and reuse its\n * `getT(locale)` 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\n const data = await fetchAll(options, locales, namespaces);\n\n return {\n locales,\n defaultLocale,\n getT(locale: Locale): TFn {\n return createT(data, locale, fallbackChain, defaultNamespace);\n },\n getCatalog(locale: Locale, namespace?: Namespace): Bundle | null {\n return data[locale]?.[namespace ?? defaultNamespace] ?? null;\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/runtime.ts","../src/types.ts","../src/cdn.ts","../src/resolver.ts"],"sourcesContent":["/**\n * `@sonenta/astro/runtime` — the framework-agnostic build-time resolver.\n *\n * Two ways to use it:\n * 1. Via the integration (default export of `@sonenta/astro`): the generated\n * `sonenta:i18n` virtual module imports `createSonentaI18n` from here.\n * 2. Directly, if you prefer the explicit website-style top-level `await`\n * without the integration:\n *\n * // src/i18n.ts\n * import { createSonentaI18n } from \"@sonenta/astro/runtime\";\n * export const i18n = await createSonentaI18n({ project, locales });\n * export const getT = i18n.getT;\n */\n\nexport {\n createSonentaI18n,\n createT,\n} from \"./resolver\";\n\nexport {\n buildBundleUrl,\n buildOverlayUrl,\n fetchBundle,\n fetchNamespace,\n fetchAll,\n fetchOverlay,\n fetchAllOverlays,\n resolveCdnBase,\n type Bundle,\n} from \"./cdn\";\n\nexport {\n DEFAULT_SURFACE_BREAKPOINTS,\n surfaceForWidth,\n} from \"./types\";\n\nexport type {\n Locale,\n Namespace,\n Vars,\n TFn,\n Surface,\n SurfaceBreakpoints,\n SonentaI18n,\n SonentaI18nOptions,\n} from \"./types\";\n","/**\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":"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","names":["t"]}
|
package/dist/runtime.d.cts
CHANGED
|
@@ -13,8 +13,34 @@ type Locale = string;
|
|
|
13
13
|
type Namespace = string;
|
|
14
14
|
/** Interpolation variables for a `t()` call: `t("greeting", { name: "Ada" })`. */
|
|
15
15
|
type Vars = Record<string, string | number>;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A device surface — the second resolution dimension layered on top of the
|
|
18
|
+
* locale chain (#911). The base bundle applies to every surface; a sparse
|
|
19
|
+
* `{ns}.{surface}.json` overlay overrides individual keys for that surface.
|
|
20
|
+
*/
|
|
21
|
+
type Surface = "desktop" | "mobile" | "tablet";
|
|
22
|
+
/**
|
|
23
|
+
* Min-width (px) thresholds mapping a viewport to a surface (mobile-first
|
|
24
|
+
* ladder, mirrors `@sonenta/react-i18next` #913): width `< mobile` → mobile;
|
|
25
|
+
* `< tablet` → tablet; otherwise desktop. Drives the `<SurfaceText>` media
|
|
26
|
+
* queries (SSG renders every surface; CSS reveals the right one).
|
|
27
|
+
*/
|
|
28
|
+
interface SurfaceBreakpoints {
|
|
29
|
+
/** Upper bound (exclusive) of the `mobile` surface, px. Default 640. */
|
|
30
|
+
mobile: number;
|
|
31
|
+
/** Upper bound (exclusive) of the `tablet` surface, px. Default 1024. */
|
|
32
|
+
tablet: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A resolved, per-locale translation function. Calling `t(key, vars?)` returns
|
|
36
|
+
* the surface-agnostic base value. `t.surface(key, surface, vars?)` returns the
|
|
37
|
+
* value for a specific device surface (overlay ?? base).
|
|
38
|
+
*/
|
|
39
|
+
interface TFn {
|
|
40
|
+
(key: string, vars?: Vars): string;
|
|
41
|
+
/** Resolve `key` for a specific device `surface` (overlay wins over base). */
|
|
42
|
+
surface(key: string, surface: Surface, vars?: Vars): string;
|
|
43
|
+
}
|
|
18
44
|
/**
|
|
19
45
|
* Configuration for the Sonenta Astro integration / runtime.
|
|
20
46
|
*
|
|
@@ -69,6 +95,20 @@ interface SonentaI18nOptions {
|
|
|
69
95
|
* build-time path.
|
|
70
96
|
*/
|
|
71
97
|
apiBase?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Device surfaces to fetch + expose (#911). When set (e.g.
|
|
100
|
+
* `["desktop", "mobile"]`), the build also pulls each sparse overlay
|
|
101
|
+
* `{ns}.{surface}.json` and `getSurfaces` / `t.surface` / `<SurfaceText>`
|
|
102
|
+
* resolve per-surface values. Omit to disable surfaces entirely (no overlay
|
|
103
|
+
* fetch, identical to v0.1 behaviour).
|
|
104
|
+
*/
|
|
105
|
+
surfaces?: Surface[];
|
|
106
|
+
/**
|
|
107
|
+
* Breakpoint ladder for `<SurfaceText>`'s responsive CSS. Default
|
|
108
|
+
* `{ mobile: 640, tablet: 1024 }`. Resolution itself renders every surface;
|
|
109
|
+
* these only decide which one CSS reveals at each viewport width.
|
|
110
|
+
*/
|
|
111
|
+
surfaceBreakpoints?: SurfaceBreakpoints;
|
|
72
112
|
/**
|
|
73
113
|
* Injectable `fetch` implementation (testing / proxies / custom agents).
|
|
74
114
|
* Defaults to the global `fetch`.
|
|
@@ -86,12 +126,32 @@ interface SonentaI18n {
|
|
|
86
126
|
readonly locales: Locale[];
|
|
87
127
|
/** The resolved source/default locale. */
|
|
88
128
|
readonly defaultLocale: Locale;
|
|
129
|
+
/** The configured device surfaces (empty when surfaces are disabled). */
|
|
130
|
+
readonly surfaces: Surface[];
|
|
131
|
+
/** The resolved surface breakpoint ladder. */
|
|
132
|
+
readonly surfaceBreakpoints: SurfaceBreakpoints;
|
|
133
|
+
/**
|
|
134
|
+
* Resolve `key` for every configured surface at `locale`, e.g.
|
|
135
|
+
* `{ desktop: "Commencer gratuitement", mobile: "Commencer" }`. A surface
|
|
136
|
+
* without an overlay for the key resolves to the base value. Empty when
|
|
137
|
+
* surfaces are disabled.
|
|
138
|
+
*/
|
|
139
|
+
getSurfaces(locale: Locale, key: string, vars?: Vars): Record<Surface, string>;
|
|
89
140
|
/**
|
|
90
141
|
* Raw fetched dictionary for `locale` / `namespace` (default namespace when
|
|
91
142
|
* omitted), or `null` when that bundle was absent (404 / plan-limit).
|
|
92
143
|
*/
|
|
93
144
|
getCatalog(locale: Locale, namespace?: Namespace): Record<string, unknown> | null;
|
|
94
145
|
}
|
|
146
|
+
/** Default surface breakpoint ladder (mirrors `@sonenta/react-i18next` #913). */
|
|
147
|
+
declare const DEFAULT_SURFACE_BREAKPOINTS: SurfaceBreakpoints;
|
|
148
|
+
/**
|
|
149
|
+
* Map a viewport width (px) to a {@link Surface} using `breakpoints`. Pure —
|
|
150
|
+
* shared with the `<SurfaceText>` CSS generation so the runtime mapping and
|
|
151
|
+
* the media queries agree. `< mobile` → mobile, `< tablet` → tablet, else
|
|
152
|
+
* desktop.
|
|
153
|
+
*/
|
|
154
|
+
declare function surfaceForWidth(width: number, breakpoints?: SurfaceBreakpoints): Surface;
|
|
95
155
|
|
|
96
156
|
/**
|
|
97
157
|
* Build-time CDN loader for Sonenta translation bundles.
|
|
@@ -115,6 +175,12 @@ declare function resolveCdnBase(opts: Pick<SonentaI18nOptions, "cdnBase">): stri
|
|
|
115
175
|
* `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`
|
|
116
176
|
*/
|
|
117
177
|
declare function buildBundleUrl(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase">, locale: Locale, namespace: Namespace): string;
|
|
178
|
+
/**
|
|
179
|
+
* Build the sparse surface-overlay URL for one `(locale, namespace, surface)`,
|
|
180
|
+
* mirroring `@sonenta/react-i18next` #913:
|
|
181
|
+
* `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.{surface}.json`
|
|
182
|
+
*/
|
|
183
|
+
declare function buildOverlayUrl(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase">, locale: Locale, namespace: Namespace, surface: Surface): string;
|
|
118
184
|
/**
|
|
119
185
|
* Fetch a single bundle. Resolves to `null` on 404 (locale absent from the
|
|
120
186
|
* project — e.g. a plan-limit-blocked language) so callers fall back to the
|
|
@@ -132,29 +198,51 @@ declare function fetchNamespace(opts: Pick<SonentaI18nOptions, "project" | "vers
|
|
|
132
198
|
* locale then namespace: `data[locale][namespace] = Bundle | null`.
|
|
133
199
|
*/
|
|
134
200
|
declare function fetchAll(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locales: Locale[], namespaces: Namespace[]): Promise<Record<Locale, Record<Namespace, Bundle | null>>>;
|
|
201
|
+
/**
|
|
202
|
+
* Fetch a single sparse surface overlay. Unlike {@link fetchBundle}, an
|
|
203
|
+
* overlay is OPTIONAL: any miss (404), non-OK status, or transport error
|
|
204
|
+
* resolves to `null` (base-only) and NEVER throws — a missing/erroring overlay
|
|
205
|
+
* must never break the build or downgrade the base render. Mirrors
|
|
206
|
+
* `@sonenta/react-i18next` #913's `{}`-on-miss behaviour.
|
|
207
|
+
*/
|
|
208
|
+
declare function fetchOverlay(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locale: Locale, namespace: Namespace, surface: Surface): Promise<Bundle | null>;
|
|
209
|
+
/**
|
|
210
|
+
* Fetch every `(locale, namespace, surface)` overlay. Returns a nested map:
|
|
211
|
+
* `overlays[locale][namespace][surface] = Bundle | null`. Empty when
|
|
212
|
+
* `surfaces` is empty.
|
|
213
|
+
*/
|
|
214
|
+
declare function fetchAllOverlays(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locales: Locale[], namespaces: Namespace[], surfaces: Surface[]): Promise<Record<Locale, Record<Namespace, Record<Surface, Bundle | null>>>>;
|
|
135
215
|
|
|
136
216
|
/**
|
|
137
217
|
* Build-time translation resolver.
|
|
138
218
|
*
|
|
139
|
-
* v0.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
219
|
+
* v0.2 SCOPE: string resolution + `{var}` interpolation + source-language
|
|
220
|
+
* fallback (v0.1) PLUS device-surface variants (#911) — a sparse
|
|
221
|
+
* `{ns}.{surface}.json` overlay over the base bundle, overlay-wins-per-key,
|
|
222
|
+
* resolved per locale. Still NO CLDR plurals and NO a11y surfaces: a `$value`
|
|
223
|
+
* that is a plural dict (object) is treated as "no string here" (deferred to
|
|
224
|
+
* the `@sonenta/i18n-core`-backed 0.3.0). The `$asset` half of an envelope is
|
|
225
|
+
* ignored for text resolution. This keeps the surface semantics a faithful,
|
|
226
|
+
* regression-locked subset of react-i18next #913 so the later core swap is
|
|
227
|
+
* behaviour-preserving.
|
|
145
228
|
*/
|
|
146
229
|
|
|
230
|
+
/** Nested base data: `data[locale][namespace] = Bundle | null`. */
|
|
231
|
+
type BaseData = Record<Locale, Record<Namespace, Bundle | null>>;
|
|
232
|
+
/** Nested overlays: `overlays[locale][namespace][surface] = Bundle | null`. */
|
|
233
|
+
type OverlayData = Record<Locale, Record<Namespace, Record<Surface, Bundle | null>>>;
|
|
147
234
|
/**
|
|
148
|
-
* Build a `t()` bound to `locale
|
|
149
|
-
*
|
|
150
|
-
*
|
|
235
|
+
* Build a `t()` (with `.surface`) bound to `locale`. Base lookup: active
|
|
236
|
+
* locale's namespace dict → each fallback locale's same dict → raw key.
|
|
237
|
+
* Surface lookup: per locale, the overlay wins over base, then the same
|
|
238
|
+
* locale-fallback walk (mirrors react-i18next #913 compose-then-fallback).
|
|
151
239
|
*/
|
|
152
|
-
declare function createT(data:
|
|
240
|
+
declare function createT(data: BaseData, overlays: OverlayData, locale: Locale, fallbackChain: Locale[], defaultNamespace: Namespace): TFn;
|
|
153
241
|
/**
|
|
154
|
-
* Fetch every configured bundle at build time and return
|
|
155
|
-
* {@link SonentaI18n} handle. Call once (top-level `await`) and
|
|
156
|
-
* `getT(locale)` across pages.
|
|
242
|
+
* Fetch every configured bundle (and surface overlay) at build time and return
|
|
243
|
+
* a resolved {@link SonentaI18n} handle. Call once (top-level `await`) and
|
|
244
|
+
* reuse its `getT(locale)` / `getSurfaces(locale, key)` across pages.
|
|
157
245
|
*/
|
|
158
246
|
declare function createSonentaI18n(options: SonentaI18nOptions): Promise<SonentaI18n>;
|
|
159
247
|
|
|
160
|
-
export { type Bundle, type Locale, type Namespace, type SonentaI18n, type SonentaI18nOptions, type TFn, type Vars, buildBundleUrl, createSonentaI18n, createT, fetchAll, fetchBundle, fetchNamespace, resolveCdnBase };
|
|
248
|
+
export { type Bundle, DEFAULT_SURFACE_BREAKPOINTS, type Locale, type Namespace, type SonentaI18n, type SonentaI18nOptions, type Surface, type SurfaceBreakpoints, type TFn, type Vars, buildBundleUrl, buildOverlayUrl, createSonentaI18n, createT, fetchAll, fetchAllOverlays, fetchBundle, fetchNamespace, fetchOverlay, resolveCdnBase, surfaceForWidth };
|
package/dist/runtime.d.ts
CHANGED
|
@@ -13,8 +13,34 @@ type Locale = string;
|
|
|
13
13
|
type Namespace = string;
|
|
14
14
|
/** Interpolation variables for a `t()` call: `t("greeting", { name: "Ada" })`. */
|
|
15
15
|
type Vars = Record<string, string | number>;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A device surface — the second resolution dimension layered on top of the
|
|
18
|
+
* locale chain (#911). The base bundle applies to every surface; a sparse
|
|
19
|
+
* `{ns}.{surface}.json` overlay overrides individual keys for that surface.
|
|
20
|
+
*/
|
|
21
|
+
type Surface = "desktop" | "mobile" | "tablet";
|
|
22
|
+
/**
|
|
23
|
+
* Min-width (px) thresholds mapping a viewport to a surface (mobile-first
|
|
24
|
+
* ladder, mirrors `@sonenta/react-i18next` #913): width `< mobile` → mobile;
|
|
25
|
+
* `< tablet` → tablet; otherwise desktop. Drives the `<SurfaceText>` media
|
|
26
|
+
* queries (SSG renders every surface; CSS reveals the right one).
|
|
27
|
+
*/
|
|
28
|
+
interface SurfaceBreakpoints {
|
|
29
|
+
/** Upper bound (exclusive) of the `mobile` surface, px. Default 640. */
|
|
30
|
+
mobile: number;
|
|
31
|
+
/** Upper bound (exclusive) of the `tablet` surface, px. Default 1024. */
|
|
32
|
+
tablet: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A resolved, per-locale translation function. Calling `t(key, vars?)` returns
|
|
36
|
+
* the surface-agnostic base value. `t.surface(key, surface, vars?)` returns the
|
|
37
|
+
* value for a specific device surface (overlay ?? base).
|
|
38
|
+
*/
|
|
39
|
+
interface TFn {
|
|
40
|
+
(key: string, vars?: Vars): string;
|
|
41
|
+
/** Resolve `key` for a specific device `surface` (overlay wins over base). */
|
|
42
|
+
surface(key: string, surface: Surface, vars?: Vars): string;
|
|
43
|
+
}
|
|
18
44
|
/**
|
|
19
45
|
* Configuration for the Sonenta Astro integration / runtime.
|
|
20
46
|
*
|
|
@@ -69,6 +95,20 @@ interface SonentaI18nOptions {
|
|
|
69
95
|
* build-time path.
|
|
70
96
|
*/
|
|
71
97
|
apiBase?: string;
|
|
98
|
+
/**
|
|
99
|
+
* Device surfaces to fetch + expose (#911). When set (e.g.
|
|
100
|
+
* `["desktop", "mobile"]`), the build also pulls each sparse overlay
|
|
101
|
+
* `{ns}.{surface}.json` and `getSurfaces` / `t.surface` / `<SurfaceText>`
|
|
102
|
+
* resolve per-surface values. Omit to disable surfaces entirely (no overlay
|
|
103
|
+
* fetch, identical to v0.1 behaviour).
|
|
104
|
+
*/
|
|
105
|
+
surfaces?: Surface[];
|
|
106
|
+
/**
|
|
107
|
+
* Breakpoint ladder for `<SurfaceText>`'s responsive CSS. Default
|
|
108
|
+
* `{ mobile: 640, tablet: 1024 }`. Resolution itself renders every surface;
|
|
109
|
+
* these only decide which one CSS reveals at each viewport width.
|
|
110
|
+
*/
|
|
111
|
+
surfaceBreakpoints?: SurfaceBreakpoints;
|
|
72
112
|
/**
|
|
73
113
|
* Injectable `fetch` implementation (testing / proxies / custom agents).
|
|
74
114
|
* Defaults to the global `fetch`.
|
|
@@ -86,12 +126,32 @@ interface SonentaI18n {
|
|
|
86
126
|
readonly locales: Locale[];
|
|
87
127
|
/** The resolved source/default locale. */
|
|
88
128
|
readonly defaultLocale: Locale;
|
|
129
|
+
/** The configured device surfaces (empty when surfaces are disabled). */
|
|
130
|
+
readonly surfaces: Surface[];
|
|
131
|
+
/** The resolved surface breakpoint ladder. */
|
|
132
|
+
readonly surfaceBreakpoints: SurfaceBreakpoints;
|
|
133
|
+
/**
|
|
134
|
+
* Resolve `key` for every configured surface at `locale`, e.g.
|
|
135
|
+
* `{ desktop: "Commencer gratuitement", mobile: "Commencer" }`. A surface
|
|
136
|
+
* without an overlay for the key resolves to the base value. Empty when
|
|
137
|
+
* surfaces are disabled.
|
|
138
|
+
*/
|
|
139
|
+
getSurfaces(locale: Locale, key: string, vars?: Vars): Record<Surface, string>;
|
|
89
140
|
/**
|
|
90
141
|
* Raw fetched dictionary for `locale` / `namespace` (default namespace when
|
|
91
142
|
* omitted), or `null` when that bundle was absent (404 / plan-limit).
|
|
92
143
|
*/
|
|
93
144
|
getCatalog(locale: Locale, namespace?: Namespace): Record<string, unknown> | null;
|
|
94
145
|
}
|
|
146
|
+
/** Default surface breakpoint ladder (mirrors `@sonenta/react-i18next` #913). */
|
|
147
|
+
declare const DEFAULT_SURFACE_BREAKPOINTS: SurfaceBreakpoints;
|
|
148
|
+
/**
|
|
149
|
+
* Map a viewport width (px) to a {@link Surface} using `breakpoints`. Pure —
|
|
150
|
+
* shared with the `<SurfaceText>` CSS generation so the runtime mapping and
|
|
151
|
+
* the media queries agree. `< mobile` → mobile, `< tablet` → tablet, else
|
|
152
|
+
* desktop.
|
|
153
|
+
*/
|
|
154
|
+
declare function surfaceForWidth(width: number, breakpoints?: SurfaceBreakpoints): Surface;
|
|
95
155
|
|
|
96
156
|
/**
|
|
97
157
|
* Build-time CDN loader for Sonenta translation bundles.
|
|
@@ -115,6 +175,12 @@ declare function resolveCdnBase(opts: Pick<SonentaI18nOptions, "cdnBase">): stri
|
|
|
115
175
|
* `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`
|
|
116
176
|
*/
|
|
117
177
|
declare function buildBundleUrl(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase">, locale: Locale, namespace: Namespace): string;
|
|
178
|
+
/**
|
|
179
|
+
* Build the sparse surface-overlay URL for one `(locale, namespace, surface)`,
|
|
180
|
+
* mirroring `@sonenta/react-i18next` #913:
|
|
181
|
+
* `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.{surface}.json`
|
|
182
|
+
*/
|
|
183
|
+
declare function buildOverlayUrl(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase">, locale: Locale, namespace: Namespace, surface: Surface): string;
|
|
118
184
|
/**
|
|
119
185
|
* Fetch a single bundle. Resolves to `null` on 404 (locale absent from the
|
|
120
186
|
* project — e.g. a plan-limit-blocked language) so callers fall back to the
|
|
@@ -132,29 +198,51 @@ declare function fetchNamespace(opts: Pick<SonentaI18nOptions, "project" | "vers
|
|
|
132
198
|
* locale then namespace: `data[locale][namespace] = Bundle | null`.
|
|
133
199
|
*/
|
|
134
200
|
declare function fetchAll(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locales: Locale[], namespaces: Namespace[]): Promise<Record<Locale, Record<Namespace, Bundle | null>>>;
|
|
201
|
+
/**
|
|
202
|
+
* Fetch a single sparse surface overlay. Unlike {@link fetchBundle}, an
|
|
203
|
+
* overlay is OPTIONAL: any miss (404), non-OK status, or transport error
|
|
204
|
+
* resolves to `null` (base-only) and NEVER throws — a missing/erroring overlay
|
|
205
|
+
* must never break the build or downgrade the base render. Mirrors
|
|
206
|
+
* `@sonenta/react-i18next` #913's `{}`-on-miss behaviour.
|
|
207
|
+
*/
|
|
208
|
+
declare function fetchOverlay(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locale: Locale, namespace: Namespace, surface: Surface): Promise<Bundle | null>;
|
|
209
|
+
/**
|
|
210
|
+
* Fetch every `(locale, namespace, surface)` overlay. Returns a nested map:
|
|
211
|
+
* `overlays[locale][namespace][surface] = Bundle | null`. Empty when
|
|
212
|
+
* `surfaces` is empty.
|
|
213
|
+
*/
|
|
214
|
+
declare function fetchAllOverlays(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locales: Locale[], namespaces: Namespace[], surfaces: Surface[]): Promise<Record<Locale, Record<Namespace, Record<Surface, Bundle | null>>>>;
|
|
135
215
|
|
|
136
216
|
/**
|
|
137
217
|
* Build-time translation resolver.
|
|
138
218
|
*
|
|
139
|
-
* v0.
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
219
|
+
* v0.2 SCOPE: string resolution + `{var}` interpolation + source-language
|
|
220
|
+
* fallback (v0.1) PLUS device-surface variants (#911) — a sparse
|
|
221
|
+
* `{ns}.{surface}.json` overlay over the base bundle, overlay-wins-per-key,
|
|
222
|
+
* resolved per locale. Still NO CLDR plurals and NO a11y surfaces: a `$value`
|
|
223
|
+
* that is a plural dict (object) is treated as "no string here" (deferred to
|
|
224
|
+
* the `@sonenta/i18n-core`-backed 0.3.0). The `$asset` half of an envelope is
|
|
225
|
+
* ignored for text resolution. This keeps the surface semantics a faithful,
|
|
226
|
+
* regression-locked subset of react-i18next #913 so the later core swap is
|
|
227
|
+
* behaviour-preserving.
|
|
145
228
|
*/
|
|
146
229
|
|
|
230
|
+
/** Nested base data: `data[locale][namespace] = Bundle | null`. */
|
|
231
|
+
type BaseData = Record<Locale, Record<Namespace, Bundle | null>>;
|
|
232
|
+
/** Nested overlays: `overlays[locale][namespace][surface] = Bundle | null`. */
|
|
233
|
+
type OverlayData = Record<Locale, Record<Namespace, Record<Surface, Bundle | null>>>;
|
|
147
234
|
/**
|
|
148
|
-
* Build a `t()` bound to `locale
|
|
149
|
-
*
|
|
150
|
-
*
|
|
235
|
+
* Build a `t()` (with `.surface`) bound to `locale`. Base lookup: active
|
|
236
|
+
* locale's namespace dict → each fallback locale's same dict → raw key.
|
|
237
|
+
* Surface lookup: per locale, the overlay wins over base, then the same
|
|
238
|
+
* locale-fallback walk (mirrors react-i18next #913 compose-then-fallback).
|
|
151
239
|
*/
|
|
152
|
-
declare function createT(data:
|
|
240
|
+
declare function createT(data: BaseData, overlays: OverlayData, locale: Locale, fallbackChain: Locale[], defaultNamespace: Namespace): TFn;
|
|
153
241
|
/**
|
|
154
|
-
* Fetch every configured bundle at build time and return
|
|
155
|
-
* {@link SonentaI18n} handle. Call once (top-level `await`) and
|
|
156
|
-
* `getT(locale)` across pages.
|
|
242
|
+
* Fetch every configured bundle (and surface overlay) at build time and return
|
|
243
|
+
* a resolved {@link SonentaI18n} handle. Call once (top-level `await`) and
|
|
244
|
+
* reuse its `getT(locale)` / `getSurfaces(locale, key)` across pages.
|
|
157
245
|
*/
|
|
158
246
|
declare function createSonentaI18n(options: SonentaI18nOptions): Promise<SonentaI18n>;
|
|
159
247
|
|
|
160
|
-
export { type Bundle, type Locale, type Namespace, type SonentaI18n, type SonentaI18nOptions, type TFn, type Vars, buildBundleUrl, createSonentaI18n, createT, fetchAll, fetchBundle, fetchNamespace, resolveCdnBase };
|
|
248
|
+
export { type Bundle, DEFAULT_SURFACE_BREAKPOINTS, type Locale, type Namespace, type SonentaI18n, type SonentaI18nOptions, type Surface, type SurfaceBreakpoints, type TFn, type Vars, buildBundleUrl, buildOverlayUrl, createSonentaI18n, createT, fetchAll, fetchAllOverlays, fetchBundle, fetchNamespace, fetchOverlay, resolveCdnBase, surfaceForWidth };
|