@astroscope/i18n 0.2.4 → 0.3.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 +144 -16
- package/dist/chunk-33JVAFGQ.js +44 -0
- package/dist/{chunk-3VF2YMJG.js → chunk-EXMM4JID.js} +2 -2
- package/dist/{chunk-ZKXMIGSF.js → chunk-MSJHXRJB.js} +1 -1
- package/dist/chunk-VGPVG5JP.js +73 -0
- package/dist/client/translate.d.ts +41 -0
- package/dist/client/{t.js → translate.js} +14 -5
- package/dist/extraction/index.js +1 -1
- package/dist/{i18n-CbSjmzlu.d.ts → i18n-DaLnNvss.d.ts} +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -3
- package/dist/internal.d.ts +2 -2
- package/dist/internal.js +2 -2
- package/dist/rich-CQUhdwBc.d.ts +15 -0
- package/dist/server/translate.d.ts +38 -0
- package/dist/server/{t.js → translate.js} +17 -6
- package/package.json +13 -11
- package/dist/chunk-EMM6LRO2.js +0 -8
- package/dist/chunk-KNDYKTHE.js +0 -31
- package/dist/client/t.d.ts +0 -20
- package/dist/server/t.d.ts +0 -17
- /package/src/{components → astro}/I18nScript.astro +0 -0
- /package/src/{components → astro}/index.ts +0 -0
package/README.md
CHANGED
|
@@ -21,11 +21,12 @@ i18n for Astro + React islands — automatic tree-shaking, parallel loading, any
|
|
|
21
21
|
## Features
|
|
22
22
|
|
|
23
23
|
- **Per-chunk translation loading** — each island gets only its translations
|
|
24
|
-
- **
|
|
24
|
+
- **Unicode MessageFormat 2** (MF2) support via `messageformat` v4
|
|
25
|
+
- **Built-in formatters** — `:number`, `:integer`, `:percent`, `:currency`, `:date`, `:time`, `:datetime`, `:unit`
|
|
25
26
|
- **Babel-based extraction** — robust AST parsing, source maps, production stripping
|
|
26
27
|
- **Manifest fallbacks** — missing translations automatically use extracted fallbacks
|
|
27
28
|
- **Full TypeScript support**
|
|
28
|
-
- **Tiny client runtime** —
|
|
29
|
+
- **Tiny client runtime** — ~8KB gzipped for translations
|
|
29
30
|
|
|
30
31
|
## Installation
|
|
31
32
|
|
|
@@ -123,7 +124,7 @@ Inject translations into the page for hydrated components:
|
|
|
123
124
|
|
|
124
125
|
```astro
|
|
125
126
|
---
|
|
126
|
-
import { I18nScript } from '@astroscope/i18n/
|
|
127
|
+
import { I18nScript } from '@astroscope/i18n/astro';
|
|
127
128
|
---
|
|
128
129
|
<html>
|
|
129
130
|
<head>
|
|
@@ -140,25 +141,27 @@ import { I18nScript } from '@astroscope/i18n/components';
|
|
|
140
141
|
```astro
|
|
141
142
|
---
|
|
142
143
|
// In .astro files
|
|
143
|
-
import { t } from '@astroscope/i18n/
|
|
144
|
+
import { t } from '@astroscope/i18n/translate';
|
|
144
145
|
---
|
|
145
146
|
<h1>{t('checkout.title', 'Order Summary')}</h1>
|
|
146
147
|
```
|
|
147
148
|
|
|
148
149
|
```tsx
|
|
149
150
|
// In React components
|
|
150
|
-
import { t } from '@astroscope/i18n/
|
|
151
|
+
import { t } from '@astroscope/i18n/translate';
|
|
151
152
|
|
|
152
153
|
export function CheckoutSummary() {
|
|
153
154
|
return (
|
|
154
155
|
<div>
|
|
155
156
|
<h1>{t('checkout.title', 'Order Summary')}</h1>
|
|
156
|
-
<p>{t('checkout.tax', 'Includes {tax} VAT', { tax: '19%' })}</p>
|
|
157
|
+
<p>{t('checkout.tax', 'Includes {$tax} VAT', { tax: '19%' })}</p>
|
|
157
158
|
</div>
|
|
158
159
|
);
|
|
159
160
|
}
|
|
160
161
|
```
|
|
161
162
|
|
|
163
|
+
> **Note:** Variables use `{$name}` syntax (with `$` prefix) per MessageFormat 2 specification.
|
|
164
|
+
|
|
162
165
|
### 6. Use i18n-aware client directives
|
|
163
166
|
|
|
164
167
|
**The problem:** With standard `client:*` directives, the translation chunk loads *after* the component module. This delays hydration while translations are fetched sequentially.
|
|
@@ -179,28 +182,90 @@ import Cart from '../components/Cart';
|
|
|
179
182
|
|
|
180
183
|
### `t(key, fallback, values?)`
|
|
181
184
|
|
|
182
|
-
Translate a key with optional interpolation values.
|
|
185
|
+
Translate a key with optional interpolation values. The fallback is used when a translation is missing and also serves as an example for translators. Uses [MessageFormat 2](https://github.com/unicode-org/message-format-wg) syntax.
|
|
183
186
|
|
|
184
187
|
```ts
|
|
185
|
-
// simple
|
|
188
|
+
// simple text
|
|
186
189
|
t('checkout.title', 'Order Summary')
|
|
187
190
|
|
|
188
|
-
// with variables
|
|
189
|
-
t('checkout.tax', 'Includes {tax} VAT', { tax: '19%' })
|
|
191
|
+
// with variables (note the $ prefix)
|
|
192
|
+
t('checkout.tax', 'Includes {$tax} VAT', { tax: '19%' })
|
|
193
|
+
|
|
194
|
+
// with pluralization (MF2 syntax)
|
|
195
|
+
t('cart.items', `.input {$count :number}
|
|
196
|
+
.match $count
|
|
197
|
+
one {{{$count} item}}
|
|
198
|
+
* {{{$count} items}}`, { count: 5 })
|
|
199
|
+
|
|
200
|
+
// with number formatting
|
|
201
|
+
t('stats.value', '{$value :number minimumFractionDigits=2}', { value: 1234.5 })
|
|
202
|
+
|
|
203
|
+
// with percentage
|
|
204
|
+
t('stats.ratio', '{$value :percent}', { value: 0.856 })
|
|
190
205
|
|
|
191
|
-
// with
|
|
192
|
-
t('
|
|
206
|
+
// with date/time formatting
|
|
207
|
+
t('event.date', '{$date :date style=long}', { date: new Date() })
|
|
208
|
+
t('event.time', '{$time :time style=short}', { time: new Date() })
|
|
209
|
+
|
|
210
|
+
// with currency (translator controls currency)
|
|
211
|
+
t('product.price', '{$price :currency currency=EUR}', { price: 99.99 })
|
|
212
|
+
|
|
213
|
+
// with currency (code controls currency via wrapped value)
|
|
214
|
+
t('product.price', '{$price :currency}', {
|
|
215
|
+
price: { valueOf: () => 99.99, options: { currency: 'EUR' } }
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
// with units
|
|
219
|
+
t('distance', '{$value :unit unit=kilometer}', { value: 42 })
|
|
193
220
|
|
|
194
221
|
// with metadata object (for extraction tooling)
|
|
195
222
|
t('cart.total', {
|
|
196
|
-
|
|
223
|
+
example: 'Total: {$amount}',
|
|
197
224
|
description: 'Cart total price',
|
|
198
225
|
variables: {
|
|
199
|
-
amount: {
|
|
226
|
+
amount: { example: '$0.00', description: 'Formatted price' }
|
|
200
227
|
}
|
|
201
228
|
}, { amount: '$49.99' })
|
|
202
229
|
```
|
|
203
230
|
|
|
231
|
+
### `rich(key, fallback, components, values?)`
|
|
232
|
+
|
|
233
|
+
Translate with embedded components using MF2 markup syntax. Returns an array of strings and JSX elements that can be rendered directly.
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
import { rich } from '@astroscope/i18n/translate';
|
|
237
|
+
|
|
238
|
+
// basic link
|
|
239
|
+
rich('tos', 'Read our {#link}Terms of Service{/link}', {
|
|
240
|
+
link: (children) => <a href="/tos">{children}</a>
|
|
241
|
+
})
|
|
242
|
+
// Returns: ['Read our ', <a href="/tos">Terms of Service</a>]
|
|
243
|
+
|
|
244
|
+
// multiple tags
|
|
245
|
+
rich('legal', 'Read our {#tos}Terms{/tos} and {#privacy}Privacy Policy{/privacy}', {
|
|
246
|
+
tos: (children) => <a href="/tos">{children}</a>,
|
|
247
|
+
privacy: (children) => <a href="/privacy">{children}</a>,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// with variables
|
|
251
|
+
rich('greeting', 'Hello {$name}, check your {#inbox}messages{/inbox}', {
|
|
252
|
+
inbox: (children) => <a href="/inbox">{children}</a>,
|
|
253
|
+
}, { name: 'Alice' })
|
|
254
|
+
|
|
255
|
+
// nested tags
|
|
256
|
+
rich('highlight', 'This is {#bold}very {#em}important{/em}{/bold}', {
|
|
257
|
+
bold: (children) => <strong>{children}</strong>,
|
|
258
|
+
em: (children) => <em>{children}</em>,
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// standalone (self-closing) tags
|
|
262
|
+
rich('install', 'Click {#icon/} to install', {
|
|
263
|
+
icon: () => <DownloadIcon />,
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The same code works in both Astro templates and React islands — `rich()` is JSX-runtime agnostic.
|
|
268
|
+
|
|
204
269
|
### `i18n` singleton
|
|
205
270
|
|
|
206
271
|
```ts
|
|
@@ -256,11 +321,74 @@ export function App() {
|
|
|
256
321
|
3. **SSR** — Middleware provides translations to `t()`, merging manifest fallbacks for missing keys
|
|
257
322
|
4. **Client** — Custom directives load only the translations needed by each chunk
|
|
258
323
|
|
|
259
|
-
The same `import { t } from '@astroscope/i18n/
|
|
324
|
+
The same `import { t } from '@astroscope/i18n/translate'` works everywhere — bundler picks the correct implementation via conditional exports (`browser` vs `default`).
|
|
260
325
|
|
|
261
326
|
### Client bundle
|
|
262
327
|
|
|
263
|
-
Translation chunks are served as raw
|
|
328
|
+
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.
|
|
329
|
+
|
|
330
|
+
> **Future:** Once browsers ship native `Intl.MessageFormat`, this 8KB runtime will be replaced by the built-in API with zero bundle cost.
|
|
331
|
+
|
|
332
|
+
## MessageFormat 2 Syntax
|
|
333
|
+
|
|
334
|
+
This library uses [Unicode MessageFormat 2](https://github.com/unicode-org/message-format-wg) (MF2), the modern standard for internationalization.
|
|
335
|
+
|
|
336
|
+
### Basic syntax
|
|
337
|
+
|
|
338
|
+
```
|
|
339
|
+
Simple text
|
|
340
|
+
Hello {$name}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Pluralization
|
|
344
|
+
|
|
345
|
+
```
|
|
346
|
+
.input {$count :number}
|
|
347
|
+
.match $count
|
|
348
|
+
one {{{$count} item}}
|
|
349
|
+
* {{{$count} items}}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Selection (gender, etc.)
|
|
353
|
+
|
|
354
|
+
```
|
|
355
|
+
.input {$gender :string}
|
|
356
|
+
.match $gender
|
|
357
|
+
male {{He liked it}}
|
|
358
|
+
female {{She liked it}}
|
|
359
|
+
* {{They liked it}}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Built-in formatters
|
|
363
|
+
|
|
364
|
+
| Formatter | Description | Example |
|
|
365
|
+
|-----------|-------------|---------|
|
|
366
|
+
| `:number` | Locale-aware number | `{$n :number}` → "1,234.56" |
|
|
367
|
+
| `:integer` | Integer (no decimals) | `{$n :integer}` → "1,235" |
|
|
368
|
+
| `:percent` | Percentage | `{$n :percent}` → "85.6%" |
|
|
369
|
+
| `:currency` | Currency | `{$n :currency currency=EUR}` → "€99.99" |
|
|
370
|
+
| `:date` | Date | `{$d :date style=long}` → "January 26, 2026" |
|
|
371
|
+
| `:time` | Time | `{$d :time style=short}` → "3:45 PM" |
|
|
372
|
+
| `:datetime` | Date + time | `{$d :datetime dateStyle=medium timeStyle=short}` |
|
|
373
|
+
| `:unit` | Units | `{$n :unit unit=kilometer}` → "42 km" |
|
|
374
|
+
|
|
375
|
+
### Currency and unit options
|
|
376
|
+
|
|
377
|
+
For `:currency` and `:unit`, the required option (`currency` or `unit`) can be:
|
|
378
|
+
|
|
379
|
+
1. **Hardcoded in translation** (translator controls):
|
|
380
|
+
```
|
|
381
|
+
{$price :currency currency=EUR}
|
|
382
|
+
```
|
|
383
|
+
Code passes plain number: `{ price: 99.99 }`
|
|
384
|
+
|
|
385
|
+
2. **Provided by code** (for dynamic currency/unit):
|
|
386
|
+
```
|
|
387
|
+
{$price :currency}
|
|
388
|
+
```
|
|
389
|
+
Code passes wrapped value: `{ price: { valueOf: () => 99.99, options: { currency: 'EUR' } } }`
|
|
390
|
+
|
|
391
|
+
> **Note:** If both translation and code specify the option, **translation wins**.
|
|
264
392
|
|
|
265
393
|
## Configuration
|
|
266
394
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/shared/compiler.ts
|
|
2
|
+
import { MessageFormat } from "messageformat";
|
|
3
|
+
import { DraftFunctions } from "messageformat/functions";
|
|
4
|
+
var mfOptions = { functions: DraftFunctions };
|
|
5
|
+
var cache = /* @__PURE__ */ new Map();
|
|
6
|
+
function getOrCreateMessageFormat(locale, message) {
|
|
7
|
+
const cacheKey = `${locale}:${message}`;
|
|
8
|
+
const cached = cache.get(cacheKey);
|
|
9
|
+
if (cached) {
|
|
10
|
+
return cached;
|
|
11
|
+
}
|
|
12
|
+
const mf = new MessageFormat(locale, message, mfOptions);
|
|
13
|
+
cache.set(cacheKey, mf);
|
|
14
|
+
return mf;
|
|
15
|
+
}
|
|
16
|
+
function compileMessage(locale, message) {
|
|
17
|
+
try {
|
|
18
|
+
const mf = getOrCreateMessageFormat(locale, message);
|
|
19
|
+
return (values) => mf.format(values);
|
|
20
|
+
} catch {
|
|
21
|
+
return () => message;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function formatMessageToParts(locale, message, values) {
|
|
25
|
+
try {
|
|
26
|
+
const mf = getOrCreateMessageFormat(locale, message);
|
|
27
|
+
return mf.formatToParts(values);
|
|
28
|
+
} catch {
|
|
29
|
+
return [{ type: "literal", value: message }];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function compileTranslations(locale, raw) {
|
|
33
|
+
const compiled = {};
|
|
34
|
+
for (const [key, message] of Object.entries(raw)) {
|
|
35
|
+
compiled[key] = compileMessage(locale, message);
|
|
36
|
+
}
|
|
37
|
+
return compiled;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
compileMessage,
|
|
42
|
+
formatMessageToParts,
|
|
43
|
+
compileTranslations
|
|
44
|
+
};
|
|
@@ -197,7 +197,7 @@ async function scan(projectRoot, logger) {
|
|
|
197
197
|
const results = await Promise.all(
|
|
198
198
|
files.map(async (file) => {
|
|
199
199
|
let code = await fs.promises.readFile(file, "utf-8");
|
|
200
|
-
if (!code.includes("@astroscope/i18n/
|
|
200
|
+
if (!code.includes("@astroscope/i18n/translate")) {
|
|
201
201
|
return { file, keys: null };
|
|
202
202
|
}
|
|
203
203
|
if (file.endsWith(".astro")) {
|
|
@@ -293,7 +293,7 @@ export function getManifest() { return _getManifest(); }
|
|
|
293
293
|
if (filename.includes("node_modules")) {
|
|
294
294
|
return null;
|
|
295
295
|
}
|
|
296
|
-
if (!code.includes("@astroscope/i18n/
|
|
296
|
+
if (!code.includes("@astroscope/i18n/translate")) {
|
|
297
297
|
return null;
|
|
298
298
|
}
|
|
299
299
|
const result = await extractKeysFromFile({
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/shared/meta.ts
|
|
2
|
+
function normalizeMeta(meta) {
|
|
3
|
+
return typeof meta === "string" ? { fallback: meta } : meta;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// src/shared/rich.ts
|
|
7
|
+
var REACT_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.element");
|
|
8
|
+
var REACT_TRANSITIONAL_ELEMENT_TYPE = /* @__PURE__ */ Symbol.for("react.transitional.element");
|
|
9
|
+
function isReactElement(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null || !("$$typeof" in value)) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
const type = value.$$typeof;
|
|
14
|
+
return type === REACT_ELEMENT_TYPE || type === REACT_TRANSITIONAL_ELEMENT_TYPE;
|
|
15
|
+
}
|
|
16
|
+
function addKeyIfNeeded(element, key) {
|
|
17
|
+
if (isReactElement(element) && element.key === null) {
|
|
18
|
+
return { ...element, key };
|
|
19
|
+
}
|
|
20
|
+
return element;
|
|
21
|
+
}
|
|
22
|
+
function isMarkupPart(part) {
|
|
23
|
+
return part.type === "markup";
|
|
24
|
+
}
|
|
25
|
+
function partsToNodes(parts, components) {
|
|
26
|
+
const result = [];
|
|
27
|
+
const stack = [];
|
|
28
|
+
let keyIndex = 0;
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
const target = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
31
|
+
if (isMarkupPart(part)) {
|
|
32
|
+
if (part.kind === "open") {
|
|
33
|
+
stack.push({ name: part.name, children: [] });
|
|
34
|
+
} else if (part.kind === "close") {
|
|
35
|
+
const frame = stack.pop();
|
|
36
|
+
if (!frame) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const component = components[frame.name];
|
|
40
|
+
const parent = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
41
|
+
if (component) {
|
|
42
|
+
parent.push(addKeyIfNeeded(component(frame.children), `rich-${keyIndex++}`));
|
|
43
|
+
} else {
|
|
44
|
+
parent.push(...frame.children);
|
|
45
|
+
}
|
|
46
|
+
} else if (part.kind === "standalone") {
|
|
47
|
+
const component = components[part.name];
|
|
48
|
+
if (component) {
|
|
49
|
+
target.push(addKeyIfNeeded(component([]), `rich-${keyIndex++}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} else if (part.type === "text") {
|
|
53
|
+
target.push(part.value);
|
|
54
|
+
} else if (part.type === "bidiIsolation") {
|
|
55
|
+
} else {
|
|
56
|
+
if ("value" in part && part.value !== void 0 && part.value !== null) {
|
|
57
|
+
const stringValue = String(part.value);
|
|
58
|
+
target.push(stringValue);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
while (stack.length > 0) {
|
|
63
|
+
const frame = stack.pop();
|
|
64
|
+
const parent = stack.length > 0 ? stack[stack.length - 1].children : result;
|
|
65
|
+
parent.push(...frame.children);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export {
|
|
71
|
+
normalizeMeta,
|
|
72
|
+
partsToNodes
|
|
73
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { R as RichComponents } from '../rich-CQUhdwBc.js';
|
|
2
|
+
import { a as TranslateFunction, T as TranslationMeta } from '../types-DNRFj1XA.js';
|
|
3
|
+
import '../types-BeDFfFIL.js';
|
|
4
|
+
|
|
5
|
+
declare function getLocale(): string;
|
|
6
|
+
/**
|
|
7
|
+
* Client-side translate function
|
|
8
|
+
*
|
|
9
|
+
* Reads from window.__i18n__.translations (injected per-chunk).
|
|
10
|
+
* In production, translations are always loaded before component code runs
|
|
11
|
+
* via the chunk loader injected by the Vite plugin.
|
|
12
|
+
*
|
|
13
|
+
* Call patterns:
|
|
14
|
+
* - Development: t('key', 'fallback') or t('key', 'fallback', values)
|
|
15
|
+
* - Production: t('key') or t('key', undefined, values)
|
|
16
|
+
*
|
|
17
|
+
* Falls back to meta.fallback (dev) or key (prod) when translation is missing.
|
|
18
|
+
*/
|
|
19
|
+
declare const t: TranslateFunction;
|
|
20
|
+
/**
|
|
21
|
+
* Client-side rich text translation function
|
|
22
|
+
*
|
|
23
|
+
* Parses MF2 markup syntax and applies component callbacks to wrap content.
|
|
24
|
+
* Works with any JSX runtime (React, Preact, etc.).
|
|
25
|
+
*
|
|
26
|
+
* @param key - Translation key
|
|
27
|
+
* @param meta - Fallback string or meta object with fallback (used as message template)
|
|
28
|
+
* @param components - Map of tag names to component callbacks
|
|
29
|
+
* @param values - Runtime interpolation values (ICU MessageFormat)
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```tsx
|
|
33
|
+
* rich('tos', 'Read our {#link}Terms{/link}', {
|
|
34
|
+
* link: (children) => <a href="/tos">{children}</a>
|
|
35
|
+
* })
|
|
36
|
+
* // Returns: ['Read our ', <a href="/tos">Terms</a>]
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
declare function rich<T = unknown>(key: string, meta?: TranslationMeta | string | undefined, components?: RichComponents<T> | undefined, values?: Record<string, unknown> | undefined): (string | T)[];
|
|
40
|
+
|
|
41
|
+
export { getLocale, rich, t };
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
normalizeMeta
|
|
3
|
-
|
|
2
|
+
normalizeMeta,
|
|
3
|
+
partsToNodes
|
|
4
|
+
} from "../chunk-VGPVG5JP.js";
|
|
4
5
|
import {
|
|
5
|
-
compileMessage
|
|
6
|
-
|
|
6
|
+
compileMessage,
|
|
7
|
+
formatMessageToParts
|
|
8
|
+
} from "../chunk-33JVAFGQ.js";
|
|
7
9
|
|
|
8
|
-
// src/client/
|
|
10
|
+
// src/client/translate.ts
|
|
9
11
|
var cache = /* @__PURE__ */ new Map();
|
|
10
12
|
function getLocale() {
|
|
11
13
|
return window.__i18n__.locale;
|
|
@@ -31,7 +33,14 @@ var t = ((key, meta, values) => {
|
|
|
31
33
|
}
|
|
32
34
|
return compiled(values);
|
|
33
35
|
});
|
|
36
|
+
function rich(key, meta, components, values) {
|
|
37
|
+
const normalizedMeta = meta ? normalizeMeta(meta) : { fallback: "" };
|
|
38
|
+
const raw = window.__i18n__.translations[key] ?? normalizedMeta.fallback ?? key;
|
|
39
|
+
const parts = formatMessageToParts(window.__i18n__.locale, raw, values);
|
|
40
|
+
return partsToNodes(parts, components ?? {});
|
|
41
|
+
}
|
|
34
42
|
export {
|
|
35
43
|
getLocale,
|
|
44
|
+
rich,
|
|
36
45
|
t
|
|
37
46
|
};
|
package/dist/extraction/index.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AstroIntegration, MiddlewareHandler, APIContext } from 'astro';
|
|
2
|
-
export { F as FallbackBehavior, I as I18nConfig, i as i18n } from './i18n-
|
|
2
|
+
export { F as FallbackBehavior, I as I18nConfig, i as i18n } from './i18n-DaLnNvss.js';
|
|
3
3
|
import { ExcludePattern } from '@astroscope/excludes';
|
|
4
4
|
export { R as RawTranslations, T as TranslationMeta } from './types-DNRFj1XA.js';
|
|
5
5
|
export { E as ExtractedKey, a as ExtractionManifest } from './types-moTgocy7.js';
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
i18nVitePlugin
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EXMM4JID.js";
|
|
4
4
|
import {
|
|
5
5
|
i18n,
|
|
6
6
|
runWithContext
|
|
7
|
-
} from "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
} from "./chunk-MSJHXRJB.js";
|
|
8
|
+
import "./chunk-33JVAFGQ.js";
|
|
9
9
|
import "./chunk-4A5BKHFN.js";
|
|
10
10
|
|
|
11
11
|
// src/integration/integration.ts
|
|
@@ -112,6 +112,7 @@ function createI18nMiddleware(options) {
|
|
|
112
112
|
const context = {
|
|
113
113
|
locale,
|
|
114
114
|
translations: i18n.getCompiledTranslations(locale),
|
|
115
|
+
rawTranslations: i18n.getTranslations(locale),
|
|
115
116
|
fallback: i18n.getConfig().fallback
|
|
116
117
|
};
|
|
117
118
|
return runWithContext(context, () => next());
|
package/dist/internal.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as I18nContext } from './i18n-
|
|
2
|
-
export { i as i18n } from './i18n-
|
|
1
|
+
import { a as I18nContext } from './i18n-DaLnNvss.js';
|
|
2
|
+
export { i as i18n } from './i18n-DaLnNvss.js';
|
|
3
3
|
export { I as I18nClientState } from './types-DNRFj1XA.js';
|
|
4
4
|
import './types-moTgocy7.js';
|
|
5
5
|
|
package/dist/internal.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component callbacks for rich text rendering.
|
|
3
|
+
* Each key maps to a function that wraps children in a component.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```tsx
|
|
7
|
+
* const components: RichComponents = {
|
|
8
|
+
* link: (children) => <a href="/tos">{children}</a>,
|
|
9
|
+
* bold: (children) => <strong>{children}</strong>,
|
|
10
|
+
* };
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
type RichComponents<T = unknown> = Record<string, (children: (string | T)[]) => T>;
|
|
14
|
+
|
|
15
|
+
export type { RichComponents as R };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { R as RichComponents } from '../rich-CQUhdwBc.js';
|
|
2
|
+
import { a as TranslateFunction, T as TranslationMeta } from '../types-DNRFj1XA.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get the current locale from request context.
|
|
6
|
+
* Returns defaultLocale if called outside a request (e.g. during build).
|
|
7
|
+
*/
|
|
8
|
+
declare function getLocale(): string;
|
|
9
|
+
/**
|
|
10
|
+
* Server-side translate function
|
|
11
|
+
*
|
|
12
|
+
* Call patterns:
|
|
13
|
+
* - Development: t('key', 'fallback') or t('key', 'fallback', values)
|
|
14
|
+
* - Production: t('key') or t('key', undefined, values)
|
|
15
|
+
*/
|
|
16
|
+
declare const t: TranslateFunction;
|
|
17
|
+
/**
|
|
18
|
+
* Server-side rich text translation function
|
|
19
|
+
*
|
|
20
|
+
* Parses MF2 markup syntax and applies component callbacks to wrap content.
|
|
21
|
+
* Works with any JSX runtime (Astro, React during SSR, etc.).
|
|
22
|
+
*
|
|
23
|
+
* @param key - Translation key
|
|
24
|
+
* @param meta - Fallback string or meta object with fallback (used as message template)
|
|
25
|
+
* @param components - Map of tag names to component callbacks
|
|
26
|
+
* @param values - Runtime interpolation values (ICU MessageFormat)
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```tsx
|
|
30
|
+
* rich('tos', 'Read our {#link}Terms{/link}', {
|
|
31
|
+
* link: (children) => <a href="/tos">{children}</a>
|
|
32
|
+
* })
|
|
33
|
+
* // Returns: ['Read our ', <a href="/tos">Terms</a>]
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare function rich<T = unknown>(key: string, meta?: TranslationMeta | string | undefined, components?: RichComponents<T> | undefined, values?: Record<string, unknown> | undefined): (string | T)[];
|
|
37
|
+
|
|
38
|
+
export { getLocale, rich, t };
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getContext,
|
|
3
3
|
i18n
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-MSJHXRJB.js";
|
|
5
5
|
import {
|
|
6
|
-
normalizeMeta
|
|
7
|
-
|
|
6
|
+
normalizeMeta,
|
|
7
|
+
partsToNodes
|
|
8
|
+
} from "../chunk-VGPVG5JP.js";
|
|
8
9
|
import {
|
|
9
|
-
compileMessage
|
|
10
|
-
|
|
10
|
+
compileMessage,
|
|
11
|
+
formatMessageToParts
|
|
12
|
+
} from "../chunk-33JVAFGQ.js";
|
|
11
13
|
|
|
12
|
-
// src/server/
|
|
14
|
+
// src/server/translate.ts
|
|
13
15
|
var fallbackCache = /* @__PURE__ */ new Map();
|
|
14
16
|
function applyFallback(key, meta, fallback) {
|
|
15
17
|
if (typeof fallback === "function") {
|
|
@@ -53,7 +55,16 @@ var t = ((key, meta, values) => {
|
|
|
53
55
|
}
|
|
54
56
|
return compiled(values);
|
|
55
57
|
});
|
|
58
|
+
function rich(key, meta, components, values) {
|
|
59
|
+
const normalizedMeta = meta ? normalizeMeta(meta) : { fallback: "" };
|
|
60
|
+
const ctx = getContext();
|
|
61
|
+
const locale = ctx?.locale ?? i18n.getConfig().defaultLocale;
|
|
62
|
+
const raw = ctx?.rawTranslations[key] ?? normalizedMeta.fallback ?? key;
|
|
63
|
+
const parts = formatMessageToParts(locale, raw, values);
|
|
64
|
+
return partsToNodes(parts, components ?? {});
|
|
65
|
+
}
|
|
56
66
|
export {
|
|
57
67
|
getLocale,
|
|
68
|
+
rich,
|
|
58
69
|
t
|
|
59
70
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astroscope/i18n",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "i18n for Astro + React islands — automatic tree-shaking, parallel loading, any translation source",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "i18n for Astro + React islands — MessageFormat 2, automatic tree-shaking, parallel loading, any translation source",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
-
"./
|
|
14
|
-
"types": "./dist/client/
|
|
15
|
-
"browser": "./dist/client/
|
|
16
|
-
"default": "./dist/server/
|
|
13
|
+
"./translate": {
|
|
14
|
+
"types": "./dist/client/translate.d.ts",
|
|
15
|
+
"browser": "./dist/client/translate.js",
|
|
16
|
+
"default": "./dist/server/translate.js"
|
|
17
17
|
},
|
|
18
18
|
"./client-directives/load.js": {
|
|
19
19
|
"types": "./dist/client/directives/load.d.ts",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
"types": "./dist/client/directives/only.d.ts",
|
|
36
36
|
"import": "./dist/client/directives/only.js"
|
|
37
37
|
},
|
|
38
|
-
"./
|
|
39
|
-
"import": "./src/
|
|
38
|
+
"./astro": {
|
|
39
|
+
"import": "./src/astro/index.ts"
|
|
40
40
|
},
|
|
41
41
|
"./internal": {
|
|
42
42
|
"types": "./dist/internal.d.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"files": [
|
|
51
51
|
"dist",
|
|
52
|
-
"src/
|
|
52
|
+
"src/astro"
|
|
53
53
|
],
|
|
54
54
|
"sideEffects": [
|
|
55
55
|
"./dist/client/directives/*.js",
|
|
@@ -69,6 +69,8 @@
|
|
|
69
69
|
"i18n",
|
|
70
70
|
"internationalization",
|
|
71
71
|
"translations",
|
|
72
|
+
"messageformat",
|
|
73
|
+
"mf2",
|
|
72
74
|
"react",
|
|
73
75
|
"islands",
|
|
74
76
|
"ssr"
|
|
@@ -80,7 +82,7 @@
|
|
|
80
82
|
},
|
|
81
83
|
"homepage": "https://github.com/smnbbrv/astroscope/tree/main/packages/i18n#readme",
|
|
82
84
|
"scripts": {
|
|
83
|
-
"build": "tsup src/index.ts src/internal.ts src/extraction/index.ts src/server/
|
|
85
|
+
"build": "tsup src/index.ts src/internal.ts src/extraction/index.ts src/server/translate.ts src/client/translate.ts src/client/directives/load.ts src/client/directives/idle.ts src/client/directives/visible.ts src/client/directives/media.ts src/client/directives/only.ts src/client/directives/utils.ts --format esm --dts --external astro --external astro/client/load.js --external astro/client/idle.js --external astro/client/visible.js --external astro/client/media.js --external astro/client/only.js --external virtual:@astroscope/i18n/manifest --external @babel/core",
|
|
84
86
|
"typecheck": "tsc --noEmit",
|
|
85
87
|
"lint": "eslint 'src/**/*.ts'",
|
|
86
88
|
"lint:fix": "eslint 'src/**/*.ts' --fix"
|
|
@@ -89,7 +91,7 @@
|
|
|
89
91
|
"@astrojs/compiler": "^2.12.2",
|
|
90
92
|
"@babel/core": "^7.28.6",
|
|
91
93
|
"@babel/preset-typescript": "^7.28.5",
|
|
92
|
-
"
|
|
94
|
+
"messageformat": "^4.0.0",
|
|
93
95
|
"glob": "^11.0.0",
|
|
94
96
|
"magic-string": "^0.30.21"
|
|
95
97
|
},
|
package/dist/chunk-EMM6LRO2.js
DELETED
package/dist/chunk-KNDYKTHE.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// src/shared/compiler.ts
|
|
2
|
-
import MessageFormat from "@messageformat/core";
|
|
3
|
-
var cache = /* @__PURE__ */ new Map();
|
|
4
|
-
function getMessageFormat(locale) {
|
|
5
|
-
let mf = cache.get(locale);
|
|
6
|
-
if (!mf) {
|
|
7
|
-
mf = new MessageFormat(locale);
|
|
8
|
-
cache.set(locale, mf);
|
|
9
|
-
}
|
|
10
|
-
return mf;
|
|
11
|
-
}
|
|
12
|
-
function compileMessage(locale, message) {
|
|
13
|
-
const mf = getMessageFormat(locale);
|
|
14
|
-
try {
|
|
15
|
-
return mf.compile(message);
|
|
16
|
-
} catch {
|
|
17
|
-
return () => message;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
function compileTranslations(locale, raw) {
|
|
21
|
-
const compiled = {};
|
|
22
|
-
for (const [key, message] of Object.entries(raw)) {
|
|
23
|
-
compiled[key] = compileMessage(locale, message);
|
|
24
|
-
}
|
|
25
|
-
return compiled;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export {
|
|
29
|
-
compileMessage,
|
|
30
|
-
compileTranslations
|
|
31
|
-
};
|
package/dist/client/t.d.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { a as TranslateFunction } from '../types-DNRFj1XA.js';
|
|
2
|
-
import '../types-BeDFfFIL.js';
|
|
3
|
-
|
|
4
|
-
declare function getLocale(): string;
|
|
5
|
-
/**
|
|
6
|
-
* Client-side translate function
|
|
7
|
-
*
|
|
8
|
-
* Reads from window.__i18n__.translations (injected per-chunk).
|
|
9
|
-
* In production, translations are always loaded before component code runs
|
|
10
|
-
* via the chunk loader injected by the Vite plugin.
|
|
11
|
-
*
|
|
12
|
-
* Call patterns:
|
|
13
|
-
* - Development: t('key', 'fallback') or t('key', 'fallback', values)
|
|
14
|
-
* - Production: t('key') or t('key', undefined, values)
|
|
15
|
-
*
|
|
16
|
-
* Falls back to meta.fallback (dev) or key (prod) when translation is missing.
|
|
17
|
-
*/
|
|
18
|
-
declare const t: TranslateFunction;
|
|
19
|
-
|
|
20
|
-
export { getLocale, t };
|
package/dist/server/t.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { a as TranslateFunction } from '../types-DNRFj1XA.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Get the current locale from request context.
|
|
5
|
-
* Returns defaultLocale if called outside a request (e.g. during build).
|
|
6
|
-
*/
|
|
7
|
-
declare function getLocale(): string;
|
|
8
|
-
/**
|
|
9
|
-
* Server-side translate function
|
|
10
|
-
*
|
|
11
|
-
* Call patterns:
|
|
12
|
-
* - Development: t('key', 'fallback') or t('key', 'fallback', values)
|
|
13
|
-
* - Production: t('key') or t('key', undefined, values)
|
|
14
|
-
*/
|
|
15
|
-
declare const t: TranslateFunction;
|
|
16
|
-
|
|
17
|
-
export { getLocale, t };
|
|
File without changes
|
|
File without changes
|