@astroscope/i18n 1.1.2 → 2.0.1

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 (75) hide show
  1. package/README.md +28 -72
  2. package/dist/babel-plugin-CWafmw5z.js.map +1 -1
  3. package/dist/client/translate.d.ts +1 -1
  4. package/dist/client/translate.d.ts.map +1 -1
  5. package/dist/client/translate.js +19 -2
  6. package/dist/client/translate.js.map +1 -1
  7. package/dist/{context-BEQ6qqX5.js → context-CYqyZJyH.js} +14 -66
  8. package/dist/context-CYqyZJyH.js.map +1 -0
  9. package/dist/extraction/index.d.ts +1 -1
  10. package/dist/extraction/index.d.ts.map +1 -1
  11. package/dist/extraction/index.js +2 -2
  12. package/dist/{i18n-Dzh54-at.d.ts → i18n-Bp8TFmd3.d.ts} +7 -8
  13. package/dist/i18n-Bp8TFmd3.d.ts.map +1 -0
  14. package/dist/index.d.ts +14 -30
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +20 -152
  17. package/dist/index.js.map +1 -1
  18. package/dist/internal.d.ts +2 -2
  19. package/dist/internal.js +1 -1
  20. package/dist/{manifest-BEm6GezL.js → manifest-Cma68XM-.js} +3 -3
  21. package/dist/manifest-Cma68XM-.js.map +1 -0
  22. package/dist/middleware-DxKY84Iv.js +235 -0
  23. package/dist/middleware-DxKY84Iv.js.map +1 -0
  24. package/dist/rich-CHisXE-A.js.map +1 -1
  25. package/dist/server/chunk-middleware-entrypoint.d.ts +5 -0
  26. package/dist/server/chunk-middleware-entrypoint.d.ts.map +1 -0
  27. package/dist/server/chunk-middleware-entrypoint.js +7 -0
  28. package/dist/server/chunk-middleware-entrypoint.js.map +1 -0
  29. package/dist/server/translate.d.ts +1 -1
  30. package/dist/server/translate.d.ts.map +1 -1
  31. package/dist/server/translate.js +5 -2
  32. package/dist/server/translate.js.map +1 -1
  33. package/dist/{types-C98CcjtN.d.ts → types-DU8QHch4.d.ts} +4 -3
  34. package/dist/types-DU8QHch4.d.ts.map +1 -0
  35. package/dist/{types-CkqZWnXH.d.ts → types-f-SNJdvf.d.ts} +8 -8
  36. package/dist/types-f-SNJdvf.d.ts.map +1 -0
  37. package/dist/{url-B8334XSD.js → url-CifeaDzl.js} +2 -12
  38. package/dist/{url-B8334XSD.js.map → url-CifeaDzl.js.map} +1 -1
  39. package/dist/{vite-plugin-BbdVKdt8.js → vite-plugin-C-jEpd8t.js} +18 -22
  40. package/dist/vite-plugin-C-jEpd8t.js.map +1 -0
  41. package/package.json +14 -35
  42. package/dist/client/directives/idle.d.ts +0 -6
  43. package/dist/client/directives/idle.d.ts.map +0 -1
  44. package/dist/client/directives/idle.js +0 -14
  45. package/dist/client/directives/idle.js.map +0 -1
  46. package/dist/client/directives/load.d.ts +0 -6
  47. package/dist/client/directives/load.d.ts.map +0 -1
  48. package/dist/client/directives/load.js +0 -10
  49. package/dist/client/directives/load.js.map +0 -1
  50. package/dist/client/directives/media.d.ts +0 -6
  51. package/dist/client/directives/media.d.ts.map +0 -1
  52. package/dist/client/directives/media.js +0 -17
  53. package/dist/client/directives/media.js.map +0 -1
  54. package/dist/client/directives/only.d.ts +0 -6
  55. package/dist/client/directives/only.d.ts.map +0 -1
  56. package/dist/client/directives/only.js +0 -10
  57. package/dist/client/directives/only.js.map +0 -1
  58. package/dist/client/directives/utils.d.ts +0 -10
  59. package/dist/client/directives/utils.d.ts.map +0 -1
  60. package/dist/client/directives/utils.js +0 -32
  61. package/dist/client/directives/utils.js.map +0 -1
  62. package/dist/client/directives/visible.d.ts +0 -6
  63. package/dist/client/directives/visible.d.ts.map +0 -1
  64. package/dist/client/directives/visible.js +0 -23
  65. package/dist/client/directives/visible.js.map +0 -1
  66. package/dist/context-BEQ6qqX5.js.map +0 -1
  67. package/dist/i18n-Dzh54-at.d.ts.map +0 -1
  68. package/dist/manifest-BEm6GezL.js.map +0 -1
  69. package/dist/state-DeudhdeT.js +0 -20
  70. package/dist/state-DeudhdeT.js.map +0 -1
  71. package/dist/types-C98CcjtN.d.ts.map +0 -1
  72. package/dist/types-CkqZWnXH.d.ts.map +0 -1
  73. package/dist/vite-plugin-BbdVKdt8.js.map +0 -1
  74. package/src/astro/I18nScript.astro +0 -16
  75. package/src/astro/index.ts +0 -2
package/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # @astroscope/i18n
2
2
 
3
- > **Note:** This package is in active development. APIs may change between versions.
4
-
5
3
  i18n for Astro + React islands — automatic tree-shaking, parallel loading, any translation source.
6
4
 
7
5
  ## Why this library?
@@ -10,21 +8,18 @@ i18n for Astro + React islands — automatic tree-shaking, parallel loading, any
10
8
 
11
9
  **Automatic tree-shaking** — Only the translations each component actually uses are delivered to the browser. No manual chunk splitting or configuration.
12
10
 
13
- **Parallel loading** — Translations load alongside component hydration via custom `client:*-x` directives, so hydration doesn't wait on a separate translation fetch.
11
+ **Parallel loading** — Translation chunks preload alongside component code with plain `client:*` directives (via `@astroscope/node`), so hydration doesn't wait on a separate translation fetch.
14
12
 
15
13
  **Unified API** — The same `t()` function works in Astro templates and React islands.
16
14
 
17
15
  **Any translation source** — Fetch translations from any provider: JSON files, database, headless CMS, TMS, or custom API. All are chunked and optimized the same way.
18
16
 
19
- **Production optimized** — Fallback strings are stripped from production bundles via Babel and kept available through the manifest.
20
-
21
17
  ## Features
22
18
 
23
19
  - **Per-chunk translation loading** — each island gets only its translations
24
20
  - **Unicode MessageFormat 2** (MF2) support via `messageformat` v4
25
21
  - **Built-in formatters** — `:number`, `:integer`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`, `:unit`
26
- - **Babel-based extraction** — AST parsing, source maps, production stripping
27
- - **Manifest fallbacks** — missing translations automatically use extracted fallbacks
22
+ - **Fallback strings in code** — used during development, stripped from production bundles, available to translators through the manifest
28
23
  - **Full TypeScript support**
29
24
  - **Tiny client runtime** — ~8KB gzipped for translations
30
25
 
@@ -102,56 +97,32 @@ export async function onStartup() {
102
97
 
103
98
  ```ts
104
99
  // src/middleware.ts
105
- import { sequence } from 'astro:middleware';
106
- import { createI18nChunkMiddleware, createI18nMiddleware, i18n } from '@astroscope/i18n';
107
-
108
- export const onRequest = sequence(
109
- createI18nChunkMiddleware(), // serves /_i18n/ translation chunks
110
- createI18nMiddleware({
111
- locale: (ctx) =>
112
- ctx.cookies.get('locale')?.value ??
113
- i18n.getConfig().defaultLocale,
114
- }),
115
- );
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
+ });
116
107
  ```
117
108
 
109
+ The middleware serving `/_i18n/` translation chunks is injected automatically by the integration.
110
+
118
111
  By default, `RECOMMENDED_EXCLUDES` (static assets like `/_astro/`) are excluded from locale context setup. To customize:
119
112
 
120
113
  ```ts
121
- import { sequence } from 'astro:middleware';
122
- import { createI18nChunkMiddleware, createI18nMiddleware, i18n } from '@astroscope/i18n';
114
+ import { createI18nMiddleware, i18n } from '@astroscope/i18n';
123
115
  import { RECOMMENDED_EXCLUDES } from '@astroscope/node/excludes';
124
116
 
125
- export const onRequest = sequence(
126
- createI18nChunkMiddleware(),
127
- createI18nMiddleware({
128
- locale: (ctx) =>
129
- ctx.cookies.get('locale')?.value ??
130
- i18n.getConfig().defaultLocale,
131
- exclude: [...RECOMMENDED_EXCLUDES, { exact: '/health' }],
132
- }),
133
- );
134
- ```
135
-
136
- ### 4. Add `<I18nScript />` to your layout
137
-
138
- Inject translations into the page for hydrated components:
139
-
140
- ```astro
141
- ---
142
- import { I18nScript } from '@astroscope/i18n/astro';
143
- ---
144
- <html>
145
- <head>
146
- <I18nScript />
147
- </head>
148
- <body>
149
- <slot />
150
- </body>
151
- </html>
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
+ });
152
123
  ```
153
124
 
154
- ### 5. Use `t()` in your components
125
+ ### 4. Use `t()` in your components
155
126
 
156
127
  ```astro
157
128
  ---
@@ -177,21 +148,7 @@ export function CheckoutSummary() {
177
148
 
178
149
  > **Note:** Variables use `{$name}` syntax (with `$` prefix) per MessageFormat 2 specification.
179
150
 
180
- ### 6. Use i18n-aware client directives
181
-
182
- **The problem:** With standard `client:*` directives, the translation chunk loads *after* the component module. This delays hydration while translations are fetched sequentially.
183
-
184
- **The solution:** Use `client:*-x` directives to load translations in parallel with the component code:
185
-
186
- ```astro
187
- ---
188
- import Cart from '../components/Cart';
189
- ---
190
- <!-- translations load alongside component code -->
191
- <Cart client:load-x />
192
- <Cart client:visible-x />
193
- <Cart client:idle-x />
194
- ```
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.
195
152
 
196
153
  ## API
197
154
 
@@ -331,18 +288,16 @@ export function App() {
331
288
 
332
289
  ## How it works
333
290
 
334
- 1. **Build time** — Babel plugin extracts all `t()` calls, maps them to chunks, strips fallbacks from production bundles
291
+ 1. **Build time** — all `t()` calls are extracted, mapped to chunks, and fallbacks are stripped from production bundles
335
292
  2. **Manifest** — Extracted keys with fallbacks are written to `i18n-manifest.json`
336
293
  3. **SSR** — Middleware provides translations to `t()`, merging manifest fallbacks for missing keys
337
- 4. **Client** — Custom directives load only the translations needed by each chunk
294
+ 4. **Client** — each island preloads only the translation chunks its code needs, in parallel with the component code
338
295
 
339
296
  The same `import { t } from '@astroscope/i18n/translate'` works everywhere — bundler picks the correct implementation via conditional exports (`browser` vs `default`).
340
297
 
341
298
  ### Client bundle
342
299
 
343
- Translation chunks are served as raw MessageFormat 2 strings and compiled on the browser on first use. This keeps chunk sizes minimal — the `messageformat` runtime is ~8KB gzipped. Compiled messages are cached for subsequent renders.
344
-
345
- > **Future:** Once browsers ship native `Intl.MessageFormat`, this 8KB runtime will be replaced by the built-in API with zero bundle cost.
300
+ Translation chunks ship as raw MessageFormat 2 strings and are compiled in the browser on first use — the whole client runtime is ~8KB gzipped.
346
301
 
347
302
  ## MessageFormat 2 Syntax
348
303
 
@@ -424,18 +379,19 @@ For `:currency` and `:unit`, the required option (`currency` or `unit`) can be:
424
379
 
425
380
  ## ESLint Plugin
426
381
 
427
- Use [`@astroscope/eslint-plugin-i18n`](../eslint-plugin-i18n) to enforce correct `t()` usage, catch build-time extraction issues, and ensure i18n-aware hydration directives.
382
+ Use the `i18n` config set from [`@astroscope/eslint-plugin`](../eslint-plugin) to enforce correct `t()` usage and catch build-time extraction issues:
428
383
 
429
384
  ```bash
430
- npm install -D @astroscope/eslint-plugin-i18n
385
+ npm install -D @astroscope/eslint-plugin
431
386
  ```
432
387
 
433
388
  ```js
434
389
  // eslint.config.js
435
- import i18n from '@astroscope/eslint-plugin-i18n';
390
+ import astroscope from '@astroscope/eslint-plugin';
436
391
 
437
392
  export default [
438
- i18n.configs.recommended,
393
+ ...astroscope.configs.recommended,
394
+ ...astroscope.configs.i18n,
439
395
  ];
440
396
  ```
441
397
 
@@ -1 +1 @@
1
- {"version":3,"file":"babel-plugin-CWafmw5z.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 meta.fallback = value;\n } else {\n report('the fallback is not a static string');\n }\n } else if (name === 'description') {\n const value = getStringValue(t, prop.value as Node);\n\n if (value !== null) {\n meta.description = value;\n } else {\n report('the description is not a static string');\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)) {\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\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 // check if callee is 't' identifier\n // we assume that it was not aliased / reassigned\n if (!path.get('callee').isIdentifier({ name: 't' })) return;\n\n const args = path.node.arguments;\n const keyArg = args[0];\n\n // check first arg\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 // end check first arg\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 // report extracted key\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) {\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}\n"],"mappings":"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"}
1
+ {"version":3,"file":"babel-plugin-CWafmw5z.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 meta.fallback = value;\n } else {\n report('the fallback is not a static string');\n }\n } else if (name === 'description') {\n const value = getStringValue(t, prop.value as Node);\n\n if (value !== null) {\n meta.description = value;\n } else {\n report('the description is not a static string');\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)) {\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\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 // check if callee is 't' identifier\n // we assume that it was not aliased / reassigned\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) {\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}\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-C98CcjtN.js";
2
+ import { a as TranslationMeta, i as TranslateFunction } from "../types-DU8QHch4.js";
3
3
  //#region src/client/translate.d.ts
4
4
  declare function getLocale(): string;
5
5
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/client/translate.ts"],"mappings":";;;iBAQgB;;;;;;;;;;;;;;cAiBH,GAAG;;;;;;;;;;;;;;;;;;;;iBA4DA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
1
+ {"version":3,"file":"translate.d.ts","names":[],"sources":["../../src/client/translate.ts"],"mappings":";;;iBAQgB;;;;;;;;;;;;;;cAiBH,GAAG;;;;;;;;;;;;;;;;;;;;iBAyDA,KAAK,aACnB,aACA,OAAO,sCACP,aAAa,eAAe,gBAC5B,SAAS,gDACE"}
@@ -1,6 +1,22 @@
1
1
  import { r as formatMessageToParts, t as compileMessage } from "../compiler-CkOVGWU5.js";
2
2
  import { n as normalizeMeta, t as partsToNodes } from "../rich-CHisXE-A.js";
3
- import { t as getI18nState } from "../state-DeudhdeT.js";
3
+ //#region src/client/state.ts
4
+ let warnedMissingState = false;
5
+ /**
6
+ * Read the client i18n state injected by the i18n middleware and islands emitter.
7
+ *
8
+ * Returns undefined and logs an actionable error (once) when the state was not
9
+ * injected — without it window.__i18n__ is undefined and translations cannot load.
10
+ */
11
+ function getI18nState() {
12
+ const state = window.__i18n__;
13
+ if (!state && !warnedMissingState) {
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.");
16
+ }
17
+ return state;
18
+ }
19
+ //#endregion
4
20
  //#region src/client/translate.ts
5
21
  const cache = /* @__PURE__ */ new Map();
6
22
  function getLocale() {
@@ -65,7 +81,8 @@ function rich(key, meta, components, values) {
65
81
  const normalizedMeta = meta ? normalizeMeta(meta) : { fallback: "" };
66
82
  const state = getI18nState();
67
83
  const raw = state?.translations[key] || normalizedMeta.fallback || key;
68
- return partsToNodes(formatMessageToParts(state?.locale ?? "", raw, values), components ?? {});
84
+ const parts = formatMessageToParts(state?.locale ?? "", raw, values);
85
+ return partsToNodes(parts, components ?? {});
69
86
  }
70
87
  //#endregion
71
88
  export { getLocale, rich, t };
@@ -1 +1 @@
1
- {"version":3,"file":"translate.js","names":[],"sources":["../../src/client/translate.ts"],"sourcesContent":["import { compileMessage, formatMessageToParts } from '../shared/compiler.js';\nimport { normalizeMeta } from '../shared/meta.js';\nimport { type RichComponents, partsToNodes } from '../shared/rich.js';\nimport type { CompiledTranslation, TranslateFunction, TranslationMeta } from '../shared/types.js';\nimport { 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 // look up raw translation\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 // production: return key as-is\n return key;\n }\n\n // check cache\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 // look up raw translation, fall back to meta.fallback or key\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":";;;;AAMA,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;CAGhC,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;EAGA,OAAO;CACT;CAGA,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;CAG3B,MAAM,MAAM,OAAO,aAAa,QAAQ,eAAe,YAAY;CAInE,OAAO,aAFO,qBAAqB,OAAO,UAAU,IAAI,KAAK,MAErC,GAAG,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 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,4 +1,4 @@
1
- import { t as getGlobalState } from "./manifest-BEm6GezL.js";
1
+ import { t as getGlobalState } from "./manifest-Cma68XM-.js";
2
2
  import { n as compileTranslations } from "./compiler-CkOVGWU5.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -26,33 +26,6 @@ function computeAllChunkHashes(translations, manifest) {
26
26
  return hashes;
27
27
  }
28
28
  //#endregion
29
- //#region src/server/utils.ts
30
- const LOWERCASE_A = 97;
31
- /**
32
- * Convert index to lowercase letter: 0→'a', 1→'b', ..., 25→'z'
33
- */
34
- function indexToLetter(index) {
35
- return String.fromCharCode(LOWERCASE_A + index);
36
- }
37
- /**
38
- * Generate short variable name using bijective base-26: 0→'a', 25→'z', 26→'aa', 27→'ab', ...
39
- */
40
- function generateBB26(index) {
41
- let name = "";
42
- let remaining = index;
43
- do {
44
- name = indexToLetter(remaining % 26) + name;
45
- remaining = Math.floor(remaining / 26) - 1;
46
- } while (remaining >= 0);
47
- return name;
48
- }
49
- /**
50
- * Generate a JS-safe variable name for the inline i18n script.
51
- */
52
- function generateVarName(index) {
53
- return `_${generateBB26(index)}`;
54
- }
55
- //#endregion
56
29
  //#region src/server/i18n.ts
57
30
  var I18nSingleton = class {
58
31
  config = null;
@@ -60,7 +33,6 @@ var I18nSingleton = class {
60
33
  mergedCache = /* @__PURE__ */ new Map();
61
34
  compiledCache = /* @__PURE__ */ new Map();
62
35
  hashCache = /* @__PURE__ */ new Map();
63
- scriptCache = /* @__PURE__ */ new Map();
64
36
  chunkCache = /* @__PURE__ */ new Map();
65
37
  manifestGetter = null;
66
38
  manifestVersion = 0;
@@ -98,7 +70,6 @@ var I18nSingleton = class {
98
70
  this.mergedCache.delete(locale);
99
71
  this.compiledCache.delete(locale);
100
72
  this.hashCache.delete(locale);
101
- this.scriptCache.delete(locale);
102
73
  for (const key of this.chunkCache.keys()) if (key.startsWith(`${locale}:`)) this.chunkCache.delete(key);
103
74
  const { chunks } = this.getManifest();
104
75
  if (Object.keys(chunks).length > 0) this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));
@@ -111,7 +82,7 @@ var I18nSingleton = class {
111
82
  const manifest = this.manifestGetter?.() ?? {
112
83
  keys: [],
113
84
  chunks: {},
114
- imports: {}
85
+ scripts: []
115
86
  };
116
87
  if (manifest.keys.length === 0) return raw;
117
88
  const withFallbacks = { ...raw };
@@ -163,40 +134,19 @@ var I18nSingleton = class {
163
134
  return body;
164
135
  }
165
136
  /**
166
- * Get the inline script for I18nScript component.
167
- * Cached per locale, invalidated when translations change.
137
+ * Hashes for chunks reachable from astro `<script>` entries. Script modules are
138
+ * not islands, so the islands emitter never covers them — the middleware
139
+ * bootstraps these few into the client state on every html response.
168
140
  */
169
- getClientScript(locale) {
170
- const cached = this.scriptCache.get(locale);
171
- if (cached) return cached;
172
- const script = this.createClientScript(locale);
173
- this.scriptCache.set(locale, script);
174
- return script;
175
- }
176
- createClientScript(locale) {
177
- const { chunks, imports } = this.getManifest();
178
- if (!(Object.keys(chunks).length > 0)) {
179
- const raw = this.getTranslations(locale);
180
- return `window.__i18n__=${JSON.stringify({
181
- locale,
182
- hashes: {},
183
- imports: {},
184
- translations: raw
185
- })};`;
186
- }
141
+ getScriptChunkHashes(locale) {
142
+ const { scripts } = this.getManifest();
187
143
  const hashes = this.getHashes(locale);
188
- const allChunks = new Set(Object.keys(hashes));
189
- for (const deps of Object.values(imports)) deps.forEach((d) => allChunks.add(d));
190
- const chunkToVar = /* @__PURE__ */ new Map();
191
- let varIndex = 0;
192
- for (const chunk of allChunks) chunkToVar.set(chunk, generateVarName(varIndex++));
193
- const varDecls = [...chunkToVar.entries()].map(([chunk, v]) => `${v}=${JSON.stringify(chunk)}`).join(",");
194
- const hashesObj = Object.entries(hashes).map(([chunk, hash]) => `[${chunkToVar.get(chunk)}]:${JSON.stringify(hash)}`).join(",");
195
- const importsObj = Object.entries(imports).filter(([chunk]) => chunkToVar.has(chunk)).map(([chunk, deps]) => {
196
- const validDeps = deps.filter((d) => chunkToVar.has(d));
197
- return validDeps.length > 0 ? `[${chunkToVar.get(chunk)}]:[${validDeps.map((d) => chunkToVar.get(d)).join(",")}]` : null;
198
- }).filter(Boolean).join(",");
199
- return `(()=>{var ${varDecls};window.__i18n__={locale:${JSON.stringify(locale)},hashes:{${hashesObj}},imports:{${importsObj}},translations:{}};})();`;
144
+ const result = {};
145
+ for (const chunk of scripts) {
146
+ const hash = hashes[chunk];
147
+ if (hash) result[chunk] = hash;
148
+ }
149
+ return result;
200
150
  }
201
151
  /**
202
152
  * Clear translations for specified locale(s).
@@ -209,7 +159,6 @@ var I18nSingleton = class {
209
159
  this.mergedCache.delete(locale);
210
160
  this.compiledCache.delete(locale);
211
161
  this.hashCache.delete(locale);
212
- this.scriptCache.delete(locale);
213
162
  for (const key of this.chunkCache.keys()) if (key.startsWith(`${locale}:`)) this.chunkCache.delete(key);
214
163
  }
215
164
  }
@@ -222,7 +171,6 @@ var I18nSingleton = class {
222
171
  this.manifestVersion = version;
223
172
  this.mergedCache.clear();
224
173
  this.compiledCache.clear();
225
- this.scriptCache.clear();
226
174
  this.chunkCache.clear();
227
175
  }
228
176
  resolveLocales(locales) {
@@ -250,4 +198,4 @@ function getContext() {
250
198
  //#endregion
251
199
  export { runWithContext as n, i18n as r, getContext as t };
252
200
 
253
- //# sourceMappingURL=context-BEQ6qqX5.js.map
201
+ //# sourceMappingURL=context-CYqyZJyH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context-CYqyZJyH.js","names":[],"sources":["../src/extraction/hash.ts","../src/server/i18n.ts","../src/server/context.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { RawTranslations } from '../shared/types.js';\nimport type { ChunkManifest } from './types.js';\n\n/**\n * Hash a string to a short hash (for cache busting)\n */\nfunction hashString(str: string): string {\n return createHash('sha256').update(str).digest('hex').substring(0, 8);\n}\n\n/**\n * Compute hash for chunk translations\n */\nexport function computeChunkHash(translations: RawTranslations, keys: string[]): string {\n const relevantTranslations: RawTranslations = {};\n\n for (const key of [...keys].sort()) {\n if (translations[key] !== undefined) {\n relevantTranslations[key] = translations[key];\n }\n }\n\n return hashString(JSON.stringify(relevantTranslations));\n}\n\n/**\n * Compute hashes for all chunks in manifest\n */\nexport function computeAllChunkHashes(translations: RawTranslations, manifest: ChunkManifest): Record<string, string> {\n const hashes: Record<string, string> = {};\n\n for (const [chunkId, keys] of Object.entries(manifest)) {\n hashes[chunkId] = computeChunkHash(translations, keys);\n }\n\n return hashes;\n}\n","import { computeAllChunkHashes } from '../extraction/hash.js';\nimport { getGlobalState } from '../extraction/manifest.js';\nimport type { ExtractionManifest } from '../extraction/types.js';\nimport { compileTranslations } from '../shared/compiler.js';\nimport type { CompiledTranslations, RawTranslations } from '../shared/types.js';\nimport type { FallbackBehavior } from './types.js';\n\nexport type I18nConfig = {\n locales: string[];\n defaultLocale?: string | undefined;\n fallback?: FallbackBehavior | undefined;\n};\n\ntype NormalizedConfig = {\n locales: string[];\n defaultLocale: string;\n fallback: FallbackBehavior;\n};\n\nclass I18nSingleton {\n // normalized user config with defaults applied\n private config: NormalizedConfig | null = null;\n\n // locale -> raw translation strings (before ICU compilation)\n private rawCache = new Map<string, RawTranslations>();\n\n // locale -> raw translations merged with manifest fallbacks\n private mergedCache = new Map<string, RawTranslations>();\n\n // locale -> compiled ICU MessageFormat functions\n private compiledCache = new Map<string, CompiledTranslations>();\n\n // locale -> chunk name -> content hash (for cache busting)\n private hashCache = new Map<string, Record<string, string>>();\n\n // \"locale:chunkName\" -> encoded chunk response body\n private chunkCache = new Map<string, Uint8Array>();\n\n // manifest getter, set on configure() via the virtual module (live data in dev mode)\n private manifestGetter: (() => ExtractionManifest) | null = null;\n\n // tracks the manifest version to detect when derived caches need invalidation\n private manifestVersion = 0;\n\n async configure(config: I18nConfig): Promise<void> {\n if (!config.locales?.length) {\n throw new Error('i18n.configure(): locales array is required and must not be empty');\n }\n\n for (const locale of config.locales) {\n if (!locale) {\n throw new Error('i18n.configure(): locale cannot be empty');\n }\n\n if (locale !== locale.trim()) {\n throw new Error(`i18n.configure(): locale \"${locale}\" has leading or trailing whitespace`);\n }\n }\n\n if (new Set(config.locales).size !== config.locales.length) {\n throw new Error('i18n.configure(): locales array contains duplicates');\n }\n\n if (config.defaultLocale !== undefined) {\n if (!config.defaultLocale) {\n throw new Error('i18n.configure(): defaultLocale cannot be empty');\n }\n\n if (config.defaultLocale !== config.defaultLocale.trim()) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" has leading or trailing whitespace`);\n }\n\n if (!config.locales.includes(config.defaultLocale)) {\n throw new Error(`i18n.configure(): defaultLocale \"${config.defaultLocale}\" is not in locales array`);\n }\n }\n\n this.config = {\n locales: config.locales,\n defaultLocale: config.defaultLocale ?? config.locales[0]!,\n fallback: config.fallback ?? 'fallback',\n };\n\n // resolved lazily so importing the singleton needs no virtual module\n if (!this.manifestGetter) {\n const m = await import('virtual:@astroscope/i18n/manifest');\n this.manifestGetter = m.getManifest;\n }\n }\n\n isConfigured(): boolean {\n return this.config !== null;\n }\n\n getConfig(): NormalizedConfig {\n if (!this.config) {\n throw new Error('i18n not configured. Call i18n.configure() first.');\n }\n\n return this.config;\n }\n\n setTranslations(locale: string, raw: RawTranslations): void {\n this.rawCache.set(locale, raw);\n this.mergedCache.delete(locale); // invalidate merged cache\n this.compiledCache.delete(locale); // invalidate compiled cache\n this.hashCache.delete(locale); // invalidate hash cache\n\n // invalidate chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n\n // recompute hashes if chunk manifest exists (production only)\n const { chunks } = this.getManifest();\n\n if (Object.keys(chunks).length > 0) {\n this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));\n }\n }\n\n getTranslations(locale: string): RawTranslations {\n this.invalidateIfManifestChanged();\n\n const cached = this.mergedCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const raw = this.rawCache.get(locale) ?? {};\n const manifest = this.manifestGetter?.() ?? { keys: [], chunks: {}, scripts: [] };\n\n if (manifest.keys.length === 0) {\n return raw;\n }\n\n // merge fallbacks for keys that don't have translations\n const withFallbacks: RawTranslations = { ...raw };\n\n for (const { key, meta } of manifest.keys) {\n if (!(key in withFallbacks) && meta.fallback) {\n withFallbacks[key] = meta.fallback;\n }\n }\n\n this.mergedCache.set(locale, withFallbacks);\n\n return withFallbacks;\n }\n\n getCompiledTranslations(locale: string): CompiledTranslations {\n const cached = this.compiledCache.get(locale);\n\n if (cached) {\n return cached;\n }\n\n const compiled = compileTranslations(locale, this.getTranslations(locale));\n\n this.compiledCache.set(locale, compiled);\n\n return compiled;\n }\n\n /**\n * Get the extraction manifest (keys + chunks).\n * - keys: translation keys extracted from codebase with metadata\n * - chunks: mapping of chunk names to their translation keys\n */\n getManifest(): ExtractionManifest {\n if (!this.manifestGetter) {\n throw new Error('i18n manifest not initialized. Call i18n.configure() first.');\n }\n\n return this.manifestGetter();\n }\n\n getHashes(locale: string): Record<string, string> {\n return this.hashCache.get(locale) ?? {};\n }\n\n /**\n * Get the encoded chunk response body for a locale/chunk combination.\n * Cached to avoid repeated JSON.stringify and encoding on each request.\n * Returns undefined if chunk not found in manifest.\n */\n getChunkBody(locale: string, chunkName: string): Uint8Array | undefined {\n const keys = this.getManifest().chunks[chunkName];\n\n if (!keys) {\n return undefined;\n }\n\n const cacheKey = `${locale}:${chunkName}`;\n const cached = this.chunkCache.get(cacheKey);\n\n if (cached) {\n return cached;\n }\n\n const translations = this.getTranslations(locale);\n const chunkTranslations: RawTranslations = {};\n\n for (const key of keys) {\n if (translations[key]) {\n chunkTranslations[key] = translations[key];\n }\n }\n\n const js = `/* @astroscope/i18n chunk: ${chunkName} */\n(function() {\n var i = window.__i18n__;\n if (i) Object.assign(i.translations, ${JSON.stringify(chunkTranslations)});\n})();\n`;\n\n const body = new TextEncoder().encode(js);\n\n this.chunkCache.set(cacheKey, body);\n\n return body;\n }\n\n /**\n * Hashes for chunks reachable from astro `<script>` entries. Script modules are\n * not islands, so the islands emitter never covers them — the middleware\n * bootstraps these few into the client state on every html response.\n */\n getScriptChunkHashes(locale: string): Record<string, string> {\n const { scripts } = this.getManifest();\n const hashes = this.getHashes(locale);\n const result: Record<string, string> = {};\n\n for (const chunk of scripts) {\n const hash = hashes[chunk];\n\n if (hash) {\n result[chunk] = hash;\n }\n }\n\n return result;\n }\n\n /**\n * Clear translations for specified locale(s).\n * @param locales - Locale(s) to clear, or all if not specified\n */\n clear(locales?: string | string[] | undefined): void {\n const targets = this.resolveLocales(locales);\n\n for (const locale of targets) {\n this.rawCache.delete(locale);\n this.mergedCache.delete(locale);\n this.compiledCache.delete(locale);\n this.hashCache.delete(locale);\n\n // clear chunk cache entries for this locale\n for (const key of this.chunkCache.keys()) {\n if (key.startsWith(`${locale}:`)) {\n this.chunkCache.delete(key);\n }\n }\n }\n }\n\n /**\n * invalidate derived caches when the extraction manifest has changed (dev mode HMR)\n */\n private invalidateIfManifestChanged(): void {\n const { version } = getGlobalState();\n\n if (version === this.manifestVersion) {\n return;\n }\n\n this.manifestVersion = version;\n this.mergedCache.clear();\n this.compiledCache.clear();\n this.chunkCache.clear();\n }\n\n private resolveLocales(locales?: string | string[] | undefined): string[] {\n if (!locales) {\n return this.config?.locales ?? [];\n }\n\n if (typeof locales === 'string') {\n return [locales];\n }\n\n return locales;\n }\n}\n\nexport const i18n = new I18nSingleton();\n","import { AsyncLocalStorage } from 'node:async_hooks';\nimport type { I18nContext } from './types.js';\n\nconst als = new AsyncLocalStorage<I18nContext>();\n\n/**\n * Run a function with i18n context (called by middleware)\n */\nexport function runWithContext<T>(context: I18nContext, fn: () => T): T {\n return als.run(context, fn);\n}\n\n/**\n * Get the current i18n context\n */\nexport function getContext(): I18nContext | null {\n return als.getStore() ?? null;\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { n as ExtractedKey, r as ExtractionManifest, t as ChunkManifest } from "../types-CkqZWnXH.js";
1
+ import { n as ExtractedKey, r as ExtractionManifest, t as ChunkManifest } from "../types-f-SNJdvf.js";
2
2
  import { t as ConsistencyCheckLevel } from "../types-Bo4jMK8g.js";
3
3
  import { AstroIntegrationLogger } from "astro";
4
4
  import { Plugin } from "vite";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/extraction/manifest.ts","../../src/extraction/vite-plugin.ts"],"mappings":";;;;;;;;;iBAgDgB,eAAe;;;KCxBnB;EACV,QAAQ;EACR,aAAa;;;;;;;;;;;;iBAuCC,eAAe,SAAS,wBAAwB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/extraction/manifest.ts","../../src/extraction/vite-plugin.ts"],"mappings":";;;;;;;;;iBA+CgB,eAAe;;;KCvBnB;EACV,QAAQ;EACR,aAAa;;;;;;;;;;;;iBAuCC,eAAe,SAAS,wBAAwB"}
@@ -1,3 +1,3 @@
1
- import { t as i18nVitePlugin } from "../vite-plugin-BbdVKdt8.js";
2
- import { n as getManifest } from "../manifest-BEm6GezL.js";
1
+ import { t as i18nVitePlugin } from "../vite-plugin-C-jEpd8t.js";
2
+ import { n as getManifest } from "../manifest-Cma68XM-.js";
3
3
  export { getManifest, i18nVitePlugin };
@@ -1,5 +1,5 @@
1
- import { a as TranslationMeta, r as RawTranslations, t as CompiledTranslations } from "./types-C98CcjtN.js";
2
- import { r as ExtractionManifest } from "./types-CkqZWnXH.js";
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";
3
3
  //#region src/server/types.d.ts
4
4
  /**
5
5
  * Fallback behavior when translation is missing
@@ -32,7 +32,6 @@ declare class I18nSingleton {
32
32
  private mergedCache;
33
33
  private compiledCache;
34
34
  private hashCache;
35
- private scriptCache;
36
35
  private chunkCache;
37
36
  private manifestGetter;
38
37
  private manifestVersion;
@@ -56,11 +55,11 @@ declare class I18nSingleton {
56
55
  */
57
56
  getChunkBody(locale: string, chunkName: string): Uint8Array | undefined;
58
57
  /**
59
- * Get the inline script for I18nScript component.
60
- * Cached per locale, invalidated when translations change.
58
+ * Hashes for chunks reachable from astro `<script>` entries. Script modules are
59
+ * not islands, so the islands emitter never covers them — the middleware
60
+ * bootstraps these few into the client state on every html response.
61
61
  */
62
- getClientScript(locale: string): string;
63
- private createClientScript;
62
+ getScriptChunkHashes(locale: string): Record<string, string>;
64
63
  /**
65
64
  * Clear translations for specified locale(s).
66
65
  * @param locales - Locale(s) to clear, or all if not specified
@@ -75,4 +74,4 @@ declare class I18nSingleton {
75
74
  declare const i18n: I18nSingleton;
76
75
  //#endregion
77
76
  export { I18nContext as i, i18n as n, FallbackBehavior as r, I18nConfig as t };
78
- //# sourceMappingURL=i18n-Dzh54-at.d.ts.map
77
+ //# sourceMappingURL=i18n-Bp8TFmd3.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-Bp8TFmd3.d.ts","names":[],"sources":["../src/server/types.ts","../src/server/i18n.ts"],"mappings":";;;;;;KAKY,oDACuB,aAAa,OAAO;;;;KAK3C;EACV;EAEA,cAAc;EAEd,iBAAiB;EACjB,UAAU;;;;KCVA;EACV;EACA;EACA,WAAW;;KAGR;EACH;EACA;EACA,UAAU;;cAGN;UAEI;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;UAGA;EAEF,UAAU,QAAQ,aAAa;EA8CrC;EAIA,aAAa;EAQb,gBAAgB,gBAAgB,KAAK;EAqBrC,gBAAgB,iBAAiB;EA8BjC,wBAAwB,iBAAiB;;;;;;EAmBzC,eAAe;EAQf,UAAU,iBAAiB;;;;;;EAS3B,aAAa,gBAAgB,oBAAoB;;;;;;EA0CjD,qBAAqB,iBAAiB;;;;;EAoBtC,MAAM;;;;UAqBE;UAaA;;cAaG,MAAI"}
package/dist/index.d.ts CHANGED
@@ -1,16 +1,19 @@
1
- import { a as TranslationMeta, r as RawTranslations } from "./types-C98CcjtN.js";
2
- import { n as ExtractedKey, r as ExtractionManifest } from "./types-CkqZWnXH.js";
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
3
  import { n as I18nOptions } from "./types-Bo4jMK8g.js";
4
- import { n as i18n, r as FallbackBehavior, t as I18nConfig } from "./i18n-Dzh54-at.js";
4
+ import { n as i18n, r as FallbackBehavior, t as I18nConfig } from "./i18n-Bp8TFmd3.js";
5
5
  import { ExcludePattern } from "@astroscope/node/excludes";
6
6
  import { APIContext, AstroIntegration, MiddlewareHandler } from "astro";
7
7
  //#region src/integration/integration.d.ts
8
8
  /**
9
9
  * Astro integration for i18n with automatic per-chunk translation loading.
10
10
  *
11
- * Registers:
12
- * - Client directives (`client:load-x`, `client:idle-x`, `client:visible-x`, `client:media-x`, `client:only-x`)
13
- * - Vite plugin for translation key extraction
11
+ * 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.
14
17
  */
15
18
  declare function i18nIntegration(options?: I18nOptions): AstroIntegration;
16
19
  //#endregion
@@ -40,32 +43,13 @@ type I18nMiddlewareOptions = {
40
43
  */
41
44
  exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | undefined;
42
45
  };
43
- /**
44
- * Create the i18n chunk middleware that serves translation chunks at `/_i18n/` endpoints.
45
- *
46
- * Place this early in your middleware sequence (before session/auth)
47
- * to avoid unnecessary overhead for static translation requests.
48
- *
49
- * @example
50
- * ```typescript
51
- * // src/middleware.ts
52
- * import { sequence } from 'astro:middleware';
53
- * import { createI18nChunkMiddleware, createI18nMiddleware } from '@astroscope/i18n';
54
- *
55
- * export const onRequest = sequence(
56
- * createI18nChunkMiddleware(), // early: serves /_i18n/ chunks
57
- * sessionMiddleware,
58
- * createI18nMiddleware({ locale: (ctx) => ... }), // after session
59
- * );
60
- * ```
61
- */
62
- declare function createI18nChunkMiddleware(): MiddlewareHandler;
63
46
  /**
64
47
  * Create the i18n locale middleware.
65
48
  *
66
- * Sets up the request context with locale and translations for use by
67
- * `t()` and `<I18nScript />`. Place this after session middleware if
68
- * your locale detection depends on session/cookies.
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.
69
53
  *
70
54
  * @example
71
55
  * ```typescript
@@ -88,5 +72,5 @@ declare function createI18nMiddleware(options: I18nMiddlewareOptions): Middlewar
88
72
  */
89
73
  declare function detectLocale(request: Request): string | undefined;
90
74
  //#endregion
91
- export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type I18nMiddlewareOptions, type RawTranslations, type TranslationMeta, createI18nChunkMiddleware, createI18nMiddleware, i18nIntegration as default, detectLocale, i18n };
75
+ export { type ExtractedKey, type ExtractionManifest, type FallbackBehavior, type I18nConfig, type I18nMiddlewareOptions, type RawTranslations, type TranslationMeta, createI18nMiddleware, i18nIntegration as default, detectLocale, i18n };
92
76
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration/integration.ts","../src/server/middleware.ts","../src/server/locale.ts"],"mappings":";;;;;;;;;;;;;;iBAawB,gBAAgB,UAAS,cAAmB;;;KCNxD;EACV,SAAS,KAAK;;;;;;;;;;;;;;;;;;;;;;EAsBd,UAAU,qBAAqB,SAAS;;;;;;;;;;;;;;;;;;;;;iBAmG1B,6BAA6B;;;;;;;;;;;;;;;;;;;;iBAkC7B,qBAAqB,SAAS,wBAAwB;;;;;;;iBC7JtD,aAAa,SAAS"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration/integration.ts","../src/server/middleware.ts","../src/server/locale.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAcwB,gBAAgB,UAAS,cAAmB;;;KCNxD;EACV,SAAS,KAAK;;;;;;;;;;;;;;;;;;;;;;EAsBd,UAAU,qBAAqB,SAAS;;;;;;;;;;;;;;;;;;;;;;iBA2H1B,qBAAqB,SAAS,wBAAwB;;;;;;;iBCpJtD,aAAa,SAAS"}