@astroscope/i18n 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/README.md +262 -0
  2. package/dist/babel-plugin-FUTQRKLT.js +121 -0
  3. package/dist/babel-plugin-K2K4ZPM5.js +118 -0
  4. package/dist/chunk-22ZZFFC2.js +159 -0
  5. package/dist/chunk-2PSXD3JJ.js +144 -0
  6. package/dist/chunk-3VI62RLT.js +176 -0
  7. package/dist/chunk-4A5BKHFN.js +16 -0
  8. package/dist/chunk-4DAQPWXI.js +36 -0
  9. package/dist/chunk-4OGX6AF5.js +10 -0
  10. package/dist/chunk-4PHYKXO3.js +227 -0
  11. package/dist/chunk-4UBFISFY.js +376 -0
  12. package/dist/chunk-5DERF6DV.js +266 -0
  13. package/dist/chunk-5KGNHFTC.js +344 -0
  14. package/dist/chunk-5PP4D2VK.js +23 -0
  15. package/dist/chunk-6BW3PN23.js +14 -0
  16. package/dist/chunk-6ESQ4JQI.js +263 -0
  17. package/dist/chunk-A7MPXMOP.js +202 -0
  18. package/dist/chunk-AWZ2SQDW.js +149 -0
  19. package/dist/chunk-AYY2YIBQ.js +52 -0
  20. package/dist/chunk-BMNEJFTD.js +56 -0
  21. package/dist/chunk-BSXQGEUK.js +16 -0
  22. package/dist/chunk-BSYTJFGF.js +21 -0
  23. package/dist/chunk-BVPXEGTT.js +12 -0
  24. package/dist/chunk-C4DJYWF6.js +193 -0
  25. package/dist/chunk-C5EJYE2P.js +245 -0
  26. package/dist/chunk-CLNOQHMS.js +254 -0
  27. package/dist/chunk-E5MCIRSU.js +144 -0
  28. package/dist/chunk-E7ZJS7WX.js +162 -0
  29. package/dist/chunk-EMM6LRO2.js +8 -0
  30. package/dist/chunk-ESFGCYYH.js +24 -0
  31. package/dist/chunk-FAKBSLNW.js +24 -0
  32. package/dist/chunk-FHBVI3TL.js +372 -0
  33. package/dist/chunk-FI5KZ5IO.js +17 -0
  34. package/dist/chunk-H5R6YMOL.js +233 -0
  35. package/dist/chunk-H62K3FGA.js +236 -0
  36. package/dist/chunk-HDG2R6XT.js +211 -0
  37. package/dist/chunk-HGXU5QAK.js +344 -0
  38. package/dist/chunk-HHRMR3TM.js +17 -0
  39. package/dist/chunk-HS4HVBAD.js +127 -0
  40. package/dist/chunk-IPJWKKHJ.js +324 -0
  41. package/dist/chunk-ITTL4V7J.js +24 -0
  42. package/dist/chunk-IWUBCMOK.js +128 -0
  43. package/dist/chunk-JJBBY2GR.js +14 -0
  44. package/dist/chunk-JJEDOJBR.js +193 -0
  45. package/dist/chunk-JTUKXNIJ.js +23 -0
  46. package/dist/chunk-K7YULG4A.js +14 -0
  47. package/dist/chunk-KNDYKTHE.js +31 -0
  48. package/dist/chunk-KNONI6DP.js +89 -0
  49. package/dist/chunk-LMTEO22Y.js +17 -0
  50. package/dist/chunk-LRLZEX4D.js +147 -0
  51. package/dist/chunk-MFKPLUAN.js +241 -0
  52. package/dist/chunk-MIWXQOO3.js +316 -0
  53. package/dist/chunk-MJRTAWPR.js +261 -0
  54. package/dist/chunk-MZEPQCXX.js +249 -0
  55. package/dist/chunk-NHHTSFA7.js +237 -0
  56. package/dist/chunk-NTTXM7AG.js +23 -0
  57. package/dist/chunk-NZFAVJXR.js +192 -0
  58. package/dist/chunk-OFNFSDW3.js +13 -0
  59. package/dist/chunk-PG3XDNGF.js +227 -0
  60. package/dist/chunk-PVEVTPNK.js +139 -0
  61. package/dist/chunk-QAO4BXS4.js +244 -0
  62. package/dist/chunk-QCTULQQK.js +34 -0
  63. package/dist/chunk-QFAAGCP5.js +177 -0
  64. package/dist/chunk-QKND2KCA.js +163 -0
  65. package/dist/chunk-QZEGY6RJ.js +151 -0
  66. package/dist/chunk-RAE2L6FT.js +193 -0
  67. package/dist/chunk-S3ABJBU3.js +255 -0
  68. package/dist/chunk-SVDVCPCV.js +236 -0
  69. package/dist/chunk-T35ZUWTL.js +153 -0
  70. package/dist/chunk-T6WV5DW6.js +156 -0
  71. package/dist/chunk-TGUXVDA3.js +82 -0
  72. package/dist/chunk-TYMO7K3A.js +173 -0
  73. package/dist/chunk-V4RBFLQL.js +23 -0
  74. package/dist/chunk-V4XWJXXZ.js +23 -0
  75. package/dist/chunk-VQCY3OKK.js +258 -0
  76. package/dist/chunk-WLGOJPDJ.js +15 -0
  77. package/dist/chunk-WUEPDJIL.js +259 -0
  78. package/dist/chunk-X3L7WZLY.js +254 -0
  79. package/dist/chunk-XDD62ZM3.js +255 -0
  80. package/dist/chunk-XFXXEDRD.js +36 -0
  81. package/dist/chunk-XKYOR5UK.js +8 -0
  82. package/dist/chunk-Y26535SI.js +239 -0
  83. package/dist/chunk-Y3PQX5TX.js +152 -0
  84. package/dist/chunk-Y4BE2V6F.js +347 -0
  85. package/dist/chunk-Z4IZYDIA.js +24 -0
  86. package/dist/chunk-Z52G4CYY.js +14 -0
  87. package/dist/chunk-ZGLUYWVM.js +128 -0
  88. package/dist/client/directives/idle.d.ts +9 -0
  89. package/dist/client/directives/idle.js +15 -0
  90. package/dist/client/directives/load.d.ts +9 -0
  91. package/dist/client/directives/load.js +15 -0
  92. package/dist/client/directives/media.d.ts +9 -0
  93. package/dist/client/directives/media.js +15 -0
  94. package/dist/client/directives/only.d.ts +9 -0
  95. package/dist/client/directives/only.js +15 -0
  96. package/dist/client/directives/utils.d.ts +10 -0
  97. package/dist/client/directives/utils.js +7 -0
  98. package/dist/client/directives/visible.d.ts +9 -0
  99. package/dist/client/directives/visible.js +15 -0
  100. package/dist/client/import-with-i18n.d.ts +16 -0
  101. package/dist/client/import-with-i18n.js +8 -0
  102. package/dist/client/t.d.ts +20 -0
  103. package/dist/client/t.js +37 -0
  104. package/dist/client-directives/idle.d.ts +9 -0
  105. package/dist/client-directives/idle.js +15 -0
  106. package/dist/client-directives/load.d.ts +9 -0
  107. package/dist/client-directives/load.js +15 -0
  108. package/dist/client-directives/media.d.ts +9 -0
  109. package/dist/client-directives/media.js +15 -0
  110. package/dist/client-directives/only.d.ts +9 -0
  111. package/dist/client-directives/only.js +15 -0
  112. package/dist/client-directives/utils.d.ts +9 -0
  113. package/dist/client-directives/utils.js +7 -0
  114. package/dist/client-directives/visible.d.ts +9 -0
  115. package/dist/client-directives/visible.js +15 -0
  116. package/dist/extraction/index.d.ts +34 -0
  117. package/dist/extraction/index.js +9 -0
  118. package/dist/i18n-3cZHoHwP.d.ts +86 -0
  119. package/dist/i18n-BGfrSNM8.d.ts +56 -0
  120. package/dist/i18n-BHU81LMA.d.ts +55 -0
  121. package/dist/i18n-BagoeH4_.d.ts +62 -0
  122. package/dist/i18n-BeL84090.d.ts +77 -0
  123. package/dist/i18n-BeRWDfHi.d.ts +55 -0
  124. package/dist/i18n-Bfs2PVcf.d.ts +79 -0
  125. package/dist/i18n-BrL7p3aN.d.ts +99 -0
  126. package/dist/i18n-CNQNCFU0.d.ts +55 -0
  127. package/dist/i18n-CX_uU8V2.d.ts +80 -0
  128. package/dist/i18n-Cr1QZCwe.d.ts +64 -0
  129. package/dist/i18n-CzTRxGeX.d.ts +80 -0
  130. package/dist/i18n-DUOFiC7g.d.ts +77 -0
  131. package/dist/i18n-DcF47BHS.d.ts +77 -0
  132. package/dist/i18n-DifWvtaO.d.ts +60 -0
  133. package/dist/i18n-DumSqX1t.d.ts +55 -0
  134. package/dist/i18n-fWTJw12h.d.ts +80 -0
  135. package/dist/import-with-i18n.d.ts +23 -0
  136. package/dist/import-with-i18n.js +7 -0
  137. package/dist/index.d.ts +68 -0
  138. package/dist/index.js +151 -0
  139. package/dist/init.d.ts +2 -0
  140. package/dist/init.js +8 -0
  141. package/dist/internal.d.ts +11 -0
  142. package/dist/internal.js +9 -0
  143. package/dist/middleware-entrypoint-C0Drh9E_.d.ts +29 -0
  144. package/dist/middleware-entrypoint-mEl66IUy.d.ts +29 -0
  145. package/dist/middleware-entrypoint.d.ts +5 -0
  146. package/dist/middleware-entrypoint.js +30 -0
  147. package/dist/server/middleware-entrypoint.d.ts +2 -0
  148. package/dist/server/middleware-entrypoint.js +28 -0
  149. package/dist/server/t.d.ts +17 -0
  150. package/dist/server/t.js +59 -0
  151. package/dist/t-BY_C9lZ1.d.ts +30 -0
  152. package/dist/t-CG3yDiEa.d.ts +30 -0
  153. package/dist/t-CHsBfrPr.d.ts +30 -0
  154. package/dist/t-DQpXaDhP.d.ts +30 -0
  155. package/dist/t.client.d.ts +11 -0
  156. package/dist/t.client.js +35 -0
  157. package/dist/t.server.d.ts +22 -0
  158. package/dist/t.server.js +12 -0
  159. package/dist/types-8uKCwFda.d.ts +24 -0
  160. package/dist/types-BC6K-pID.d.ts +110 -0
  161. package/dist/types-BUV7BZML.d.ts +55 -0
  162. package/dist/types-BWGB322F.d.ts +46 -0
  163. package/dist/types-B_-RxW4A.d.ts +7 -0
  164. package/dist/types-BaaSiz9n.d.ts +7 -0
  165. package/dist/types-Bgl2V5xo.d.ts +102 -0
  166. package/dist/types-BwhB6IT4.d.ts +111 -0
  167. package/dist/types-Bzp6KxsI.d.ts +50 -0
  168. package/dist/types-C6rOJFY2.d.ts +24 -0
  169. package/dist/types-C7lD-Woj.d.ts +50 -0
  170. package/dist/types-CLWZeQhD.d.ts +54 -0
  171. package/dist/types-COXt613w.d.ts +19 -0
  172. package/dist/types-CUlkbl9-.d.ts +50 -0
  173. package/dist/types-CVv7GGPj.d.ts +46 -0
  174. package/dist/types-CiIDN2sW.d.ts +64 -0
  175. package/dist/types-Cr86qjYl.d.ts +58 -0
  176. package/dist/types-CxnObiEO.d.ts +7 -0
  177. package/dist/types-D44mFUjA.d.ts +97 -0
  178. package/dist/types-D85m9UTC.d.ts +137 -0
  179. package/dist/types-DWhsmSPP.d.ts +102 -0
  180. package/dist/types-DdprXGJ1.d.ts +46 -0
  181. package/dist/types-DhVJ2ZIo.d.ts +43 -0
  182. package/dist/types-DnbOuM1L.d.ts +7 -0
  183. package/dist/types-DsNK_9sI.d.ts +24 -0
  184. package/dist/types-DvWBr9Xp.d.ts +17 -0
  185. package/dist/types-DwkIalh6.d.ts +24 -0
  186. package/dist/types-OYRgjtE_.d.ts +110 -0
  187. package/dist/types-SV8S05GX.d.ts +7 -0
  188. package/dist/types-TYn9WnxY.d.ts +46 -0
  189. package/dist/types-aXCBbdhq.d.ts +55 -0
  190. package/dist/types-gn4aNKGx.d.ts +54 -0
  191. package/dist/types-nippsMkm.d.ts +54 -0
  192. package/package.json +105 -0
  193. package/src/components/I18nScript.astro +37 -0
  194. package/src/components/index.ts +2 -0
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # @astroscope/i18n
2
+
3
+ i18n for Astro + React islands — automatic tree-shaking, parallel loading, any translation source.
4
+
5
+ ## Why this library?
6
+
7
+ **SSR-first** — The only i18n solution built specifically for SSR + islands architecture. Works seamlessly with Astro's partial hydration model where most i18n libraries fail.
8
+
9
+ **Automatic tree-shaking** — Only translations actually used by each component are delivered to the browser. No manual chunk splitting, no configuration. It just works.
10
+
11
+ **Parallel loading** — Translations load alongside component hydration via custom `client:*-x` directives. No waiting for translations before rendering.
12
+
13
+ **Unified API** — Same `t()` function works identically in Astro templates and React islands.
14
+
15
+ **Any translation source** — Fetch translations from any provider: JSON files, database, headless CMS, TMS, or custom API. All of them will be optimized and chunked automatically.
16
+
17
+ **Production optimized** — Fallback strings are stripped from production bundles via Babel, reducing bundle size while keeping fallbacks available via the manifest.
18
+
19
+ ## Features
20
+
21
+ - **Per-chunk translation loading** — each island gets only its translations
22
+ - **ICU MessageFormat** support via `@messageformat/core`
23
+ - **Babel-based extraction** — robust AST parsing, source maps, production stripping
24
+ - **Manifest fallbacks** — missing translations automatically use extracted fallbacks
25
+ - **Full TypeScript support**
26
+ - **Tiny client runtime** — no heavy i18n framework
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install @astroscope/i18n @astroscope/boot
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### 1. Add the integration
37
+
38
+ ```ts
39
+ // astro.config.ts
40
+ import { defineConfig } from 'astro/config';
41
+ import boot from '@astroscope/boot';
42
+ import i18n from '@astroscope/i18n';
43
+
44
+ export default defineConfig({
45
+ integrations: [
46
+ boot(),
47
+ i18n(),
48
+ ],
49
+ });
50
+ ```
51
+
52
+ ### 2. Configure i18n in your boot file
53
+
54
+ ```ts
55
+ // src/boot.ts
56
+ import { i18n, type RawTranslations } from '@astroscope/i18n';
57
+
58
+ async function fetchTranslations(locale: string): Promise<RawTranslations> {
59
+ // fetch from your CMS, API, or local files
60
+ const response = await fetch(`https://api.example.com/translations/${locale}`);
61
+ return response.json();
62
+ }
63
+
64
+ export async function onStartup() {
65
+ await i18n.configure({
66
+ locales: ['en', 'de'],
67
+ defaultLocale: 'en', // optional, defaults to first locale
68
+ });
69
+
70
+ // load translations for all locales
71
+ const [en, de] = await Promise.all([
72
+ fetchTranslations('en'),
73
+ fetchTranslations('de'),
74
+ ]);
75
+
76
+ i18n.setTranslations('en', en);
77
+ i18n.setTranslations('de', de);
78
+ }
79
+ ```
80
+
81
+ ### 3. Add the middleware
82
+
83
+ ```ts
84
+ // src/middleware.ts
85
+ import { sequence } from 'astro:middleware';
86
+ import { createI18nChunkMiddleware, createI18nMiddleware, i18n } from '@astroscope/i18n';
87
+
88
+ export const onRequest = sequence(
89
+ createI18nChunkMiddleware(), // serves /_i18n/ translation chunks
90
+ createI18nMiddleware({
91
+ locale: (ctx) =>
92
+ ctx.cookies.get('locale')?.value ??
93
+ i18n.getConfig().defaultLocale,
94
+ }),
95
+ );
96
+ ```
97
+
98
+ ### 4. Add `<I18nScript />` to your layout
99
+
100
+ Inject translations into the page for hydrated components:
101
+
102
+ ```astro
103
+ ---
104
+ import { I18nScript } from '@astroscope/i18n/components';
105
+ ---
106
+ <html>
107
+ <head>
108
+ <I18nScript />
109
+ </head>
110
+ <body>
111
+ <slot />
112
+ </body>
113
+ </html>
114
+ ```
115
+
116
+ ### 5. Use `t()` in your components
117
+
118
+ ```astro
119
+ ---
120
+ // In .astro files
121
+ import { t } from '@astroscope/i18n/t';
122
+ ---
123
+ <h1>{t('checkout.title', 'Order Summary')}</h1>
124
+ ```
125
+
126
+ ```tsx
127
+ // In React components
128
+ import { t } from '@astroscope/i18n/t';
129
+
130
+ export function CheckoutSummary() {
131
+ return (
132
+ <div>
133
+ <h1>{t('checkout.title', 'Order Summary')}</h1>
134
+ <p>{t('checkout.tax', 'Includes {tax} VAT', { tax: '19%' })}</p>
135
+ </div>
136
+ );
137
+ }
138
+ ```
139
+
140
+ ### 6. Use i18n-aware client directives
141
+
142
+ **The problem:** With standard `client:*` directives, the translation chunk loads *after* the component module. This delays hydration while translations are fetched sequentially.
143
+
144
+ **The solution:** Use `client:*-x` directives to load translations in parallel with the component code:
145
+
146
+ ```astro
147
+ ---
148
+ import Cart from '../components/Cart';
149
+ ---
150
+ <!-- translations load alongside component code -->
151
+ <Cart client:load-x />
152
+ <Cart client:visible-x />
153
+ <Cart client:idle-x />
154
+ ```
155
+
156
+ ## API
157
+
158
+ ### `t(key, fallback, values?)`
159
+
160
+ Translate a key with optional interpolation values.
161
+
162
+ ```ts
163
+ // simple
164
+ t('checkout.title', 'Order Summary')
165
+
166
+ // with variables
167
+ t('checkout.tax', 'Includes {tax} VAT', { tax: '19%' })
168
+
169
+ // with pluralization (ICU MessageFormat)
170
+ t('cart.items', '{count, plural, one {# item} other {# items}}', { count: 5 })
171
+
172
+ // with metadata object (for extraction tooling)
173
+ t('cart.total', {
174
+ fallback: 'Total: {amount}',
175
+ description: 'Cart total price',
176
+ variables: {
177
+ amount: { fallback: '$0.00', description: 'Formatted price' }
178
+ }
179
+ }, { amount: '$49.99' })
180
+ ```
181
+
182
+ ### `i18n` singleton
183
+
184
+ ```ts
185
+ import { i18n } from '@astroscope/i18n';
186
+
187
+ // configure (call once at startup)
188
+ await i18n.configure({
189
+ locales: ['en', 'de'],
190
+ defaultLocale: 'en',
191
+ });
192
+
193
+ // set translations for a locale
194
+ i18n.setTranslations('en', { 'key': 'value' });
195
+
196
+ // get raw translations (includes manifest fallbacks)
197
+ i18n.getTranslations('en');
198
+
199
+ // get compiled translations (ICU MessageFormat functions)
200
+ i18n.getCompiledTranslations('en');
201
+
202
+ // get extraction manifest
203
+ // has all extracted keys with their metadata
204
+ // you can use this to generate translation files or upload to a TMS / CMS
205
+ i18n.getManifest();
206
+
207
+ // clear cached translations
208
+ i18n.clear(); // all locales
209
+ i18n.clear('en'); // specific locale
210
+ ```
211
+
212
+ ### Lazy loading with React.lazy()
213
+
214
+ Translations load automatically for lazy-loaded components.
215
+
216
+ ```tsx
217
+ import { Suspense, lazy } from 'react';
218
+
219
+ const StatsModal = lazy(() => import('./StatsModal'));
220
+
221
+ export function App() {
222
+ return (
223
+ <Suspense fallback={<div>Loading...</div>}>
224
+ <StatsModal />
225
+ </Suspense>
226
+ );
227
+ }
228
+ ```
229
+
230
+ ## How it works
231
+
232
+ 1. **Build time** — Babel plugin extracts all `t()` calls, maps them to chunks, strips fallbacks from production bundles
233
+ 2. **Manifest** — Extracted keys with fallbacks are written to `i18n-manifest.json`
234
+ 3. **SSR** — Middleware provides translations to `t()`, merging manifest fallbacks for missing keys
235
+ 4. **Client** — Custom directives load only the translations needed by each chunk
236
+
237
+ The same `import { t } from '@astroscope/i18n/t'` works everywhere — bundler picks the correct implementation via conditional exports (`browser` vs `default`).
238
+
239
+ ### Client bundle
240
+
241
+ Translation chunks are served as raw ICU MessageFormat strings and compiled on the browser on first use. This keeps chunk sizes minimal but includes `@messageformat/core` (~15KB gzipped) in your client bundle. Compiled messages are cached for subsequent renders.
242
+
243
+ ## Configuration
244
+
245
+ ### i18n.configure()
246
+
247
+ | Option | Type | Default | Description |
248
+ |--------|------|---------|-------------|
249
+ | `locales` | `string[]` | required | Supported locales |
250
+ | `defaultLocale` | `string` | first locale | Default/fallback locale |
251
+ | `fallback` | `FallbackBehavior` | `'fallback'` | Behavior when translation missing |
252
+
253
+ ### FallbackBehavior
254
+
255
+ - `'fallback'` — Use the fallback string from manifest (default)
256
+ - `'key'` — Return the translation key
257
+ - `'throw'` — Throw an error
258
+ - `(key, meta) => string` — Custom function
259
+
260
+ ## License
261
+
262
+ MIT
@@ -0,0 +1,121 @@
1
+ // src/extraction/babel-plugin.ts
2
+ function getStringValue(t, node) {
3
+ if (t.isStringLiteral(node)) {
4
+ return node.value;
5
+ }
6
+ if (t.isTemplateLiteral(node) && node.quasis.length === 1 && node.expressions.length === 0) {
7
+ return node.quasis[0]?.value.cooked ?? null;
8
+ }
9
+ return null;
10
+ }
11
+ function extractMeta(t, node) {
12
+ const stringValue = getStringValue(t, node);
13
+ if (stringValue !== null) {
14
+ return { fallback: stringValue };
15
+ }
16
+ if (t.isObjectExpression(node)) {
17
+ return extractMetaFromObject(t, node);
18
+ }
19
+ return { fallback: "" };
20
+ }
21
+ function extractMetaFromObject(t, obj) {
22
+ const meta = { fallback: "" };
23
+ for (const prop of obj.properties) {
24
+ if (!t.isObjectProperty(prop)) continue;
25
+ if (!t.isIdentifier(prop.key)) continue;
26
+ const name = prop.key.name;
27
+ if (name === "fallback") {
28
+ const value = getStringValue(t, prop.value);
29
+ if (value !== null) {
30
+ meta.fallback = value;
31
+ }
32
+ } else if (name === "description") {
33
+ const value = getStringValue(t, prop.value);
34
+ if (value !== null) {
35
+ meta.description = value;
36
+ }
37
+ } else if (name === "variables" && t.isObjectExpression(prop.value)) {
38
+ meta.variables = extractVariables(t, prop.value);
39
+ }
40
+ }
41
+ return meta;
42
+ }
43
+ function extractVariables(t, obj) {
44
+ const result = {};
45
+ for (const prop of obj.properties) {
46
+ if (!t.isObjectProperty(prop)) continue;
47
+ if (!t.isIdentifier(prop.key)) continue;
48
+ const varName = prop.key.name;
49
+ if (t.isObjectExpression(prop.value)) {
50
+ const varDef = {};
51
+ for (const varProp of prop.value.properties) {
52
+ if (!t.isObjectProperty(varProp)) continue;
53
+ if (!t.isIdentifier(varProp.key)) continue;
54
+ const varPropName = varProp.key.name;
55
+ const value = getStringValue(t, varProp.value);
56
+ if (value !== null) {
57
+ if (varPropName === "fallback") {
58
+ varDef.fallback = value;
59
+ } else if (varPropName === "description") {
60
+ varDef.description = value;
61
+ }
62
+ }
63
+ }
64
+ result[varName] = varDef;
65
+ }
66
+ }
67
+ return Object.keys(result).length > 0 ? result : void 0;
68
+ }
69
+ function isMetaArgument(t, node) {
70
+ if (t.isStringLiteral(node) || t.isTemplateLiteral(node)) {
71
+ return true;
72
+ }
73
+ if (t.isObjectExpression(node)) {
74
+ for (const prop of node.properties) {
75
+ if (t.isObjectProperty(prop) && t.isIdentifier(prop.key) && prop.key.name === "fallback") {
76
+ return true;
77
+ }
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ function i18nExtractPlugin({ types: t }) {
83
+ return {
84
+ name: "@astroscope/i18n/extract",
85
+ visitor: {
86
+ CallExpression(path, state) {
87
+ if (!path.get("callee").isIdentifier({ name: "t" })) return;
88
+ const args = path.node.arguments;
89
+ if (args.length < 1) return;
90
+ const keyArg = args[0];
91
+ if (!t.isExpression(keyArg)) return;
92
+ const key = getStringValue(t, keyArg);
93
+ if (key === null) return;
94
+ let meta = { fallback: "" };
95
+ if (args.length >= 2 && t.isExpression(args[1]) && isMetaArgument(t, args[1])) {
96
+ meta = extractMeta(t, args[1]);
97
+ }
98
+ state.opts.onKeyExtracted({
99
+ key,
100
+ meta,
101
+ file: state.filename ?? "",
102
+ line: path.node.loc?.start.line ?? 0
103
+ });
104
+ if (state.opts.stripFallbacks && args.length >= 2) {
105
+ const secondArg = args[1];
106
+ if (!t.isExpression(secondArg)) return;
107
+ if (isMetaArgument(t, secondArg)) {
108
+ if (args.length === 2) {
109
+ path.node.arguments = [args[0]];
110
+ } else if (args.length >= 3) {
111
+ path.node.arguments = [args[0], args[2]];
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ };
118
+ }
119
+ export {
120
+ i18nExtractPlugin as default
121
+ };
@@ -0,0 +1,118 @@
1
+ // src/extraction/babel-plugin.ts
2
+ function getStringValue(t, node) {
3
+ if (t.isStringLiteral(node)) {
4
+ return node.value;
5
+ }
6
+ if (t.isTemplateLiteral(node) && node.quasis.length === 1 && node.expressions.length === 0) {
7
+ return node.quasis[0]?.value.cooked ?? null;
8
+ }
9
+ return null;
10
+ }
11
+ function extractMeta(t, node, warn) {
12
+ const stringValue = getStringValue(t, node);
13
+ if (stringValue !== null) {
14
+ return { fallback: stringValue };
15
+ }
16
+ if (t.isTemplateLiteral(node) && node.expressions.length > 0) {
17
+ warn("t() meta contains template literal with expressions - cannot extract statically");
18
+ return { fallback: "" };
19
+ }
20
+ if (t.isObjectExpression(node)) {
21
+ const meta = { fallback: "" };
22
+ for (const prop of node.properties) {
23
+ if (t.isSpreadElement(prop)) {
24
+ warn("t() meta contains spread element - cannot extract statically");
25
+ continue;
26
+ }
27
+ if (!t.isObjectProperty(prop)) continue;
28
+ if (!t.isIdentifier(prop.key)) continue;
29
+ const name = prop.key.name;
30
+ if (name === "fallback") {
31
+ const value = getStringValue(t, prop.value);
32
+ if (value !== null) {
33
+ meta.fallback = value;
34
+ } else {
35
+ warn("t() fallback is not a static string - cannot extract");
36
+ }
37
+ } else if (name === "description") {
38
+ const value = getStringValue(t, prop.value);
39
+ if (value !== null) {
40
+ meta.description = value;
41
+ } else {
42
+ warn("t() description is not a static string - cannot extract");
43
+ }
44
+ } else if (name === "variables" && t.isObjectExpression(prop.value)) {
45
+ meta.variables = extractVariables(t, prop.value, warn);
46
+ }
47
+ }
48
+ return meta;
49
+ }
50
+ warn("t() meta is not a static string or object - cannot extract");
51
+ return { fallback: "" };
52
+ }
53
+ function extractVariables(t, obj, warn) {
54
+ const result = {};
55
+ for (const prop of obj.properties) {
56
+ if (t.isSpreadElement(prop)) {
57
+ warn("t() variables contains spread element - cannot extract statically");
58
+ continue;
59
+ }
60
+ if (!t.isObjectProperty(prop)) continue;
61
+ if (!t.isIdentifier(prop.key)) continue;
62
+ const varName = prop.key.name;
63
+ if (t.isObjectExpression(prop.value)) {
64
+ const varDef = {};
65
+ for (const varProp of prop.value.properties) {
66
+ if (t.isSpreadElement(varProp)) {
67
+ warn(`t() variable "${varName}" contains spread element - cannot extract statically`);
68
+ continue;
69
+ }
70
+ if (!t.isObjectProperty(varProp)) continue;
71
+ if (!t.isIdentifier(varProp.key)) continue;
72
+ const varPropName = varProp.key.name;
73
+ const value = getStringValue(t, varProp.value);
74
+ if (value !== null) {
75
+ if (varPropName === "fallback") {
76
+ varDef.fallback = value;
77
+ } else if (varPropName === "description") {
78
+ varDef.description = value;
79
+ }
80
+ } else if (varPropName === "fallback" || varPropName === "description") {
81
+ warn(`t() variable "${varName}.${varPropName}" is not a static string - cannot extract`);
82
+ }
83
+ }
84
+ result[varName] = varDef;
85
+ }
86
+ }
87
+ return Object.keys(result).length > 0 ? result : void 0;
88
+ }
89
+ function i18nExtractPlugin({ types: t }) {
90
+ return {
91
+ name: "@astroscope/i18n/extract",
92
+ visitor: {
93
+ CallExpression(path, state) {
94
+ if (!path.get("callee").isIdentifier({ name: "t" })) return;
95
+ const args = path.node.arguments;
96
+ const keyArg = args[0];
97
+ if (!t.isExpression(keyArg)) return;
98
+ const key = getStringValue(t, keyArg);
99
+ if (key === null) return;
100
+ const file = state.filename ?? "";
101
+ const line = path.node.loc?.start.line ?? 0;
102
+ const warn = (message) => state.opts.logger.warn(`${message} at ${file}:${line}`);
103
+ const meta = args.length >= 2 && t.isExpression(args[1]) ? extractMeta(t, args[1], warn) : { fallback: "" };
104
+ state.opts.onKeyExtracted({ key, meta, file, line });
105
+ if (state.opts.stripFallbacks && args.length >= 2) {
106
+ if (args.length === 2) {
107
+ path.node.arguments = [args[0]];
108
+ } else {
109
+ path.node.arguments = [args[0], t.identifier("undefined"), args[2]];
110
+ }
111
+ }
112
+ }
113
+ }
114
+ };
115
+ }
116
+ export {
117
+ i18nExtractPlugin
118
+ };
@@ -0,0 +1,159 @@
1
+ import {
2
+ compileTranslations
3
+ } from "./chunk-KNDYKTHE.js";
4
+
5
+ // src/server/i18n.ts
6
+ import fs from "fs";
7
+
8
+ // src/extraction/hash.ts
9
+ import { createHash } from "crypto";
10
+ function hashString(str) {
11
+ return createHash("sha256").update(str).digest("hex").substring(0, 8);
12
+ }
13
+ function computeChunkHash(translations, keys) {
14
+ const relevantTranslations = {};
15
+ for (const key of keys.sort()) {
16
+ if (translations[key]) {
17
+ relevantTranslations[key] = translations[key];
18
+ }
19
+ }
20
+ return hashString(JSON.stringify(relevantTranslations));
21
+ }
22
+ function computeAllChunkHashes(translations, manifest) {
23
+ const hashes = {};
24
+ for (const [chunkId, keys] of Object.entries(manifest)) {
25
+ hashes[chunkId] = computeChunkHash(translations, keys);
26
+ }
27
+ return hashes;
28
+ }
29
+
30
+ // src/server/i18n.ts
31
+ function loadExtractionManifest() {
32
+ const candidates = [
33
+ "./i18n-manifest.json",
34
+ // same directory
35
+ "../i18n-manifest.json"
36
+ // parent (when code is in chunks/)
37
+ ];
38
+ for (const candidate of candidates) {
39
+ try {
40
+ const manifestPath = new URL(candidate, import.meta.url);
41
+ const content = fs.readFileSync(manifestPath, "utf-8");
42
+ return JSON.parse(content);
43
+ } catch {
44
+ }
45
+ }
46
+ return void 0;
47
+ }
48
+ var I18nSingleton = class {
49
+ config = null;
50
+ rawCache = /* @__PURE__ */ new Map();
51
+ compiledCache = /* @__PURE__ */ new Map();
52
+ extractionManifest = void 0;
53
+ // undefined = not loaded yet
54
+ hashCache = /* @__PURE__ */ new Map();
55
+ configure(config) {
56
+ if (!config.locales?.length) {
57
+ throw new Error("i18n.configure(): locales array is required and must not be empty");
58
+ }
59
+ for (const locale of config.locales) {
60
+ if (!locale) {
61
+ throw new Error("i18n.configure(): locale cannot be empty");
62
+ }
63
+ if (locale !== locale.trim()) {
64
+ throw new Error(`i18n.configure(): locale "${locale}" has leading or trailing whitespace`);
65
+ }
66
+ }
67
+ if (new Set(config.locales).size !== config.locales.length) {
68
+ throw new Error("i18n.configure(): locales array contains duplicates");
69
+ }
70
+ if (config.defaultLocale !== void 0) {
71
+ if (!config.defaultLocale) {
72
+ throw new Error("i18n.configure(): defaultLocale cannot be empty");
73
+ }
74
+ if (config.defaultLocale !== config.defaultLocale.trim()) {
75
+ throw new Error(`i18n.configure(): defaultLocale "${config.defaultLocale}" has leading or trailing whitespace`);
76
+ }
77
+ if (!config.locales.includes(config.defaultLocale)) {
78
+ throw new Error(`i18n.configure(): defaultLocale "${config.defaultLocale}" is not in locales array`);
79
+ }
80
+ }
81
+ this.config = config;
82
+ }
83
+ isConfigured() {
84
+ return this.config !== null;
85
+ }
86
+ getConfig() {
87
+ if (!this.config) {
88
+ throw new Error("i18n not configured. Call i18n.configure() first.");
89
+ }
90
+ return this.config;
91
+ }
92
+ getDefaultLocale() {
93
+ const config = this.getConfig();
94
+ return config.defaultLocale ?? config.locales[0];
95
+ }
96
+ getFallbackBehavior() {
97
+ return this.getConfig().fallback ?? "fallback";
98
+ }
99
+ setTranslations(locale, raw) {
100
+ this.rawCache.set(locale, raw);
101
+ this.compiledCache.set(locale, compileTranslations(locale, raw));
102
+ this.hashCache.delete(locale);
103
+ const chunks = this.getChunkManifest();
104
+ if (chunks) {
105
+ this.hashCache.set(locale, computeAllChunkHashes(raw, chunks));
106
+ }
107
+ }
108
+ getTranslations(locale) {
109
+ return this.rawCache.get(locale) ?? {};
110
+ }
111
+ getCompiledTranslations(locale) {
112
+ return this.compiledCache.get(locale) ?? {};
113
+ }
114
+ getChunkManifest() {
115
+ return this.getExtractionManifest()?.chunks ?? {};
116
+ }
117
+ /**
118
+ * Get extracted translation keys with metadata.
119
+ * Use this to populate your CMS with keys extracted from the codebase.
120
+ */
121
+ getExtractedKeys() {
122
+ return this.getExtractionManifest()?.keys ?? null;
123
+ }
124
+ /**
125
+ * Get the full extraction manifest (keys + chunks).
126
+ * Returns null in dev mode (manifest doesn't exist).
127
+ */
128
+ getExtractionManifest() {
129
+ if (this.extractionManifest === void 0) {
130
+ this.extractionManifest = loadExtractionManifest() ?? null;
131
+ }
132
+ return this.extractionManifest;
133
+ }
134
+ getHashes(locale) {
135
+ return this.hashCache.get(locale) ?? {};
136
+ }
137
+ /**
138
+ * Clear translations for specified locale(s).
139
+ * @param locales - Locale(s) to clear, or all if not specified
140
+ */
141
+ clear(locales) {
142
+ const targets = this.resolveLocales(locales);
143
+ for (const locale of targets) {
144
+ this.rawCache.delete(locale);
145
+ this.compiledCache.delete(locale);
146
+ this.hashCache.delete(locale);
147
+ }
148
+ }
149
+ resolveLocales(locales) {
150
+ if (!locales) return this.config?.locales ?? [];
151
+ if (typeof locales === "string") return [locales];
152
+ return locales;
153
+ }
154
+ };
155
+ var i18n = new I18nSingleton();
156
+
157
+ export {
158
+ i18n
159
+ };