@sonenta/astro 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/virtual.ts","../src/integration.ts","../src/types.ts","../src/cdn.ts","../src/resolver.ts"],"sourcesContent":["/**\n * `@sonenta/astro` — official Astro integration for Sonenta i18n.\n *\n * v0.1 (standalone, pre-`@sonenta/i18n-core`): build-time CDN fetch + string\n * resolution + `{var}` interpolation + source-language fallback, with ZERO\n * client-side JS (pure SSG). The public API here is frozen forward-compatible:\n * the 0.2.0 core-backed release swaps the internals and adds surfaces / a11y /\n * CLDR plurals / variants ADDITIVELY, without changing anything below.\n *\n * Default export = the integration; named exports = the build-time runtime\n * helpers (also available standalone at `@sonenta/astro/runtime`).\n */\n\nimport { sonenta } from \"./integration\";\n\nexport { sonenta } from \"./integration\";\nexport default sonenta;\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 * Vite plugin that serves the `sonenta:i18n` virtual module. The integration\n * wires this into the consumer's Astro/Vite build; the generated module pulls\n * the configured bundles at build time (via `@sonenta/astro/runtime`) and\n * re-exports a ready `getT`.\n */\n\nimport type { Plugin } from \"vite\";\nimport type { SonentaI18nOptions } from \"./types\";\n\n/** Public virtual id and its Vite-internal resolved form. */\nconst VIRTUAL_ID = \"sonenta:i18n\";\nconst RESOLVED_ID = \"\\0sonenta:i18n\";\n\n/**\n * Options that can cross the build/runtime boundary: everything except the\n * non-serializable `fetchImpl` (build always uses the global `fetch`).\n */\nfunction serializableOptions(\n options: SonentaI18nOptions,\n): Omit<SonentaI18nOptions, \"fetchImpl\"> {\n const { fetchImpl: _omit, ...rest } = options;\n return rest;\n}\n\n/** The generated source of the `sonenta:i18n` module. */\nfunction moduleSource(options: SonentaI18nOptions): string {\n const opts = JSON.stringify(serializableOptions(options));\n return [\n `import { createSonentaI18n } from \"@sonenta/astro/runtime\";`,\n `const __i18n = await createSonentaI18n(${opts});`,\n `export const getT = (locale) => __i18n.getT(locale);`,\n `export const locales = __i18n.locales;`,\n `export const defaultLocale = __i18n.defaultLocale;`,\n `export const getCatalog = (locale, ns) => __i18n.getCatalog(locale, ns);`,\n `export default __i18n;`,\n ``,\n ].join(\"\\n\");\n}\n\n/** Build the Vite plugin that resolves and loads `sonenta:i18n`. */\nexport function sonentaI18nVitePlugin(options: SonentaI18nOptions): Plugin {\n return {\n name: \"sonenta:astro-i18n\",\n enforce: \"pre\",\n resolveId(id) {\n if (id === VIRTUAL_ID) return RESOLVED_ID;\n return null;\n },\n load(id) {\n if (id === RESOLVED_ID) return moduleSource(options);\n return null;\n },\n };\n}\n\nexport { VIRTUAL_ID, RESOLVED_ID };\n","/**\n * The Sonenta Astro integration — default export of `@sonenta/astro`.\n *\n * Usage (astro.config.mjs):\n *\n * import { defineConfig } from \"astro/config\";\n * import sonenta from \"@sonenta/astro\";\n *\n * export default defineConfig({\n * integrations: [\n * sonenta({\n * project: \"your-project-uuid\",\n * locales: [\"fr\", \"en\", \"es\"],\n * defaultLocale: \"fr\",\n * namespaces: [\"common\"],\n * }),\n * ],\n * });\n *\n * Then in any `.astro` page:\n *\n * ---\n * import { getT } from \"sonenta:i18n\";\n * const t = getT(Astro.currentLocale ?? \"fr\");\n * ---\n * <h1>{t(\"home.title\")}</h1>\n * <p>{t(\"home.greeting\", { name: \"Ada\" })}</p>\n */\n\nimport type { AstroIntegration } from \"astro\";\nimport { sonentaI18nVitePlugin } from \"./virtual\";\nimport type { SonentaI18nOptions } from \"./types\";\n\n/** Ambient types injected for the `sonenta:i18n` virtual module. */\nconst VIRTUAL_DTS = `declare module \"sonenta:i18n\" {\n import type { SonentaI18n, TFn, Locale, Namespace } from \"@sonenta/astro/runtime\";\n /** Translation function bound to \\`locale\\`. */\n export const getT: (locale: Locale) => TFn;\n /** Locales fetched and frozen into the build. */\n export const locales: Locale[];\n /** Resolved source/default locale. */\n export const defaultLocale: Locale;\n /** Raw fetched dictionary for a locale/namespace, or null when absent. */\n export const getCatalog: (\n locale: Locale,\n namespace?: Namespace,\n ) => Record<string, unknown> | null;\n const i18n: SonentaI18n;\n export default i18n;\n}\n`;\n\n/**\n * Create the Sonenta i18n integration. Bundles are fetched at build time and\n * exposed through the `sonenta:i18n` virtual module (zero client-side JS).\n */\nexport function sonenta(options: SonentaI18nOptions): AstroIntegration {\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 return {\n name: \"@sonenta/astro\",\n hooks: {\n \"astro:config:setup\": ({ updateConfig, logger }) => {\n if (options.fetchImpl) {\n logger.warn(\n \"`fetchImpl` is ignored by the integration; the build uses the global fetch. Use `@sonenta/astro/runtime` directly if you need a custom fetch.\",\n );\n }\n updateConfig({\n vite: { plugins: [sonentaI18nVitePlugin(options)] },\n });\n logger.info(\n `i18n wired for project ${options.project} (${options.locales.length} locales, build-time CDN fetch).`,\n );\n },\n \"astro:config:done\": ({ injectTypes }) => {\n injectTypes({ filename: \"sonenta-i18n.d.ts\", content: VIRTUAL_DTS });\n },\n },\n };\n}\n\nexport default sonenta;\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":[]}
@@ -0,0 +1,53 @@
1
+ import { AstroIntegration } from 'astro';
2
+ import { SonentaI18nOptions } from './runtime.cjs';
3
+ export { Bundle, Locale, Namespace, SonentaI18n, TFn, Vars, buildBundleUrl, createSonentaI18n, createT, fetchAll, fetchBundle, fetchNamespace, resolveCdnBase } from './runtime.cjs';
4
+
5
+ /**
6
+ * The Sonenta Astro integration — default export of `@sonenta/astro`.
7
+ *
8
+ * Usage (astro.config.mjs):
9
+ *
10
+ * import { defineConfig } from "astro/config";
11
+ * import sonenta from "@sonenta/astro";
12
+ *
13
+ * export default defineConfig({
14
+ * integrations: [
15
+ * sonenta({
16
+ * project: "your-project-uuid",
17
+ * locales: ["fr", "en", "es"],
18
+ * defaultLocale: "fr",
19
+ * namespaces: ["common"],
20
+ * }),
21
+ * ],
22
+ * });
23
+ *
24
+ * Then in any `.astro` page:
25
+ *
26
+ * ---
27
+ * import { getT } from "sonenta:i18n";
28
+ * const t = getT(Astro.currentLocale ?? "fr");
29
+ * ---
30
+ * <h1>{t("home.title")}</h1>
31
+ * <p>{t("home.greeting", { name: "Ada" })}</p>
32
+ */
33
+
34
+ /**
35
+ * Create the Sonenta i18n integration. Bundles are fetched at build time and
36
+ * exposed through the `sonenta:i18n` virtual module (zero client-side JS).
37
+ */
38
+ declare function sonenta(options: SonentaI18nOptions): AstroIntegration;
39
+
40
+ /**
41
+ * `@sonenta/astro` — official Astro integration for Sonenta i18n.
42
+ *
43
+ * v0.1 (standalone, pre-`@sonenta/i18n-core`): build-time CDN fetch + string
44
+ * resolution + `{var}` interpolation + source-language fallback, with ZERO
45
+ * client-side JS (pure SSG). The public API here is frozen forward-compatible:
46
+ * the 0.2.0 core-backed release swaps the internals and adds surfaces / a11y /
47
+ * CLDR plurals / variants ADDITIVELY, without changing anything below.
48
+ *
49
+ * Default export = the integration; named exports = the build-time runtime
50
+ * helpers (also available standalone at `@sonenta/astro/runtime`).
51
+ */
52
+
53
+ export { SonentaI18nOptions, sonenta as default, sonenta };
@@ -0,0 +1,53 @@
1
+ import { AstroIntegration } from 'astro';
2
+ import { SonentaI18nOptions } from './runtime.js';
3
+ export { Bundle, Locale, Namespace, SonentaI18n, TFn, Vars, buildBundleUrl, createSonentaI18n, createT, fetchAll, fetchBundle, fetchNamespace, resolveCdnBase } from './runtime.js';
4
+
5
+ /**
6
+ * The Sonenta Astro integration — default export of `@sonenta/astro`.
7
+ *
8
+ * Usage (astro.config.mjs):
9
+ *
10
+ * import { defineConfig } from "astro/config";
11
+ * import sonenta from "@sonenta/astro";
12
+ *
13
+ * export default defineConfig({
14
+ * integrations: [
15
+ * sonenta({
16
+ * project: "your-project-uuid",
17
+ * locales: ["fr", "en", "es"],
18
+ * defaultLocale: "fr",
19
+ * namespaces: ["common"],
20
+ * }),
21
+ * ],
22
+ * });
23
+ *
24
+ * Then in any `.astro` page:
25
+ *
26
+ * ---
27
+ * import { getT } from "sonenta:i18n";
28
+ * const t = getT(Astro.currentLocale ?? "fr");
29
+ * ---
30
+ * <h1>{t("home.title")}</h1>
31
+ * <p>{t("home.greeting", { name: "Ada" })}</p>
32
+ */
33
+
34
+ /**
35
+ * Create the Sonenta i18n integration. Bundles are fetched at build time and
36
+ * exposed through the `sonenta:i18n` virtual module (zero client-side JS).
37
+ */
38
+ declare function sonenta(options: SonentaI18nOptions): AstroIntegration;
39
+
40
+ /**
41
+ * `@sonenta/astro` — official Astro integration for Sonenta i18n.
42
+ *
43
+ * v0.1 (standalone, pre-`@sonenta/i18n-core`): build-time CDN fetch + string
44
+ * resolution + `{var}` interpolation + source-language fallback, with ZERO
45
+ * client-side JS (pure SSG). The public API here is frozen forward-compatible:
46
+ * the 0.2.0 core-backed release swaps the internals and adds surfaces / a11y /
47
+ * CLDR plurals / variants ADDITIVELY, without changing anything below.
48
+ *
49
+ * Default export = the integration; named exports = the build-time runtime
50
+ * helpers (also available standalone at `@sonenta/astro/runtime`).
51
+ */
52
+
53
+ export { SonentaI18nOptions, sonenta as default, sonenta };
package/dist/index.js ADDED
@@ -0,0 +1,107 @@
1
+ import {
2
+ buildBundleUrl,
3
+ createSonentaI18n,
4
+ createT,
5
+ fetchAll,
6
+ fetchBundle,
7
+ fetchNamespace,
8
+ resolveCdnBase
9
+ } from "./chunk-CK2DAB6G.js";
10
+
11
+ // src/virtual.ts
12
+ var VIRTUAL_ID = "sonenta:i18n";
13
+ var RESOLVED_ID = "\0sonenta:i18n";
14
+ function serializableOptions(options) {
15
+ const { fetchImpl: _omit, ...rest } = options;
16
+ return rest;
17
+ }
18
+ function moduleSource(options) {
19
+ const opts = JSON.stringify(serializableOptions(options));
20
+ return [
21
+ `import { createSonentaI18n } from "@sonenta/astro/runtime";`,
22
+ `const __i18n = await createSonentaI18n(${opts});`,
23
+ `export const getT = (locale) => __i18n.getT(locale);`,
24
+ `export const locales = __i18n.locales;`,
25
+ `export const defaultLocale = __i18n.defaultLocale;`,
26
+ `export const getCatalog = (locale, ns) => __i18n.getCatalog(locale, ns);`,
27
+ `export default __i18n;`,
28
+ ``
29
+ ].join("\n");
30
+ }
31
+ function sonentaI18nVitePlugin(options) {
32
+ return {
33
+ name: "sonenta:astro-i18n",
34
+ enforce: "pre",
35
+ resolveId(id) {
36
+ if (id === VIRTUAL_ID) return RESOLVED_ID;
37
+ return null;
38
+ },
39
+ load(id) {
40
+ if (id === RESOLVED_ID) return moduleSource(options);
41
+ return null;
42
+ }
43
+ };
44
+ }
45
+
46
+ // src/integration.ts
47
+ var VIRTUAL_DTS = `declare module "sonenta:i18n" {
48
+ import type { SonentaI18n, TFn, Locale, Namespace } from "@sonenta/astro/runtime";
49
+ /** Translation function bound to \`locale\`. */
50
+ export const getT: (locale: Locale) => TFn;
51
+ /** Locales fetched and frozen into the build. */
52
+ export const locales: Locale[];
53
+ /** Resolved source/default locale. */
54
+ export const defaultLocale: Locale;
55
+ /** Raw fetched dictionary for a locale/namespace, or null when absent. */
56
+ export const getCatalog: (
57
+ locale: Locale,
58
+ namespace?: Namespace,
59
+ ) => Record<string, unknown> | null;
60
+ const i18n: SonentaI18n;
61
+ export default i18n;
62
+ }
63
+ `;
64
+ function sonenta(options) {
65
+ if (!options?.project) {
66
+ throw new Error("[@sonenta/astro] `project` (UUID) is required.");
67
+ }
68
+ if (!options.locales?.length) {
69
+ throw new Error("[@sonenta/astro] `locales` must be a non-empty array.");
70
+ }
71
+ return {
72
+ name: "@sonenta/astro",
73
+ hooks: {
74
+ "astro:config:setup": ({ updateConfig, logger }) => {
75
+ if (options.fetchImpl) {
76
+ logger.warn(
77
+ "`fetchImpl` is ignored by the integration; the build uses the global fetch. Use `@sonenta/astro/runtime` directly if you need a custom fetch."
78
+ );
79
+ }
80
+ updateConfig({
81
+ vite: { plugins: [sonentaI18nVitePlugin(options)] }
82
+ });
83
+ logger.info(
84
+ `i18n wired for project ${options.project} (${options.locales.length} locales, build-time CDN fetch).`
85
+ );
86
+ },
87
+ "astro:config:done": ({ injectTypes }) => {
88
+ injectTypes({ filename: "sonenta-i18n.d.ts", content: VIRTUAL_DTS });
89
+ }
90
+ }
91
+ };
92
+ }
93
+
94
+ // src/index.ts
95
+ var index_default = sonenta;
96
+ export {
97
+ buildBundleUrl,
98
+ createSonentaI18n,
99
+ createT,
100
+ index_default as default,
101
+ fetchAll,
102
+ fetchBundle,
103
+ fetchNamespace,
104
+ resolveCdnBase,
105
+ sonenta
106
+ };
107
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/virtual.ts","../src/integration.ts","../src/index.ts"],"sourcesContent":["/**\n * Vite plugin that serves the `sonenta:i18n` virtual module. The integration\n * wires this into the consumer's Astro/Vite build; the generated module pulls\n * the configured bundles at build time (via `@sonenta/astro/runtime`) and\n * re-exports a ready `getT`.\n */\n\nimport type { Plugin } from \"vite\";\nimport type { SonentaI18nOptions } from \"./types\";\n\n/** Public virtual id and its Vite-internal resolved form. */\nconst VIRTUAL_ID = \"sonenta:i18n\";\nconst RESOLVED_ID = \"\\0sonenta:i18n\";\n\n/**\n * Options that can cross the build/runtime boundary: everything except the\n * non-serializable `fetchImpl` (build always uses the global `fetch`).\n */\nfunction serializableOptions(\n options: SonentaI18nOptions,\n): Omit<SonentaI18nOptions, \"fetchImpl\"> {\n const { fetchImpl: _omit, ...rest } = options;\n return rest;\n}\n\n/** The generated source of the `sonenta:i18n` module. */\nfunction moduleSource(options: SonentaI18nOptions): string {\n const opts = JSON.stringify(serializableOptions(options));\n return [\n `import { createSonentaI18n } from \"@sonenta/astro/runtime\";`,\n `const __i18n = await createSonentaI18n(${opts});`,\n `export const getT = (locale) => __i18n.getT(locale);`,\n `export const locales = __i18n.locales;`,\n `export const defaultLocale = __i18n.defaultLocale;`,\n `export const getCatalog = (locale, ns) => __i18n.getCatalog(locale, ns);`,\n `export default __i18n;`,\n ``,\n ].join(\"\\n\");\n}\n\n/** Build the Vite plugin that resolves and loads `sonenta:i18n`. */\nexport function sonentaI18nVitePlugin(options: SonentaI18nOptions): Plugin {\n return {\n name: \"sonenta:astro-i18n\",\n enforce: \"pre\",\n resolveId(id) {\n if (id === VIRTUAL_ID) return RESOLVED_ID;\n return null;\n },\n load(id) {\n if (id === RESOLVED_ID) return moduleSource(options);\n return null;\n },\n };\n}\n\nexport { VIRTUAL_ID, RESOLVED_ID };\n","/**\n * The Sonenta Astro integration — default export of `@sonenta/astro`.\n *\n * Usage (astro.config.mjs):\n *\n * import { defineConfig } from \"astro/config\";\n * import sonenta from \"@sonenta/astro\";\n *\n * export default defineConfig({\n * integrations: [\n * sonenta({\n * project: \"your-project-uuid\",\n * locales: [\"fr\", \"en\", \"es\"],\n * defaultLocale: \"fr\",\n * namespaces: [\"common\"],\n * }),\n * ],\n * });\n *\n * Then in any `.astro` page:\n *\n * ---\n * import { getT } from \"sonenta:i18n\";\n * const t = getT(Astro.currentLocale ?? \"fr\");\n * ---\n * <h1>{t(\"home.title\")}</h1>\n * <p>{t(\"home.greeting\", { name: \"Ada\" })}</p>\n */\n\nimport type { AstroIntegration } from \"astro\";\nimport { sonentaI18nVitePlugin } from \"./virtual\";\nimport type { SonentaI18nOptions } from \"./types\";\n\n/** Ambient types injected for the `sonenta:i18n` virtual module. */\nconst VIRTUAL_DTS = `declare module \"sonenta:i18n\" {\n import type { SonentaI18n, TFn, Locale, Namespace } from \"@sonenta/astro/runtime\";\n /** Translation function bound to \\`locale\\`. */\n export const getT: (locale: Locale) => TFn;\n /** Locales fetched and frozen into the build. */\n export const locales: Locale[];\n /** Resolved source/default locale. */\n export const defaultLocale: Locale;\n /** Raw fetched dictionary for a locale/namespace, or null when absent. */\n export const getCatalog: (\n locale: Locale,\n namespace?: Namespace,\n ) => Record<string, unknown> | null;\n const i18n: SonentaI18n;\n export default i18n;\n}\n`;\n\n/**\n * Create the Sonenta i18n integration. Bundles are fetched at build time and\n * exposed through the `sonenta:i18n` virtual module (zero client-side JS).\n */\nexport function sonenta(options: SonentaI18nOptions): AstroIntegration {\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 return {\n name: \"@sonenta/astro\",\n hooks: {\n \"astro:config:setup\": ({ updateConfig, logger }) => {\n if (options.fetchImpl) {\n logger.warn(\n \"`fetchImpl` is ignored by the integration; the build uses the global fetch. Use `@sonenta/astro/runtime` directly if you need a custom fetch.\",\n );\n }\n updateConfig({\n vite: { plugins: [sonentaI18nVitePlugin(options)] },\n });\n logger.info(\n `i18n wired for project ${options.project} (${options.locales.length} locales, build-time CDN fetch).`,\n );\n },\n \"astro:config:done\": ({ injectTypes }) => {\n injectTypes({ filename: \"sonenta-i18n.d.ts\", content: VIRTUAL_DTS });\n },\n },\n };\n}\n\nexport default sonenta;\n","/**\n * `@sonenta/astro` — official Astro integration for Sonenta i18n.\n *\n * v0.1 (standalone, pre-`@sonenta/i18n-core`): build-time CDN fetch + string\n * resolution + `{var}` interpolation + source-language fallback, with ZERO\n * client-side JS (pure SSG). The public API here is frozen forward-compatible:\n * the 0.2.0 core-backed release swaps the internals and adds surfaces / a11y /\n * CLDR plurals / variants ADDITIVELY, without changing anything below.\n *\n * Default export = the integration; named exports = the build-time runtime\n * helpers (also available standalone at `@sonenta/astro/runtime`).\n */\n\nimport { sonenta } from \"./integration\";\n\nexport { sonenta } from \"./integration\";\nexport default sonenta;\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"],"mappings":";;;;;;;;;;;AAWA,IAAM,aAAa;AACnB,IAAM,cAAc;AAMpB,SAAS,oBACP,SACuC;AACvC,QAAM,EAAE,WAAW,OAAO,GAAG,KAAK,IAAI;AACtC,SAAO;AACT;AAGA,SAAS,aAAa,SAAqC;AACzD,QAAM,OAAO,KAAK,UAAU,oBAAoB,OAAO,CAAC;AACxD,SAAO;AAAA,IACL;AAAA,IACA,0CAA0C,IAAI;AAAA,IAC9C;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;AAGO,SAAS,sBAAsB,SAAqC;AACzE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,UAAU,IAAI;AACZ,UAAI,OAAO,WAAY,QAAO;AAC9B,aAAO;AAAA,IACT;AAAA,IACA,KAAK,IAAI;AACP,UAAI,OAAO,YAAa,QAAO,aAAa,OAAO;AACnD,aAAO;AAAA,IACT;AAAA,EACF;AACF;;;ACpBA,IAAM,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAsBb,SAAS,QAAQ,SAA+C;AACrE,MAAI,CAAC,SAAS,SAAS;AACrB,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AACA,MAAI,CAAC,QAAQ,SAAS,QAAQ;AAC5B,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA,MACL,sBAAsB,CAAC,EAAE,cAAc,OAAO,MAAM;AAClD,YAAI,QAAQ,WAAW;AACrB,iBAAO;AAAA,YACL;AAAA,UACF;AAAA,QACF;AACA,qBAAa;AAAA,UACX,MAAM,EAAE,SAAS,CAAC,sBAAsB,OAAO,CAAC,EAAE;AAAA,QACpD,CAAC;AACD,eAAO;AAAA,UACL,0BAA0B,QAAQ,OAAO,KAAK,QAAQ,QAAQ,MAAM;AAAA,QACtE;AAAA,MACF;AAAA,MACA,qBAAqB,CAAC,EAAE,YAAY,MAAM;AACxC,oBAAY,EAAE,UAAU,qBAAqB,SAAS,YAAY,CAAC;AAAA,MACrE;AAAA,IACF;AAAA,EACF;AACF;;;ACpEA,IAAO,gBAAQ;","names":[]}
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/runtime.ts
21
+ var runtime_exports = {};
22
+ __export(runtime_exports, {
23
+ buildBundleUrl: () => buildBundleUrl,
24
+ createSonentaI18n: () => createSonentaI18n,
25
+ createT: () => createT,
26
+ fetchAll: () => fetchAll,
27
+ fetchBundle: () => fetchBundle,
28
+ fetchNamespace: () => fetchNamespace,
29
+ resolveCdnBase: () => resolveCdnBase
30
+ });
31
+ module.exports = __toCommonJS(runtime_exports);
32
+
33
+ // src/types.ts
34
+ var DEFAULT_CDN_BASE = "https://cdn.sonenta.com";
35
+ var DEFAULT_VERSION = "main";
36
+ var DEFAULT_NAMESPACE = "common";
37
+
38
+ // src/cdn.ts
39
+ function trimSlashes(s) {
40
+ return s.replace(/\/+$/, "");
41
+ }
42
+ function resolveCdnBase(opts) {
43
+ const env = typeof process !== "undefined" ? process.env?.SONENTA_CDN_BASE : void 0;
44
+ return trimSlashes(opts.cdnBase ?? env ?? DEFAULT_CDN_BASE);
45
+ }
46
+ function buildBundleUrl(opts, locale, namespace) {
47
+ const base = resolveCdnBase(opts);
48
+ const version = opts.version ?? DEFAULT_VERSION;
49
+ return `${base}/p/${opts.project}/${version}/latest/${locale}/${namespace}.json`;
50
+ }
51
+ async function fetchBundle(opts, locale, namespace) {
52
+ const url = buildBundleUrl(opts, locale, namespace);
53
+ const doFetch = opts.fetchImpl ?? fetch;
54
+ let res;
55
+ try {
56
+ res = await doFetch(url, { method: "GET" });
57
+ } catch (e) {
58
+ throw new Error(
59
+ `[@sonenta/astro] CDN fetch failed for ${locale}/${namespace}: ${e.message}`
60
+ );
61
+ }
62
+ if (!res.ok) {
63
+ if (res.status === 404) return null;
64
+ throw new Error(`[@sonenta/astro] CDN ${res.status} on ${url}`);
65
+ }
66
+ return await res.json();
67
+ }
68
+ async function fetchNamespace(opts, locales, namespace) {
69
+ const entries = await Promise.all(
70
+ locales.map(
71
+ async (l) => [l, await fetchBundle(opts, l, namespace)]
72
+ )
73
+ );
74
+ return Object.fromEntries(entries);
75
+ }
76
+ async function fetchAll(opts, locales, namespaces) {
77
+ const out = {};
78
+ for (const l of locales) out[l] = {};
79
+ await Promise.all(
80
+ locales.flatMap(
81
+ (l) => namespaces.map(async (ns) => {
82
+ out[l][ns] = await fetchBundle(opts, l, ns);
83
+ })
84
+ )
85
+ );
86
+ return out;
87
+ }
88
+
89
+ // src/resolver.ts
90
+ function normalizeFallback(fallbackLng, defaultLocale) {
91
+ if (fallbackLng == null) return [defaultLocale];
92
+ return Array.isArray(fallbackLng) ? fallbackLng : [fallbackLng];
93
+ }
94
+ function splitKey(key, defaultNamespace) {
95
+ const i = key.indexOf(":");
96
+ if (i === -1) return [defaultNamespace, key];
97
+ return [key.slice(0, i), key.slice(i + 1)];
98
+ }
99
+ function walk(bundle, path) {
100
+ if (!bundle) return void 0;
101
+ let cursor = bundle;
102
+ for (const part of path.split(".")) {
103
+ if (cursor && typeof cursor === "object" && part in cursor) {
104
+ cursor = cursor[part];
105
+ } else {
106
+ return void 0;
107
+ }
108
+ }
109
+ return cursor;
110
+ }
111
+ function interpolate(template, vars) {
112
+ if (!vars) return template;
113
+ return template.replace(
114
+ /\{(\w+)\}/g,
115
+ (_, name) => name in vars ? String(vars[name]) : `{${name}}`
116
+ );
117
+ }
118
+ function createT(data, locale, fallbackChain, defaultNamespace) {
119
+ const order = [locale, ...fallbackChain.filter((l) => l !== locale)];
120
+ return function t(key, vars) {
121
+ const [ns, rest] = splitKey(key, defaultNamespace);
122
+ for (const l of order) {
123
+ const hit = walk(data[l]?.[ns], rest);
124
+ if (typeof hit === "string") return interpolate(hit, vars);
125
+ }
126
+ return key;
127
+ };
128
+ }
129
+ async function createSonentaI18n(options) {
130
+ if (!options.project) {
131
+ throw new Error("[@sonenta/astro] `project` (UUID) is required.");
132
+ }
133
+ if (!options.locales?.length) {
134
+ throw new Error("[@sonenta/astro] `locales` must be a non-empty array.");
135
+ }
136
+ const locales = options.locales;
137
+ const defaultLocale = options.defaultLocale ?? locales[0];
138
+ const namespaces = options.namespaces?.length ? options.namespaces : [DEFAULT_NAMESPACE];
139
+ const defaultNamespace = namespaces[0];
140
+ const fallbackChain = normalizeFallback(options.fallbackLng, defaultLocale);
141
+ const data = await fetchAll(options, locales, namespaces);
142
+ return {
143
+ locales,
144
+ defaultLocale,
145
+ getT(locale) {
146
+ return createT(data, locale, fallbackChain, defaultNamespace);
147
+ },
148
+ getCatalog(locale, namespace) {
149
+ return data[locale]?.[namespace ?? defaultNamespace] ?? null;
150
+ }
151
+ };
152
+ }
153
+ // Annotate the CommonJS export names for ESM import in node:
154
+ 0 && (module.exports = {
155
+ buildBundleUrl,
156
+ createSonentaI18n,
157
+ createT,
158
+ fetchAll,
159
+ fetchBundle,
160
+ fetchNamespace,
161
+ resolveCdnBase
162
+ });
163
+ //# sourceMappingURL=runtime.cjs.map
@@ -0,0 +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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AC8GO,IAAM,mBAAmB;AAIzB,IAAM,kBAAkB;AAExB,IAAM,oBAAoB;;;AC9FjC,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;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;;;ACnGA,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;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;AAOO,SAAS,QACd,MACA,QACA,eACA,kBACK;AAGL,QAAM,QAAQ,CAAC,QAAQ,GAAG,cAAc,OAAO,CAAC,MAAM,MAAM,MAAM,CAAC;AACnE,SAAO,SAAS,EAAE,KAAa,MAAqB;AAClD,UAAM,CAAC,IAAI,IAAI,IAAI,SAAS,KAAK,gBAAgB;AACjD,eAAW,KAAK,OAAO;AACrB,YAAM,MAAM,KAAK,KAAK,CAAC,IAAI,EAAE,GAAG,IAAI;AACpC,UAAI,OAAO,QAAQ,SAAU,QAAO,YAAY,KAAK,IAAI;AAAA,IAC3D;AAEA,WAAO;AAAA,EACT;AACF;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;AAE1E,QAAM,OAAO,MAAM,SAAS,SAAS,SAAS,UAAU;AAExD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,KAAK,QAAqB;AACxB,aAAO,QAAQ,MAAM,QAAQ,eAAe,gBAAgB;AAAA,IAC9D;AAAA,IACA,WAAW,QAAgB,WAAsC;AAC/D,aAAO,KAAK,MAAM,IAAI,aAAa,gBAAgB,KAAK;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Public types for `@sonenta/astro` v0.1 (standalone, pre-core).
3
+ *
4
+ * FORWARD-COMPATIBILITY CONTRACT (frozen day-one — see CONTRACT.md): every
5
+ * option key here is the SAME key the future `@sonenta/i18n-core`-backed
6
+ * release will consume, so the later refactor (0.2.0) is a non-breaking,
7
+ * additive internal swap. New capabilities (surfaces, a11y accessors, CLDR
8
+ * plurals, variants) arrive ADDITIVELY; nothing here changes meaning.
9
+ */
10
+ /** A BCP-47 locale code, e.g. `"fr"`, `"en"`, `"fr-CA"`. */
11
+ type Locale = string;
12
+ /** A bundle namespace (the `{ns}.json` file on the CDN), e.g. `"common"`. */
13
+ type Namespace = string;
14
+ /** Interpolation variables for a `t()` call: `t("greeting", { name: "Ada" })`. */
15
+ type Vars = Record<string, string | number>;
16
+ /** A resolved, per-locale translation function. */
17
+ type TFn = (key: string, vars?: Vars) => string;
18
+ /**
19
+ * Configuration for the Sonenta Astro integration / runtime.
20
+ *
21
+ * Bundles are fetched at BUILD time from
22
+ * `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`
23
+ * (the canonical Sonenta CDN layout, identical to `@sonenta/react-i18next`).
24
+ */
25
+ interface SonentaI18nOptions {
26
+ /** Project UUID from your Sonenta dashboard. Required. */
27
+ project: string;
28
+ /**
29
+ * Released version slug or pinned content hash. Default `"main"`.
30
+ * The CDN serves the latest release of this version under `/latest/`.
31
+ */
32
+ version?: string;
33
+ /**
34
+ * Locales to fetch and freeze into the static build. Required, non-empty.
35
+ * A locale whose bundle 404s (e.g. a plan-limit-blocked language) is kept
36
+ * as `null` and transparently falls back to the source language.
37
+ */
38
+ locales: Locale[];
39
+ /**
40
+ * Source / default locale. Default = `locales[0]`. Used as the implicit
41
+ * final fallback target and as Astro's source language.
42
+ */
43
+ defaultLocale?: Locale;
44
+ /**
45
+ * Ordered fallback chain applied when a key is missing in the active
46
+ * locale. Default = `[defaultLocale]`. v0.1 applies these locales in
47
+ * order; BCP-47 variant→base inheritance (`fr-CA → fr`) is deliberately
48
+ * left to the core-backed release.
49
+ */
50
+ fallbackLng?: Locale | Locale[];
51
+ /**
52
+ * Namespaces (bundle files) to fetch. Default `["common"]`. The first
53
+ * entry is the default namespace for un-prefixed keys; address others with
54
+ * the i18next-style `"ns:key"` syntax.
55
+ */
56
+ namespaces?: Namespace[];
57
+ /**
58
+ * CDN host root for translation bundles, WITHOUT the `/p` segment.
59
+ * Default `"https://cdn.sonenta.com"`. Overridable via the
60
+ * `SONENTA_CDN_BASE` env var (also a bare host; for local dev point it at
61
+ * your translation CDN). The `/p/{project}/{version}/latest/...` path is
62
+ * appended by the loader.
63
+ */
64
+ cdnBase?: string;
65
+ /**
66
+ * API host root. Reserved for forward-compatibility (the core-backed
67
+ * release uses it for the public language manifest and dev-mode runtime
68
+ * fetch). Default `"https://api.sonenta.dev"`. Unused by v0.1's prod
69
+ * build-time path.
70
+ */
71
+ apiBase?: string;
72
+ /**
73
+ * Injectable `fetch` implementation (testing / proxies / custom agents).
74
+ * Defaults to the global `fetch`.
75
+ */
76
+ fetchImpl?: typeof fetch;
77
+ }
78
+ /**
79
+ * The resolved Sonenta i18n handle returned by `createSonentaI18n` and
80
+ * exposed through the `sonenta:i18n` virtual module.
81
+ */
82
+ interface SonentaI18n {
83
+ /** Build a translation function bound to `locale`. */
84
+ getT(locale: Locale): TFn;
85
+ /** The locales that were fetched (the configured `locales`). */
86
+ readonly locales: Locale[];
87
+ /** The resolved source/default locale. */
88
+ readonly defaultLocale: Locale;
89
+ /**
90
+ * Raw fetched dictionary for `locale` / `namespace` (default namespace when
91
+ * omitted), or `null` when that bundle was absent (404 / plan-limit).
92
+ */
93
+ getCatalog(locale: Locale, namespace?: Namespace): Record<string, unknown> | null;
94
+ }
95
+
96
+ /**
97
+ * Build-time CDN loader for Sonenta translation bundles.
98
+ *
99
+ * Mirrors the canonical Sonenta CDN layout used by `@sonenta/react-i18next`:
100
+ * `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`
101
+ *
102
+ * Pure data fetch — no DOM, no React, no runtime. Called during `astro build`
103
+ * so the strings inline into the static HTML (zero client JS).
104
+ */
105
+
106
+ /** A fetched bundle (a flat or nested dictionary of message strings). */
107
+ type Bundle = Record<string, unknown>;
108
+ /**
109
+ * Resolve the effective CDN host (no `/p`): explicit option wins, then the
110
+ * `SONENTA_CDN_BASE` env var, then the production default.
111
+ */
112
+ declare function resolveCdnBase(opts: Pick<SonentaI18nOptions, "cdnBase">): string;
113
+ /**
114
+ * Build the bundle URL for one `(locale, namespace)` pair.
115
+ * `{cdnBase}/p/{project}/{version}/latest/{locale}/{namespace}.json`
116
+ */
117
+ declare function buildBundleUrl(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase">, locale: Locale, namespace: Namespace): string;
118
+ /**
119
+ * Fetch a single bundle. Resolves to `null` on 404 (locale absent from the
120
+ * project — e.g. a plan-limit-blocked language) so callers fall back to the
121
+ * source language. Any other non-OK status or transport error throws so a
122
+ * misconfigured build fails loudly rather than silently shipping empty pages.
123
+ */
124
+ declare function fetchBundle(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locale: Locale, namespace: Namespace): Promise<Bundle | null>;
125
+ /**
126
+ * Fetch one namespace across every locale, in parallel. Absent locales map to
127
+ * `null`. Returns `Record<Locale, Bundle | null>`.
128
+ */
129
+ declare function fetchNamespace(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locales: Locale[], namespace: Namespace): Promise<Record<Locale, Bundle | null>>;
130
+ /**
131
+ * Fetch every `(locale, namespace)` pair. Returns a nested map keyed by
132
+ * locale then namespace: `data[locale][namespace] = Bundle | null`.
133
+ */
134
+ declare function fetchAll(opts: Pick<SonentaI18nOptions, "project" | "version" | "cdnBase" | "fetchImpl">, locales: Locale[], namespaces: Namespace[]): Promise<Record<Locale, Record<Namespace, Bundle | null>>>;
135
+
136
+ /**
137
+ * Build-time translation resolver.
138
+ *
139
+ * v0.1 SCOPE (frozen — see CONTRACT.md): string resolution + `{var}`
140
+ * interpolation + source-language fallback ONLY. Deliberately NO CLDR
141
+ * plurals, NO surfaces, NO a11y accessors, NO BCP-47 variant→base
142
+ * inheritance — those carry real i18n SEMANTICS owned by `@sonenta/i18n-core`
143
+ * and arrive additively in 0.2.0. Keeping them out of v0.1 means there is no
144
+ * naive semantics to break when the core swaps in underneath this same API.
145
+ */
146
+
147
+ /**
148
+ * Build a `t()` bound to `locale`, given the fetched data and resolution
149
+ * order. Lookup: active locale's namespace dict → each fallback locale's same
150
+ * namespace dict → the raw key (i18next-parity missing-key behaviour).
151
+ */
152
+ declare function createT(data: Record<Locale, Record<Namespace, Bundle | null>>, locale: Locale, fallbackChain: Locale[], defaultNamespace: Namespace): TFn;
153
+ /**
154
+ * Fetch every configured bundle at build time and return a resolved
155
+ * {@link SonentaI18n} handle. Call once (top-level `await`) and reuse its
156
+ * `getT(locale)` across pages.
157
+ */
158
+ declare function createSonentaI18n(options: SonentaI18nOptions): Promise<SonentaI18n>;
159
+
160
+ export { type Bundle, type Locale, type Namespace, type SonentaI18n, type SonentaI18nOptions, type TFn, type Vars, buildBundleUrl, createSonentaI18n, createT, fetchAll, fetchBundle, fetchNamespace, resolveCdnBase };