@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/README.md CHANGED
@@ -62,6 +62,22 @@ i18n({
62
62
  - **`'warn'`** — Log a warning but continue
63
63
  - **`'off'`** — Disable consistency checking
64
64
 
65
+ #### Middleware
66
+
67
+ The integration injects two middlewares: one serving the `/_i18n/` translation chunks ahead of your own middleware, and the locale middleware after it — it resolves the request's locale and provides the request context for `t()`. Running after your middleware means the locale resolver sees what a session middleware put on `locals`.
68
+
69
+ Both middlewares are injected in the order of your `integrations` array — keep `i18n()` before integrations whose middleware calls `t()` (e.g. `@astroscope/wormhole`).
70
+
71
+ By default, `RECOMMENDED_EXCLUDES` (static assets like `/_astro/`) are excluded from locale context setup. To customize:
72
+
73
+ ```ts
74
+ import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';
75
+
76
+ i18n({
77
+ exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],
78
+ })
79
+ ```
80
+
65
81
  ### 2. Configure i18n in your boot file
66
82
 
67
83
  > **Note:** `i18n.configure` must be awaited during boot, before any requests are handled.
@@ -80,6 +96,9 @@ export async function onStartup() {
80
96
  await i18n.configure({
81
97
  locales: ['en', 'de'],
82
98
  defaultLocale: 'en', // optional, defaults to first locale
99
+ // optional, per request: a cookie, the session on `locals`, the url, `detectLocale(request)`
100
+ // for Accept-Language — `undefined` falls back to defaultLocale
101
+ locale: (ctx) => ctx.cookies.get('locale')?.value,
83
102
  });
84
103
 
85
104
  // load translations for all locales
@@ -93,36 +112,7 @@ export async function onStartup() {
93
112
  }
94
113
  ```
95
114
 
96
- ### 3. Add the middleware
97
-
98
- ```ts
99
- // src/middleware.ts
100
- import { createI18nMiddleware, i18n } from '@astroscope/i18n';
101
-
102
- export const onRequest = createI18nMiddleware({
103
- locale: (ctx) =>
104
- ctx.cookies.get('locale')?.value ??
105
- i18n.getConfig().defaultLocale,
106
- });
107
- ```
108
-
109
- The middleware serving `/_i18n/` translation chunks is injected automatically by the integration.
110
-
111
- By default, `RECOMMENDED_EXCLUDES` (static assets like `/_astro/`) are excluded from locale context setup. To customize:
112
-
113
- ```ts
114
- import { createI18nMiddleware, i18n } from '@astroscope/i18n';
115
- import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';
116
-
117
- export const onRequest = createI18nMiddleware({
118
- locale: (ctx) =>
119
- ctx.cookies.get('locale')?.value ??
120
- i18n.getConfig().defaultLocale,
121
- exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],
122
- });
123
- ```
124
-
125
- ### 4. Use `t()` in your components
115
+ ### 3. Use `t()` in your components
126
116
 
127
117
  ```astro
128
118
  ---
@@ -148,7 +138,7 @@ export function CheckoutSummary() {
148
138
 
149
139
  > **Note:** Variables use `{$name}` syntax (with `$` prefix) per MessageFormat 2 specification.
150
140
 
151
- That's it — client delivery is automatic. Use plain `client:*` directives; translations are injected into the page and preloaded per island by `@astroscope/node`, no layout script or special directives needed.
141
+ That's it — client delivery is automatic. Use plain `client:*` directives; translations are injected into the page and preloaded per island by `@astroscope/node`.
152
142
 
153
143
  ## API
154
144
 
@@ -192,10 +182,10 @@ t('distance', '{$value :unit unit=kilometer}', { value: 42 })
192
182
 
193
183
  // with metadata object (for extraction tooling)
194
184
  t('cart.total', {
195
- example: 'Total: {$amount}',
185
+ fallback: 'Total: {$amount}',
196
186
  description: 'Cart total price',
197
187
  variables: {
198
- amount: { example: '$0.00', description: 'Formatted price' }
188
+ amount: { fallback: '$0.00', description: 'Formatted price' }
199
189
  }
200
190
  }, { amount: '$49.99' })
201
191
  ```
@@ -255,7 +245,7 @@ i18n.setTranslations('en', { 'key': 'value' });
255
245
  // get raw translations (includes manifest fallbacks)
256
246
  i18n.getTranslations('en');
257
247
 
258
- // get compiled translations (ICU MessageFormat functions)
248
+ // get compiled translations (MessageFormat 2 functions)
259
249
  i18n.getCompiledTranslations('en');
260
250
 
261
251
  // get extraction manifest
@@ -290,7 +280,7 @@ export function App() {
290
280
 
291
281
  1. **Build time** — all `t()` calls are extracted, mapped to chunks, and fallbacks are stripped from production bundles
292
282
  2. **Manifest** — Extracted keys with fallbacks are written to `i18n-manifest.json`
293
- 3. **SSR** — Middleware provides translations to `t()`, merging manifest fallbacks for missing keys
283
+ 3. **SSR** — the locale middleware provides translations to `t()`, merging manifest fallbacks for missing keys
294
284
  4. **Client** — each island preloads only the translation chunks its code needs, in parallel with the component code
295
285
 
296
286
  The same `import { t } from '@astroscope/i18n/translate'` works everywhere — bundler picks the correct implementation via conditional exports (`browser` vs `default`).
@@ -378,6 +368,7 @@ For `:currency` and `:unit`, the required option (`currency` or `unit`) can be:
378
368
  | `locales` | `string[]` | required | Supported locales |
379
369
  | `defaultLocale` | `string` | first locale | Default/fallback locale |
380
370
  | `fallback` | `FallbackBehavior` | `'fallback'` | Behavior when translation missing |
371
+ | `locale` | `(ctx: APIContext) => string \| undefined` | `defaultLocale` for every request | Per-request locale resolver, run by the locale middleware |
381
372
 
382
373
  ### FallbackBehavior
383
374
 
@@ -29,12 +29,12 @@ function extractMeta(t, node, report) {
29
29
  const name = prop.key.name;
30
30
  if (name === "fallback") {
31
31
  const value = getStringValue(t, prop.value);
32
- if (value !== null) meta.fallback = value;
33
- else report("the fallback is not a static string");
32
+ if (value === null) report("the fallback is not a static string");
33
+ else meta.fallback = value;
34
34
  } else if (name === "description") {
35
35
  const value = getStringValue(t, prop.value);
36
- if (value !== null) meta.description = value;
37
- else report("the description is not a static string");
36
+ if (value === null) report("the description is not a static string");
37
+ else meta.description = value;
38
38
  } else if (name === "variables" && t.isObjectExpression(prop.value)) meta.variables = extractVariables(t, prop.value, report);
39
39
  }
40
40
  return meta;
@@ -55,24 +55,23 @@ function extractVariables(t, obj, report) {
55
55
  if (!t.isObjectProperty(prop)) continue;
56
56
  if (!t.isIdentifier(prop.key)) continue;
57
57
  const varName = prop.key.name;
58
- if (t.isObjectExpression(prop.value)) {
59
- const varDef = {};
60
- for (const varProp of prop.value.properties) {
61
- if (t.isSpreadElement(varProp)) {
62
- report(`variable "${varName}" contains a spread element`);
63
- continue;
64
- }
65
- if (!t.isObjectProperty(varProp)) continue;
66
- if (!t.isIdentifier(varProp.key)) continue;
67
- const varPropName = varProp.key.name;
68
- const value = getStringValue(t, varProp.value);
69
- if (value !== null) {
70
- if (varPropName === "fallback") varDef.fallback = value;
71
- else if (varPropName === "description") varDef.description = value;
72
- } else if (varPropName === "fallback" || varPropName === "description") report(`variable "${varName}.${varPropName}" is not a static string`);
58
+ if (!t.isObjectExpression(prop.value)) continue;
59
+ const varDef = {};
60
+ for (const varProp of prop.value.properties) {
61
+ if (t.isSpreadElement(varProp)) {
62
+ report(`variable "${varName}" contains a spread element`);
63
+ continue;
73
64
  }
74
- result[varName] = varDef;
65
+ if (!t.isObjectProperty(varProp)) continue;
66
+ if (!t.isIdentifier(varProp.key)) continue;
67
+ const varPropName = varProp.key.name;
68
+ const value = getStringValue(t, varProp.value);
69
+ if (value !== null) {
70
+ if (varPropName === "fallback") varDef.fallback = value;
71
+ else if (varPropName === "description") varDef.description = value;
72
+ } else if (varPropName === "fallback" || varPropName === "description") report(`variable "${varName}.${varPropName}" is not a static string`);
75
73
  }
74
+ result[varName] = varDef;
76
75
  }
77
76
  return Object.keys(result).length > 0 ? result : void 0;
78
77
  }
@@ -109,19 +108,18 @@ function i18nExtractPlugin({ types: t }) {
109
108
  file,
110
109
  line
111
110
  });
112
- if (state.opts.stripFallbacks && args.length >= 2) {
113
- const firstArg = args[0];
114
- if (args.length === 2) path.node.arguments = [firstArg];
115
- else path.node.arguments = [
116
- firstArg,
117
- t.identifier("undefined"),
118
- args[2]
119
- ];
120
- }
111
+ if (!state.opts.stripFallbacks || args.length < 2) return;
112
+ const firstArg = args[0];
113
+ if (args.length === 2) path.node.arguments = [firstArg];
114
+ else path.node.arguments = [
115
+ firstArg,
116
+ t.identifier("undefined"),
117
+ args[2]
118
+ ];
121
119
  } }
122
120
  };
123
121
  }
124
122
  //#endregion
125
123
  export { i18nExtractPlugin };
126
124
 
127
- //# sourceMappingURL=babel-plugin-CWafmw5z.js.map
125
+ //# sourceMappingURL=babel-plugin-DJZS9L_W.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"babel-plugin-DJZS9L_W.js","names":[],"sources":["../src/extraction/babel-plugin.ts"],"sourcesContent":["import type { NodePath, PluginObject, PluginPass } from '@babel/core';\nimport type * as BabelTypes from '@babel/types';\nimport type { Expression, Node, ObjectExpression } from '@babel/types';\nimport type { TranslationMeta, VariableDef } from '../shared/types.js';\nimport type { ExtractedKeyOccurrence, ExtractionError } from './types.js';\n\nexport type I18nExtractPluginOpts = {\n onKeyExtracted: (key: ExtractedKeyOccurrence) => void;\n onExtractionError: (error: ExtractionError) => void;\n stripFallbacks: boolean;\n};\n\ntype PluginState = PluginPass & { opts: I18nExtractPluginOpts };\n\ntype ReportFn = (reason: string) => void;\n\n/**\n * Extract string value from AST node\n */\nfunction getStringValue(t: typeof BabelTypes, node: Node): string | null {\n if (t.isStringLiteral(node)) {\n return node.value;\n }\n\n if (t.isTemplateLiteral(node) && node.quasis.length === 1 && node.expressions.length === 0) {\n return node.quasis[0]?.value.cooked ?? null;\n }\n\n return null;\n}\n\n/**\n * Extract TranslationMeta from the second argument of t()\n */\nfunction extractMeta(t: typeof BabelTypes, node: Expression, report: ReportFn): TranslationMeta {\n // string shorthand: t('key', 'fallback')\n const stringValue = getStringValue(t, node);\n\n if (stringValue !== null) {\n return { fallback: stringValue };\n }\n\n // dynamic template literal: t('key', `Hello ${name}`)\n if (t.isTemplateLiteral(node) && node.expressions.length > 0) {\n report('the fallback is a template literal with expressions');\n\n return { fallback: '' };\n }\n\n // object form: t('key', { fallback: '...', description: '...', variables: {...} })\n if (t.isObjectExpression(node)) {\n const meta: TranslationMeta = { fallback: '' };\n\n for (const prop of node.properties) {\n // spread elements cannot be extracted statically\n if (t.isSpreadElement(prop)) {\n report('the meta object contains a spread element');\n\n continue;\n }\n\n if (!t.isObjectProperty(prop)) continue;\n if (!t.isIdentifier(prop.key)) continue;\n\n const name = prop.key.name;\n\n if (name === 'fallback') {\n const value = getStringValue(t, prop.value as Node);\n\n if (value === null) {\n report('the fallback is not a static string');\n } else {\n meta.fallback = value;\n }\n } else if (name === 'description') {\n const value = getStringValue(t, prop.value as Node);\n\n if (value === null) {\n report('the description is not a static string');\n } else {\n meta.description = value;\n }\n } else if (name === 'variables' && t.isObjectExpression(prop.value)) {\n meta.variables = extractVariables(t, prop.value, report);\n }\n }\n\n return meta;\n }\n\n // dynamic meta (e.g. variable reference)\n report('the meta is neither a static string nor an object literal');\n\n return { fallback: '' };\n}\n\n/**\n * Extract variables definition from object expression\n */\nfunction extractVariables(\n t: typeof BabelTypes,\n obj: ObjectExpression,\n report: ReportFn,\n): Record<string, VariableDef> | undefined {\n const result: Record<string, VariableDef> = {};\n\n for (const prop of obj.properties) {\n if (t.isSpreadElement(prop)) {\n report('the variables object contains a spread element');\n continue;\n }\n\n if (!t.isObjectProperty(prop)) continue;\n if (!t.isIdentifier(prop.key)) continue;\n\n const varName = prop.key.name;\n\n if (!t.isObjectExpression(prop.value)) continue;\n\n const varDef: VariableDef = {};\n\n for (const varProp of prop.value.properties) {\n if (t.isSpreadElement(varProp)) {\n report(`variable \"${varName}\" contains a spread element`);\n continue;\n }\n\n if (!t.isObjectProperty(varProp)) continue;\n if (!t.isIdentifier(varProp.key)) continue;\n\n const varPropName = varProp.key.name;\n const value = getStringValue(t, varProp.value as Node);\n\n if (value !== null) {\n if (varPropName === 'fallback') {\n varDef.fallback = value;\n } else if (varPropName === 'description') {\n varDef.description = value;\n }\n } else if (varPropName === 'fallback' || varPropName === 'description') {\n report(`variable \"${varName}.${varPropName}\" is not a static string`);\n }\n }\n\n result[varName] = varDef;\n }\n\n return Object.keys(result).length > 0 ? result : undefined;\n}\n\n/**\n * Babel plugin for i18n extraction and transformation\n *\n * - Extracts t() calls with key and metadata\n * - Strips fallback argument in production builds\n */\nexport function i18nExtractPlugin({ types: t }: { types: typeof BabelTypes }): PluginObject<PluginState> {\n return {\n name: '@astroscope/i18n/extract',\n visitor: {\n CallExpression(path: NodePath<BabelTypes.CallExpression>, state: PluginState) {\n // matched by name only — an aliased or reassigned `t` is invisible here\n if (!path.get('callee').isIdentifier({ name: 't' })) return;\n\n const args = path.node.arguments;\n const keyArg = args[0];\n\n if (!t.isExpression(keyArg)) return;\n\n const key = getStringValue(t, keyArg);\n\n if (key === null) return; // first arg must be string\n\n const file = state.filename ?? '';\n const line = path.node.loc?.start.line ?? 0;\n const column = path.node.loc?.start.column ?? 0;\n const report: ReportFn = (reason) => state.opts.onExtractionError({ key, reason, file, line, column });\n\n // second arg is always meta (fallback string or object)\n // may not exist if we're processing already-transformed code\n const meta = args.length >= 2 && t.isExpression(args[1]) ? extractMeta(t, args[1], report) : { fallback: '' };\n\n state.opts.onKeyExtracted({ key, meta, file, line });\n\n // strip fallback in production\n // t('key', 'fallback') → t('key')\n // t('key', 'fallback', values) → t('key', undefined, values)\n if (!state.opts.stripFallbacks || args.length < 2) return;\n\n const firstArg = args[0]!;\n\n if (args.length === 2) {\n path.node.arguments = [firstArg];\n } else {\n path.node.arguments = [firstArg, t.identifier('undefined'), args[2]!];\n }\n },\n },\n };\n}\n"],"mappings":"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"}
@@ -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/client/translate.d.ts
4
4
  export declare function getLocale(): string;
5
5
  /**
@@ -25,7 +25,7 @@ export declare const t: TranslateFunction;
25
25
  * @param key - Translation key
26
26
  * @param meta - Fallback string or meta object with fallback (used as message template)
27
27
  * @param components - Map of tag names to component callbacks
28
- * @param values - Runtime interpolation values (ICU MessageFormat)
28
+ * @param values - Runtime interpolation values (MessageFormat 2)
29
29
  *
30
30
  * @example
31
31
  * ```tsx
@@ -1,5 +1,5 @@
1
1
  import { r as formatMessageToParts, t as compileMessage } from "../compiler-CkOVGWU5.js";
2
- import { n as normalizeMeta, t as partsToNodes } from "../rich-CHisXE-A.js";
2
+ import { n as normalizeMeta, t as partsToNodes } from "../rich-BDqjhv2h.js";
3
3
  //#region src/client/state.ts
4
4
  let warnedMissingState = false;
5
5
  /**
@@ -12,7 +12,7 @@ function getI18nState() {
12
12
  const state = window.__i18n__;
13
13
  if (!state && !warnedMissingState) {
14
14
  warnedMissingState = true;
15
- console.error("[@astroscope/i18n] window.__i18n__ is not defined — translations will not work on the client. Make sure createI18nMiddleware() runs for this page (it injects the state) and, for per-island hashes, that the site uses the @astroscope/node adapter.");
15
+ console.error("[@astroscope/i18n] window.__i18n__ is not defined — translations will not work on the client. Make sure the @astroscope/i18n integration is set up and the page is not in its `exclude` list (the locale middleware injects the state) and, for per-island hashes, that the site uses the @astroscope/node adapter.");
16
16
  }
17
17
  return state;
18
18
  }
@@ -67,7 +67,7 @@ const t = ((key, meta, values) => {
67
67
  * @param key - Translation key
68
68
  * @param meta - Fallback string or meta object with fallback (used as message template)
69
69
  * @param components - Map of tag names to component callbacks
70
- * @param values - Runtime interpolation values (ICU MessageFormat)
70
+ * @param values - Runtime interpolation values (MessageFormat 2)
71
71
  *
72
72
  * @example
73
73
  * ```tsx
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","names":[],"sources":["../../src/client/state.ts","../../src/client/translate.ts"],"sourcesContent":["import type { I18nClientState } from '../shared/types.js';\nimport './types.js';\n\nlet warnedMissingState = false;\n\n/**\n * Read the client i18n state injected by the i18n middleware and islands emitter.\n *\n * Returns undefined and logs an actionable error (once) when the state was not\n * injected — without it window.__i18n__ is undefined and translations cannot load.\n */\nexport function getI18nState(): I18nClientState | undefined {\n const state = window.__i18n__;\n\n if (!state && !warnedMissingState) {\n warnedMissingState = true;\n\n console.error(\n '[@astroscope/i18n] window.__i18n__ is not defined — translations will not work on the client. ' +\n 'Make sure createI18nMiddleware() runs for this page (it injects the state) and, for per-island ' +\n 'hashes, that the site uses the @astroscope/node adapter.',\n );\n }\n\n return state;\n}\n","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 { getI18nState } from './state.js';\n\nconst cache = new Map<string, CompiledTranslation>();\n\nexport function getLocale(): string {\n return getI18nState()?.locale ?? '';\n}\n\n/**\n * Client-side translate function\n *\n * Reads from window.__i18n__.translations (injected per-chunk).\n * In production, translations are always loaded before component code runs\n * via the chunk loader injected by the Vite plugin.\n *\n * Call patterns:\n * - Development: t('key', 'fallback') or t('key', 'fallback', values)\n * - Production: t('key') or t('key', undefined, values)\n *\n * Falls back to meta.fallback (dev) or key (prod) when translation is missing.\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 state = getI18nState();\n const locale = state?.locale ?? '';\n\n const raw = state?.translations[key];\n\n if (!raw) {\n // translation missing: use fallback (dev) or key (prod)\n if (normalizedMeta.fallback) {\n let compiled = cache.get(`__fallback__${key}`);\n\n if (!compiled) {\n compiled = compileMessage(locale, normalizedMeta.fallback);\n cache.set(`__fallback__${key}`, compiled);\n }\n\n return compiled(values);\n }\n\n return key;\n }\n\n let compiled = cache.get(key);\n\n if (!compiled) {\n compiled = compileMessage(locale, raw);\n cache.set(key, compiled);\n }\n\n return compiled(values);\n}) as TranslateFunction;\n\n/**\n * Client-side rich text translation function\n *\n * Parses MF2 markup syntax and applies component callbacks to wrap content.\n * Works with any JSX runtime (React, Preact, 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 state = getI18nState();\n\n const raw = state?.translations[key] || normalizedMeta.fallback || key;\n\n const parts = formatMessageToParts(state?.locale ?? '', raw, values);\n\n return partsToNodes(parts, components ?? {});\n}\n"],"mappings":";;;AAGA,IAAI,qBAAqB;;;;;;;AAQzB,SAAgB,eAA4C;CAC1D,MAAM,QAAQ,OAAO;CAErB,IAAI,CAAC,SAAS,CAAC,oBAAoB;EACjC,qBAAqB;EAErB,QAAQ,MACN,uPAGF;CACF;CAEA,OAAO;AACT;;;ACnBA,MAAM,wBAAQ,IAAI,IAAiC;AAEnD,SAAgB,YAAoB;CAClC,OAAO,aAAa,CAAC,EAAE,UAAU;AACnC;;;;;;;;;;;;;;AAeA,MAAa,MACX,KACA,MACA,WACW;CACX,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,QAAQ,aAAa;CAC3B,MAAM,SAAS,OAAO,UAAU;CAEhC,MAAM,MAAM,OAAO,aAAa;CAEhC,IAAI,CAAC,KAAK;EAER,IAAI,eAAe,UAAU;GAC3B,IAAI,WAAW,MAAM,IAAI,eAAe,KAAK;GAE7C,IAAI,CAAC,UAAU;IACb,WAAW,eAAe,QAAQ,eAAe,QAAQ;IACzD,MAAM,IAAI,eAAe,OAAO,QAAQ;GAC1C;GAEA,OAAO,SAAS,MAAM;EACxB;EAEA,OAAO;CACT;CAEA,IAAI,WAAW,MAAM,IAAI,GAAG;CAE5B,IAAI,CAAC,UAAU;EACb,WAAW,eAAe,QAAQ,GAAG;EACrC,MAAM,IAAI,KAAK,QAAQ;CACzB;CAEA,OAAO,SAAS,MAAM;AACxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,KACd,KACA,MACA,YACA,QACgB;CAChB,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,QAAQ,aAAa;CAE3B,MAAM,MAAM,OAAO,aAAa,QAAQ,eAAe,YAAY;CAEnE,MAAM,QAAQ,qBAAqB,OAAO,UAAU,IAAI,KAAK,MAAM;CAEnE,OAAO,aAAa,OAAO,cAAc,CAAC,CAAC;AAC7C"}
1
+ {"version":3,"file":"translate.js","names":[],"sources":["../../src/client/state.ts","../../src/client/translate.ts"],"sourcesContent":["import type { I18nClientState } from '../shared/types.js';\nimport './types.js';\n\nlet warnedMissingState = false;\n\n/**\n * Read the client i18n state injected by the i18n middleware and islands emitter.\n *\n * Returns undefined and logs an actionable error (once) when the state was not\n * injected — without it window.__i18n__ is undefined and translations cannot load.\n */\nexport function getI18nState(): I18nClientState | undefined {\n const state = window.__i18n__;\n\n if (!state && !warnedMissingState) {\n warnedMissingState = true;\n\n console.error(\n '[@astroscope/i18n] window.__i18n__ is not defined — translations will not work on the client. ' +\n 'Make sure the @astroscope/i18n integration is set up and the page is not in its `exclude` list ' +\n '(the locale middleware injects the state) and, for per-island hashes, that the site uses the ' +\n '@astroscope/node adapter.',\n );\n }\n\n return state;\n}\n","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 { getI18nState } from './state.js';\n\nconst cache = new Map<string, CompiledTranslation>();\n\nexport function getLocale(): string {\n return getI18nState()?.locale ?? '';\n}\n\n/**\n * Client-side translate function\n *\n * Reads from window.__i18n__.translations (injected per-chunk).\n * In production, translations are always loaded before component code runs\n * via the chunk loader injected by the Vite plugin.\n *\n * Call patterns:\n * - Development: t('key', 'fallback') or t('key', 'fallback', values)\n * - Production: t('key') or t('key', undefined, values)\n *\n * Falls back to meta.fallback (dev) or key (prod) when translation is missing.\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 state = getI18nState();\n const locale = state?.locale ?? '';\n\n const raw = state?.translations[key];\n\n if (!raw) {\n // translation missing: use fallback (dev) or key (prod)\n if (normalizedMeta.fallback) {\n let compiled = cache.get(`__fallback__${key}`);\n\n if (!compiled) {\n compiled = compileMessage(locale, normalizedMeta.fallback);\n cache.set(`__fallback__${key}`, compiled);\n }\n\n return compiled(values);\n }\n\n return key;\n }\n\n let compiled = cache.get(key);\n\n if (!compiled) {\n compiled = compileMessage(locale, raw);\n cache.set(key, compiled);\n }\n\n return compiled(values);\n}) as TranslateFunction;\n\n/**\n * Client-side rich text translation function\n *\n * Parses MF2 markup syntax and applies component callbacks to wrap content.\n * Works with any JSX runtime (React, Preact, 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 state = getI18nState();\n\n const raw = state?.translations[key] || normalizedMeta.fallback || key;\n\n const parts = formatMessageToParts(state?.locale ?? '', raw, values);\n\n return partsToNodes(parts, components ?? {});\n}\n"],"mappings":";;;AAGA,IAAI,qBAAqB;;;;;;;AAQzB,SAAgB,eAA4C;CAC1D,MAAM,QAAQ,OAAO;CAErB,IAAI,CAAC,SAAS,CAAC,oBAAoB;EACjC,qBAAqB;EAErB,QAAQ,MACN,qTAIF;CACF;CAEA,OAAO;AACT;;;ACpBA,MAAM,wBAAQ,IAAI,IAAiC;AAEnD,SAAgB,YAAoB;CAClC,OAAO,aAAa,CAAC,EAAE,UAAU;AACnC;;;;;;;;;;;;;;AAeA,MAAa,MACX,KACA,MACA,WACW;CACX,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,QAAQ,aAAa;CAC3B,MAAM,SAAS,OAAO,UAAU;CAEhC,MAAM,MAAM,OAAO,aAAa;CAEhC,IAAI,CAAC,KAAK;EAER,IAAI,eAAe,UAAU;GAC3B,IAAI,WAAW,MAAM,IAAI,eAAe,KAAK;GAE7C,IAAI,CAAC,UAAU;IACb,WAAW,eAAe,QAAQ,eAAe,QAAQ;IACzD,MAAM,IAAI,eAAe,OAAO,QAAQ;GAC1C;GAEA,OAAO,SAAS,MAAM;EACxB;EAEA,OAAO;CACT;CAEA,IAAI,WAAW,MAAM,IAAI,GAAG;CAE5B,IAAI,CAAC,UAAU;EACb,WAAW,eAAe,QAAQ,GAAG;EACrC,MAAM,IAAI,KAAK,QAAQ;CACzB;CAEA,OAAO,SAAS,MAAM;AACxB;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,KACd,KACA,MACA,YACA,QACgB;CAChB,MAAM,iBAAkC,OAAO,cAAc,IAAI,IAAI,EAAE,UAAU,GAAG;CAEpF,MAAM,QAAQ,aAAa;CAE3B,MAAM,MAAM,OAAO,aAAa,QAAQ,eAAe,YAAY;CAEnE,MAAM,QAAQ,qBAAqB,OAAO,UAAU,IAAI,KAAK,MAAM;CAEnE,OAAO,aAAa,OAAO,cAAc,CAAC,CAAC;AAC7C"}
@@ -0,0 +1,19 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/server/context.ts
3
+ const als = new AsyncLocalStorage();
4
+ /**
5
+ * Run a function with i18n context (called by middleware)
6
+ */
7
+ function runWithContext(context, fn) {
8
+ return als.run(context, fn);
9
+ }
10
+ /**
11
+ * Get the current i18n context
12
+ */
13
+ function getContext() {
14
+ return als.getStore() ?? null;
15
+ }
16
+ //#endregion
17
+ export { runWithContext as n, getContext as t };
18
+
19
+ //# sourceMappingURL=context-BCUJxb8p.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-BCUJxb8p.js","names":[],"sources":["../src/server/context.ts"],"sourcesContent":["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":";;AAGA,MAAM,MAAM,IAAI,kBAA+B;;;;AAK/C,SAAgB,eAAkB,SAAsB,IAAgB;CACtE,OAAO,IAAI,IAAI,SAAS,EAAE;AAC5B;;;;AAKA,SAAgB,aAAiC;CAC/C,OAAO,IAAI,SAAS,KAAK;AAC3B"}
@@ -1,5 +1,6 @@
1
- import { n as ExtractedKey, r as ExtractionManifest, t as ChunkManifest } from "../types-f-SNJdvf.js";
2
- import { t as ConsistencyCheckLevel } from "../types-Bo4jMK8g.js";
1
+ import { n as ExtractedKey, r as ExtractionManifest, t as ChunkManifest } from "../types-BC-tQ30r.js";
2
+ import { t as ConsistencyCheckLevel } from "../types-LT4Zo0bM.js";
3
+ import { ExcludePattern } from "@astroscope/node/excludes";
3
4
  import { AstroIntegrationLogger } from "astro";
4
5
  import { Plugin } from "vite";
5
6
  //#region src/extraction/manifest.d.ts
@@ -13,6 +14,8 @@ export declare function getManifest(): ExtractionManifest;
13
14
  type I18nVitePluginOptions = {
14
15
  logger: AstroIntegrationLogger;
15
16
  consistency: ConsistencyCheckLevel;
17
+ /** the locale middleware's exclude patterns, null for the default */
18
+ exclude: ExcludePattern[] | null;
16
19
  };
17
20
  /**
18
21
  * Vite plugin for i18n extraction, chunk mapping, and loader injection
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/extraction/manifest.ts","../../src/extraction/vite-plugin.ts"],"mappings":";;;;;;;;;;wBA4CgB,eAAe;;;KCjBnB;EACV,QAAQ;EACR,aAAa;;EAEb,SAAS;;;;;;;;;;;;wBAuCK,eAAe,SAAS,wBAAwB"}
@@ -1,3 +1,3 @@
1
- import { t as i18nVitePlugin } from "../vite-plugin-C-jEpd8t.js";
2
- import { n as getManifest } from "../manifest-Cma68XM-.js";
1
+ import { t as i18nVitePlugin } from "../vite-plugin-CZqtAqQM.js";
2
+ import { n as getManifest } from "../manifest-X-UhW6tH.js";
3
3
  export { getManifest, i18nVitePlugin };
@@ -1,21 +1,25 @@
1
- import { t as getGlobalState } from "./manifest-Cma68XM-.js";
1
+ import { t as getGlobalState } from "./manifest-X-UhW6tH.js";
2
2
  import { n as compileTranslations } from "./compiler-CkOVGWU5.js";
3
3
  import { createObservableGauge } from "@astroscope/node/telemetry";
4
4
  import { createHash } from "node:crypto";
5
- import { AsyncLocalStorage } from "node:async_hooks";
6
5
  //#region src/extraction/hash.ts
7
6
  /**
8
7
  * Hash a string to a short hash (for cache busting)
9
8
  */
10
9
  function hashString(str) {
11
- return createHash("sha256").update(str).digest("hex").substring(0, 8);
10
+ return createHash("sha256").update(str).digest("hex").slice(0, 8);
11
+ }
12
+ function compareKeys(a, b) {
13
+ if (a === b) return 0;
14
+ return a < b ? -1 : 1;
12
15
  }
13
16
  /**
14
17
  * Compute hash for chunk translations
15
18
  */
16
19
  function computeChunkHash(translations, keys) {
17
20
  const relevantTranslations = {};
18
- for (const key of [...keys].sort()) if (translations[key] !== void 0) relevantTranslations[key] = translations[key];
21
+ const sortedKeys = keys.toSorted(compareKeys);
22
+ for (const key of sortedKeys) if (translations[key] !== void 0) relevantTranslations[key] = translations[key];
19
23
  return hashString(JSON.stringify(relevantTranslations));
20
24
  }
21
25
  /**
@@ -38,6 +42,20 @@ var I18nSingleton = class {
38
42
  updatedAt = /* @__PURE__ */ new Map();
39
43
  manifestGetter = null;
40
44
  manifestVersion = 0;
45
+ /** Invalidate derived caches when the extraction manifest changed (dev HMR). */
46
+ invalidateIfManifestChanged() {
47
+ const { version } = getGlobalState();
48
+ if (version === this.manifestVersion) return;
49
+ this.manifestVersion = version;
50
+ this.mergedCache.clear();
51
+ this.compiledCache.clear();
52
+ this.chunkCache.clear();
53
+ }
54
+ resolveLocales(locales) {
55
+ if (!locales) return this.config?.locales ?? [];
56
+ if (typeof locales === "string") return [locales];
57
+ return locales;
58
+ }
41
59
  async configure(config) {
42
60
  if (!config.locales?.length) throw new Error("i18n.configure(): locales array is required and must not be empty");
43
61
  for (const locale of config.locales) {
@@ -53,12 +71,12 @@ var I18nSingleton = class {
53
71
  this.config = {
54
72
  locales: config.locales,
55
73
  defaultLocale: config.defaultLocale ?? config.locales[0],
56
- fallback: config.fallback ?? "fallback"
74
+ fallback: config.fallback ?? "fallback",
75
+ locale: config.locale
57
76
  };
58
- if (!this.manifestGetter) {
59
- const m = await import("virtual:@astroscope/i18n/manifest");
60
- this.manifestGetter = m.getManifest;
61
- }
77
+ if (this.manifestGetter) return;
78
+ const m = await import("virtual:@astroscope/i18n/manifest");
79
+ this.manifestGetter = m.getManifest;
62
80
  }
63
81
  isConfigured() {
64
82
  return this.config !== null;
@@ -89,7 +107,7 @@ var I18nSingleton = class {
89
107
  };
90
108
  if (manifest.keys.length === 0) return raw;
91
109
  const withFallbacks = { ...raw };
92
- for (const { key, meta } of manifest.keys) if (!(key in withFallbacks) && meta.fallback) withFallbacks[key] = meta.fallback;
110
+ for (const { key, meta } of manifest.keys) if (!Object.hasOwn(withFallbacks, key) && meta.fallback) withFallbacks[key] = meta.fallback;
93
111
  this.mergedCache.set(locale, withFallbacks);
94
112
  return withFallbacks;
95
113
  }
@@ -125,7 +143,10 @@ var I18nSingleton = class {
125
143
  if (cached) return cached;
126
144
  const translations = this.getTranslations(locale);
127
145
  const chunkTranslations = {};
128
- for (const key of keys) if (translations[key]) chunkTranslations[key] = translations[key];
146
+ for (const key of keys) {
147
+ const translation = translations[key];
148
+ if (translation) chunkTranslations[key] = translation;
149
+ }
129
150
  const js = `/* @astroscope/i18n chunk: ${chunkName} */
130
151
  (function() {
131
152
  var i = window.__i18n__;
@@ -173,22 +194,6 @@ var I18nSingleton = class {
173
194
  seconds: (now - at) / 1e3
174
195
  }));
175
196
  }
176
- /**
177
- * invalidate derived caches when the extraction manifest has changed (dev mode HMR)
178
- */
179
- invalidateIfManifestChanged() {
180
- const { version } = getGlobalState();
181
- if (version === this.manifestVersion) return;
182
- this.manifestVersion = version;
183
- this.mergedCache.clear();
184
- this.compiledCache.clear();
185
- this.chunkCache.clear();
186
- }
187
- resolveLocales(locales) {
188
- if (!locales) return this.config?.locales ?? [];
189
- if (typeof locales === "string") return [locales];
190
- return locales;
191
- }
192
197
  };
193
198
  const i18n = new I18nSingleton();
194
199
  createObservableGauge("astro.i18n.translations.age", {
@@ -199,21 +204,6 @@ createObservableGauge("astro.i18n.translations.age", {
199
204
  for (const { locale, seconds } of i18n.getTranslationsAge()) result.observe(seconds, { "astro.i18n.locale": locale });
200
205
  });
201
206
  //#endregion
202
- //#region src/server/context.ts
203
- const als = new AsyncLocalStorage();
204
- /**
205
- * Run a function with i18n context (called by middleware)
206
- */
207
- function runWithContext(context, fn) {
208
- return als.run(context, fn);
209
- }
210
- /**
211
- * Get the current i18n context
212
- */
213
- function getContext() {
214
- return als.getStore() ?? null;
215
- }
216
- //#endregion
217
- export { runWithContext as n, i18n as r, getContext as t };
207
+ export { i18n as t };
218
208
 
219
- //# sourceMappingURL=context-CgqIoo00.js.map
209
+ //# sourceMappingURL=i18n-C47eQi8U.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-C47eQi8U.js","names":[],"sources":["../src/extraction/hash.ts","../src/server/i18n.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').slice(0, 8);\n}\n\n// code-unit order (what the default sort does): the order feeds the hash, so it must not depend on icu locale data\nfunction compareKeys(a: string, b: string): number {\n if (a === b) return 0;\n\n return a < b ? -1 : 1;\n}\n\n/**\n * Compute hash for chunk translations\n */\nexport function computeChunkHash(translations: RawTranslations, keys: string[]): string {\n const relevantTranslations: RawTranslations = {};\n const sortedKeys = keys.toSorted(compareKeys);\n\n for (const key of sortedKeys) {\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 type { APIContext } from 'astro';\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\n/**\n * Resolves a request's locale — from a cookie, the session on `locals`, the url or\n * `detectLocale(request)`. `undefined` falls back to the default locale.\n */\nexport type LocaleResolver = (ctx: APIContext) => string | undefined;\n\nexport type I18nConfig = {\n locales: string[];\n defaultLocale?: string | undefined;\n fallback?: FallbackBehavior | undefined;\n /**\n * Called by the locale middleware for every request it does not exclude. Without\n * a resolver every request renders in the default locale.\n */\n locale?: LocaleResolver | undefined;\n};\n\ntype NormalizedConfig = {\n locales: string[];\n defaultLocale: string;\n fallback: FallbackBehavior;\n locale: LocaleResolver | undefined;\n};\n\nclass I18nSingleton {\n private config: NormalizedConfig | null = null;\n\n // locale -> raw translation strings (before MessageFormat 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 MessageFormat 2 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 /** Invalidate derived caches when the extraction manifest changed (dev HMR). */\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 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 locale: config.locale,\n };\n\n // resolved lazily so importing the singleton needs no virtual module\n if (this.manifestGetter) {\n return;\n }\n\n const m = await import('virtual:@astroscope/i18n/manifest');\n\n this.manifestGetter = m.getManifest;\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);\n this.compiledCache.delete(locale);\n this.hashCache.delete(locale);\n\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 (!Object.hasOwn(withFallbacks, key) && 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 const translation = translations[key];\n\n if (translation) {\n chunkTranslations[key] = translation;\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 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\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"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
- import { a as TranslationMeta, r as RawTranslations, t as CompiledTranslations } from "./types-DU8QHch4.js";
2
- import { r as ExtractionManifest } from "./types-f-SNJdvf.js";
1
+ import { a as TranslationMeta, r as RawTranslations, t as CompiledTranslations } from "./types-BjTD5hUT.js";
2
+ import { r as ExtractionManifest } from "./types-BC-tQ30r.js";
3
+ import { APIContext } from "astro";
3
4
  //#region src/server/types.d.ts
4
5
  /**
5
6
  * Fallback behavior when translation is missing
@@ -16,15 +17,26 @@ type I18nContext = {
16
17
  };
17
18
  //#endregion
18
19
  //#region src/server/i18n.d.ts
20
+ /**
21
+ * Resolves a request's locale — from a cookie, the session on `locals`, the url or
22
+ * `detectLocale(request)`. `undefined` falls back to the default locale.
23
+ */
24
+ type LocaleResolver = (ctx: APIContext) => string | undefined;
19
25
  type I18nConfig = {
20
26
  locales: string[];
21
27
  defaultLocale?: string | undefined;
22
28
  fallback?: FallbackBehavior | undefined;
29
+ /**
30
+ * Called by the locale middleware for every request it does not exclude. Without
31
+ * a resolver every request renders in the default locale.
32
+ */
33
+ locale?: LocaleResolver | undefined;
23
34
  };
24
35
  type NormalizedConfig = {
25
36
  locales: string[];
26
37
  defaultLocale: string;
27
38
  fallback: FallbackBehavior;
39
+ locale: LocaleResolver | undefined;
28
40
  };
29
41
  declare class I18nSingleton {
30
42
  private config;
@@ -36,6 +48,9 @@ declare class I18nSingleton {
36
48
  private updatedAt;
37
49
  private manifestGetter;
38
50
  private manifestVersion;
51
+ /** Invalidate derived caches when the extraction manifest changed (dev HMR). */
52
+ private invalidateIfManifestChanged;
53
+ private resolveLocales;
39
54
  configure(config: I18nConfig): Promise<void>;
40
55
  isConfigured(): boolean;
41
56
  getConfig(): NormalizedConfig;
@@ -71,13 +86,8 @@ declare class I18nSingleton {
71
86
  locale: string;
72
87
  seconds: number;
73
88
  }[];
74
- /**
75
- * invalidate derived caches when the extraction manifest has changed (dev mode HMR)
76
- */
77
- private invalidateIfManifestChanged;
78
- private resolveLocales;
79
89
  }
80
90
  declare const i18n: I18nSingleton;
81
91
  //#endregion
82
- export { I18nContext as i, i18n as n, FallbackBehavior as r, I18nConfig as t };
83
- //# sourceMappingURL=i18n-Czkrfb_4.d.ts.map
92
+ export { I18nContext as a, FallbackBehavior as i, LocaleResolver as n, i18n as r, I18nConfig as t };
93
+ //# sourceMappingURL=i18n-C4nlkQTf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-C4nlkQTf.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;;;;;;;;KCJA,kBAAkB,KAAK;KAEvB;EACV;EACA;EACA,WAAW;;;;;EAKX,SAAS;;KAGN;EACH;EACA;EACA,UAAU;EACV,QAAQ;;cAGJ;UACI;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;;UAGA;UAaA;EAYF,UAAU,QAAQ,aAAa;EAkDrC;EAIA,aAAa;EAQb,gBAAgB,gBAAgB,KAAK;EAqBrC,gBAAgB,iBAAiB;EA8BjC,wBAAwB,iBAAiB;;;;;;EAmBzC,eAAe;EAQf,UAAU,iBAAiB;;;;;;EAS3B,aAAa,gBAAgB,oBAAoB;;;;;;EA4CjD,qBAAqB,iBAAiB;;;;;EAoBtC,MAAM;;EAmBN,mBAAmB;IAAqB;IAAgB;;;cAK7C,MAAI"}