@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.
- package/README.md +262 -0
- package/dist/babel-plugin-FUTQRKLT.js +121 -0
- package/dist/babel-plugin-K2K4ZPM5.js +118 -0
- package/dist/chunk-22ZZFFC2.js +159 -0
- package/dist/chunk-2PSXD3JJ.js +144 -0
- package/dist/chunk-3VI62RLT.js +176 -0
- package/dist/chunk-4A5BKHFN.js +16 -0
- package/dist/chunk-4DAQPWXI.js +36 -0
- package/dist/chunk-4OGX6AF5.js +10 -0
- package/dist/chunk-4PHYKXO3.js +227 -0
- package/dist/chunk-4UBFISFY.js +376 -0
- package/dist/chunk-5DERF6DV.js +266 -0
- package/dist/chunk-5KGNHFTC.js +344 -0
- package/dist/chunk-5PP4D2VK.js +23 -0
- package/dist/chunk-6BW3PN23.js +14 -0
- package/dist/chunk-6ESQ4JQI.js +263 -0
- package/dist/chunk-A7MPXMOP.js +202 -0
- package/dist/chunk-AWZ2SQDW.js +149 -0
- package/dist/chunk-AYY2YIBQ.js +52 -0
- package/dist/chunk-BMNEJFTD.js +56 -0
- package/dist/chunk-BSXQGEUK.js +16 -0
- package/dist/chunk-BSYTJFGF.js +21 -0
- package/dist/chunk-BVPXEGTT.js +12 -0
- package/dist/chunk-C4DJYWF6.js +193 -0
- package/dist/chunk-C5EJYE2P.js +245 -0
- package/dist/chunk-CLNOQHMS.js +254 -0
- package/dist/chunk-E5MCIRSU.js +144 -0
- package/dist/chunk-E7ZJS7WX.js +162 -0
- package/dist/chunk-EMM6LRO2.js +8 -0
- package/dist/chunk-ESFGCYYH.js +24 -0
- package/dist/chunk-FAKBSLNW.js +24 -0
- package/dist/chunk-FHBVI3TL.js +372 -0
- package/dist/chunk-FI5KZ5IO.js +17 -0
- package/dist/chunk-H5R6YMOL.js +233 -0
- package/dist/chunk-H62K3FGA.js +236 -0
- package/dist/chunk-HDG2R6XT.js +211 -0
- package/dist/chunk-HGXU5QAK.js +344 -0
- package/dist/chunk-HHRMR3TM.js +17 -0
- package/dist/chunk-HS4HVBAD.js +127 -0
- package/dist/chunk-IPJWKKHJ.js +324 -0
- package/dist/chunk-ITTL4V7J.js +24 -0
- package/dist/chunk-IWUBCMOK.js +128 -0
- package/dist/chunk-JJBBY2GR.js +14 -0
- package/dist/chunk-JJEDOJBR.js +193 -0
- package/dist/chunk-JTUKXNIJ.js +23 -0
- package/dist/chunk-K7YULG4A.js +14 -0
- package/dist/chunk-KNDYKTHE.js +31 -0
- package/dist/chunk-KNONI6DP.js +89 -0
- package/dist/chunk-LMTEO22Y.js +17 -0
- package/dist/chunk-LRLZEX4D.js +147 -0
- package/dist/chunk-MFKPLUAN.js +241 -0
- package/dist/chunk-MIWXQOO3.js +316 -0
- package/dist/chunk-MJRTAWPR.js +261 -0
- package/dist/chunk-MZEPQCXX.js +249 -0
- package/dist/chunk-NHHTSFA7.js +237 -0
- package/dist/chunk-NTTXM7AG.js +23 -0
- package/dist/chunk-NZFAVJXR.js +192 -0
- package/dist/chunk-OFNFSDW3.js +13 -0
- package/dist/chunk-PG3XDNGF.js +227 -0
- package/dist/chunk-PVEVTPNK.js +139 -0
- package/dist/chunk-QAO4BXS4.js +244 -0
- package/dist/chunk-QCTULQQK.js +34 -0
- package/dist/chunk-QFAAGCP5.js +177 -0
- package/dist/chunk-QKND2KCA.js +163 -0
- package/dist/chunk-QZEGY6RJ.js +151 -0
- package/dist/chunk-RAE2L6FT.js +193 -0
- package/dist/chunk-S3ABJBU3.js +255 -0
- package/dist/chunk-SVDVCPCV.js +236 -0
- package/dist/chunk-T35ZUWTL.js +153 -0
- package/dist/chunk-T6WV5DW6.js +156 -0
- package/dist/chunk-TGUXVDA3.js +82 -0
- package/dist/chunk-TYMO7K3A.js +173 -0
- package/dist/chunk-V4RBFLQL.js +23 -0
- package/dist/chunk-V4XWJXXZ.js +23 -0
- package/dist/chunk-VQCY3OKK.js +258 -0
- package/dist/chunk-WLGOJPDJ.js +15 -0
- package/dist/chunk-WUEPDJIL.js +259 -0
- package/dist/chunk-X3L7WZLY.js +254 -0
- package/dist/chunk-XDD62ZM3.js +255 -0
- package/dist/chunk-XFXXEDRD.js +36 -0
- package/dist/chunk-XKYOR5UK.js +8 -0
- package/dist/chunk-Y26535SI.js +239 -0
- package/dist/chunk-Y3PQX5TX.js +152 -0
- package/dist/chunk-Y4BE2V6F.js +347 -0
- package/dist/chunk-Z4IZYDIA.js +24 -0
- package/dist/chunk-Z52G4CYY.js +14 -0
- package/dist/chunk-ZGLUYWVM.js +128 -0
- package/dist/client/directives/idle.d.ts +9 -0
- package/dist/client/directives/idle.js +15 -0
- package/dist/client/directives/load.d.ts +9 -0
- package/dist/client/directives/load.js +15 -0
- package/dist/client/directives/media.d.ts +9 -0
- package/dist/client/directives/media.js +15 -0
- package/dist/client/directives/only.d.ts +9 -0
- package/dist/client/directives/only.js +15 -0
- package/dist/client/directives/utils.d.ts +10 -0
- package/dist/client/directives/utils.js +7 -0
- package/dist/client/directives/visible.d.ts +9 -0
- package/dist/client/directives/visible.js +15 -0
- package/dist/client/import-with-i18n.d.ts +16 -0
- package/dist/client/import-with-i18n.js +8 -0
- package/dist/client/t.d.ts +20 -0
- package/dist/client/t.js +37 -0
- package/dist/client-directives/idle.d.ts +9 -0
- package/dist/client-directives/idle.js +15 -0
- package/dist/client-directives/load.d.ts +9 -0
- package/dist/client-directives/load.js +15 -0
- package/dist/client-directives/media.d.ts +9 -0
- package/dist/client-directives/media.js +15 -0
- package/dist/client-directives/only.d.ts +9 -0
- package/dist/client-directives/only.js +15 -0
- package/dist/client-directives/utils.d.ts +9 -0
- package/dist/client-directives/utils.js +7 -0
- package/dist/client-directives/visible.d.ts +9 -0
- package/dist/client-directives/visible.js +15 -0
- package/dist/extraction/index.d.ts +34 -0
- package/dist/extraction/index.js +9 -0
- package/dist/i18n-3cZHoHwP.d.ts +86 -0
- package/dist/i18n-BGfrSNM8.d.ts +56 -0
- package/dist/i18n-BHU81LMA.d.ts +55 -0
- package/dist/i18n-BagoeH4_.d.ts +62 -0
- package/dist/i18n-BeL84090.d.ts +77 -0
- package/dist/i18n-BeRWDfHi.d.ts +55 -0
- package/dist/i18n-Bfs2PVcf.d.ts +79 -0
- package/dist/i18n-BrL7p3aN.d.ts +99 -0
- package/dist/i18n-CNQNCFU0.d.ts +55 -0
- package/dist/i18n-CX_uU8V2.d.ts +80 -0
- package/dist/i18n-Cr1QZCwe.d.ts +64 -0
- package/dist/i18n-CzTRxGeX.d.ts +80 -0
- package/dist/i18n-DUOFiC7g.d.ts +77 -0
- package/dist/i18n-DcF47BHS.d.ts +77 -0
- package/dist/i18n-DifWvtaO.d.ts +60 -0
- package/dist/i18n-DumSqX1t.d.ts +55 -0
- package/dist/i18n-fWTJw12h.d.ts +80 -0
- package/dist/import-with-i18n.d.ts +23 -0
- package/dist/import-with-i18n.js +7 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +151 -0
- package/dist/init.d.ts +2 -0
- package/dist/init.js +8 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.js +9 -0
- package/dist/middleware-entrypoint-C0Drh9E_.d.ts +29 -0
- package/dist/middleware-entrypoint-mEl66IUy.d.ts +29 -0
- package/dist/middleware-entrypoint.d.ts +5 -0
- package/dist/middleware-entrypoint.js +30 -0
- package/dist/server/middleware-entrypoint.d.ts +2 -0
- package/dist/server/middleware-entrypoint.js +28 -0
- package/dist/server/t.d.ts +17 -0
- package/dist/server/t.js +59 -0
- package/dist/t-BY_C9lZ1.d.ts +30 -0
- package/dist/t-CG3yDiEa.d.ts +30 -0
- package/dist/t-CHsBfrPr.d.ts +30 -0
- package/dist/t-DQpXaDhP.d.ts +30 -0
- package/dist/t.client.d.ts +11 -0
- package/dist/t.client.js +35 -0
- package/dist/t.server.d.ts +22 -0
- package/dist/t.server.js +12 -0
- package/dist/types-8uKCwFda.d.ts +24 -0
- package/dist/types-BC6K-pID.d.ts +110 -0
- package/dist/types-BUV7BZML.d.ts +55 -0
- package/dist/types-BWGB322F.d.ts +46 -0
- package/dist/types-B_-RxW4A.d.ts +7 -0
- package/dist/types-BaaSiz9n.d.ts +7 -0
- package/dist/types-Bgl2V5xo.d.ts +102 -0
- package/dist/types-BwhB6IT4.d.ts +111 -0
- package/dist/types-Bzp6KxsI.d.ts +50 -0
- package/dist/types-C6rOJFY2.d.ts +24 -0
- package/dist/types-C7lD-Woj.d.ts +50 -0
- package/dist/types-CLWZeQhD.d.ts +54 -0
- package/dist/types-COXt613w.d.ts +19 -0
- package/dist/types-CUlkbl9-.d.ts +50 -0
- package/dist/types-CVv7GGPj.d.ts +46 -0
- package/dist/types-CiIDN2sW.d.ts +64 -0
- package/dist/types-Cr86qjYl.d.ts +58 -0
- package/dist/types-CxnObiEO.d.ts +7 -0
- package/dist/types-D44mFUjA.d.ts +97 -0
- package/dist/types-D85m9UTC.d.ts +137 -0
- package/dist/types-DWhsmSPP.d.ts +102 -0
- package/dist/types-DdprXGJ1.d.ts +46 -0
- package/dist/types-DhVJ2ZIo.d.ts +43 -0
- package/dist/types-DnbOuM1L.d.ts +7 -0
- package/dist/types-DsNK_9sI.d.ts +24 -0
- package/dist/types-DvWBr9Xp.d.ts +17 -0
- package/dist/types-DwkIalh6.d.ts +24 -0
- package/dist/types-OYRgjtE_.d.ts +110 -0
- package/dist/types-SV8S05GX.d.ts +7 -0
- package/dist/types-TYn9WnxY.d.ts +46 -0
- package/dist/types-aXCBbdhq.d.ts +55 -0
- package/dist/types-gn4aNKGx.d.ts +54 -0
- package/dist/types-nippsMkm.d.ts +54 -0
- package/package.json +105 -0
- package/src/components/I18nScript.astro +37 -0
- 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
|
+
};
|