@astroscope/i18n 2.1.1 → 3.0.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.
Files changed (53) hide show
  1. package/README.md +26 -35
  2. package/dist/{babel-plugin-CWafmw5z.js → babel-plugin-DJZS9L_W.js} +28 -30
  3. package/dist/babel-plugin-DJZS9L_W.js.map +1 -0
  4. package/dist/client/translate.d.ts +2 -2
  5. package/dist/client/translate.js +3 -3
  6. package/dist/client/translate.js.map +1 -1
  7. package/dist/context-BCUJxb8p.js +19 -0
  8. package/dist/context-BCUJxb8p.js.map +1 -0
  9. package/dist/extraction/index.d.ts +5 -2
  10. package/dist/extraction/index.d.ts.map +1 -1
  11. package/dist/extraction/index.js +2 -2
  12. package/dist/{context-CgqIoo00.js → i18n-C47eQi8U.js} +34 -44
  13. package/dist/i18n-C47eQi8U.js.map +1 -0
  14. package/dist/{i18n-Czkrfb_4.d.ts → i18n-C4nlkQTf.d.ts} +19 -9
  15. package/dist/i18n-C4nlkQTf.d.ts.map +1 -0
  16. package/dist/index.d.ts +14 -60
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +22 -14
  19. package/dist/index.js.map +1 -1
  20. package/dist/internal.d.ts +2 -2
  21. package/dist/internal.js +2 -1
  22. package/dist/{manifest-Cma68XM-.js → manifest-X-UhW6tH.js} +2 -2
  23. package/dist/manifest-X-UhW6tH.js.map +1 -0
  24. package/dist/{middleware-OnRVbxvs.js → middleware-BSEtDRYy.js} +21 -23
  25. package/dist/middleware-BSEtDRYy.js.map +1 -0
  26. package/dist/{rich-CHisXE-A.js → rich-BDqjhv2h.js} +31 -20
  27. package/dist/rich-BDqjhv2h.js.map +1 -0
  28. package/dist/server/chunk-middleware-entrypoint.js +1 -1
  29. package/dist/server/locale-middleware-entrypoint.d.ts +4 -0
  30. package/dist/server/locale-middleware-entrypoint.d.ts.map +1 -0
  31. package/dist/server/locale-middleware-entrypoint.js +8 -0
  32. package/dist/server/locale-middleware-entrypoint.js.map +1 -0
  33. package/dist/server/translate.d.ts +2 -2
  34. package/dist/server/translate.d.ts.map +1 -1
  35. package/dist/server/translate.js +4 -3
  36. package/dist/server/translate.js.map +1 -1
  37. package/dist/{types-f-SNJdvf.d.ts → types-BC-tQ30r.d.ts} +2 -2
  38. package/dist/{types-f-SNJdvf.d.ts.map → types-BC-tQ30r.d.ts.map} +1 -1
  39. package/dist/{types-DU8QHch4.d.ts → types-BjTD5hUT.d.ts} +6 -4
  40. package/dist/{types-DU8QHch4.d.ts.map → types-BjTD5hUT.d.ts.map} +1 -1
  41. package/dist/{types-Bo4jMK8g.d.ts → types-LT4Zo0bM.d.ts} +7 -1
  42. package/dist/types-LT4Zo0bM.d.ts.map +1 -0
  43. package/dist/{vite-plugin-C-jEpd8t.js → vite-plugin-CZqtAqQM.js} +83 -73
  44. package/dist/vite-plugin-CZqtAqQM.js.map +1 -0
  45. package/package.json +14 -9
  46. package/dist/babel-plugin-CWafmw5z.js.map +0 -1
  47. package/dist/context-CgqIoo00.js.map +0 -1
  48. package/dist/i18n-Czkrfb_4.d.ts.map +0 -1
  49. package/dist/manifest-Cma68XM-.js.map +0 -1
  50. package/dist/middleware-OnRVbxvs.js.map +0 -1
  51. package/dist/rich-CHisXE-A.js.map +0 -1
  52. package/dist/types-Bo4jMK8g.d.ts.map +0 -1
  53. package/dist/vite-plugin-C-jEpd8t.js.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,70 +1,24 @@
1
- import { a as TranslationMeta, r as RawTranslations } from "./types-DU8QHch4.js";
2
- import { n as ExtractedKey, r as ExtractionManifest } from "./types-f-SNJdvf.js";
3
- import { n as I18nOptions } from "./types-Bo4jMK8g.js";
4
- import { n as i18n, r as FallbackBehavior, t as I18nConfig } from "./i18n-Czkrfb_4.js";
5
- import { ExcludePattern } from "@astroscope/node/excludes";
6
- import { APIContext, AstroIntegration, MiddlewareHandler } from "astro";
1
+ import { a as TranslationMeta, r as RawTranslations } from "./types-BjTD5hUT.js";
2
+ import { n as ExtractedKey, r as ExtractionManifest } from "./types-BC-tQ30r.js";
3
+ import { n as I18nOptions } from "./types-LT4Zo0bM.js";
4
+ import { i as FallbackBehavior, n as LocaleResolver, r as i18n, t as I18nConfig } from "./i18n-C4nlkQTf.js";
5
+ import { AstroIntegration } from "astro";
7
6
  //#region src/integration/integration.d.ts
8
7
  /**
9
8
  * Astro integration for i18n with automatic per-chunk translation loading.
10
9
  *
11
10
  * Registers the vite plugin for translation key extraction, chunk mapping and
12
- * loader injection, and injects the chunk middleware serving `/_i18n/`
13
- * translation chunks. Client state delivery and translation preloading happen at
14
- * runtime: the i18n middleware bootstraps `window.__i18n__`, and the islands
15
- * emitter (via `@astroscope/node`) slices hashes and preloads translation chunks
16
- * per island — plain `client:*` directives, no forks.
11
+ * loader injection, and injects both middlewares: the chunk middleware serving
12
+ * `/_i18n/` translation chunks ahead of the project's own middleware, and the
13
+ * locale middleware after it, so the locale resolver from `i18n.configure` sees
14
+ * what a session middleware put on `locals`. Client state delivery and
15
+ * translation preloading happen at runtime: the locale middleware bootstraps
16
+ * `window.__i18n__`, and the islands emitter (via `@astroscope/node`) slices
17
+ * hashes and preloads translation chunks per island — plain `client:*`
18
+ * directives, no forks.
17
19
  */
18
20
  export default function i18nIntegration(options?: I18nOptions): AstroIntegration;
19
21
  //#endregion
20
- //#region src/server/middleware.d.ts
21
- type I18nMiddlewareOptions = {
22
- locale: (ctx: APIContext) => string;
23
- /**
24
- * Patterns to exclude from locale context setup.
25
- * Defaults to RECOMMENDED_EXCLUDES if not provided.
26
- *
27
- * @example
28
- * ```ts
29
- * import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';
30
- *
31
- * // extend defaults
32
- * createI18nMiddleware({
33
- * locale: (ctx) => ctx.locals.session?.locale ?? 'en',
34
- * exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],
35
- * })
36
- *
37
- * // disable excludes entirely
38
- * createI18nMiddleware({
39
- * locale: (ctx) => ctx.locals.session?.locale ?? 'en',
40
- * exclude: [],
41
- * })
42
- * ```
43
- */
44
- exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | undefined;
45
- };
46
- /**
47
- * Create the i18n locale middleware.
48
- *
49
- * Sets up the request context with locale and translations for `t()`, injects
50
- * the client i18n state into html responses, and records the locale for the
51
- * islands emitter. Place this after session middleware if your locale detection
52
- * depends on session/cookies.
53
- *
54
- * @example
55
- * ```typescript
56
- * import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';
57
- *
58
- * export const i18nMiddleware = createI18nMiddleware({
59
- * locale: (ctx) =>
60
- * ctx.cookies.get('locale')?.value ??
61
- * detectLocale(ctx.request) ??
62
- * i18n.getConfig().defaultLocale,
63
- * });
64
- * ```
65
- */
66
- export declare function createI18nMiddleware(options: I18nMiddlewareOptions): MiddlewareHandler;
67
- //#endregion
68
22
  //#region src/server/locale.d.ts
69
23
  /**
70
24
  * Detect locale from Accept-Language header.
@@ -72,5 +26,5 @@ export declare function createI18nMiddleware(options: I18nMiddlewareOptions): Mi
72
26
  */
73
27
  export declare function detectLocale(request: Request): string | undefined;
74
28
  //#endregion
75
- export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type I18nMiddlewareOptions, type RawTranslations, type TranslationMeta, i18n };
29
+ export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type I18nOptions, type LocaleResolver, type RawTranslations, type TranslationMeta, i18n };
76
30
  //# 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":";;;;;;;;;;;;;;;;;wBAcwB,gBAAgB,UAAS,cAAmB;;;KCNxD;EACV,SAAS,KAAK;;;;;;;;;;;;;;;;;;;;;;EAsBd,UAAU,qBAAqB,SAAS;;;;;;;;;;;;;;;;;;;;;;wBA2H1B,qBAAqB,SAAS,wBAAwB;;;;;;;wBCpJtD,aAAa,SAAS"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration/integration.ts","../src/server/locale.ts"],"mappings":";;;;;;;;;;;;;;;;;;;wBAiBwB,gBAAgB,UAAS,cAAmB;;;;;;;wBCXpD,aAAa,SAAS"}
package/dist/index.js CHANGED
@@ -1,19 +1,21 @@
1
- import { t as i18nVitePlugin } from "./vite-plugin-C-jEpd8t.js";
2
- import { r as i18n } from "./context-CgqIoo00.js";
3
- import { n as createI18nMiddleware } from "./middleware-OnRVbxvs.js";
1
+ import { t as i18nVitePlugin } from "./vite-plugin-CZqtAqQM.js";
2
+ import { t as i18n } from "./i18n-C47eQi8U.js";
4
3
  //#region src/integration/integration.ts
5
4
  /**
6
5
  * Astro integration for i18n with automatic per-chunk translation loading.
7
6
  *
8
7
  * Registers the vite plugin for translation key extraction, chunk mapping and
9
- * loader injection, and injects the chunk middleware serving `/_i18n/`
10
- * translation chunks. Client state delivery and translation preloading happen at
11
- * runtime: the i18n middleware bootstraps `window.__i18n__`, and the islands
12
- * emitter (via `@astroscope/node`) slices hashes and preloads translation chunks
13
- * per island — plain `client:*` directives, no forks.
8
+ * loader injection, and injects both middlewares: the chunk middleware serving
9
+ * `/_i18n/` translation chunks ahead of the project's own middleware, and the
10
+ * locale middleware after it, so the locale resolver from `i18n.configure` sees
11
+ * what a session middleware put on `locals`. Client state delivery and
12
+ * translation preloading happen at runtime: the locale middleware bootstraps
13
+ * `window.__i18n__`, and the islands emitter (via `@astroscope/node`) slices
14
+ * hashes and preloads translation chunks per island — plain `client:*`
15
+ * directives, no forks.
14
16
  */
15
17
  function i18nIntegration(options = {}) {
16
- const { consistency = "error" } = options;
18
+ const { consistency = "error", exclude = null } = options;
17
19
  return {
18
20
  name: "@astroscope/i18n",
19
21
  hooks: { "astro:config:setup": ({ addMiddleware, updateConfig, logger }) => {
@@ -21,9 +23,14 @@ function i18nIntegration(options = {}) {
21
23
  order: "pre",
22
24
  entrypoint: "@astroscope/i18n/chunk-middleware"
23
25
  });
26
+ addMiddleware({
27
+ order: "post",
28
+ entrypoint: "@astroscope/i18n/locale-middleware"
29
+ });
24
30
  updateConfig({ vite: { plugins: [i18nVitePlugin({
25
31
  logger,
26
- consistency
32
+ consistency,
33
+ exclude
27
34
  })] } });
28
35
  } }
29
36
  };
@@ -42,13 +49,14 @@ function detectLocale(request) {
42
49
  const preferred = acceptLanguage.split(",").map((part) => {
43
50
  const [lang, q = "q=1"] = part.split(";").map((s) => s.trim());
44
51
  return {
45
- lang: lang?.split("-")[0]?.toLowerCase(),
46
- q: parseFloat(q.replace("q=", ""))
52
+ lang: lang?.split("-", 1)[0]?.toLowerCase(),
53
+ q: Number.parseFloat(q.replace("q=", ""))
47
54
  };
48
- }).filter((p) => !!p.lang).sort((a, b) => b.q - a.q);
55
+ }).filter((p) => !!p.lang);
56
+ preferred.sort((a, b) => b.q - a.q);
49
57
  for (const { lang } of preferred) if (locales.includes(lang)) return lang;
50
58
  }
51
59
  //#endregion
52
- export { createI18nMiddleware, i18nIntegration as default, detectLocale, i18n };
60
+ export { i18nIntegration as default, detectLocale, i18n };
53
61
 
54
62
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/integration/integration.ts","../src/server/locale.ts"],"sourcesContent":["import type { AstroIntegration } from 'astro';\nimport { i18nVitePlugin } from '../extraction/vite-plugin.js';\nimport type { I18nOptions } from './types.js';\n\n/**\n * Astro integration for i18n with automatic per-chunk translation loading.\n *\n * Registers the vite plugin for translation key extraction, chunk mapping and\n * loader injection, and injects the chunk middleware serving `/_i18n/`\n * translation chunks. Client state delivery and translation preloading happen at\n * runtime: the i18n middleware bootstraps `window.__i18n__`, and the islands\n * emitter (via `@astroscope/node`) slices hashes and preloads translation chunks\n * per island — plain `client:*` directives, no forks.\n */\nexport default function i18nIntegration(options: I18nOptions = {}): AstroIntegration {\n const { consistency = 'error' } = options;\n\n return {\n name: '@astroscope/i18n',\n hooks: {\n 'astro:config:setup': ({ addMiddleware, updateConfig, logger }) => {\n addMiddleware({ order: 'pre', entrypoint: '@astroscope/i18n/chunk-middleware' });\n\n updateConfig({\n vite: {\n plugins: [i18nVitePlugin({ logger, consistency })],\n },\n });\n },\n },\n };\n}\n","import { i18n } from './i18n.js';\n\n/**\n * Detect locale from Accept-Language header.\n * Returns best match from configured locales, or undefined if none match.\n */\nexport function detectLocale(request: Request): string | undefined {\n if (!i18n.isConfigured()) return undefined;\n\n const { locales } = i18n.getConfig();\n const acceptLanguage = request.headers.get('accept-language');\n\n if (!acceptLanguage) return undefined;\n\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n // optional whitespace around `;` is legal per rfc 9110\n const [lang, q = 'q=1'] = part.split(';').map((s) => s.trim());\n const baseLang = lang?.split('-')[0]?.toLowerCase();\n\n return {\n lang: baseLang,\n q: parseFloat(q.replace('q=', '')),\n };\n })\n .filter((p): p is { lang: string; q: number } => !!p.lang)\n .sort((a, b) => b.q - a.q);\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) {\n return lang;\n }\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAwB,gBAAgB,UAAuB,CAAC,GAAqB;CACnF,MAAM,EAAE,cAAc,YAAY;CAElC,OAAO;EACL,MAAM;EACN,OAAO,EACL,uBAAuB,EAAE,eAAe,cAAc,aAAa;GACjE,cAAc;IAAE,OAAO;IAAO,YAAY;GAAoC,CAAC;GAE/E,aAAa,EACX,MAAM,EACJ,SAAS,CAAC,eAAe;IAAE;IAAQ;GAAY,CAAC,CAAC,EACnD,EACF,CAAC;EACH,EACF;CACF;AACF;;;;;;;ACzBA,SAAgB,aAAa,SAAsC;CACjE,IAAI,CAAC,KAAK,aAAa,GAAG,OAAO,KAAA;CAEjC,MAAM,EAAE,YAAY,KAAK,UAAU;CACnC,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,iBAAiB;CAE5D,IAAI,CAAC,gBAAgB,OAAO,KAAA;CAE5B,MAAM,YAAY,eACf,MAAM,GAAG,CAAC,CACV,KAAK,SAAS;EAEb,MAAM,CAAC,MAAM,IAAI,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC;EAG7D,OAAO;GACL,MAHe,MAAM,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,YAAY;GAIhD,GAAG,WAAW,EAAE,QAAQ,MAAM,EAAE,CAAC;EACnC;CACF,CAAC,CAAC,CACD,QAAQ,MAAwC,CAAC,CAAC,EAAE,IAAI,CAAC,CACzD,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;CAE3B,KAAK,MAAM,EAAE,UAAU,WACrB,IAAI,QAAQ,SAAS,IAAI,GACvB,OAAO;AAKb"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/integration/integration.ts","../src/server/locale.ts"],"sourcesContent":["import type { AstroIntegration } from 'astro';\nimport { i18nVitePlugin } from '../extraction/vite-plugin.js';\nimport type { I18nOptions } from './types.js';\n\n/**\n * Astro integration for i18n with automatic per-chunk translation loading.\n *\n * Registers the vite plugin for translation key extraction, chunk mapping and\n * loader injection, and injects both middlewares: the chunk middleware serving\n * `/_i18n/` translation chunks ahead of the project's own middleware, and the\n * locale middleware after it, so the locale resolver from `i18n.configure` sees\n * what a session middleware put on `locals`. Client state delivery and\n * translation preloading happen at runtime: the locale middleware bootstraps\n * `window.__i18n__`, and the islands emitter (via `@astroscope/node`) slices\n * hashes and preloads translation chunks per island — plain `client:*`\n * directives, no forks.\n */\nexport default function i18nIntegration(options: I18nOptions = {}): AstroIntegration {\n const { consistency = 'error', exclude = null } = options;\n\n return {\n name: '@astroscope/i18n',\n hooks: {\n 'astro:config:setup': ({ addMiddleware, updateConfig, logger }) => {\n addMiddleware({ order: 'pre', entrypoint: '@astroscope/i18n/chunk-middleware' });\n addMiddleware({ order: 'post', entrypoint: '@astroscope/i18n/locale-middleware' });\n\n updateConfig({\n vite: {\n plugins: [i18nVitePlugin({ logger, consistency, exclude })],\n },\n });\n },\n },\n };\n}\n","import { i18n } from './i18n.js';\n\n/**\n * Detect locale from Accept-Language header.\n * Returns best match from configured locales, or undefined if none match.\n */\nexport function detectLocale(request: Request): string | undefined {\n if (!i18n.isConfigured()) return undefined;\n\n const { locales } = i18n.getConfig();\n const acceptLanguage = request.headers.get('accept-language');\n\n if (!acceptLanguage) return undefined;\n\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n // optional whitespace around `;` is legal per rfc 9110\n const [lang, q = 'q=1'] = part.split(';').map((s) => s.trim());\n const baseLang = lang?.split('-', 1)[0]?.toLowerCase();\n\n return {\n lang: baseLang,\n // eslint-disable-next-line unicorn/prefer-number-coercion -- Number() differs on malformed q values ('' → 0, '0.8x' → NaN)\n q: Number.parseFloat(q.replace('q=', '')),\n };\n })\n .filter((p): p is { lang: string; q: number } => !!p.lang);\n\n preferred.sort((a, b) => b.q - a.q);\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) {\n return lang;\n }\n }\n\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAiBA,SAAwB,gBAAgB,UAAuB,CAAC,GAAqB;CACnF,MAAM,EAAE,cAAc,SAAS,UAAU,SAAS;CAElD,OAAO;EACL,MAAM;EACN,OAAO,EACL,uBAAuB,EAAE,eAAe,cAAc,aAAa;GACjE,cAAc;IAAE,OAAO;IAAO,YAAY;GAAoC,CAAC;GAC/E,cAAc;IAAE,OAAO;IAAQ,YAAY;GAAqC,CAAC;GAEjF,aAAa,EACX,MAAM,EACJ,SAAS,CAAC,eAAe;IAAE;IAAQ;IAAa;GAAQ,CAAC,CAAC,EAC5D,EACF,CAAC;EACH,EACF;CACF;AACF;;;;;;;AC7BA,SAAgB,aAAa,SAAsC;CACjE,IAAI,CAAC,KAAK,aAAa,GAAG,OAAO,KAAA;CAEjC,MAAM,EAAE,YAAY,KAAK,UAAU;CACnC,MAAM,iBAAiB,QAAQ,QAAQ,IAAI,iBAAiB;CAE5D,IAAI,CAAC,gBAAgB,OAAO,KAAA;CAE5B,MAAM,YAAY,eACf,MAAM,GAAG,CAAC,CACV,KAAK,SAAS;EAEb,MAAM,CAAC,MAAM,IAAI,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,KAAK,MAAM,EAAE,KAAK,CAAC;EAG7D,OAAO;GACL,MAHe,MAAM,MAAM,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,YAAY;GAKnD,GAAG,OAAO,WAAW,EAAE,QAAQ,MAAM,EAAE,CAAC;EAC1C;CACF,CAAC,CAAC,CACD,QAAQ,MAAwC,CAAC,CAAC,EAAE,IAAI;CAE3D,UAAU,MAAM,GAAG,MAAM,EAAE,IAAI,EAAE,CAAC;CAElC,KAAK,MAAM,EAAE,UAAU,WACrB,IAAI,QAAQ,SAAS,IAAI,GACvB,OAAO;AAKb"}
@@ -1,5 +1,5 @@
1
- import { n as I18nClientState } from "./types-DU8QHch4.js";
2
- import { i as I18nContext, n as i18n } from "./i18n-Czkrfb_4.js";
1
+ import { n as I18nClientState } from "./types-BjTD5hUT.js";
2
+ import { a as I18nContext, r as i18n } from "./i18n-C4nlkQTf.js";
3
3
  //#region src/server/context.d.ts
4
4
  /**
5
5
  * Get the current i18n context
package/dist/internal.js CHANGED
@@ -1,2 +1,3 @@
1
- import { r as i18n, t as getContext } from "./context-CgqIoo00.js";
1
+ import { t as i18n } from "./i18n-C47eQi8U.js";
2
+ import { t as getContext } from "./context-BCUJxb8p.js";
2
3
  export { getContext, i18n };
@@ -8,7 +8,7 @@ import path from "node:path";
8
8
  const I18N_MANIFEST_GLOBAL_KEY = "__astroscope_i18n_manifest__";
9
9
  function getGlobalState() {
10
10
  const g = globalThis;
11
- if (!g[I18N_MANIFEST_GLOBAL_KEY]) g[I18N_MANIFEST_GLOBAL_KEY] = {
11
+ g[I18N_MANIFEST_GLOBAL_KEY] ??= {
12
12
  extractedKeys: [],
13
13
  chunkManifest: {},
14
14
  scriptChunks: [],
@@ -41,4 +41,4 @@ function getManifest() {
41
41
  //#endregion
42
42
  export { getManifest as n, getGlobalState as t };
43
43
 
44
- //# sourceMappingURL=manifest-Cma68XM-.js.map
44
+ //# sourceMappingURL=manifest-X-UhW6tH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest-X-UhW6tH.js","names":[],"sources":["../src/extraction/manifest.ts"],"sourcesContent":["/**\n * Manifest state management for i18n extraction\n *\n * Separated from vite-plugin to avoid bundling Babel at runtime\n */\n\nimport path from 'node:path';\nimport type { ChunkManifest, ExtractedKey, ExtractionManifest } from './types.js';\n\n// global state for dev mode live access via globalThis\n// this allows the virtual module to access current extraction data\n// even after initial import (since dev mode transforms are on-demand)\nconst I18N_MANIFEST_GLOBAL_KEY = '__astroscope_i18n_manifest__';\n\nexport type GlobalI18nState = {\n extractedKeys: ExtractedKey[];\n chunkManifest: ChunkManifest;\n scriptChunks: string[];\n\n // project root for relative paths\n projectRoot: string;\n\n // incremented on each sync to signal cache invalidation\n version: number;\n};\n\nexport function getGlobalState(): GlobalI18nState {\n const g = globalThis as Record<string, unknown>;\n\n g[I18N_MANIFEST_GLOBAL_KEY] ??= {\n extractedKeys: [],\n chunkManifest: {},\n scriptChunks: [],\n projectRoot: '',\n version: 0,\n } satisfies GlobalI18nState;\n\n return g[I18N_MANIFEST_GLOBAL_KEY] as GlobalI18nState;\n}\n\n/**\n * Get the current extraction manifest (for dev mode live access).\n * This bypasses Vite's module caching to return fresh data.\n */\nexport function getManifest(): ExtractionManifest {\n const state = getGlobalState();\n\n const keys = state.extractedKeys.map((key) => ({\n ...key,\n // make file paths relative to project root for better readability\n // and avoiding leaking absolute paths from dev or ci environments\n files: key.files.map((fileLocation) => {\n // split on the trailing `:line` only — the path itself may contain colons (windows drive letters)\n const match = /^(.+):(\\d+)$/.exec(fileLocation);\n const [filePath, line] = match ? [match[1], match[2]] : [fileLocation, undefined];\n\n if (!filePath) return fileLocation;\n\n const relativePath = state.projectRoot ? path.relative(state.projectRoot, filePath) : filePath;\n\n return line ? `${relativePath}:${line}` : relativePath;\n }),\n }));\n\n return { keys, chunks: state.chunkManifest, scripts: state.scriptChunks };\n}\n"],"mappings":";;;;;;;AAYA,MAAM,2BAA2B;AAcjC,SAAgB,iBAAkC;CAChD,MAAM,IAAI;CAEV,EAAE,8BAA8B;EAC9B,eAAe,CAAC;EAChB,eAAe,CAAC;EAChB,cAAc,CAAC;EACf,aAAa;EACb,SAAS;CACX;CAEA,OAAO,EAAE;AACX;;;;;AAMA,SAAgB,cAAkC;CAChD,MAAM,QAAQ,eAAe;CAmB7B,OAAO;EAAE,MAjBI,MAAM,cAAc,KAAK,SAAS;GAC7C,GAAG;GAGH,OAAO,IAAI,MAAM,KAAK,iBAAiB;IAErC,MAAM,QAAQ,eAAe,KAAK,YAAY;IAC9C,MAAM,CAAC,UAAU,QAAQ,QAAQ,CAAC,MAAM,IAAI,MAAM,EAAE,IAAI,CAAC,cAAc,KAAA,CAAS;IAEhF,IAAI,CAAC,UAAU,OAAO;IAEtB,MAAM,eAAe,MAAM,cAAc,KAAK,SAAS,MAAM,aAAa,QAAQ,IAAI;IAEtF,OAAO,OAAO,GAAG,aAAa,GAAG,SAAS;GAC5C,CAAC;EACH,EAEY;EAAG,QAAQ,MAAM;EAAe,SAAS,MAAM;CAAa;AAC1E"}
@@ -1,5 +1,6 @@
1
1
  import { t as buildI18nChunkUrl } from "./url-CifeaDzl.js";
2
- import { n as runWithContext, r as i18n } from "./context-CgqIoo00.js";
2
+ import { t as i18n } from "./i18n-C47eQi8U.js";
3
+ import { n as runWithContext } from "./context-BCUJxb8p.js";
3
4
  import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
4
5
  import { log, overrideRequestRoute } from "@astroscope/node/log";
5
6
  import { registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node/islands";
@@ -12,7 +13,7 @@ import { registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node
12
13
  */
13
14
  /** translation values may contain `<\/script>` — escape `<` so they cannot end the tag */
14
15
  function jsonForScript(value) {
15
- return JSON.stringify(value).replace(/</g, "\\u003c");
16
+ return JSON.stringify(value).replaceAll("<", String.raw`\u003c`);
16
17
  }
17
18
  /** the full dev-mode state: every translation for the locale */
18
19
  function createFullStateScript(locale) {
@@ -141,7 +142,8 @@ function registerI18nEmitters() {
141
142
  }
142
143
  const merge = {};
143
144
  let fresh = false;
144
- for (const [chunk, hash] of chunks) if (!emitted.has(chunk)) {
145
+ for (const [chunk, hash] of chunks) {
146
+ if (emitted.has(chunk)) continue;
145
147
  emitted.add(chunk);
146
148
  merge[chunk] = hash;
147
149
  fresh = true;
@@ -216,43 +218,39 @@ function createI18nChunkMiddleware() {
216
218
  /**
217
219
  * Create the i18n locale middleware.
218
220
  *
219
- * Sets up the request context with locale and translations for `t()`, injects
220
- * the client i18n state into html responses, and records the locale for the
221
- * islands emitter. Place this after session middleware if your locale detection
222
- * depends on session/cookies.
221
+ * Resolves the request's locale through the resolver from `i18n.configure({ locale })`
222
+ * (the default locale when there is none or it returns `undefined`), sets up the
223
+ * request context for `t()`, records the locale for the islands emitter, which
224
+ * delivers the client state, and runs the rest of the chain inside that context.
223
225
  *
224
- * @example
225
- * ```typescript
226
- * import { createI18nMiddleware, detectLocale, i18n } from '@astroscope/i18n';
226
+ * Injected by the integration as `post` middleware, after the project's own, so the
227
+ * resolver sees whatever a session middleware put on `locals` — not part of the
228
+ * public API.
227
229
  *
228
- * export const i18nMiddleware = createI18nMiddleware({
229
- * locale: (ctx) =>
230
- * ctx.cookies.get('locale')?.value ??
231
- * detectLocale(ctx.request) ??
232
- * i18n.getConfig().defaultLocale,
233
- * });
234
- * ```
230
+ * @internal
235
231
  */
236
- function createI18nMiddleware(options) {
232
+ function createI18nLocaleMiddleware(options = {}) {
237
233
  registerI18nEmitters();
234
+ const exclude = options.exclude ?? RECOMMENDED_EXCLUDES;
238
235
  return (ctx, next) => {
239
- if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) return next();
236
+ if (shouldExclude(ctx, exclude)) return next();
240
237
  if (!i18n.isConfigured()) {
241
238
  log.error("i18n not configured, passing through");
242
239
  return next();
243
240
  }
244
- const locale = options.locale(ctx);
241
+ const config = i18n.getConfig();
242
+ const locale = config.locale?.(ctx) ?? config.defaultLocale;
245
243
  setRequestLocale(ctx, locale);
246
244
  const context = {
247
245
  locale,
248
246
  translations: i18n.getCompiledTranslations(locale),
249
247
  rawTranslations: i18n.getTranslations(locale),
250
- fallback: i18n.getConfig().fallback
248
+ fallback: config.fallback
251
249
  };
252
250
  return runWithContext(context, () => next());
253
251
  };
254
252
  }
255
253
  //#endregion
256
- export { createI18nMiddleware as n, createI18nChunkMiddleware as t };
254
+ export { createI18nLocaleMiddleware as n, createI18nChunkMiddleware as t };
257
255
 
258
- //# sourceMappingURL=middleware-OnRVbxvs.js.map
256
+ //# sourceMappingURL=middleware-BSEtDRYy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware-BSEtDRYy.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).replaceAll('<', String.raw`\\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 * It is keyed on `context.locals`, the one object astro keeps for the whole\n * request: `context.request` is replaced by every `next(url)` rewrite, once per\n * nesting level of `sequence`, so the request the middleware saw is not the one\n * the islands middleware holds when the emitters run.\n */\n\nconst requestLocales = new WeakMap<object, string>();\nconst emittedByRequest = new WeakMap<object, 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(context: APIContext, locale: string): void {\n requestLocales.set(context.locals, 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.locals);\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.locals);\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 = emittedByRequest.get(context.locals);\n\n if (!emitted) {\n emitted = new Set();\n emittedByRequest.set(context.locals, 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 continue;\n }\n\n emitted.add(chunk);\n merge[chunk] = hash;\n fresh = true;\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 { 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 I18nLocaleMiddlewareOptions = {\n /** patterns to skip locale context setup for; defaults to RECOMMENDED_EXCLUDES */\n exclude?: ExcludePattern[] | 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 // {locale}/{chunkName}.{hash}.js — sliced by hand, this runs for every chunk request\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 * Resolves the request's locale through the resolver from `i18n.configure({ locale })`\n * (the default locale when there is none or it returns `undefined`), sets up the\n * request context for `t()`, records the locale for the islands emitter, which\n * delivers the client state, and runs the rest of the chain inside that context.\n *\n * Injected by the integration as `post` middleware, after the project's own, so the\n * resolver sees whatever a session middleware put on `locals` — not part of the\n * public API.\n *\n * @internal\n */\nexport function createI18nLocaleMiddleware(options: I18nLocaleMiddlewareOptions = {}): MiddlewareHandler {\n registerI18nEmitters();\n\n const exclude = options.exclude ?? RECOMMENDED_EXCLUDES;\n\n return (ctx, next) => {\n if (shouldExclude(ctx, exclude)) {\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 config = i18n.getConfig();\n const locale = config.locale?.(ctx) ?? config.defaultLocale;\n\n // the islands emitter runs while the response streams, outside this ALS scope\n setRequestLocale(ctx, locale);\n\n const context: I18nContext = {\n locale,\n translations: i18n.getCompiledTranslations(locale),\n rawTranslations: i18n.getTranslations(locale),\n fallback: config.fallback,\n };\n\n return runWithContext(context, () => next());\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAUA,SAAgB,cAAc,OAAwB;CACpD,OAAO,KAAK,UAAU,KAAK,CAAC,CAAC,WAAW,KAAK,OAAO,GAAG,QAAQ;AACjE;;AAGA,SAAgB,sBAAsB,QAAwB;CAI5D,OAAO,sFAFO,cAAc;EAAE;EAAQ,QAAQ,CAAC;EAAG,cAD7B,KAAK,gBAAgB,MACmB;CAAE,CAEkC,EAAE;AACrG;;AAGA,SAAgB,yBAAyB,QAA+B;CACtE,MAAM,SAAS,KAAK,qBAAqB,MAAM;CAE/C,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,WAAW,GACjC,OAAO;CAKT,OAAO,sCAFM,cAAc;EAAE;EAAQ,QAAQ,CAAC;EAAG,cAAc,CAAC;CAAE,CAElB,EAAE,0BAA0B,cAAc,MAAM,EAAE;AACpG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACMA,MAAM,iCAAiB,IAAI,QAAwB;AACnD,MAAM,mCAAmB,IAAI,QAA6B;AAI1D,MAAM,SAAS,CAAC,CAAE,YAA4C,KAAK;AAEnE,IAAI,aAAa;AAEjB,SAAgB,iBAAiB,SAAqB,QAAsB;CAC1E,eAAe,IAAI,QAAQ,QAAQ,MAAM;AAC3C;;AAGA,SAAS,eAAe,KAAqB;CAC3C,OAAO,IAAI,MAAM,IAAI,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,SAAS,EAAE;AAChE;AAeA,MAAM,yBAAS,IAAI,QAAkE;AACrF,MAAM,wBAAQ,IAAI,IAAoB;AAEtC,SAAS,SAAS,QAAoB,QAAgB,QAA6C;CACjG,IAAI,WAAW,OAAO,IAAI,MAAM;CAEhC,IAAI,CAAC,UAAU;EACb,2BAAW,IAAI,QAAQ;EACvB,OAAO,IAAI,QAAQ,QAAQ;CAC7B;CAEA,IAAI,QAAQ,SAAS,IAAI,MAAM;CAE/B,IAAI,OACF,OAAO;CAGT,MAAM,cAAc,SAAuC;EACzD,MAAM,SAA6B,CAAC;EAEpC,KAAK,MAAM,OAAO,MAAM;GACtB,MAAM,QAAQ,eAAe,GAAG;GAChC,MAAM,OAAO,OAAO;GAEpB,IAAI,MACF,OAAO,KAAK,CAAC,OAAO,IAAI,CAAC;EAE7B;EAEA,OAAO;CACT;CAEA,MAAM,SAAS,WAAW,OAAO,WAAW;CAE5C,QAAQ;EACN;EACA,OAAO,WAAW,OAAO,aAAa,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,kBAAkB,QAAQ,OAAO,IAAI,CAAC;EACrG,SAAS,OAAO,KAAK,CAAC,OAAO,UAAU,kBAAkB,QAAQ,OAAO,IAAI,CAAC;CAC/E;CACA,SAAS,IAAI,QAAQ,KAAK;CAE1B,OAAO;AACT;AAEA,SAAS,QAAQ,QAAwB;CACvC,IAAI,OAAO,MAAM,IAAI,MAAM;CAE3B,IAAI,SAAS,KAAA,GAAW;EACtB,OAAO,cAAc;GAAE;GAAQ,QAAQ,CAAC;GAAG,cAAc,CAAC;EAAE,CAAC;EAC7D,MAAM,IAAI,QAAQ,IAAI;CACxB;CAEA,OAAO;AACT;;;;AAKA,SAAgB,0BAA0B,MAAe,QAAyB;CAChF,QAAQ,YAAY;EAClB,IAAI,CAAC,KAAK,aAAa,GACrB,OAAO;EAGT,MAAM,SAAS,eAAe,IAAI,QAAQ,MAAM;EAEhD,IAAI,WAAW,KAAA,GACb,OAAO;EAGT,IAAI,KACF,OAAO,EAAE,MAAM,sBAAsB,MAAM,EAAE;EAG/C,IAAI,OAAO,KAAK,KAAK,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,WAAW,GACpD,OAAO;EAGT,OAAO,EAAE,WAAW,yBAAyB,MAAM,EAAE;CACvD;AACF;AAEA,SAAgB,uBAA6B;CAC3C,IAAI,YACF;CAGF,aAAa;CAEb,wBAAwB,0BAA0B,CAAC;CAEnD,uBAAuB,QAAQ,YAAY;EAEzC,IAAI,CAAC,WAAW,CAAC,KAAK,aAAa,GACjC,OAAO;EAGT,MAAM,SAAS,eAAe,IAAI,QAAQ,MAAM;EAEhD,IAAI,WAAW,KAAA,GACb,OAAO;EAIT,MAAM,EAAE,QAAQ,OAAO,YAAY,SAAS,QAAQ,QADrC,KAAK,UAAU,MACmC,CAAC;EAElE,IAAI,UAAU,iBAAiB,IAAI,QAAQ,MAAM;EAEjD,IAAI,CAAC,SAAS;GACZ,0BAAU,IAAI,IAAI;GAClB,iBAAiB,IAAI,QAAQ,QAAQ,OAAO;EAC9C;EAEA,MAAM,QAAgC,CAAC;EACvC,IAAI,QAAQ;EAEZ,KAAK,MAAM,CAAC,OAAO,SAAS,QAAQ;GAClC,IAAI,QAAQ,IAAI,KAAK,GACnB;GAGF,QAAQ,IAAI,KAAK;GACjB,MAAM,SAAS;GACf,QAAQ;EACV;EAEA,MAAM,OAAO,QACT,sCAAsC,QAAQ,MAAM,EAAE,0BAA0B,cAAc,KAAK,EAAE,iBACrG,KAAA;EAEJ,IAAI,CAAC,QAAQ,MAAM,WAAW,KAAK,QAAQ,WAAW,GACpD,OAAO;EAGT,OAAO;GACL;GACA,OAAO,MAAM,SAAS,IAAI,QAAQ,KAAA;GAClC,SAAS,QAAQ,SAAS,IAAI,UAAU,KAAA;EAC1C;CACF,CAAC;AACH;;;ACrMA,MAAM,uBAAuB;AAI7B,MAAM,sBAAsB;;;;;AAM5B,SAAS,oBAAoB,UAAwC;CACnE,IAAI,CAAC,SAAS,WAAW,oBAAoB,GAC3C;CAGF,IAAI,CAAC,KAAK,aAAa,GAAG;EACxB,IAAI,MAAM,sCAAsC;EAEhD;CACF;CAEA,MAAM,OAAO,SAAS,MAAM,CAA2B;CAGvD,MAAM,WAAW,KAAK,QAAQ,GAAG;CAEjC,IAAI,aAAa,IACf;CAGF,MAAM,SAAS,KAAK,MAAM,GAAG,QAAQ;CAKrC,IAAI,CAFW,KAAK,UAEV,CAAC,CAAC,QAAQ,SAAS,MAAM,GACjC,OAAO,IAAI,SAAS,wBAAwB;EAC1C,QAAQ;EACR,SAAS,EAAE,gBAAgB,yBAAyB;CACtD,CAAC;CAGH,MAAM,OAAO,KAAK,MAAM,WAAW,CAAC;CAEpC,IAAI,CAAC,KAAK,SAAS,KAAK,GACtB;CAGF,MAAM,YAAY,KAAK,MAAM,GAAG,EAAE;CAClC,MAAM,aAAa,UAAU,YAAY,GAAG;CAE5C,IAAI,eAAe,IACjB;CAGF,MAAM,YAAY,UAAU,MAAM,GAAG,UAAU;CAC/C,MAAM,OAAO,KAAK,aAAa,QAAQ,SAAS;CAEhD,IAAI,CAAC,MACH,OAAO,IAAI,SAAS,uBAAuB,UAAU,MAAM;EACzD,QAAQ;EACR,SAAS,EAAE,gBAAgB,yBAAyB;CACtD,CAAC;CAGH,OAAO,IAAI,SAAS,MAAkB;EACpC,QAAQ;EACR,SAAS;GACP,gBAAgB;GAChB,kBAAkB,OAAO,KAAK,UAAU;GACxC,iBAAiB;EACnB;CACF,CAAC;AACH;;;;;;;;;AAUA,SAAgB,4BAA+C;CAC7D,QAAQ,KAAK,SAAS;EACpB,MAAM,WAAW,oBAAoB,IAAI,IAAI,QAAQ;EAErD,IAAI,CAAC,UACH,OAAO,KAAK;EAId,qBAAqB,mBAAmB;EAExC,OAAO;CACT;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,2BAA2B,UAAuC,CAAC,GAAsB;CACvG,qBAAqB;CAErB,MAAM,UAAU,QAAQ,WAAW;CAEnC,QAAQ,KAAK,SAAS;EACpB,IAAI,cAAc,KAAK,OAAO,GAC5B,OAAO,KAAK;EAGd,IAAI,CAAC,KAAK,aAAa,GAAG;GACxB,IAAI,MAAM,sCAAsC;GAEhD,OAAO,KAAK;EACd;EAEA,MAAM,SAAS,KAAK,UAAU;EAC9B,MAAM,SAAS,OAAO,SAAS,GAAG,KAAK,OAAO;EAG9C,iBAAiB,KAAK,MAAM;EAE5B,MAAM,UAAuB;GAC3B;GACA,cAAc,KAAK,wBAAwB,MAAM;GACjD,iBAAiB,KAAK,gBAAgB,MAAM;GAC5C,UAAU,OAAO;EACnB;EAEA,OAAO,eAAe,eAAe,KAAK,CAAC;CAC7C;AACF"}
@@ -32,6 +32,31 @@ function addKeyIfNeeded(element, key) {
32
32
  function isMarkupPart(part) {
33
33
  return part.type === "markup";
34
34
  }
35
+ function applyMarkupPart(part, stack, result, components, nextKey) {
36
+ const target = stack.length > 0 ? stack.at(-1).children : result;
37
+ switch (part.kind) {
38
+ case "open":
39
+ stack.push({
40
+ name: part.name,
41
+ children: []
42
+ });
43
+ break;
44
+ case "close": {
45
+ const frame = stack.pop();
46
+ if (!frame) break;
47
+ const component = components[frame.name];
48
+ const parent = stack.length > 0 ? stack.at(-1).children : result;
49
+ if (component) parent.push(addKeyIfNeeded(component(frame.children), nextKey()));
50
+ else parent.push(...frame.children);
51
+ break;
52
+ }
53
+ case "standalone": {
54
+ const component = components[part.name];
55
+ if (component) target.push(addKeyIfNeeded(component([]), nextKey()));
56
+ break;
57
+ }
58
+ }
59
+ }
35
60
  /**
36
61
  * Convert MF2 formatToParts() output to a tree of nodes.
37
62
  * Works with any JSX runtime (Astro, React, etc.) - the callbacks
@@ -51,25 +76,11 @@ function partsToNodes(parts, components) {
51
76
  const result = [];
52
77
  const stack = [];
53
78
  let keyIndex = 0;
79
+ const nextKey = () => `rich-${keyIndex++}`;
54
80
  for (const part of parts) {
55
- const target = stack.length > 0 ? stack[stack.length - 1].children : result;
56
- if (isMarkupPart(part)) {
57
- if (part.kind === "open") stack.push({
58
- name: part.name,
59
- children: []
60
- });
61
- else if (part.kind === "close") {
62
- const frame = stack.pop();
63
- if (!frame) continue;
64
- const component = components[frame.name];
65
- const parent = stack.length > 0 ? stack[stack.length - 1].children : result;
66
- if (component) parent.push(addKeyIfNeeded(component(frame.children), `rich-${keyIndex++}`));
67
- else parent.push(...frame.children);
68
- } else if (part.kind === "standalone") {
69
- const component = components[part.name];
70
- if (component) target.push(addKeyIfNeeded(component([]), `rich-${keyIndex++}`));
71
- }
72
- } else if (part.type === "text") target.push(part.value);
81
+ const target = stack.length > 0 ? stack.at(-1).children : result;
82
+ if (isMarkupPart(part)) applyMarkupPart(part, stack, result, components, nextKey);
83
+ else if (part.type === "text") target.push(part.value);
73
84
  else if (part.type === "bidiIsolation") {} else if ("value" in part && part.value !== void 0 && part.value !== null) {
74
85
  const stringValue = String(part.value);
75
86
  target.push(stringValue);
@@ -77,11 +88,11 @@ function partsToNodes(parts, components) {
77
88
  }
78
89
  while (stack.length > 0) {
79
90
  const frame = stack.pop();
80
- (stack.length > 0 ? stack[stack.length - 1].children : result).push(...frame.children);
91
+ (stack.length > 0 ? stack.at(-1).children : result).push(...frame.children);
81
92
  }
82
93
  return result;
83
94
  }
84
95
  //#endregion
85
96
  export { normalizeMeta as n, partsToNodes as t };
86
97
 
87
- //# sourceMappingURL=rich-CHisXE-A.js.map
98
+ //# sourceMappingURL=rich-BDqjhv2h.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-BDqjhv2h.js","names":[],"sources":["../src/shared/meta.ts","../src/shared/rich.ts"],"sourcesContent":["import type { TranslationMeta } from './types.js';\n\n/**\n * Normalize meta argument to TranslationMeta object\n */\nexport function normalizeMeta(meta: TranslationMeta | string): TranslationMeta {\n return typeof meta === 'string' ? { fallback: meta } : meta;\n}\n","import type { MessageMarkupPart, MessagePart } from 'messageformat';\n\n// MessagePart<string> covers all built-in part types\ntype Part = MessagePart<string>;\n\n/**\n * Component callbacks for rich text rendering.\n * Each key maps to a function that wraps children in a component.\n *\n * @example\n * ```tsx\n * const components: RichComponents = {\n * link: (children) => <a href=\"/tos\">{children}</a>,\n * bold: (children) => <strong>{children}</strong>,\n * };\n * ```\n */\nexport type RichComponents<T = unknown> = Record<string, (children: (string | T)[]) => T>;\n\ntype StackFrame<T> = {\n name: string;\n children: (string | T)[];\n};\n\n// react element symbols - Symbol.for() returns the same symbol React uses internally\nconst REACT_ELEMENT_TYPE = Symbol.for('react.element');\nconst REACT_TRANSITIONAL_ELEMENT_TYPE = Symbol.for('react.transitional.element');\n\ntype ReactElementLike = {\n $$typeof: symbol;\n key: string | null;\n};\n\n/**\n * Duck-type check for React elements without importing React.\n * Uses Symbol.for() which returns the same symbol React uses internally.\n */\nfunction isReactElement(value: unknown): value is ReactElementLike {\n if (typeof value !== 'object' || value === null || !('$$typeof' in value)) {\n return false;\n }\n\n const type = (value as ReactElementLike).$$typeof;\n\n return type === REACT_ELEMENT_TYPE || type === REACT_TRANSITIONAL_ELEMENT_TYPE;\n}\n\n/**\n * Add a key to a React element if it doesn't have one.\n * Creates a shallow copy since React elements are frozen.\n */\nfunction addKeyIfNeeded<T>(element: T, key: string): T {\n if (isReactElement(element) && element.key === null) {\n return { ...element, key } as T;\n }\n\n return element;\n}\n\nfunction isMarkupPart(part: Part): part is MessageMarkupPart {\n return part.type === 'markup';\n}\n\nfunction applyMarkupPart<T>(\n part: MessageMarkupPart,\n stack: StackFrame<T>[],\n result: (string | T)[],\n components: RichComponents<T>,\n nextKey: () => string,\n): void {\n const target = stack.length > 0 ? stack.at(-1)!.children : result;\n\n switch (part.kind) {\n case 'open': {\n stack.push({ name: part.name, children: [] });\n\n break;\n }\n\n case 'close': {\n const frame = stack.pop();\n\n if (!frame) {\n // mismatched close tag, ignore\n break;\n }\n\n const component = components[frame.name];\n const parent = stack.length > 0 ? stack.at(-1)!.children : result;\n\n if (component) {\n parent.push(addKeyIfNeeded(component(frame.children), nextKey()));\n } else {\n // no component for this tag, just flatten children\n parent.push(...frame.children);\n }\n\n break;\n }\n\n case 'standalone': {\n // self-closing: {#icon/}\n const component = components[part.name];\n\n if (component) {\n target.push(addKeyIfNeeded(component([]), nextKey()));\n }\n\n break;\n }\n }\n}\n\n/**\n * Convert MF2 formatToParts() output to a tree of nodes.\n * Works with any JSX runtime (Astro, React, etc.) - the callbacks\n * return whatever JSX elements the active runtime compiles to.\n *\n * @example\n * MF2 message: \"Read our {#link}Terms{/link}\"\n * Parts: [\n * { type: 'text', value: 'Read our ' },\n * { type: 'markup', kind: 'open', name: 'link' },\n * { type: 'text', value: 'Terms' },\n * { type: 'markup', kind: 'close', name: 'link' },\n * ]\n * Result: ['Read our ', <a href=\"/tos\">Terms</a>]\n */\nexport function partsToNodes<T>(parts: Part[], components: RichComponents<T>): (string | T)[] {\n const result: (string | T)[] = [];\n const stack: StackFrame<T>[] = [];\n let keyIndex = 0;\n const nextKey = () => `rich-${keyIndex++}`;\n\n for (const part of parts) {\n const target = stack.length > 0 ? stack.at(-1)!.children : result;\n\n if (isMarkupPart(part)) {\n applyMarkupPart(part, stack, result, components, nextKey);\n } else if (part.type === 'text') {\n // plain text content - MessageTextPart.value is a string\n target.push(part.value as string);\n } else if (part.type === 'bidiIsolation') {\n // bidi isolation marks are direction control characters, not content\n } else {\n if ('value' in part && part.value !== undefined && part.value !== null) {\n const stringValue = String(part.value);\n\n target.push(stringValue);\n }\n }\n }\n\n // handle unclosed tags by flattening remaining stack frames\n while (stack.length > 0) {\n const frame = stack.pop()!;\n const parent = stack.length > 0 ? stack.at(-1)!.children : result;\n\n parent.push(...frame.children);\n }\n\n return result;\n}\n"],"mappings":";;;;AAKA,SAAgB,cAAc,MAAiD;CAC7E,OAAO,OAAO,SAAS,WAAW,EAAE,UAAU,KAAK,IAAI;AACzD;;;ACkBA,MAAM,qBAAqB,OAAO,IAAI,eAAe;AACrD,MAAM,kCAAkC,OAAO,IAAI,4BAA4B;;;;;AAW/E,SAAS,eAAe,OAA2C;CACjE,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,EAAE,cAAc,QACjE,OAAO;CAGT,MAAM,OAAQ,MAA2B;CAEzC,OAAO,SAAS,sBAAsB,SAAS;AACjD;;;;;AAMA,SAAS,eAAkB,SAAY,KAAgB;CACrD,IAAI,eAAe,OAAO,KAAK,QAAQ,QAAQ,MAC7C,OAAO;EAAE,GAAG;EAAS;CAAI;CAG3B,OAAO;AACT;AAEA,SAAS,aAAa,MAAuC;CAC3D,OAAO,KAAK,SAAS;AACvB;AAEA,SAAS,gBACP,MACA,OACA,QACA,YACA,SACM;CACN,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW;CAE3D,QAAQ,KAAK,MAAb;EACE,KAAK;GACH,MAAM,KAAK;IAAE,MAAM,KAAK;IAAM,UAAU,CAAC;GAAE,CAAC;GAE5C;EAGF,KAAK,SAAS;GACZ,MAAM,QAAQ,MAAM,IAAI;GAExB,IAAI,CAAC,OAEH;GAGF,MAAM,YAAY,WAAW,MAAM;GACnC,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW;GAE3D,IAAI,WACF,OAAO,KAAK,eAAe,UAAU,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC;QAGhE,OAAO,KAAK,GAAG,MAAM,QAAQ;GAG/B;EACF;EAEA,KAAK,cAAc;GAEjB,MAAM,YAAY,WAAW,KAAK;GAElC,IAAI,WACF,OAAO,KAAK,eAAe,UAAU,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC;GAGtD;EACF;CACF;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,aAAgB,OAAe,YAA+C;CAC5F,MAAM,SAAyB,CAAC;CAChC,MAAM,QAAyB,CAAC;CAChC,IAAI,WAAW;CACf,MAAM,gBAAgB,QAAQ;CAE9B,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW;EAE3D,IAAI,aAAa,IAAI,GACnB,gBAAgB,MAAM,OAAO,QAAQ,YAAY,OAAO;OACnD,IAAI,KAAK,SAAS,QAEvB,OAAO,KAAK,KAAK,KAAe;OAC3B,IAAI,KAAK,SAAS,iBAAiB,CAE1C,OACE,IAAI,WAAW,QAAQ,KAAK,UAAU,KAAA,KAAa,KAAK,UAAU,MAAM;GACtE,MAAM,cAAc,OAAO,KAAK,KAAK;GAErC,OAAO,KAAK,WAAW;EACzB;CAEJ;CAGA,OAAO,MAAM,SAAS,GAAG;EACvB,MAAM,QAAQ,MAAM,IAAI;EAGxB,CAFe,MAAM,SAAS,IAAI,MAAM,GAAG,EAAE,CAAC,CAAE,WAAW,OAAA,CAEpD,KAAK,GAAG,MAAM,QAAQ;CAC/B;CAEA,OAAO;AACT"}
@@ -1,4 +1,4 @@
1
- import { t as createI18nChunkMiddleware } from "../middleware-OnRVbxvs.js";
1
+ import { t as createI18nChunkMiddleware } from "../middleware-BSEtDRYy.js";
2
2
  //#region src/server/chunk-middleware-entrypoint.ts
3
3
  const onRequest = createI18nChunkMiddleware();
4
4
  //#endregion
@@ -0,0 +1,4 @@
1
+ //#region src/server/locale-middleware-entrypoint.d.ts
2
+ export declare const onRequest: import("astro").MiddlewareHandler;
3
+ //#endregion
4
+ //# sourceMappingURL=locale-middleware-entrypoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-middleware-entrypoint.d.ts","names":[],"sources":["../../src/server/locale-middleware-entrypoint.ts"],"mappings":";qBAGa,2BAAS"}
@@ -0,0 +1,8 @@
1
+ import { n as createI18nLocaleMiddleware } from "../middleware-BSEtDRYy.js";
2
+ import { exclude } from "virtual:@astroscope/i18n/config";
3
+ //#region src/server/locale-middleware-entrypoint.ts
4
+ const onRequest = createI18nLocaleMiddleware({ exclude: exclude ?? void 0 });
5
+ //#endregion
6
+ export { onRequest };
7
+
8
+ //# sourceMappingURL=locale-middleware-entrypoint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-middleware-entrypoint.js","names":[],"sources":["../../src/server/locale-middleware-entrypoint.ts"],"sourcesContent":["import { exclude } from 'virtual:@astroscope/i18n/config';\nimport { createI18nLocaleMiddleware } from './middleware.js';\n\nexport const onRequest = createI18nLocaleMiddleware({ exclude: exclude ?? undefined });\n"],"mappings":";;;AAGA,MAAa,YAAY,2BAA2B,EAAE,SAAS,WAAW,KAAA,EAAU,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { t as RichComponents } from "../rich-BWszYAzS.js";
2
- import { a as TranslationMeta, i as TranslateFunction } from "../types-DU8QHch4.js";
2
+ import { a as TranslationMeta, i as TranslateFunction } from "../types-BjTD5hUT.js";
3
3
  //#region src/server/translate.d.ts
4
4
  /**
5
5
  * Get the current locale from request context.
@@ -23,7 +23,7 @@ export declare const t: TranslateFunction;
23
23
  * @param key - Translation key
24
24
  * @param meta - Fallback string or meta object with fallback (used as message template)
25
25
  * @param components - Map of tag names to component callbacks
26
- * @param values - Runtime interpolation values (ICU MessageFormat)
26
+ * @param values - Runtime interpolation values (MessageFormat 2)
27
27
  *
28
28
  * @example
29
29
  * ```tsx
@@ -1 +1 @@
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
+ {"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/server/translate.ts"],"mappings":";;;;;;;wBAqDgB;;;;;;;;qBAYH,GAAG;;;;;;;;;;;;;;;;;;;;wBA2DA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
@@ -1,6 +1,7 @@
1
- import { r as i18n, t as getContext } from "../context-CgqIoo00.js";
1
+ import { t as i18n } from "../i18n-C47eQi8U.js";
2
2
  import { r as formatMessageToParts, t as compileMessage } from "../compiler-CkOVGWU5.js";
3
- import { n as normalizeMeta, t as partsToNodes } from "../rich-CHisXE-A.js";
3
+ import { t as getContext } from "../context-BCUJxb8p.js";
4
+ import { n as normalizeMeta, t as partsToNodes } from "../rich-BDqjhv2h.js";
4
5
  import { createCounter } from "@astroscope/node/telemetry";
5
6
  import { log } from "@astroscope/node/log";
6
7
  //#region src/server/translate.ts
@@ -76,7 +77,7 @@ const t = ((key, meta, values) => {
76
77
  * @param key - Translation key
77
78
  * @param meta - Fallback string or meta object with fallback (used as message template)
78
79
  * @param components - Map of tag names to component callbacks
79
- * @param values - Runtime interpolation values (ICU MessageFormat)
80
+ * @param values - Runtime interpolation values (MessageFormat 2)
80
81
  *
81
82
  * @example
82
83
  * ```tsx
@@ -1 +1 @@
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"}
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 }\n case 'fallback': {\n return meta.fallback || key;\n }\n case 'throw': {\n throw new Error(`Missing translation for key: ${key}`);\n }\n default: {\n return meta.fallback || key;\n }\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 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 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 (MessageFormat 2)\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;EAET,KAAK,YACH,OAAO,KAAK,YAAY;EAE1B,KAAK,SACH,MAAM,IAAI,MAAM,gCAAgC,KAAK;EAEvD,SACE,OAAO,KAAK,YAAY;CAE5B;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;EACR,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;EAErE,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"}
@@ -1,4 +1,4 @@
1
- import { a as TranslationMeta } from "./types-DU8QHch4.js";
1
+ import { a as TranslationMeta } from "./types-BjTD5hUT.js";
2
2
  //#region src/extraction/types.d.ts
3
3
  /**
4
4
  * Deduplicated translation key with all file locations (used in manifest)
@@ -28,4 +28,4 @@ type ExtractionManifest = {
28
28
  };
29
29
  //#endregion
30
30
  export { ExtractedKey as n, ExtractionManifest as r, ChunkManifest as t };
31
- //# sourceMappingURL=types-f-SNJdvf.d.ts.map
31
+ //# sourceMappingURL=types-BC-tQ30r.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-f-SNJdvf.d.ts","names":[],"sources":["../src/extraction/types.ts"],"mappings":";;;;;KA8BY;EACV;EACA,MAAM;;EAEN;;;;;KAMU,gBAAgB;;;;KAKhB;EACV,MAAM;EACN,QAAQ;;;;;;EAMR"}
1
+ {"version":3,"file":"types-BC-tQ30r.d.ts","names":[],"sources":["../src/extraction/types.ts"],"mappings":";;;;;KA8BY;EACV;EACA,MAAM;;EAEN;;;;;KAMU,gBAAgB;;;;KAKhB;EACV,MAAM;EACN,QAAQ;;;;;;EAMR"}
@@ -41,14 +41,16 @@ type I18nClientState = {
41
41
  *
42
42
  * @param key - translation key
43
43
  * @param meta - fallback string or meta object with fallback
44
- * @param values - runtime interpolation values (ICU MessageFormat)
44
+ * @param values - runtime interpolation values (MessageFormat 2)
45
45
  *
46
46
  * @example
47
47
  * t('checkout.title', 'Order Summary')
48
- * t('cart.items', '{count, plural, one {# item} other {# items}}', { count: itemCount })
49
- * t('cart.total', { fallback: 'Total: {amount}' }, { amount: '$49.99' })
48
+ * t('cart.items', '.input {$count :number}\n.match $count\none {{{$count} item}}\n* {{{$count} items}}', {
49
+ * count: itemCount,
50
+ * })
51
+ * t('cart.total', { fallback: 'Total: {$amount}' }, { amount: '$49.99' })
50
52
  */
51
53
  type TranslateFunction = (key: string, meta: TranslationMeta | string, values?: Record<string, unknown> | undefined) => string;
52
54
  //#endregion
53
55
  export { TranslationMeta as a, TranslateFunction as i, I18nClientState as n, RawTranslations as r, CompiledTranslations as t };
54
- //# sourceMappingURL=types-DU8QHch4.d.ts.map
56
+ //# sourceMappingURL=types-BjTD5hUT.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-DU8QHch4.d.ts","names":[],"sources":["../src/shared/types.ts"],"mappings":";;;;KAGY;EACV;EACA;;;;;KAMU;EACV;EACA,YAAY,eAAe;EAC3B;;;;;KAMU,uBAAuB,SAAS;;;;KAKhC,uBAAuB,eAAe;;;;KAKtC,kBAAkB;;;;;;KAOlB;EACV;EACA,QAAQ;EACR,cAAc;;;;;;;;;;;;;;KAeJ,qBACV,aACA,MAAM,0BACN,SAAS"}
1
+ {"version":3,"file":"types-BjTD5hUT.d.ts","names":[],"sources":["../src/shared/types.ts"],"mappings":";;;;KAGY;EACV;EACA;;;;;KAMU;EACV;EACA,YAAY,eAAe;EAC3B;;;;;KAMU,uBAAuB,SAAS;;;;KAKhC,uBAAuB,eAAe;;;;KAKtC,kBAAkB;;;;;;KAOlB;EACV;EACA,QAAQ;EACR,cAAc;;;;;;;;;;;;;;;;KAiBJ,qBACV,aACA,MAAM,0BACN,SAAS"}