@astroscope/i18n 2.0.1 → 2.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.
package/README.md CHANGED
@@ -299,6 +299,15 @@ The same `import { t } from '@astroscope/i18n/translate'` works everywhere — b
299
299
 
300
300
  Translation chunks ship as raw MessageFormat 2 strings and are compiled in the browser on first use — the whole client runtime is ~8KB gzipped.
301
301
 
302
+ ### Telemetry
303
+
304
+ Through the adapter's telemetry (no-op without its SDK):
305
+
306
+ - `astro.i18n.missing` {`astro.i18n.locale`} — lookups that fell back. `t()` caches the fallback per locale until the next `setTranslations`, so a key counts once per translations version; `rich()` counts per call. The key itself goes to a debug log line (`missing translation`), never into the label.
307
+ - `astro.i18n.translations.age` {`astro.i18n.locale`} — seconds since the locale's last `setTranslations`. An app that refreshes translations periodically shows a sawtooth; a stalled refresh loop shows a line going up.
308
+
309
+ `/_i18n/` chunk requests are excluded from request logging and telemetry by the adapter's default excludes.
310
+
302
311
  ## MessageFormat 2 Syntax
303
312
 
304
313
  This library uses [Unicode MessageFormat 2](https://github.com/unicode-org/message-format-wg) (MF2), the modern standard for internationalization.
@@ -1,7 +1,7 @@
1
1
  import { t as RichComponents } from "../rich-BWszYAzS.js";
2
2
  import { a as TranslationMeta, i as TranslateFunction } from "../types-DU8QHch4.js";
3
3
  //#region src/client/translate.d.ts
4
- declare function getLocale(): string;
4
+ export declare function getLocale(): string;
5
5
  /**
6
6
  * Client-side translate function
7
7
  *
@@ -15,7 +15,7 @@ declare function getLocale(): string;
15
15
  *
16
16
  * Falls back to meta.fallback (dev) or key (prod) when translation is missing.
17
17
  */
18
- declare const t: TranslateFunction;
18
+ export declare const t: TranslateFunction;
19
19
  /**
20
20
  * Client-side rich text translation function
21
21
  *
@@ -35,7 +35,6 @@ declare const t: TranslateFunction;
35
35
  * // Returns: ['Read our ', <a href="/tos">Terms</a>]
36
36
  * ```
37
37
  */
38
- declare function rich<T = unknown>(key: string, meta?: TranslationMeta | string | undefined, components?: RichComponents<T> | undefined, values?: Record<string, unknown> | undefined): (string | T)[];
38
+ export declare function rich<T = unknown>(key: string, meta?: TranslationMeta | string | undefined, components?: RichComponents<T> | undefined, values?: Record<string, unknown> | undefined): (string | T)[];
39
39
  //#endregion
40
- export { getLocale, rich, t };
41
40
  //# sourceMappingURL=translate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/client/translate.ts"],"mappings":";;;iBAQgB;;;;;;;;;;;;;;cAiBH,GAAG;;;;;;;;;;;;;;;;;;;;iBAyDA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
1
+ {"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/client/translate.ts"],"mappings":";;;wBAQgB;;;;;;;;;;;;;;qBAiBH,GAAG;;;;;;;;;;;;;;;;;;;;wBAyDA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
@@ -1,5 +1,6 @@
1
1
  import { t as getGlobalState } from "./manifest-Cma68XM-.js";
2
2
  import { n as compileTranslations } from "./compiler-CkOVGWU5.js";
3
+ import { createObservableGauge } from "@astroscope/node/telemetry";
3
4
  import { createHash } from "node:crypto";
4
5
  import { AsyncLocalStorage } from "node:async_hooks";
5
6
  //#region src/extraction/hash.ts
@@ -34,6 +35,7 @@ var I18nSingleton = class {
34
35
  compiledCache = /* @__PURE__ */ new Map();
35
36
  hashCache = /* @__PURE__ */ new Map();
36
37
  chunkCache = /* @__PURE__ */ new Map();
38
+ updatedAt = /* @__PURE__ */ new Map();
37
39
  manifestGetter = null;
38
40
  manifestVersion = 0;
39
41
  async configure(config) {
@@ -67,6 +69,7 @@ var I18nSingleton = class {
67
69
  }
68
70
  setTranslations(locale, raw) {
69
71
  this.rawCache.set(locale, raw);
72
+ this.updatedAt.set(locale, Date.now());
70
73
  this.mergedCache.delete(locale);
71
74
  this.compiledCache.delete(locale);
72
75
  this.hashCache.delete(locale);
@@ -156,12 +159,20 @@ var I18nSingleton = class {
156
159
  const targets = this.resolveLocales(locales);
157
160
  for (const locale of targets) {
158
161
  this.rawCache.delete(locale);
162
+ this.updatedAt.delete(locale);
159
163
  this.mergedCache.delete(locale);
160
164
  this.compiledCache.delete(locale);
161
165
  this.hashCache.delete(locale);
162
166
  for (const key of this.chunkCache.keys()) if (key.startsWith(`${locale}:`)) this.chunkCache.delete(key);
163
167
  }
164
168
  }
169
+ /** seconds since each locale's translations were last set */
170
+ getTranslationsAge(now = Date.now()) {
171
+ return [...this.updatedAt].map(([locale, at]) => ({
172
+ locale,
173
+ seconds: (now - at) / 1e3
174
+ }));
175
+ }
165
176
  /**
166
177
  * invalidate derived caches when the extraction manifest has changed (dev mode HMR)
167
178
  */
@@ -180,6 +191,13 @@ var I18nSingleton = class {
180
191
  }
181
192
  };
182
193
  const i18n = new I18nSingleton();
194
+ createObservableGauge("astro.i18n.translations.age", {
195
+ scope: "@astroscope/i18n",
196
+ description: "Seconds since the translations of a locale were last set",
197
+ unit: "s"
198
+ }, (result) => {
199
+ for (const { locale, seconds } of i18n.getTranslationsAge()) result.observe(seconds, { "astro.i18n.locale": locale });
200
+ });
183
201
  //#endregion
184
202
  //#region src/server/context.ts
185
203
  const als = new AsyncLocalStorage();
@@ -198,4 +216,4 @@ function getContext() {
198
216
  //#endregion
199
217
  export { runWithContext as n, i18n as r, getContext as t };
200
218
 
201
- //# sourceMappingURL=context-CYqyZJyH.js.map
219
+ //# sourceMappingURL=context-CgqIoo00.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-CgqIoo00.js","names":[],"sources":["../src/extraction/hash.ts","../src/server/i18n.ts","../src/server/context.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { RawTranslations } from '../shared/types.js';\nimport type { ChunkManifest } from './types.js';\n\n/**\n * Hash a string to a short hash (for cache busting)\n */\nfunction hashString(str: string): string {\n return createHash('sha256').update(str).digest('hex').substring(0, 8);\n}\n\n/**\n * Compute hash for chunk translations\n */\nexport function computeChunkHash(translations: RawTranslations, keys: string[]): string {\n const relevantTranslations: RawTranslations = {};\n\n for (const key of [...keys].sort()) {\n if (translations[key] !== undefined) {\n relevantTranslations[key] = translations[key];\n }\n }\n\n return hashString(JSON.stringify(relevantTranslations));\n}\n\n/**\n * Compute hashes for all chunks in manifest\n */\nexport function computeAllChunkHashes(translations: RawTranslations, manifest: ChunkManifest): Record<string, string> {\n const hashes: Record<string, string> = {};\n\n for (const [chunkId, keys] of Object.entries(manifest)) {\n hashes[chunkId] = computeChunkHash(translations, keys);\n }\n\n return hashes;\n}\n","import { createObservableGauge } from '@astroscope/node/telemetry';\nimport { computeAllChunkHashes } from '../extraction/hash.js';\nimport { getGlobalState } from '../extraction/manifest.js';\nimport type { ExtractionManifest } from '../extraction/types.js';\nimport { compileTranslations } from '../shared/compiler.js';\nimport type { CompiledTranslations, RawTranslations } from '../shared/types.js';\nimport type { FallbackBehavior } from './types.js';\n\nexport type I18nConfig = {\n locales: string[];\n defaultLocale?: string | undefined;\n fallback?: FallbackBehavior | undefined;\n};\n\ntype NormalizedConfig = {\n locales: string[];\n defaultLocale: string;\n fallback: FallbackBehavior;\n};\n\nclass I18nSingleton {\n // normalized user config with defaults applied\n private config: NormalizedConfig | null = null;\n\n // locale -> raw translation strings (before ICU compilation)\n private rawCache = new Map<string, RawTranslations>();\n\n // locale -> raw translations merged with manifest fallbacks\n private mergedCache = new Map<string, RawTranslations>();\n\n // locale -> compiled ICU MessageFormat functions\n private compiledCache = new Map<string, CompiledTranslations>();\n\n // locale -> chunk name -> content hash (for cache busting)\n private hashCache = new Map<string, Record<string, string>>();\n\n // \"locale:chunkName\" -> encoded chunk response body\n private chunkCache = new Map<string, Uint8Array>();\n\n // locale -> epoch ms of the last setTranslations, for the age gauge\n private updatedAt = new Map<string, number>();\n\n // manifest getter, set on configure() via the virtual module (live data in dev mode)\n private manifestGetter: (() => ExtractionManifest) | null = null;\n\n // tracks the manifest version to detect when derived caches need invalidation\n private manifestVersion = 0;\n\n async configure(config: I18nConfig): Promise<void> {\n if (!config.locales?.length) {\n throw new Error('i18n.configure(): locales array is required and must not be empty');\n }\n\n for (const locale of config.locales) {\n if (!locale) {\n throw new Error('i18n.configure(): locale cannot be empty');\n }\n\n if (locale !== locale.trim()) {\n throw new Error(`i18n.configure(): locale \"${locale}\" has leading or trailing whitespace`);\n }\n }\n\n if (new Set(config.locales).size !== config.locales.length) {\n throw new Error('i18n.configure(): locales array contains duplicates');\n }\n\n if (config.defaultLocale !== undefined) {\n if (!config.defaultLocale) {\n throw new Error('i18n.configure(): defaultLocale cannot be empty');\n }\n\n if (config.defaultLocale !== config.defaultLocale.trim()) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" has leading or trailing whitespace`);\n }\n\n if (!config.locales.includes(config.defaultLocale)) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" is not in locales array`);\n }\n }\n\n this.config = {\n locales: config.locales,\n defaultLocale: config.defaultLocale ?? config.locales[0]!,\n fallback: config.fallback ?? 'fallback',\n };\n\n // resolved lazily so importing the singleton needs no virtual module\n if (!this.manifestGetter) {\n const m = await import('virtual:@astroscope/i18n/manifest');\n this.manifestGetter = m.getManifest;\n }\n }\n\n isConfigured(): boolean {\n return this.config !== null;\n }\n\n getConfig(): NormalizedConfig {\n if (!this.config) {\n throw new Error('i18n not configured. Call i18n.configure() first.');\n }\n\n return this.config;\n }\n\n setTranslations(locale: string, raw: RawTranslations): void {\n this.rawCache.set(locale, raw);\n this.updatedAt.set(locale, Date.now());\n this.mergedCache.delete(locale); // invalidate merged cache\n this.compiledCache.delete(locale); // invalidate compiled cache\n this.hashCache.delete(locale); // invalidate hash cache\n\n // invalidate chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n\n // recompute hashes if chunk manifest exists (production only)\n const { chunks } = this.getManifest();\n\n if (Object.keys(chunks).length > 0) {\n this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));\n }\n }\n\n getTranslations(locale: string): RawTranslations {\n this.invalidateIfManifestChanged();\n\n const cached = this.mergedCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const raw = this.rawCache.get(locale) ?? {};\n const manifest = this.manifestGetter?.() ?? { keys: [], chunks: {}, scripts: [] };\n\n if (manifest.keys.length === 0) {\n return raw;\n }\n\n // merge fallbacks for keys that don't have translations\n const withFallbacks: RawTranslations = { ...raw };\n\n for (const { key, meta } of manifest.keys) {\n if (!(key in withFallbacks) && meta.fallback) {\n withFallbacks[key] = meta.fallback;\n }\n }\n\n this.mergedCache.set(locale, withFallbacks);\n\n return withFallbacks;\n }\n\n getCompiledTranslations(locale: string): CompiledTranslations {\n const cached = this.compiledCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const compiled = compileTranslations(locale, this.getTranslations(locale));\n\n this.compiledCache.set(locale, compiled);\n\n return compiled;\n }\n\n /**\n * Get the extraction manifest (keys + chunks).\n * - keys: translation keys extracted from codebase with metadata\n * - chunks: mapping of chunk names to their translation keys\n */\n getManifest(): ExtractionManifest {\n if (!this.manifestGetter) {\n throw new Error('i18n manifest not initialized. Call i18n.configure() first.');\n }\n\n return this.manifestGetter();\n }\n\n getHashes(locale: string): Record<string, string> {\n return this.hashCache.get(locale) ?? {};\n }\n\n /**\n * Get the encoded chunk response body for a locale/chunk combination.\n * Cached to avoid repeated JSON.stringify and encoding on each request.\n * Returns undefined if chunk not found in manifest.\n */\n getChunkBody(locale: string, chunkName: string): Uint8Array | undefined {\n const keys = this.getManifest().chunks[chunkName];\n\n if (!keys) {\n return undefined;\n }\n\n const cacheKey = `${locale}:${chunkName}`;\n const cached = this.chunkCache.get(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const translations = this.getTranslations(locale);\n const chunkTranslations: RawTranslations = {};\n\n for (const key of keys) {\n if (translations[key]) {\n chunkTranslations[key] = translations[key];\n }\n }\n\n const js = `/* @astroscope/i18n chunk: ${chunkName} */\n(function() {\n var i = window.__i18n__;\n if (i) Object.assign(i.translations, ${JSON.stringify(chunkTranslations)});\n})();\n`;\n\n const body = new TextEncoder().encode(js);\n\n this.chunkCache.set(cacheKey, body);\n\n return body;\n }\n\n /**\n * Hashes for chunks reachable from astro `<script>` entries. Script modules are\n * not islands, so the islands emitter never covers them — the middleware\n * bootstraps these few into the client state on every html response.\n */\n getScriptChunkHashes(locale: string): Record<string, string> {\n const { scripts } = this.getManifest();\n const hashes = this.getHashes(locale);\n const result: Record<string, string> = {};\n\n for (const chunk of scripts) {\n const hash = hashes[chunk];\n\n if (hash) {\n result[chunk] = hash;\n }\n }\n\n return result;\n }\n\n /**\n * Clear translations for specified locale(s).\n * @param locales - Locale(s) to clear, or all if not specified\n */\n clear(locales?: string | string[] | undefined): void {\n const targets = this.resolveLocales(locales);\n\n for (const locale of targets) {\n this.rawCache.delete(locale);\n this.updatedAt.delete(locale);\n this.mergedCache.delete(locale);\n this.compiledCache.delete(locale);\n this.hashCache.delete(locale);\n\n // clear chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n }\n }\n\n /** seconds since each locale's translations were last set */\n getTranslationsAge(now = Date.now()): { locale: string; seconds: number }[] {\n return [...this.updatedAt].map(([locale, at]) => ({ locale, seconds: (now - at) / 1000 }));\n }\n\n /**\n * invalidate derived caches when the extraction manifest has changed (dev mode HMR)\n */\n private invalidateIfManifestChanged(): void {\n const { version } = getGlobalState();\n\n if (version === this.manifestVersion) {\n return;\n }\n\n this.manifestVersion = version;\n this.mergedCache.clear();\n this.compiledCache.clear();\n this.chunkCache.clear();\n }\n\n private resolveLocales(locales?: string | string[] | undefined): string[] {\n if (!locales) {\n return this.config?.locales ?? [];\n }\n\n if (typeof locales === 'string') {\n return [locales];\n }\n\n return locales;\n }\n}\n\nexport const i18n = new I18nSingleton();\n\n// the one signal for a stalled refresh loop: an app that reloads translations\n// periodically shows a sawtooth here, a stuck one a line going up\ncreateObservableGauge(\n 'astro.i18n.translations.age',\n { scope: '@astroscope/i18n', description: 'Seconds since the translations of a locale were last set', unit: 's' },\n (result) => {\n for (const { locale, seconds } of i18n.getTranslationsAge()) {\n result.observe(seconds, { 'astro.i18n.locale': locale });\n }\n },\n);\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { I18nContext } from './types.js';\n\nconst als = new AsyncLocalStorage<I18nContext>();\n\n/**\n * Run a function with i18n context (called by middleware)\n */\nexport function runWithContext<T>(context: I18nContext, fn: () => T): T {\n return als.run(context, fn);\n}\n\n/**\n * Get the current i18n context\n */\nexport function getContext(): I18nContext | null {\n return als.getStore() ?? null;\n}\n"],"mappings":"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"}
@@ -7,7 +7,7 @@ import { Plugin } from "vite";
7
7
  * Get the current extraction manifest (for dev mode live access).
8
8
  * This bypasses Vite's module caching to return fresh data.
9
9
  */
10
- declare function getManifest(): ExtractionManifest;
10
+ export declare function getManifest(): ExtractionManifest;
11
11
  //#endregion
12
12
  //#region src/extraction/vite-plugin.d.ts
13
13
  type I18nVitePluginOptions = {
@@ -24,7 +24,7 @@ type I18nVitePluginOptions = {
24
24
  * 4. Generates a manifest for syncing with translation providers
25
25
  * 5. Injects translation loader into chunks that use t()
26
26
  */
27
- declare function i18nVitePlugin(options: I18nVitePluginOptions): Plugin;
27
+ export declare function i18nVitePlugin(options: I18nVitePluginOptions): Plugin;
28
28
  //#endregion
29
- export { type ChunkManifest, type ExtractedKey, type ExtractionManifest, type I18nVitePluginOptions, getManifest, i18nVitePlugin };
29
+ export type { ChunkManifest, ExtractedKey, ExtractionManifest, I18nVitePluginOptions };
30
30
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/extraction/manifest.ts","../../src/extraction/vite-plugin.ts"],"mappings":";;;;;;;;;iBA+CgB,eAAe;;;KCvBnB;EACV,QAAQ;EACR,aAAa;;;;;;;;;;;;iBAuCC,eAAe,SAAS,wBAAwB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/extraction/manifest.ts","../../src/extraction/vite-plugin.ts"],"mappings":";;;;;;;;;wBA+CgB,eAAe;;;KCvBnB;EACV,QAAQ;EACR,aAAa;;;;;;;;;;;;wBAuCC,eAAe,SAAS,wBAAwB"}
@@ -33,6 +33,7 @@ declare class I18nSingleton {
33
33
  private compiledCache;
34
34
  private hashCache;
35
35
  private chunkCache;
36
+ private updatedAt;
36
37
  private manifestGetter;
37
38
  private manifestVersion;
38
39
  configure(config: I18nConfig): Promise<void>;
@@ -65,6 +66,11 @@ declare class I18nSingleton {
65
66
  * @param locales - Locale(s) to clear, or all if not specified
66
67
  */
67
68
  clear(locales?: string | string[] | undefined): void;
69
+ /** seconds since each locale's translations were last set */
70
+ getTranslationsAge(now?: number): {
71
+ locale: string;
72
+ seconds: number;
73
+ }[];
68
74
  /**
69
75
  * invalidate derived caches when the extraction manifest has changed (dev mode HMR)
70
76
  */
@@ -74,4 +80,4 @@ declare class I18nSingleton {
74
80
  declare const i18n: I18nSingleton;
75
81
  //#endregion
76
82
  export { I18nContext as i, i18n as n, FallbackBehavior as r, I18nConfig as t };
77
- //# sourceMappingURL=i18n-Bp8TFmd3.d.ts.map
83
+ //# sourceMappingURL=i18n-Czkrfb_4.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-Czkrfb_4.d.ts","names":[],"sources":["../src/server/types.ts","../src/server/i18n.ts"],"mappings":";;;;;;KAKY,oDACuB,aAAa,OAAO;;;;KAK3C;EACV;EAEA,cAAc;EAEd,iBAAiB;EACjB,UAAU;;;;KCTA;EACV;EACA;EACA,WAAW;;KAGR;EACH;EACA;EACA,UAAU;;cAGN;UAEI;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;EAEF,UAAU,QAAQ,aAAa;EA8CrC;EAIA,aAAa;EAQb,gBAAgB,gBAAgB,KAAK;EAsBrC,gBAAgB,iBAAiB;EA8BjC,wBAAwB,iBAAiB;;;;;;EAmBzC,eAAe;EAQf,UAAU,iBAAiB;;;;;;EAS3B,aAAa,gBAAgB,oBAAoB;;;;;;EA0CjD,qBAAqB,iBAAiB;;;;;EAoBtC,MAAM;;EAoBN,mBAAmB;IAAqB;IAAgB;;;;;UAOhD;UAaA;;cAaG,MAAI"}
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { a as TranslationMeta, r as RawTranslations } from "./types-DU8QHch4.js";
2
2
  import { n as ExtractedKey, r as ExtractionManifest } from "./types-f-SNJdvf.js";
3
3
  import { n as I18nOptions } from "./types-Bo4jMK8g.js";
4
- import { n as i18n, r as FallbackBehavior, t as I18nConfig } from "./i18n-Bp8TFmd3.js";
4
+ import { n as i18n, r as FallbackBehavior, t as I18nConfig } from "./i18n-Czkrfb_4.js";
5
5
  import { ExcludePattern } from "@astroscope/node/excludes";
6
6
  import { APIContext, AstroIntegration, MiddlewareHandler } from "astro";
7
7
  //#region src/integration/integration.d.ts
@@ -15,7 +15,7 @@ import { APIContext, AstroIntegration, MiddlewareHandler } from "astro";
15
15
  * emitter (via `@astroscope/node`) slices hashes and preloads translation chunks
16
16
  * per island — plain `client:*` directives, no forks.
17
17
  */
18
- declare function i18nIntegration(options?: I18nOptions): AstroIntegration;
18
+ export default function i18nIntegration(options?: I18nOptions): AstroIntegration;
19
19
  //#endregion
20
20
  //#region src/server/middleware.d.ts
21
21
  type I18nMiddlewareOptions = {
@@ -63,14 +63,14 @@ type I18nMiddlewareOptions = {
63
63
  * });
64
64
  * ```
65
65
  */
66
- declare function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler;
66
+ export declare function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler;
67
67
  //#endregion
68
68
  //#region src/server/locale.d.ts
69
69
  /**
70
70
  * Detect locale from Accept-Language header.
71
71
  * Returns best match from configured locales, or undefined if none match.
72
72
  */
73
- declare function detectLocale(request: Request): string | undefined;
73
+ export declare function detectLocale(request: Request): string | undefined;
74
74
  //#endregion
75
- export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type I18nMiddlewareOptions, type RawTranslations, type TranslationMeta, createI18nMiddleware, i18nIntegration as default, detectLocale, i18n };
75
+ export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type I18nMiddlewareOptions, type RawTranslations, type TranslationMeta, i18n };
76
76
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration/integration.ts","../src/server/middleware.ts","../src/server/locale.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAcwB,gBAAgB,UAAS,cAAmB;;;KCNxD;EACV,SAAS,KAAK;;;;;;;;;;;;;;;;;;;;;;EAsBd,UAAU,qBAAqB,SAAS;;;;;;;;;;;;;;;;;;;;;;iBA2H1B,qBAAqB,SAAS,wBAAwB;;;;;;;iBCpJtD,aAAa,SAAS"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration/integration.ts","../src/server/middleware.ts","../src/server/locale.ts"],"mappings":";;;;;;;;;;;;;;;;;wBAcwB,gBAAgB,UAAS,cAAmB;;;KCNxD;EACV,SAAS,KAAK;;;;;;;;;;;;;;;;;;;;;;EAsBd,UAAU,qBAAqB,SAAS;;;;;;;;;;;;;;;;;;;;;;wBA2H1B,qBAAqB,SAAS,wBAAwB;;;;;;;wBCpJtD,aAAa,SAAS"}
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { t as i18nVitePlugin } from "./vite-plugin-C-jEpd8t.js";
2
- import { r as i18n } from "./context-CYqyZJyH.js";
3
- import { n as createI18nMiddleware } from "./middleware-DxKY84Iv.js";
2
+ import { r as i18n } from "./context-CgqIoo00.js";
3
+ import { n as createI18nMiddleware } from "./middleware-M9Q8sVRc.js";
4
4
  //#region src/integration/integration.ts
5
5
  /**
6
6
  * Astro integration for i18n with automatic per-chunk translation loading.
@@ -1,10 +1,10 @@
1
1
  import { n as I18nClientState } from "./types-DU8QHch4.js";
2
- import { i as I18nContext, n as i18n } from "./i18n-Bp8TFmd3.js";
2
+ import { i as I18nContext, n as i18n } from "./i18n-Czkrfb_4.js";
3
3
  //#region src/server/context.d.ts
4
4
  /**
5
5
  * Get the current i18n context
6
6
  */
7
- declare function getContext(): I18nContext | null;
7
+ export declare function getContext(): I18nContext | null;
8
8
  //#endregion
9
- export { type I18nClientState, getContext, i18n };
9
+ export { type I18nClientState, i18n };
10
10
  //# sourceMappingURL=internal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"internal.d.ts","names":[],"sources":["../src/server/context.ts"],"mappings":";;;;;;iBAegB,cAAc"}
1
+ {"version":3,"file":"internal.d.ts","names":[],"sources":["../src/server/context.ts"],"mappings":";;;;;;wBAegB,cAAc"}
package/dist/internal.js CHANGED
@@ -1,2 +1,2 @@
1
- import { r as i18n, t as getContext } from "./context-CYqyZJyH.js";
1
+ import { r as i18n, t as getContext } from "./context-CgqIoo00.js";
2
2
  export { getContext, i18n };
@@ -1,5 +1,5 @@
1
1
  import { t as buildI18nChunkUrl } from "./url-CifeaDzl.js";
2
- import { n as runWithContext, r as i18n } from "./context-CYqyZJyH.js";
2
+ import { n as runWithContext, r as i18n } from "./context-CgqIoo00.js";
3
3
  import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
4
4
  import { log, overrideRequestRoute } from "@astroscope/node/log";
5
5
  import { registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node/islands";
@@ -45,16 +45,14 @@ function createScriptHashesScript(locale) {
45
45
  * least a network round-trip later — so each island's hashes win their race
46
46
  * even while the rest of the document is still streaming. Chunks already
47
47
  * covered earlier in the document are skipped.
48
- * - translation-chunk preload links for the static closure, emitted as link tags
49
- * for immediate islands.
50
- * - translation-chunk eager imports for the full closure — dynamic imports
51
- * included — fired by the deferred gate the moment a directive schedules
52
- * hydration. Translation chunks are idempotent data modules, so evaluating
53
- * them ahead of the component costs nothing, and the loader's awaited import
54
- * then hits the module cache instead of fetching serially after the component
55
- * graph evaluated. Every island carries its complete i18n set (no per-document
56
- * dedup — the gate dedups at fire time), so a chunk shared between islands is
57
- * covered by whichever gate fires first.
48
+ * - translation-chunk preload links for the static closure (link tags on
49
+ * immediate islands).
50
+ * - translation-chunk eager imports for the full closure, dynamic chunks
51
+ * included, fired by the deferred gate: the chunks are idempotent data
52
+ * modules, so early evaluation is free and the loader's awaited import hits
53
+ * the module cache instead of fetching after the component graph evaluated.
54
+ * No per-document dedup — a shared chunk is covered by whichever island's
55
+ * gate fires first.
58
56
  *
59
57
  * The locale is recorded per request by the i18n middleware — the emitter runs
60
58
  * while the response streams, outside the middleware's AsyncLocalStorage scope.
@@ -70,6 +68,46 @@ function setRequestLocale(request, locale) {
70
68
  function urlToChunkName(url) {
71
69
  return url.slice(url.lastIndexOf("/") + 1).replace(/\.js$/, "");
72
70
  }
71
+ const slices = /* @__PURE__ */ new WeakMap();
72
+ const inits = /* @__PURE__ */ new Map();
73
+ function sliceFor(island, locale, hashes) {
74
+ let byIsland = slices.get(hashes);
75
+ if (!byIsland) {
76
+ byIsland = /* @__PURE__ */ new WeakMap();
77
+ slices.set(hashes, byIsland);
78
+ }
79
+ let slice = byIsland.get(island);
80
+ if (slice) return slice;
81
+ const translated = (urls) => {
82
+ const result = [];
83
+ for (const url of urls) {
84
+ const chunk = urlToChunkName(url);
85
+ const hash = hashes[chunk];
86
+ if (hash) result.push([chunk, hash]);
87
+ }
88
+ return result;
89
+ };
90
+ const chunks = translated(island.fullClosure);
91
+ slice = {
92
+ chunks,
93
+ links: translated(island.staticClosure).map(([chunk, hash]) => buildI18nChunkUrl(locale, chunk, hash)),
94
+ imports: chunks.map(([chunk, hash]) => buildI18nChunkUrl(locale, chunk, hash))
95
+ };
96
+ byIsland.set(island, slice);
97
+ return slice;
98
+ }
99
+ function initFor(locale) {
100
+ let init = inits.get(locale);
101
+ if (init === void 0) {
102
+ init = jsonForScript({
103
+ locale,
104
+ hashes: {},
105
+ translations: {}
106
+ });
107
+ inits.set(locale, init);
108
+ }
109
+ return init;
110
+ }
73
111
  /**
74
112
  * The document-level bootstrap for `window.__i18n__`, exported for tests.
75
113
  */
@@ -91,39 +129,20 @@ function registerI18nEmitters() {
91
129
  if (!context || !i18n.isConfigured()) return null;
92
130
  const locale = requestLocales.get(context.request);
93
131
  if (locale === void 0) return null;
94
- const hashes = i18n.getHashes(locale);
132
+ const { chunks, links, imports } = sliceFor(island, locale, i18n.getHashes(locale));
95
133
  let emitted = emittedByContext.get(context);
96
134
  if (!emitted) {
97
135
  emitted = /* @__PURE__ */ new Set();
98
136
  emittedByContext.set(context, emitted);
99
137
  }
100
138
  const merge = {};
101
- for (const url of island.fullClosure) {
102
- const chunk = urlToChunkName(url);
103
- const hash = hashes[chunk];
104
- if (hash && !emitted.has(chunk)) {
105
- emitted.add(chunk);
106
- merge[chunk] = hash;
107
- }
139
+ let fresh = false;
140
+ for (const [chunk, hash] of chunks) if (!emitted.has(chunk)) {
141
+ emitted.add(chunk);
142
+ merge[chunk] = hash;
143
+ fresh = true;
108
144
  }
109
- const links = [];
110
- for (const url of island.staticClosure) {
111
- const chunk = urlToChunkName(url);
112
- const hash = hashes[chunk];
113
- if (hash) links.push(buildI18nChunkUrl(locale, chunk, hash));
114
- }
115
- const imports = [];
116
- for (const url of island.fullClosure) {
117
- const chunk = urlToChunkName(url);
118
- const hash = hashes[chunk];
119
- if (hash) imports.push(buildI18nChunkUrl(locale, chunk, hash));
120
- }
121
- const init = jsonForScript({
122
- locale,
123
- hashes: {},
124
- translations: {}
125
- });
126
- const html = Object.keys(merge).length > 0 ? `<script>{const i=window.__i18n__??=${init};Object.assign(i.hashes,${jsonForScript(merge)});}<\/script>` : void 0;
145
+ const html = fresh ? `<script>{const i=window.__i18n__??=${initFor(locale)};Object.assign(i.hashes,${jsonForScript(merge)});}<\/script>` : void 0;
127
146
  if (!html && links.length === 0 && imports.length === 0) return null;
128
147
  return {
129
148
  html,
@@ -212,7 +231,7 @@ function createI18nChunkMiddleware() {
212
231
  */
213
232
  function createI18nMiddleware(options) {
214
233
  registerI18nEmitters();
215
- return async (ctx, next) => {
234
+ return (ctx, next) => {
216
235
  if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) return next();
217
236
  if (!i18n.isConfigured()) {
218
237
  log.error("i18n not configured, passing through");
@@ -232,4 +251,4 @@ function createI18nMiddleware(options) {
232
251
  //#endregion
233
252
  export { createI18nMiddleware as n, createI18nChunkMiddleware as t };
234
253
 
235
- //# sourceMappingURL=middleware-DxKY84Iv.js.map
254
+ //# sourceMappingURL=middleware-M9Q8sVRc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware-M9Q8sVRc.js","names":[],"sources":["../src/server/client-state.ts","../src/server/islands-emitter.ts","../src/server/middleware.ts"],"sourcesContent":["import { i18n } from './i18n.js';\n\n/**\n * Scripts bootstrapping `window.__i18n__` on html responses, delivered through the\n * `@astroscope/node` document emitter (see islands-emitter.ts for the placement\n * policy). Both writers merge orderlessly (`??=` + assign), so emitter scripts and\n * the bootstrap can arrive in either order.\n */\n\n/** translation values may contain `</script>` — escape `<` so they cannot end the tag */\nexport function jsonForScript(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n}\n\n/** the full dev-mode state: every translation for the locale */\nexport function createFullStateScript(locale: string): string {\n const translations = i18n.getTranslations(locale);\n const state = jsonForScript({ locale, hashes: {}, translations });\n\n return `<script>window.__i18n__=Object.assign(window.__i18n__??{hashes:{},translations:{}},${state});</script>`;\n}\n\n/** the prod bootstrap: chunk hashes for astro `<script>` entry consumers */\nexport function createScriptHashesScript(locale: string): string | null {\n const hashes = i18n.getScriptChunkHashes(locale);\n\n if (Object.keys(hashes).length === 0) {\n return null;\n }\n\n const init = jsonForScript({ locale, hashes: {}, translations: {} });\n\n return `<script>{const i=window.__i18n__??=${init};Object.assign(i.hashes,${jsonForScript(hashes)});}</script>`;\n}\n","import {\n type DocumentEmitter,\n type IslandInfo,\n registerDocumentEmitter,\n registerIslandEmitter,\n} from '@astroscope/node/islands';\nimport type { APIContext } from 'astro';\nimport { buildI18nChunkUrl } from '../shared/url.js';\nimport { createFullStateScript, createScriptHashesScript, jsonForScript } from './client-state.js';\nimport { i18n } from './i18n.js';\n\n/**\n * Contributes the i18n slice for each island the `@astroscope/node` islands\n * middleware streams past:\n *\n * - an inline merge script with the hashes of the island's full chunk closure —\n * dynamic imports included, since a `React.lazy` chunk's injected loader reads\n * its hash at execution time. Emitted before the island tag, it is parsed\n * strictly before the island connects, and the island's chunks execute at\n * least a network round-trip later — so each island's hashes win their race\n * even while the rest of the document is still streaming. Chunks already\n * covered earlier in the document are skipped.\n * - translation-chunk preload links for the static closure (link tags on\n * immediate islands).\n * - translation-chunk eager imports for the full closure, dynamic chunks\n * included, fired by the deferred gate: the chunks are idempotent data\n * modules, so early evaluation is free and the loader's awaited import hits\n * the module cache instead of fetching after the component graph evaluated.\n * No per-document dedup — a shared chunk is covered by whichever island's\n * gate fires first.\n *\n * The locale is recorded per request by the i18n middleware — the emitter runs\n * while the response streams, outside the middleware's AsyncLocalStorage scope.\n */\n\nconst requestLocales = new WeakMap<Request, string>();\nconst emittedByContext = new WeakMap<APIContext, Set<string>>();\n\n// vite defines import.meta.env when bundling the middleware; typed structurally\n// so the package compiles without vite's global client types\nconst IS_DEV = !!(import.meta as { env?: { DEV?: boolean } }).env?.DEV;\n\nlet registered = false;\n\nexport function setRequestLocale(request: Request, locale: string): void {\n requestLocales.set(request, locale);\n}\n\n/** public url of a chunk → manifest chunk name, e.g. `…/_astro/Cart.abc.js` → `Cart.abc` */\nfunction urlToChunkName(url: string): string {\n return url.slice(url.lastIndexOf('/') + 1).replace(/\\.js$/, '');\n}\n\n/** an island's translated chunks and their urls for one locale */\ntype IslandSlice = {\n /** full closure: what the merge script covers */\n chunks: [name: string, hash: string][];\n /** static closure: preload links */\n links: string[];\n /** full closure: eager imports */\n imports: string[];\n};\n\n// islands arrive as the same object per component/renderer/directive and the\n// hashes object is replaced whenever translations change, so keying on both\n// caches each slice for exactly as long as it is valid\nconst slices = new WeakMap<Record<string, string>, WeakMap<IslandInfo, IslandSlice>>();\nconst inits = new Map<string, string>();\n\nfunction sliceFor(island: IslandInfo, locale: string, hashes: Record<string, string>): IslandSlice {\n let byIsland = slices.get(hashes);\n\n if (!byIsland) {\n byIsland = new WeakMap();\n slices.set(hashes, byIsland);\n }\n\n let slice = byIsland.get(island);\n\n if (slice) {\n return slice;\n }\n\n const translated = (urls: string[]): [string, string][] => {\n const result: [string, string][] = [];\n\n for (const url of urls) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash) {\n result.push([chunk, hash]);\n }\n }\n\n return result;\n };\n\n const chunks = translated(island.fullClosure);\n\n slice = {\n chunks,\n links: translated(island.staticClosure).map(([chunk, hash]) => buildI18nChunkUrl(locale, chunk, hash)),\n imports: chunks.map(([chunk, hash]) => buildI18nChunkUrl(locale, chunk, hash)),\n };\n byIsland.set(island, slice);\n\n return slice;\n}\n\nfunction initFor(locale: string): string {\n let init = inits.get(locale);\n\n if (init === undefined) {\n init = jsonForScript({ locale, hashes: {}, translations: {} });\n inits.set(locale, init);\n }\n\n return init;\n}\n\n/**\n * The document-level bootstrap for `window.__i18n__`, exported for tests.\n */\nexport function createI18nDocumentEmitter(dev: boolean = IS_DEV): DocumentEmitter {\n return (context) => {\n if (!i18n.isConfigured()) {\n return null;\n }\n\n const locale = requestLocales.get(context.request);\n\n if (locale === undefined) {\n return null;\n }\n\n if (dev) {\n return { head: createFullStateScript(locale) };\n }\n\n if (Object.keys(i18n.getManifest().chunks).length === 0) {\n return null;\n }\n\n return { end: () => createScriptHashesScript(locale) };\n };\n}\n\nexport function registerI18nEmitters(): void {\n if (registered) {\n return;\n }\n\n registered = true;\n\n registerDocumentEmitter(createI18nDocumentEmitter());\n\n registerIslandEmitter((island, context) => {\n // no request context (prerendered build pass) — the locale is unknowable\n if (!context || !i18n.isConfigured()) {\n return null;\n }\n\n const locale = requestLocales.get(context.request);\n\n if (locale === undefined) {\n return null;\n }\n\n const hashes = i18n.getHashes(locale);\n const { chunks, links, imports } = sliceFor(island, locale, hashes);\n\n let emitted = emittedByContext.get(context);\n\n if (!emitted) {\n emitted = new Set();\n emittedByContext.set(context, emitted);\n }\n\n const merge: Record<string, string> = {};\n let fresh = false;\n\n for (const [chunk, hash] of chunks) {\n if (!emitted.has(chunk)) {\n emitted.add(chunk);\n merge[chunk] = hash;\n fresh = true;\n }\n }\n\n const html = fresh\n ? `<script>{const i=window.__i18n__??=${initFor(locale)};Object.assign(i.hashes,${jsonForScript(merge)});}</script>`\n : undefined;\n\n if (!html && links.length === 0 && imports.length === 0) {\n return null;\n }\n\n return {\n html,\n links: links.length > 0 ? links : undefined,\n imports: imports.length > 0 ? imports : undefined,\n };\n });\n}\n","import { type ExcludePattern, RECOMMENDED_EXCLUDES, shouldExclude } from '@astroscope/node/excludes';\nimport { log, overrideRequestRoute } from '@astroscope/node/log';\nimport type { APIContext, MiddlewareHandler } from 'astro';\nimport { runWithContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport { registerI18nEmitters, setRequestLocale } from './islands-emitter.js';\nimport type { I18nContext } from './types.js';\n\nexport type I18nMiddlewareOptions = {\n locale: (ctx: APIContext) => string;\n /**\n * Patterns to exclude from locale context setup.\n * Defaults to RECOMMENDED_EXCLUDES if not provided.\n *\n * @example\n * ```ts\n * import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';\n *\n * // extend defaults\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],\n * })\n *\n * // disable excludes entirely\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [],\n * })\n * ```\n */\n exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | undefined;\n};\n\nconst I18N_ENDPOINT_PREFIX = '/_i18n/';\n\n// astro has no route behind the chunk endpoint, so requests would otherwise be\n// logged and measured under whatever routing matched — typically `/404`\nconst I18N_ENDPOINT_ROUTE = '/_i18n/[locale]/[chunk]';\n\n/**\n * Serve a translation chunk. Returns `undefined` when the path isn't a chunk\n * request, and the caller passes it on to astro.\n */\nfunction createChunkResponse(pathname: string): Response | undefined {\n if (!pathname.startsWith(I18N_ENDPOINT_PREFIX)) {\n return undefined;\n }\n\n if (!i18n.isConfigured()) {\n log.error('i18n not configured, passing through');\n\n return undefined;\n }\n\n const path = pathname.slice(I18N_ENDPOINT_PREFIX.length);\n\n // attempt to parse as efficient as possible\n // expected path format: {locale}/{chunkName}.{hash}.js\n // avoiding regex or split for performance\n const slashIdx = path.indexOf('/');\n\n if (slashIdx === -1) {\n return undefined;\n }\n\n const locale = path.slice(0, slashIdx);\n\n // validate locale against configured locales to prevent arbitrary locale injection\n const config = i18n.getConfig();\n\n if (!config.locales.includes(locale)) {\n return new Response('/* unknown locale */', {\n status: 404,\n headers: { 'Content-Type': 'application/javascript' },\n });\n }\n\n const rest = path.slice(slashIdx + 1);\n\n if (!rest.endsWith('.js')) {\n return undefined;\n }\n\n const withoutJs = rest.slice(0, -3);\n const lastDotIdx = withoutJs.lastIndexOf('.');\n\n if (lastDotIdx === -1) {\n return undefined;\n }\n\n const chunkName = withoutJs.slice(0, lastDotIdx);\n const body = i18n.getChunkBody(locale, chunkName);\n\n if (!body) {\n return new Response(`/* chunk not found: ${chunkName} */`, {\n status: 404,\n headers: { 'Content-Type': 'application/javascript' },\n });\n }\n\n return new Response(body as BodyInit, {\n status: 200,\n headers: {\n 'Content-Type': 'text/javascript; charset=utf-8',\n 'Content-Length': String(body.byteLength),\n 'Cache-Control': 'public, max-age=31536000, immutable',\n },\n });\n}\n\n/**\n * Create the i18n chunk middleware that serves translation chunks at `/_i18n/` endpoints.\n *\n * Injected by the integration as `pre` middleware (before any user middleware) —\n * not part of the public API.\n *\n * @internal\n */\nexport function createI18nChunkMiddleware(): MiddlewareHandler {\n return (ctx, next) => {\n const response = createChunkResponse(ctx.url.pathname);\n\n if (!response) {\n return next();\n }\n\n // this middleware, not astro's routing, is what serves the request\n overrideRequestRoute(I18N_ENDPOINT_ROUTE);\n\n return response;\n };\n}\n\n/**\n * Create the i18n locale middleware.\n *\n * Sets up the request context with locale and translations for `t()`, injects\n * the client i18n state into html responses, and records the locale for the\n * islands emitter. Place this after session middleware if your locale detection\n * depends on session/cookies.\n *\n * @example\n * ```typescript\n * import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';\n *\n * export const i18nMiddleware = createI18nMiddleware({\n * locale: (ctx) =>\n * ctx.cookies.get('locale')?.value ??\n * detectLocale(ctx.request) ??\n * i18n.getConfig().defaultLocale,\n * });\n * ```\n */\nexport function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler {\n registerI18nEmitters();\n\n return (ctx, next) => {\n if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) {\n return next();\n }\n\n if (!i18n.isConfigured()) {\n log.error('i18n not configured, passing through');\n\n return next();\n }\n\n const locale = options.locale(ctx);\n\n // the islands emitter runs while the response streams, outside this ALS scope\n setRequestLocale(ctx.request, locale);\n\n const context: I18nContext = {\n locale,\n translations: i18n.getCompiledTranslations(locale),\n rawTranslations: i18n.getTranslations(locale),\n fallback: i18n.getConfig().fallback,\n };\n\n return runWithContext(context, () => next());\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,4 @@
1
1
  //#region src/server/chunk-middleware-entrypoint.d.ts
2
- declare const onRequest: import("astro").MiddlewareHandler;
2
+ export declare const onRequest: import("astro").MiddlewareHandler;
3
3
  //#endregion
4
- export { onRequest };
5
4
  //# sourceMappingURL=chunk-middleware-entrypoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-middleware-entrypoint.d.ts","names":[],"sources":["../../src/server/chunk-middleware-entrypoint.ts"],"mappings":";cAEa,2BAAS"}
1
+ {"version":3,"file":"chunk-middleware-entrypoint.d.ts","names":[],"sources":["../../src/server/chunk-middleware-entrypoint.ts"],"mappings":";qBAEa,2BAAS"}
@@ -1,4 +1,4 @@
1
- import { t as createI18nChunkMiddleware } from "../middleware-DxKY84Iv.js";
1
+ import { t as createI18nChunkMiddleware } from "../middleware-M9Q8sVRc.js";
2
2
  //#region src/server/chunk-middleware-entrypoint.ts
3
3
  const onRequest = createI18nChunkMiddleware();
4
4
  //#endregion
@@ -5,7 +5,7 @@ import { a as TranslationMeta, i as TranslateFunction } from "../types-DU8QHch4.
5
5
  * Get the current locale from request context.
6
6
  * Returns defaultLocale if called outside a request (e.g. during build).
7
7
  */
8
- declare function getLocale(): string;
8
+ export declare function getLocale(): string;
9
9
  /**
10
10
  * Server-side translate function
11
11
  *
@@ -13,7 +13,7 @@ declare function getLocale(): string;
13
13
  * - Development: t('key', 'fallback') or t('key', 'fallback', values)
14
14
  * - Production: t('key') or t('key', undefined, values)
15
15
  */
16
- declare const t: TranslateFunction;
16
+ export declare const t: TranslateFunction;
17
17
  /**
18
18
  * Server-side rich text translation function
19
19
  *
@@ -33,7 +33,6 @@ declare const t: TranslateFunction;
33
33
  * // Returns: ['Read our ', <a href="/tos">Terms</a>]
34
34
  * ```
35
35
  */
36
- declare function rich<T = unknown>(key: string, meta?: TranslationMeta | string | undefined, components?: RichComponents<T> | undefined, values?: Record<string, unknown> | undefined): (string | T)[];
36
+ export declare function rich<T = unknown>(key: string, meta?: TranslationMeta | string | undefined, components?: RichComponents<T> | undefined, values?: Record<string, unknown> | undefined): (string | T)[];
37
37
  //#endregion
38
- export { getLocale, rich, t };
39
38
  //# sourceMappingURL=translate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/server/translate.ts"],"mappings":";;;;;;;iBAmCgB;;;;;;;;cAYH,GAAG;;;;;;;;;;;;;;;;;;;;iBA4DA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
1
+ {"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/server/translate.ts"],"mappings":";;;;;;;wBAiDgB;;;;;;;;qBAYH,GAAG;;;;;;;;;;;;;;;;;;;;wBA8DA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
@@ -1,8 +1,22 @@
1
- import { r as i18n, t as getContext } from "../context-CYqyZJyH.js";
1
+ import { r as i18n, t as getContext } from "../context-CgqIoo00.js";
2
2
  import { r as formatMessageToParts, t as compileMessage } from "../compiler-CkOVGWU5.js";
3
3
  import { n as normalizeMeta, t as partsToNodes } from "../rich-CHisXE-A.js";
4
+ import { createCounter } from "@astroscope/node/telemetry";
5
+ import { log } from "@astroscope/node/log";
4
6
  //#region src/server/translate.ts
5
7
  const fallbackCache = /* @__PURE__ */ new Map();
8
+ const missing = createCounter("astro.i18n.missing", {
9
+ scope: "@astroscope/i18n",
10
+ description: "Translation lookups that fell back (a key once per locale and translations version for t())",
11
+ unit: "{lookup}"
12
+ });
13
+ function reportMissing(key, locale) {
14
+ missing.add(1, { "astro.i18n.locale": locale });
15
+ log.debug({
16
+ key,
17
+ locale
18
+ }, "missing translation");
19
+ }
6
20
  /**
7
21
  * Apply fallback behavior when translation is missing
8
22
  */
@@ -45,6 +59,7 @@ const t = ((key, meta, values) => {
45
59
  }
46
60
  const compiled = ctx.translations[key];
47
61
  if (!compiled) {
62
+ reportMissing(key, ctx.locale);
48
63
  const fallbackValue = applyFallback(key, normalizedMeta, ctx.fallback);
49
64
  const compiledFallback = compileMessage(ctx.locale, fallbackValue);
50
65
  ctx.translations[key] = compiledFallback;
@@ -75,7 +90,9 @@ function rich(key, meta, components, values) {
75
90
  const normalizedMeta = meta ? normalizeMeta(meta) : { fallback: "" };
76
91
  const ctx = getContext();
77
92
  const locale = ctx?.locale ?? i18n.getConfig().defaultLocale;
78
- const raw = ctx?.rawTranslations[key] || normalizedMeta.fallback || key;
93
+ const translated = ctx?.rawTranslations[key];
94
+ if (ctx && !translated) reportMissing(key, ctx.locale);
95
+ const raw = translated || normalizedMeta.fallback || key;
79
96
  const parts = formatMessageToParts(locale, raw, values);
80
97
  return partsToNodes(parts, components ?? {});
81
98
  }
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","names":[],"sources":["../../src/server/translate.ts"],"sourcesContent":["import { compileMessage, formatMessageToParts } from '../shared/compiler.js';\nimport { normalizeMeta } from '../shared/meta.js';\nimport { type RichComponents, partsToNodes } from '../shared/rich.js';\nimport type { CompiledTranslation, TranslateFunction, TranslationMeta } from '../shared/types.js';\nimport { getContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport type { FallbackBehavior } from './types.js';\n\n// cache for fallbacks compiled outside request context (e.g. during build)\nconst fallbackCache = new Map<string, CompiledTranslation>();\n\n/**\n * Apply fallback behavior when translation is missing\n */\nfunction applyFallback(key: string, meta: TranslationMeta, fallback: FallbackBehavior): string {\n if (typeof fallback === 'function') {\n return fallback(key, meta);\n }\n\n switch (fallback) {\n case 'key':\n return key;\n case 'fallback':\n return meta.fallback || key;\n case 'throw':\n throw new Error(`Missing translation for key: ${key}`);\n default:\n return meta.fallback || key;\n }\n}\n\n/**\n * Get the current locale from request context.\n * Returns defaultLocale if called outside a request (e.g. during build).\n */\nexport function getLocale(): string {\n const ctx = getContext();\n return ctx?.locale ?? i18n.getConfig().defaultLocale;\n}\n\n/**\n * Server-side translate function\n *\n * Call patterns:\n * - Development: t('key', 'fallback') or t('key', 'fallback', values)\n * - Production: t('key') or t('key', undefined, values)\n */\nexport const t: TranslateFunction = ((\n key: string,\n meta?: TranslationMeta | string,\n values?: Record<string, unknown>,\n): string => {\n const normalizedMeta: TranslationMeta = meta ? normalizeMeta(meta) : { fallback: '' };\n\n const ctx = getContext();\n\n if (!ctx) {\n // no context = probably during build or outside request\n // use cached compiled fallback to avoid recompiling on every call\n const defaultLocale = i18n.getConfig().defaultLocale;\n const fallbackStr = normalizedMeta.fallback || key;\n const cacheKey = `${defaultLocale}:${fallbackStr}`;\n let compiled = fallbackCache.get(cacheKey);\n\n if (!compiled) {\n compiled = compileMessage(defaultLocale, fallbackStr);\n fallbackCache.set(cacheKey, compiled);\n }\n\n return compiled(values);\n }\n\n const compiled = ctx.translations[key];\n\n if (!compiled) {\n const fallbackValue = applyFallback(key, normalizedMeta, ctx.fallback);\n\n // compile and cache the fallback for consistency\n const compiledFallback = compileMessage(ctx.locale, fallbackValue);\n\n ctx.translations[key] = compiledFallback;\n\n return compiledFallback(values);\n }\n\n return compiled(values);\n}) as TranslateFunction;\n\n/**\n * Server-side rich text translation function\n *\n * Parses MF2 markup syntax and applies component callbacks to wrap content.\n * Works with any JSX runtime (Astro, React during SSR, etc.).\n *\n * @param key - Translation key\n * @param meta - Fallback string or meta object with fallback (used as message template)\n * @param components - Map of tag names to component callbacks\n * @param values - Runtime interpolation values (ICU MessageFormat)\n *\n * @example\n * ```tsx\n * rich('tos', 'Read our {#link}Terms{/link}', {\n * link: (children) => <a href=\"/tos\">{children}</a>\n * })\n * // Returns: ['Read our ', <a href=\"/tos\">Terms</a>]\n * ```\n */\nexport function rich<T = unknown>(\n key: string,\n meta?: TranslationMeta | string | undefined,\n components?: RichComponents<T> | undefined,\n values?: Record<string, unknown> | undefined,\n): (string | T)[] {\n const normalizedMeta: TranslationMeta = meta ? normalizeMeta(meta) : { fallback: '' };\n\n const ctx = getContext();\n const locale = ctx?.locale ?? i18n.getConfig().defaultLocale;\n\n const raw = ctx?.rawTranslations[key] || normalizedMeta.fallback || key;\n\n const parts = formatMessageToParts(locale, raw, values);\n\n return partsToNodes(parts, components ?? {});\n}\n"],"mappings":";;;;AASA,MAAM,gCAAgB,IAAI,IAAiC;;;;AAK3D,SAAS,cAAc,KAAa,MAAuB,UAAoC;CAC7F,IAAI,OAAO,aAAa,YACtB,OAAO,SAAS,KAAK,IAAI;CAG3B,QAAQ,UAAR;EACE,KAAK,OACH,OAAO;EACT,KAAK,YACH,OAAO,KAAK,YAAY;EAC1B,KAAK,SACH,MAAM,IAAI,MAAM,gCAAgC,KAAK;EACvD,SACE,OAAO,KAAK,YAAY;CAC5B;AACF;;;;;AAMA,SAAgB,YAAoB;CAElC,OADY,WACH,CAAC,EAAE,UAAU,KAAK,UAAU,CAAC,CAAC;AACzC;;;;;;;;AASA,MAAa,MACX,KACA,MACA,WACW;CACX,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,MAAM,WAAW;CAEvB,IAAI,CAAC,KAAK;EAGR,MAAM,gBAAgB,KAAK,UAAU,CAAC,CAAC;EACvC,MAAM,cAAc,eAAe,YAAY;EAC/C,MAAM,WAAW,GAAG,cAAc,GAAG;EACrC,IAAI,WAAW,cAAc,IAAI,QAAQ;EAEzC,IAAI,CAAC,UAAU;GACb,WAAW,eAAe,eAAe,WAAW;GACpD,cAAc,IAAI,UAAU,QAAQ;EACtC;EAEA,OAAO,SAAS,MAAM;CACxB;CAEA,MAAM,WAAW,IAAI,aAAa;CAElC,IAAI,CAAC,UAAU;EACb,MAAM,gBAAgB,cAAc,KAAK,gBAAgB,IAAI,QAAQ;EAGrE,MAAM,mBAAmB,eAAe,IAAI,QAAQ,aAAa;EAEjE,IAAI,aAAa,OAAO;EAExB,OAAO,iBAAiB,MAAM;CAChC;CAEA,OAAO,SAAS,MAAM;AACxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,KACd,KACA,MACA,YACA,QACgB;CAChB,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,MAAM,WAAW;CACvB,MAAM,SAAS,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC;CAE/C,MAAM,MAAM,KAAK,gBAAgB,QAAQ,eAAe,YAAY;CAEpE,MAAM,QAAQ,qBAAqB,QAAQ,KAAK,MAAM;CAEtD,OAAO,aAAa,OAAO,cAAc,CAAC,CAAC;AAC7C"}
1
+ {"version":3,"file":"translate.js","names":[],"sources":["../../src/server/translate.ts"],"sourcesContent":["import { log } from '@astroscope/node/log';\nimport { createCounter } from '@astroscope/node/telemetry';\nimport { compileMessage, formatMessageToParts } from '../shared/compiler.js';\nimport { normalizeMeta } from '../shared/meta.js';\nimport { type RichComponents, partsToNodes } from '../shared/rich.js';\nimport type { CompiledTranslation, TranslateFunction, TranslationMeta } from '../shared/types.js';\nimport { getContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport type { FallbackBehavior } from './types.js';\n\n// cache for fallbacks compiled outside request context (e.g. during build)\nconst fallbackCache = new Map<string, CompiledTranslation>();\n\nconst missing = createCounter('astro.i18n.missing', {\n scope: '@astroscope/i18n',\n description: 'Translation lookups that fell back (a key once per locale and translations version for t())',\n unit: '{lookup}',\n});\n\n// the key stays out of the metric label (unbounded) and goes to the log instead\nfunction reportMissing(key: string, locale: string): void {\n missing.add(1, { 'astro.i18n.locale': locale });\n log.debug({ key, locale }, 'missing translation');\n}\n\n/**\n * Apply fallback behavior when translation is missing\n */\nfunction applyFallback(key: string, meta: TranslationMeta, fallback: FallbackBehavior): string {\n if (typeof fallback === 'function') {\n return fallback(key, meta);\n }\n\n switch (fallback) {\n case 'key':\n return key;\n case 'fallback':\n return meta.fallback || key;\n case 'throw':\n throw new Error(`Missing translation for key: ${key}`);\n default:\n return meta.fallback || key;\n }\n}\n\n/**\n * Get the current locale from request context.\n * Returns defaultLocale if called outside a request (e.g. during build).\n */\nexport function getLocale(): string {\n const ctx = getContext();\n return ctx?.locale ?? i18n.getConfig().defaultLocale;\n}\n\n/**\n * Server-side translate function\n *\n * Call patterns:\n * - Development: t('key', 'fallback') or t('key', 'fallback', values)\n * - Production: t('key') or t('key', undefined, values)\n */\nexport const t: TranslateFunction = ((\n key: string,\n meta?: TranslationMeta | string,\n values?: Record<string, unknown>,\n): string => {\n const normalizedMeta: TranslationMeta = meta ? normalizeMeta(meta) : { fallback: '' };\n\n const ctx = getContext();\n\n if (!ctx) {\n // no context = probably during build or outside request\n // use cached compiled fallback to avoid recompiling on every call\n const defaultLocale = i18n.getConfig().defaultLocale;\n const fallbackStr = normalizedMeta.fallback || key;\n const cacheKey = `${defaultLocale}:${fallbackStr}`;\n let compiled = fallbackCache.get(cacheKey);\n\n if (!compiled) {\n compiled = compileMessage(defaultLocale, fallbackStr);\n fallbackCache.set(cacheKey, compiled);\n }\n\n return compiled(values);\n }\n\n const compiled = ctx.translations[key];\n\n if (!compiled) {\n reportMissing(key, ctx.locale);\n\n const fallbackValue = applyFallback(key, normalizedMeta, ctx.fallback);\n\n // compile and cache the fallback for consistency\n const compiledFallback = compileMessage(ctx.locale, fallbackValue);\n\n ctx.translations[key] = compiledFallback;\n\n return compiledFallback(values);\n }\n\n return compiled(values);\n}) as TranslateFunction;\n\n/**\n * Server-side rich text translation function\n *\n * Parses MF2 markup syntax and applies component callbacks to wrap content.\n * Works with any JSX runtime (Astro, React during SSR, etc.).\n *\n * @param key - Translation key\n * @param meta - Fallback string or meta object with fallback (used as message template)\n * @param components - Map of tag names to component callbacks\n * @param values - Runtime interpolation values (ICU MessageFormat)\n *\n * @example\n * ```tsx\n * rich('tos', 'Read our {#link}Terms{/link}', {\n * link: (children) => <a href=\"/tos\">{children}</a>\n * })\n * // Returns: ['Read our ', <a href=\"/tos\">Terms</a>]\n * ```\n */\nexport function rich<T = unknown>(\n key: string,\n meta?: TranslationMeta | string | undefined,\n components?: RichComponents<T> | undefined,\n values?: Record<string, unknown> | undefined,\n): (string | T)[] {\n const normalizedMeta: TranslationMeta = meta ? normalizeMeta(meta) : { fallback: '' };\n\n const ctx = getContext();\n const locale = ctx?.locale ?? i18n.getConfig().defaultLocale;\n\n const translated = ctx?.rawTranslations[key];\n\n if (ctx && !translated) {\n reportMissing(key, ctx.locale);\n }\n\n const raw = translated || normalizedMeta.fallback || key;\n\n const parts = formatMessageToParts(locale, raw, values);\n\n return partsToNodes(parts, components ?? {});\n}\n"],"mappings":";;;;;;AAWA,MAAM,gCAAgB,IAAI,IAAiC;AAE3D,MAAM,UAAU,cAAc,sBAAsB;CAClD,OAAO;CACP,aAAa;CACb,MAAM;AACR,CAAC;AAGD,SAAS,cAAc,KAAa,QAAsB;CACxD,QAAQ,IAAI,GAAG,EAAE,qBAAqB,OAAO,CAAC;CAC9C,IAAI,MAAM;EAAE;EAAK;CAAO,GAAG,qBAAqB;AAClD;;;;AAKA,SAAS,cAAc,KAAa,MAAuB,UAAoC;CAC7F,IAAI,OAAO,aAAa,YACtB,OAAO,SAAS,KAAK,IAAI;CAG3B,QAAQ,UAAR;EACE,KAAK,OACH,OAAO;EACT,KAAK,YACH,OAAO,KAAK,YAAY;EAC1B,KAAK,SACH,MAAM,IAAI,MAAM,gCAAgC,KAAK;EACvD,SACE,OAAO,KAAK,YAAY;CAC5B;AACF;;;;;AAMA,SAAgB,YAAoB;CAElC,OADY,WACH,CAAC,EAAE,UAAU,KAAK,UAAU,CAAC,CAAC;AACzC;;;;;;;;AASA,MAAa,MACX,KACA,MACA,WACW;CACX,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,MAAM,WAAW;CAEvB,IAAI,CAAC,KAAK;EAGR,MAAM,gBAAgB,KAAK,UAAU,CAAC,CAAC;EACvC,MAAM,cAAc,eAAe,YAAY;EAC/C,MAAM,WAAW,GAAG,cAAc,GAAG;EACrC,IAAI,WAAW,cAAc,IAAI,QAAQ;EAEzC,IAAI,CAAC,UAAU;GACb,WAAW,eAAe,eAAe,WAAW;GACpD,cAAc,IAAI,UAAU,QAAQ;EACtC;EAEA,OAAO,SAAS,MAAM;CACxB;CAEA,MAAM,WAAW,IAAI,aAAa;CAElC,IAAI,CAAC,UAAU;EACb,cAAc,KAAK,IAAI,MAAM;EAE7B,MAAM,gBAAgB,cAAc,KAAK,gBAAgB,IAAI,QAAQ;EAGrE,MAAM,mBAAmB,eAAe,IAAI,QAAQ,aAAa;EAEjE,IAAI,aAAa,OAAO;EAExB,OAAO,iBAAiB,MAAM;CAChC;CAEA,OAAO,SAAS,MAAM;AACxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,KACd,KACA,MACA,YACA,QACgB;CAChB,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,MAAM,WAAW;CACvB,MAAM,SAAS,KAAK,UAAU,KAAK,UAAU,CAAC,CAAC;CAE/C,MAAM,aAAa,KAAK,gBAAgB;CAExC,IAAI,OAAO,CAAC,YACV,cAAc,KAAK,IAAI,MAAM;CAG/B,MAAM,MAAM,cAAc,eAAe,YAAY;CAErD,MAAM,QAAQ,qBAAqB,QAAQ,KAAK,MAAM;CAEtD,OAAO,aAAa,OAAO,cAAc,CAAC,CAAC;AAC7C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/i18n",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "i18n for Astro + React islands - MessageFormat 2, automatic tree-shaking, parallel loading, any translation source",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -65,20 +65,20 @@
65
65
  "@babel/core": "^8.0.1",
66
66
  "@babel/preset-typescript": "^8.0.1",
67
67
  "@jridgewell/trace-mapping": "^0.3.31",
68
- "magic-string": "^1.2.2",
68
+ "magic-string": "^1.2.3",
69
69
  "messageformat": "^4.0.0"
70
70
  },
71
71
  "devDependencies": {
72
- "@astroscope/node": "^2.1.0",
72
+ "@astroscope/node": "^3.0.0",
73
73
  "@babel/types": "^8.0.4",
74
74
  "@types/babel__core": "^7.20.5",
75
- "astro": "^7.2.6",
76
- "tsdown": "^0.22.14",
75
+ "astro": "^7.3.2",
76
+ "tsdown": "^0.23.0",
77
77
  "typescript": "^6.0.3",
78
78
  "vite": "^8.2.2"
79
79
  },
80
80
  "peerDependencies": {
81
- "@astroscope/node": "^2.1.0",
81
+ "@astroscope/node": "^3.0.0",
82
82
  "astro": "^7.0.0",
83
83
  "vite": "^8.0.0"
84
84
  },
@@ -1 +0,0 @@
1
- {"version":3,"file":"context-CYqyZJyH.js","names":[],"sources":["../src/extraction/hash.ts","../src/server/i18n.ts","../src/server/context.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { RawTranslations } from '../shared/types.js';\nimport type { ChunkManifest } from './types.js';\n\n/**\n * Hash a string to a short hash (for cache busting)\n */\nfunction hashString(str: string): string {\n return createHash('sha256').update(str).digest('hex').substring(0, 8);\n}\n\n/**\n * Compute hash for chunk translations\n */\nexport function computeChunkHash(translations: RawTranslations, keys: string[]): string {\n const relevantTranslations: RawTranslations = {};\n\n for (const key of [...keys].sort()) {\n if (translations[key] !== undefined) {\n relevantTranslations[key] = translations[key];\n }\n }\n\n return hashString(JSON.stringify(relevantTranslations));\n}\n\n/**\n * Compute hashes for all chunks in manifest\n */\nexport function computeAllChunkHashes(translations: RawTranslations, manifest: ChunkManifest): Record<string, string> {\n const hashes: Record<string, string> = {};\n\n for (const [chunkId, keys] of Object.entries(manifest)) {\n hashes[chunkId] = computeChunkHash(translations, keys);\n }\n\n return hashes;\n}\n","import { computeAllChunkHashes } from '../extraction/hash.js';\nimport { getGlobalState } from '../extraction/manifest.js';\nimport type { ExtractionManifest } from '../extraction/types.js';\nimport { compileTranslations } from '../shared/compiler.js';\nimport type { CompiledTranslations, RawTranslations } from '../shared/types.js';\nimport type { FallbackBehavior } from './types.js';\n\nexport type I18nConfig = {\n locales: string[];\n defaultLocale?: string | undefined;\n fallback?: FallbackBehavior | undefined;\n};\n\ntype NormalizedConfig = {\n locales: string[];\n defaultLocale: string;\n fallback: FallbackBehavior;\n};\n\nclass I18nSingleton {\n // normalized user config with defaults applied\n private config: NormalizedConfig | null = null;\n\n // locale -> raw translation strings (before ICU compilation)\n private rawCache = new Map<string, RawTranslations>();\n\n // locale -> raw translations merged with manifest fallbacks\n private mergedCache = new Map<string, RawTranslations>();\n\n // locale -> compiled ICU MessageFormat functions\n private compiledCache = new Map<string, CompiledTranslations>();\n\n // locale -> chunk name -> content hash (for cache busting)\n private hashCache = new Map<string, Record<string, string>>();\n\n // \"locale:chunkName\" -> encoded chunk response body\n private chunkCache = new Map<string, Uint8Array>();\n\n // manifest getter, set on configure() via the virtual module (live data in dev mode)\n private manifestGetter: (() => ExtractionManifest) | null = null;\n\n // tracks the manifest version to detect when derived caches need invalidation\n private manifestVersion = 0;\n\n async configure(config: I18nConfig): Promise<void> {\n if (!config.locales?.length) {\n throw new Error('i18n.configure(): locales array is required and must not be empty');\n }\n\n for (const locale of config.locales) {\n if (!locale) {\n throw new Error('i18n.configure(): locale cannot be empty');\n }\n\n if (locale !== locale.trim()) {\n throw new Error(`i18n.configure(): locale \"${locale}\" has leading or trailing whitespace`);\n }\n }\n\n if (new Set(config.locales).size !== config.locales.length) {\n throw new Error('i18n.configure(): locales array contains duplicates');\n }\n\n if (config.defaultLocale !== undefined) {\n if (!config.defaultLocale) {\n throw new Error('i18n.configure(): defaultLocale cannot be empty');\n }\n\n if (config.defaultLocale !== config.defaultLocale.trim()) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" has leading or trailing whitespace`);\n }\n\n if (!config.locales.includes(config.defaultLocale)) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" is not in locales array`);\n }\n }\n\n this.config = {\n locales: config.locales,\n defaultLocale: config.defaultLocale ?? config.locales[0]!,\n fallback: config.fallback ?? 'fallback',\n };\n\n // resolved lazily so importing the singleton needs no virtual module\n if (!this.manifestGetter) {\n const m = await import('virtual:@astroscope/i18n/manifest');\n this.manifestGetter = m.getManifest;\n }\n }\n\n isConfigured(): boolean {\n return this.config !== null;\n }\n\n getConfig(): NormalizedConfig {\n if (!this.config) {\n throw new Error('i18n not configured. Call i18n.configure() first.');\n }\n\n return this.config;\n }\n\n setTranslations(locale: string, raw: RawTranslations): void {\n this.rawCache.set(locale, raw);\n this.mergedCache.delete(locale); // invalidate merged cache\n this.compiledCache.delete(locale); // invalidate compiled cache\n this.hashCache.delete(locale); // invalidate hash cache\n\n // invalidate chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n\n // recompute hashes if chunk manifest exists (production only)\n const { chunks } = this.getManifest();\n\n if (Object.keys(chunks).length > 0) {\n this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));\n }\n }\n\n getTranslations(locale: string): RawTranslations {\n this.invalidateIfManifestChanged();\n\n const cached = this.mergedCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const raw = this.rawCache.get(locale) ?? {};\n const manifest = this.manifestGetter?.() ?? { keys: [], chunks: {}, scripts: [] };\n\n if (manifest.keys.length === 0) {\n return raw;\n }\n\n // merge fallbacks for keys that don't have translations\n const withFallbacks: RawTranslations = { ...raw };\n\n for (const { key, meta } of manifest.keys) {\n if (!(key in withFallbacks) && meta.fallback) {\n withFallbacks[key] = meta.fallback;\n }\n }\n\n this.mergedCache.set(locale, withFallbacks);\n\n return withFallbacks;\n }\n\n getCompiledTranslations(locale: string): CompiledTranslations {\n const cached = this.compiledCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const compiled = compileTranslations(locale, this.getTranslations(locale));\n\n this.compiledCache.set(locale, compiled);\n\n return compiled;\n }\n\n /**\n * Get the extraction manifest (keys + chunks).\n * - keys: translation keys extracted from codebase with metadata\n * - chunks: mapping of chunk names to their translation keys\n */\n getManifest(): ExtractionManifest {\n if (!this.manifestGetter) {\n throw new Error('i18n manifest not initialized. Call i18n.configure() first.');\n }\n\n return this.manifestGetter();\n }\n\n getHashes(locale: string): Record<string, string> {\n return this.hashCache.get(locale) ?? {};\n }\n\n /**\n * Get the encoded chunk response body for a locale/chunk combination.\n * Cached to avoid repeated JSON.stringify and encoding on each request.\n * Returns undefined if chunk not found in manifest.\n */\n getChunkBody(locale: string, chunkName: string): Uint8Array | undefined {\n const keys = this.getManifest().chunks[chunkName];\n\n if (!keys) {\n return undefined;\n }\n\n const cacheKey = `${locale}:${chunkName}`;\n const cached = this.chunkCache.get(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const translations = this.getTranslations(locale);\n const chunkTranslations: RawTranslations = {};\n\n for (const key of keys) {\n if (translations[key]) {\n chunkTranslations[key] = translations[key];\n }\n }\n\n const js = `/* @astroscope/i18n chunk: ${chunkName} */\n(function() {\n var i = window.__i18n__;\n if (i) Object.assign(i.translations, ${JSON.stringify(chunkTranslations)});\n})();\n`;\n\n const body = new TextEncoder().encode(js);\n\n this.chunkCache.set(cacheKey, body);\n\n return body;\n }\n\n /**\n * Hashes for chunks reachable from astro `<script>` entries. Script modules are\n * not islands, so the islands emitter never covers them — the middleware\n * bootstraps these few into the client state on every html response.\n */\n getScriptChunkHashes(locale: string): Record<string, string> {\n const { scripts } = this.getManifest();\n const hashes = this.getHashes(locale);\n const result: Record<string, string> = {};\n\n for (const chunk of scripts) {\n const hash = hashes[chunk];\n\n if (hash) {\n result[chunk] = hash;\n }\n }\n\n return result;\n }\n\n /**\n * Clear translations for specified locale(s).\n * @param locales - Locale(s) to clear, or all if not specified\n */\n clear(locales?: string | string[] | undefined): void {\n const targets = this.resolveLocales(locales);\n\n for (const locale of targets) {\n this.rawCache.delete(locale);\n this.mergedCache.delete(locale);\n this.compiledCache.delete(locale);\n this.hashCache.delete(locale);\n\n // clear chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n }\n }\n\n /**\n * invalidate derived caches when the extraction manifest has changed (dev mode HMR)\n */\n private invalidateIfManifestChanged(): void {\n const { version } = getGlobalState();\n\n if (version === this.manifestVersion) {\n return;\n }\n\n this.manifestVersion = version;\n this.mergedCache.clear();\n this.compiledCache.clear();\n this.chunkCache.clear();\n }\n\n private resolveLocales(locales?: string | string[] | undefined): string[] {\n if (!locales) {\n return this.config?.locales ?? [];\n }\n\n if (typeof locales === 'string') {\n return [locales];\n }\n\n return locales;\n }\n}\n\nexport const i18n = new I18nSingleton();\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { I18nContext } from './types.js';\n\nconst als = new AsyncLocalStorage<I18nContext>();\n\n/**\n * Run a function with i18n context (called by middleware)\n */\nexport function runWithContext<T>(context: I18nContext, fn: () => T): T {\n return als.run(context, fn);\n}\n\n/**\n * Get the current i18n context\n */\nexport function getContext(): I18nContext | null {\n return als.getStore() ?? null;\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"i18n-Bp8TFmd3.d.ts","names":[],"sources":["../src/server/types.ts","../src/server/i18n.ts"],"mappings":";;;;;;KAKY,oDACuB,aAAa,OAAO;;;;KAK3C;EACV;EAEA,cAAc;EAEd,iBAAiB;EACjB,UAAU;;;;KCVA;EACV;EACA;EACA,WAAW;;KAGR;EACH;EACA;EACA,UAAU;;cAGN;UAEI;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;EAEF,UAAU,QAAQ,aAAa;EA8CrC;EAIA,aAAa;EAQb,gBAAgB,gBAAgB,KAAK;EAqBrC,gBAAgB,iBAAiB;EA8BjC,wBAAwB,iBAAiB;;;;;;EAmBzC,eAAe;EAQf,UAAU,iBAAiB;;;;;;EAS3B,aAAa,gBAAgB,oBAAoB;;;;;;EA0CjD,qBAAqB,iBAAiB;;;;;EAoBtC,MAAM;;;;UAqBE;UAaA;;cAaG,MAAI"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"middleware-DxKY84Iv.js","names":[],"sources":["../src/server/client-state.ts","../src/server/islands-emitter.ts","../src/server/middleware.ts"],"sourcesContent":["import { i18n } from './i18n.js';\n\n/**\n * Scripts bootstrapping `window.__i18n__` on html responses, delivered through the\n * `@astroscope/node` document emitter (see islands-emitter.ts for the placement\n * policy). Both writers merge orderlessly (`??=` + assign), so emitter scripts and\n * the bootstrap can arrive in either order.\n */\n\n/** translation values may contain `</script>` — escape `<` so they cannot end the tag */\nexport function jsonForScript(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n}\n\n/** the full dev-mode state: every translation for the locale */\nexport function createFullStateScript(locale: string): string {\n const translations = i18n.getTranslations(locale);\n const state = jsonForScript({ locale, hashes: {}, translations });\n\n return `<script>window.__i18n__=Object.assign(window.__i18n__??{hashes:{},translations:{}},${state});</script>`;\n}\n\n/** the prod bootstrap: chunk hashes for astro `<script>` entry consumers */\nexport function createScriptHashesScript(locale: string): string | null {\n const hashes = i18n.getScriptChunkHashes(locale);\n\n if (Object.keys(hashes).length === 0) {\n return null;\n }\n\n const init = jsonForScript({ locale, hashes: {}, translations: {} });\n\n return `<script>{const i=window.__i18n__??=${init};Object.assign(i.hashes,${jsonForScript(hashes)});}</script>`;\n}\n","import { type DocumentEmitter, registerDocumentEmitter, registerIslandEmitter } from '@astroscope/node/islands';\nimport type { APIContext } from 'astro';\nimport { buildI18nChunkUrl } from '../shared/url.js';\nimport { createFullStateScript, createScriptHashesScript, jsonForScript } from './client-state.js';\nimport { i18n } from './i18n.js';\n\n/**\n * Contributes the i18n slice for each island the `@astroscope/node` islands\n * middleware streams past:\n *\n * - an inline merge script with the hashes of the island's full chunk closure —\n * dynamic imports included, since a `React.lazy` chunk's injected loader reads\n * its hash at execution time. Emitted before the island tag, it is parsed\n * strictly before the island connects, and the island's chunks execute at\n * least a network round-trip later — so each island's hashes win their race\n * even while the rest of the document is still streaming. Chunks already\n * covered earlier in the document are skipped.\n * - translation-chunk preload links for the static closure, emitted as link tags\n * for immediate islands.\n * - translation-chunk eager imports for the full closure — dynamic imports\n * included — fired by the deferred gate the moment a directive schedules\n * hydration. Translation chunks are idempotent data modules, so evaluating\n * them ahead of the component costs nothing, and the loader's awaited import\n * then hits the module cache instead of fetching serially after the component\n * graph evaluated. Every island carries its complete i18n set (no per-document\n * dedup — the gate dedups at fire time), so a chunk shared between islands is\n * covered by whichever gate fires first.\n *\n * The locale is recorded per request by the i18n middleware — the emitter runs\n * while the response streams, outside the middleware's AsyncLocalStorage scope.\n */\n\nconst requestLocales = new WeakMap<Request, string>();\nconst emittedByContext = new WeakMap<APIContext, Set<string>>();\n\n// vite defines import.meta.env when bundling the middleware; typed structurally\n// so the package compiles without vite's global client types\nconst IS_DEV = !!(import.meta as { env?: { DEV?: boolean } }).env?.DEV;\n\nlet registered = false;\n\nexport function setRequestLocale(request: Request, locale: string): void {\n requestLocales.set(request, locale);\n}\n\n/** public url of a chunk → manifest chunk name, e.g. `…/_astro/Cart.abc.js` → `Cart.abc` */\nfunction urlToChunkName(url: string): string {\n return url.slice(url.lastIndexOf('/') + 1).replace(/\\.js$/, '');\n}\n\n/**\n * The document-level bootstrap for `window.__i18n__`, exported for tests.\n */\nexport function createI18nDocumentEmitter(dev: boolean = IS_DEV): DocumentEmitter {\n return (context) => {\n if (!i18n.isConfigured()) {\n return null;\n }\n\n const locale = requestLocales.get(context.request);\n\n if (locale === undefined) {\n return null;\n }\n\n if (dev) {\n return { head: createFullStateScript(locale) };\n }\n\n if (Object.keys(i18n.getManifest().chunks).length === 0) {\n return null;\n }\n\n return { end: () => createScriptHashesScript(locale) };\n };\n}\n\nexport function registerI18nEmitters(): void {\n if (registered) {\n return;\n }\n\n registered = true;\n\n registerDocumentEmitter(createI18nDocumentEmitter());\n\n registerIslandEmitter((island, context) => {\n // no request context (prerendered build pass) — the locale is unknowable\n if (!context || !i18n.isConfigured()) {\n return null;\n }\n\n const locale = requestLocales.get(context.request);\n\n if (locale === undefined) {\n return null;\n }\n\n const hashes = i18n.getHashes(locale);\n\n let emitted = emittedByContext.get(context);\n\n if (!emitted) {\n emitted = new Set();\n emittedByContext.set(context, emitted);\n }\n\n const merge: Record<string, string> = {};\n\n for (const url of island.fullClosure) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash && !emitted.has(chunk)) {\n emitted.add(chunk);\n merge[chunk] = hash;\n }\n }\n\n const links: string[] = [];\n\n for (const url of island.staticClosure) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash) {\n links.push(buildI18nChunkUrl(locale, chunk, hash));\n }\n }\n\n const imports: string[] = [];\n\n for (const url of island.fullClosure) {\n const chunk = urlToChunkName(url);\n const hash = hashes[chunk];\n\n if (hash) {\n imports.push(buildI18nChunkUrl(locale, chunk, hash));\n }\n }\n\n const init = jsonForScript({ locale, hashes: {}, translations: {} });\n const html =\n Object.keys(merge).length > 0\n ? `<script>{const i=window.__i18n__??=${init};Object.assign(i.hashes,${jsonForScript(merge)});}</script>`\n : undefined;\n\n if (!html && links.length === 0 && imports.length === 0) {\n return null;\n }\n\n return {\n html,\n links: links.length > 0 ? links : undefined,\n imports: imports.length > 0 ? imports : undefined,\n };\n });\n}\n","import { type ExcludePattern, RECOMMENDED_EXCLUDES, shouldExclude } from '@astroscope/node/excludes';\nimport { log, overrideRequestRoute } from '@astroscope/node/log';\nimport type { APIContext, MiddlewareHandler } from 'astro';\nimport { runWithContext } from './context.js';\nimport { i18n } from './i18n.js';\nimport { registerI18nEmitters, setRequestLocale } from './islands-emitter.js';\nimport type { I18nContext } from './types.js';\n\nexport type I18nMiddlewareOptions = {\n locale: (ctx: APIContext) => string;\n /**\n * Patterns to exclude from locale context setup.\n * Defaults to RECOMMENDED_EXCLUDES if not provided.\n *\n * @example\n * ```ts\n * import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';\n *\n * // extend defaults\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],\n * })\n *\n * // disable excludes entirely\n * createI18nMiddleware({\n * locale: (ctx) => ctx.locals.session?.locale ?? 'en',\n * exclude: [],\n * })\n * ```\n */\n exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | undefined;\n};\n\nconst I18N_ENDPOINT_PREFIX = '/_i18n/';\n\n// astro has no route behind the chunk endpoint, so requests would otherwise be\n// logged and measured under whatever routing matched — typically `/404`\nconst I18N_ENDPOINT_ROUTE = '/_i18n/[locale]/[chunk]';\n\n/**\n * Serve a translation chunk. Returns `undefined` when the path isn't a chunk\n * request, and the caller passes it on to astro.\n */\nfunction createChunkResponse(pathname: string): Response | undefined {\n if (!pathname.startsWith(I18N_ENDPOINT_PREFIX)) {\n return undefined;\n }\n\n if (!i18n.isConfigured()) {\n log.error('i18n not configured, passing through');\n\n return undefined;\n }\n\n const path = pathname.slice(I18N_ENDPOINT_PREFIX.length);\n\n // attempt to parse as efficient as possible\n // expected path format: {locale}/{chunkName}.{hash}.js\n // avoiding regex or split for performance\n const slashIdx = path.indexOf('/');\n\n if (slashIdx === -1) {\n return undefined;\n }\n\n const locale = path.slice(0, slashIdx);\n\n // validate locale against configured locales to prevent arbitrary locale injection\n const config = i18n.getConfig();\n\n if (!config.locales.includes(locale)) {\n return new Response('/* unknown locale */', {\n status: 404,\n headers: { 'Content-Type': 'application/javascript' },\n });\n }\n\n const rest = path.slice(slashIdx + 1);\n\n if (!rest.endsWith('.js')) {\n return undefined;\n }\n\n const withoutJs = rest.slice(0, -3);\n const lastDotIdx = withoutJs.lastIndexOf('.');\n\n if (lastDotIdx === -1) {\n return undefined;\n }\n\n const chunkName = withoutJs.slice(0, lastDotIdx);\n const body = i18n.getChunkBody(locale, chunkName);\n\n if (!body) {\n return new Response(`/* chunk not found: ${chunkName} */`, {\n status: 404,\n headers: { 'Content-Type': 'application/javascript' },\n });\n }\n\n return new Response(body as BodyInit, {\n status: 200,\n headers: {\n 'Content-Type': 'text/javascript; charset=utf-8',\n 'Content-Length': String(body.byteLength),\n 'Cache-Control': 'public, max-age=31536000, immutable',\n },\n });\n}\n\n/**\n * Create the i18n chunk middleware that serves translation chunks at `/_i18n/` endpoints.\n *\n * Injected by the integration as `pre` middleware (before any user middleware) —\n * not part of the public API.\n *\n * @internal\n */\nexport function createI18nChunkMiddleware(): MiddlewareHandler {\n return (ctx, next) => {\n const response = createChunkResponse(ctx.url.pathname);\n\n if (!response) {\n return next();\n }\n\n // this middleware, not astro's routing, is what serves the request\n overrideRequestRoute(I18N_ENDPOINT_ROUTE);\n\n return response;\n };\n}\n\n/**\n * Create the i18n locale middleware.\n *\n * Sets up the request context with locale and translations for `t()`, injects\n * the client i18n state into html responses, and records the locale for the\n * islands emitter. Place this after session middleware if your locale detection\n * depends on session/cookies.\n *\n * @example\n * ```typescript\n * import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';\n *\n * export const i18nMiddleware = createI18nMiddleware({\n * locale: (ctx) =>\n * ctx.cookies.get('locale')?.value ??\n * detectLocale(ctx.request) ??\n * i18n.getConfig().defaultLocale,\n * });\n * ```\n */\nexport function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler {\n registerI18nEmitters();\n\n return async (ctx, next) => {\n if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) {\n return next();\n }\n\n if (!i18n.isConfigured()) {\n log.error('i18n not configured, passing through');\n\n return next();\n }\n\n const locale = options.locale(ctx);\n\n // the islands emitter runs while the response streams, outside this ALS scope\n setRequestLocale(ctx.request, locale);\n\n const context: I18nContext = {\n locale,\n translations: i18n.getCompiledTranslations(locale),\n rawTranslations: i18n.getTranslations(locale),\n fallback: i18n.getConfig().fallback,\n };\n\n return runWithContext(context, () => next());\n };\n}\n"],"mappings":"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"}