astro-intl 2.0.3 → 2.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 +701 -701
- package/dist/__tests__/config-messages.test.js +3 -2
- package/dist/__tests__/fallback-routes.test.d.ts +1 -0
- package/dist/__tests__/fallback-routes.test.js +48 -0
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +29 -1
- package/dist/middleware.js +3 -1
- package/dist/store.d.ts +3 -1
- package/dist/store.js +64 -44
- package/dist/types/index.d.ts +6 -0
- package/package.json +99 -99
package/README.md
CHANGED
|
@@ -1,701 +1,701 @@
|
|
|
1
|
-
# astro-intl
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## ✨
|
|
6
|
-
|
|
7
|
-
- 🔒 **Type-safe**:
|
|
8
|
-
- 🎯 **
|
|
9
|
-
- ⚛️ **React
|
|
10
|
-
- 🧡 **Svelte
|
|
11
|
-
- 🎨 **Markup
|
|
12
|
-
- 📁 **Namespaces**:
|
|
13
|
-
- 🌐 **
|
|
14
|
-
- 🛡️ **Concurrency-safe**:
|
|
15
|
-
- 🌍 **Multi-runtime**: Compatible
|
|
16
|
-
- ⚙️ **
|
|
17
|
-
- 🗺️ **
|
|
18
|
-
- 🔄 **
|
|
19
|
-
- 🔗 **
|
|
20
|
-
- 📦 **Sub-path imports**: `astro-intl/react`, `astro-intl/svelte`, `astro-intl/routing`, `astro-intl/middleware`
|
|
21
|
-
|
|
22
|
-
##
|
|
23
|
-
|
|
24
|
-
### Breaking changes
|
|
25
|
-
|
|
26
|
-
1. **`getTranslationsReact`
|
|
27
|
-
|
|
28
|
-
```diff
|
|
29
|
-
- import { getTranslationsReact } from "astro-intl";
|
|
30
|
-
+ import { getTranslations } from "astro-intl/react";
|
|
31
|
-
|
|
32
|
-
- const t = getTranslationsReact();
|
|
33
|
-
+ const t = getTranslations();
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
2. **Sub-path imports
|
|
37
|
-
- React: `astro-intl/react`
|
|
38
|
-
- Svelte: `astro-intl/svelte`
|
|
39
|
-
|
|
40
|
-
3.
|
|
41
|
-
|
|
42
|
-
###
|
|
43
|
-
|
|
44
|
-
- **Svelte
|
|
45
|
-
- **`createGetTranslations` factory**
|
|
46
|
-
- **`parseRichSegments()`**
|
|
47
|
-
|
|
48
|
-
##
|
|
49
|
-
|
|
50
|
-
###
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
```bash
|
|
55
|
-
npx astro add astro-intl
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- ✅
|
|
61
|
-
- ✅
|
|
62
|
-
- ✅
|
|
63
|
-
|
|
64
|
-
###
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npm install astro-intl
|
|
70
|
-
# o
|
|
71
|
-
pnpm add astro-intl
|
|
72
|
-
# o
|
|
73
|
-
yarn add astro-intl
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
```js
|
|
79
|
-
import { defineConfig } from "astro/config";
|
|
80
|
-
import astroIntl from "astro-intl";
|
|
81
|
-
|
|
82
|
-
export default defineConfig({
|
|
83
|
-
integrations: [
|
|
84
|
-
astroIntl({
|
|
85
|
-
defaultLocale: "en", //
|
|
86
|
-
}),
|
|
87
|
-
],
|
|
88
|
-
});
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
## 🎯
|
|
92
|
-
|
|
93
|
-
###
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
// src/i18n/es.json
|
|
99
|
-
{
|
|
100
|
-
"welcome": "Bienvenido",
|
|
101
|
-
"nav": {
|
|
102
|
-
"home": "Inicio",
|
|
103
|
-
"about": "Acerca de"
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// src/i18n/en.json
|
|
108
|
-
{
|
|
109
|
-
"welcome": "Welcome",
|
|
110
|
-
"nav": {
|
|
111
|
-
"home": "Home",
|
|
112
|
-
"about": "About"
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/i18n/index.ts
|
|
117
|
-
import es from './es.json';
|
|
118
|
-
import en from './en.json';
|
|
119
|
-
|
|
120
|
-
export const ui = { es, en };
|
|
121
|
-
export type Messages = typeof es;
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
###
|
|
125
|
-
|
|
126
|
-
```astro
|
|
127
|
-
---
|
|
128
|
-
import { setRequestLocale, getTranslations } from 'astro-intl';
|
|
129
|
-
import { ui } from '../i18n';
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
await setRequestLocale(Astro.url, async (locale) => ({
|
|
133
|
-
locale,
|
|
134
|
-
messages: ui[locale as keyof typeof ui]
|
|
135
|
-
}));
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
const t = getTranslations();
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
<h1>{t('welcome')}</h1>
|
|
142
|
-
<nav>
|
|
143
|
-
<a href="/">{t('nav.home')}</a>
|
|
144
|
-
<a href="/about">{t('nav.about')}</a>
|
|
145
|
-
</nav>
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
###
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
```json
|
|
153
|
-
// src/i18n/en.json
|
|
154
|
-
{
|
|
155
|
-
"greeting": "Hello, {name}!",
|
|
156
|
-
"info": "You have {count} items"
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
```astro
|
|
161
|
-
---
|
|
162
|
-
const t = getTranslations();
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
<p>{t('greeting', { name: 'John' })}</p> <!-- "Hello, John!" -->
|
|
166
|
-
<p>{t('info', { count: 5 })}</p> <!-- "You have 5 items" -->
|
|
167
|
-
<p>{t('greeting')}</p> <!-- "Hello, {name}!" (
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
###
|
|
173
|
-
|
|
174
|
-
```astro
|
|
175
|
-
---
|
|
176
|
-
// src/i18n/es.json
|
|
177
|
-
// { "terms": "Acepto los <link>términos y condiciones</link>" }
|
|
178
|
-
|
|
179
|
-
const t = getTranslations();
|
|
180
|
-
---
|
|
181
|
-
|
|
182
|
-
<p set:html={t.markup('terms', {
|
|
183
|
-
link: (chunks) => `<a href="/terms">${chunks}</a>`
|
|
184
|
-
})} />
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Markup
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
```astro
|
|
192
|
-
---
|
|
193
|
-
// src/i18n/en.json
|
|
194
|
-
// { "welcome": "Hello {name}, click <link>here</link> to continue" }
|
|
195
|
-
|
|
196
|
-
const t = getTranslations();
|
|
197
|
-
---
|
|
198
|
-
|
|
199
|
-
<p set:html={t.markup('welcome', {
|
|
200
|
-
values: { name: 'John' },
|
|
201
|
-
tags: {
|
|
202
|
-
link: (chunks) => `<a href="/home">${chunks}</a>`
|
|
203
|
-
}
|
|
204
|
-
})} />
|
|
205
|
-
<!-- "Hello John, click <a href="/home">here</a> to continue" -->
|
|
206
|
-
```
|
|
207
|
-
|
|
208
|
-
###
|
|
209
|
-
|
|
210
|
-
> **v2**:
|
|
211
|
-
|
|
212
|
-
```tsx
|
|
213
|
-
import { getTranslations } from "astro-intl/react";
|
|
214
|
-
|
|
215
|
-
export function MyComponent() {
|
|
216
|
-
const t = getTranslations();
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<div>
|
|
220
|
-
<h1>{t("welcome")}</h1>
|
|
221
|
-
<nav>
|
|
222
|
-
<a href="/">{t("nav.home")}</a>
|
|
223
|
-
</nav>
|
|
224
|
-
</div>
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
####
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
```tsx
|
|
234
|
-
import { createGetTranslations } from "astro-intl/react";
|
|
235
|
-
import { ui } from "../i18n";
|
|
236
|
-
|
|
237
|
-
const getT = createGetTranslations(ui, "en");
|
|
238
|
-
|
|
239
|
-
export function MyComponent({ lang }: { lang: string }) {
|
|
240
|
-
const t = getT(lang, "nav");
|
|
241
|
-
return <a href="/">{t("home")}</a>;
|
|
242
|
-
}
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
###
|
|
246
|
-
|
|
247
|
-
```tsx
|
|
248
|
-
import { getTranslations } from "astro-intl/react";
|
|
249
|
-
|
|
250
|
-
export function MyComponent() {
|
|
251
|
-
const t = getTranslations();
|
|
252
|
-
|
|
253
|
-
// src/i18n/es.json
|
|
254
|
-
// { "terms": "Acepto los <link>términos y condiciones</link>" }
|
|
255
|
-
|
|
256
|
-
return (
|
|
257
|
-
<p>
|
|
258
|
-
{t.rich("terms", {
|
|
259
|
-
link: (chunks) => <a href="/terms">{chunks}</a>,
|
|
260
|
-
})}
|
|
261
|
-
</p>
|
|
262
|
-
);
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
###
|
|
267
|
-
|
|
268
|
-
> **v2**:
|
|
269
|
-
|
|
270
|
-
```svelte
|
|
271
|
-
<script>
|
|
272
|
-
import { getTranslations } from 'astro-intl/svelte';
|
|
273
|
-
|
|
274
|
-
const t = getTranslations();
|
|
275
|
-
</script>
|
|
276
|
-
|
|
277
|
-
<h1>{t('welcome')}</h1>
|
|
278
|
-
<nav>
|
|
279
|
-
<a href="/">{t('nav.home')}</a>
|
|
280
|
-
</nav>
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
#### Rich text
|
|
284
|
-
|
|
285
|
-
`t.rich()`
|
|
286
|
-
|
|
287
|
-
```svelte
|
|
288
|
-
<script>
|
|
289
|
-
import { getTranslations, renderRichText } from 'astro-intl/svelte';
|
|
290
|
-
|
|
291
|
-
// { "terms": "Acepto los <link>términos y condiciones</link>" }
|
|
292
|
-
const t = getTranslations();
|
|
293
|
-
const segments = t.rich('terms', ['link']);
|
|
294
|
-
|
|
295
|
-
const html = renderRichText(segments, {
|
|
296
|
-
tags: { link: 'a' }, //
|
|
297
|
-
});
|
|
298
|
-
</script>
|
|
299
|
-
|
|
300
|
-
<p>{@html html}</p>
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
```svelte
|
|
306
|
-
<script>
|
|
307
|
-
import { getTranslations, renderRichText } from 'astro-intl/svelte';
|
|
308
|
-
|
|
309
|
-
const t = getTranslations();
|
|
310
|
-
const segments = t.rich('terms', ['link']);
|
|
311
|
-
|
|
312
|
-
const html = renderRichText(segments, {
|
|
313
|
-
components: {
|
|
314
|
-
link: (chunks) => `<a href="/terms" class="underline">${chunks}</a>`,
|
|
315
|
-
},
|
|
316
|
-
});
|
|
317
|
-
</script>
|
|
318
|
-
|
|
319
|
-
<p>{@html html}</p>
|
|
320
|
-
```
|
|
321
|
-
|
|
322
|
-
####
|
|
323
|
-
|
|
324
|
-
```svelte
|
|
325
|
-
<script>
|
|
326
|
-
import { createGetTranslations } from 'astro-intl/svelte';
|
|
327
|
-
import { ui } from '../i18n';
|
|
328
|
-
|
|
329
|
-
const getT = createGetTranslations(ui, 'en');
|
|
330
|
-
|
|
331
|
-
export let lang;
|
|
332
|
-
const t = getT(lang, 'nav');
|
|
333
|
-
</script>
|
|
334
|
-
|
|
335
|
-
<a href="/">{t('home')}</a>
|
|
336
|
-
```
|
|
337
|
-
|
|
338
|
-
### Type-safety
|
|
339
|
-
|
|
340
|
-
```astro
|
|
341
|
-
---
|
|
342
|
-
import { setRequestLocale, getTranslations } from 'astro-intl';
|
|
343
|
-
import { ui, type Messages } from '../i18n';
|
|
344
|
-
|
|
345
|
-
await setRequestLocale(Astro.url, async (locale) => ({
|
|
346
|
-
locale,
|
|
347
|
-
messages: ui[locale as keyof typeof ui]
|
|
348
|
-
}));
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
const t = getTranslations<Messages>();
|
|
352
|
-
|
|
353
|
-
// TypeScript
|
|
354
|
-
// t('nav.home') ✓
|
|
355
|
-
// t('nav.invalid') ✗ TypeScript
|
|
356
|
-
---
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
###
|
|
360
|
-
|
|
361
|
-
```astro
|
|
362
|
-
---
|
|
363
|
-
//
|
|
364
|
-
const t = getTranslations<Messages>('nav');
|
|
365
|
-
---
|
|
366
|
-
|
|
367
|
-
<nav>
|
|
368
|
-
<a href="/">{t('home')}</a> <!--
|
|
369
|
-
<a href="/about">{t('about')}</a>
|
|
370
|
-
</nav>
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
##
|
|
374
|
-
|
|
375
|
-
###
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
```ts
|
|
380
|
-
// src/i18n/routing.ts
|
|
381
|
-
export const routing = {
|
|
382
|
-
locales: ["en", "es"],
|
|
383
|
-
defaultLocale: "en",
|
|
384
|
-
routes: {
|
|
385
|
-
home: { en: "/", es: "/" },
|
|
386
|
-
about: { en: "/about", es: "/sobre-nosotros" },
|
|
387
|
-
blog: { en: "/blog/[slug]", es: "/blog/[slug]" },
|
|
388
|
-
shop: { en: "/shop/[category]/[id]", es: "/tienda/[category]/[id]" },
|
|
389
|
-
},
|
|
390
|
-
} as const;
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
###
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
```ts
|
|
398
|
-
// src/middleware.ts
|
|
399
|
-
import "@/i18n/request";
|
|
400
|
-
import { createIntlMiddleware } from "astro-intl/middleware";
|
|
401
|
-
import { routing } from "@/i18n/routing";
|
|
402
|
-
|
|
403
|
-
export const onRequest = createIntlMiddleware(routing);
|
|
404
|
-
```
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
###
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
```js
|
|
413
|
-
// astro.config.mjs
|
|
414
|
-
import { defineConfig } from "astro/config";
|
|
415
|
-
import astroIntl from "astro-intl";
|
|
416
|
-
|
|
417
|
-
export default defineConfig({
|
|
418
|
-
integrations: [
|
|
419
|
-
astroIntl({
|
|
420
|
-
defaultLocale: "en",
|
|
421
|
-
locales: ["en", "es"],
|
|
422
|
-
routes: {
|
|
423
|
-
about: { en: "/about", es: "/sobre-nosotros" },
|
|
424
|
-
},
|
|
425
|
-
}),
|
|
426
|
-
],
|
|
427
|
-
});
|
|
428
|
-
```
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
```astro
|
|
433
|
-
---
|
|
434
|
-
// src/pages/[lang]/sobre-nosotros.astro
|
|
435
|
-
export { default } from "./about.astro";
|
|
436
|
-
export { getStaticPaths } from "./about.astro";
|
|
437
|
-
---
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
###
|
|
441
|
-
|
|
442
|
-
```astro
|
|
443
|
-
---
|
|
444
|
-
import { path } from "astro-intl/routing";
|
|
445
|
-
---
|
|
446
|
-
|
|
447
|
-
<a href={path("about")}>About</a>
|
|
448
|
-
<!-- locale "en" → /en/about -->
|
|
449
|
-
<!-- locale "es" → /es/sobre-nosotros -->
|
|
450
|
-
|
|
451
|
-
<a href={path("shop", { locale: "es", params: { category: "ropa", id: "42" } })}>
|
|
452
|
-
|
|
453
|
-
</a>
|
|
454
|
-
<!-- → /es/tienda/ropa/42 -->
|
|
455
|
-
```
|
|
456
|
-
|
|
457
|
-
###
|
|
458
|
-
|
|
459
|
-
```astro
|
|
460
|
-
---
|
|
461
|
-
import { switchLocalePath } from "astro-intl/routing";
|
|
462
|
-
---
|
|
463
|
-
|
|
464
|
-
<a href={switchLocalePath(Astro.url.pathname, "en")}>English</a>
|
|
465
|
-
<a href={switchLocalePath(Astro.url.pathname, "es")}>Español</a>
|
|
466
|
-
<!--
|
|
467
|
-
<!--
|
|
468
|
-
```
|
|
469
|
-
|
|
470
|
-
##
|
|
471
|
-
|
|
472
|
-
### `astroIntl(options?)`
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
**
|
|
477
|
-
|
|
478
|
-
- `defaultLocale?: string` -
|
|
479
|
-
- `enabled?: boolean` -
|
|
480
|
-
- `messages?: MessagesConfig` -
|
|
481
|
-
- `locales?: string[]` -
|
|
482
|
-
- `routes?: RoutesMap` -
|
|
483
|
-
|
|
484
|
-
### `setRequestLocale(url, getConfig?)`
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
**
|
|
489
|
-
|
|
490
|
-
- `url: URL` -
|
|
491
|
-
- `getConfig?: (locale: string) => RequestConfig | Promise<RequestConfig>` -
|
|
492
|
-
|
|
493
|
-
**
|
|
494
|
-
|
|
495
|
-
```ts
|
|
496
|
-
await setRequestLocale(Astro.url, async (locale) => ({
|
|
497
|
-
locale,
|
|
498
|
-
messages: ui[locale],
|
|
499
|
-
}));
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
### `runWithLocale(url, fn, getConfig?)`
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
**
|
|
507
|
-
|
|
508
|
-
- `url: URL` -
|
|
509
|
-
- `fn: () => R | Promise<R>` -
|
|
510
|
-
- `getConfig?: GetRequestConfigFn` -
|
|
511
|
-
|
|
512
|
-
**
|
|
513
|
-
|
|
514
|
-
```ts
|
|
515
|
-
// src/middleware.ts
|
|
516
|
-
import { runWithLocale } from "astro-intl";
|
|
517
|
-
|
|
518
|
-
export const onRequest = async (context, next) => {
|
|
519
|
-
return runWithLocale(
|
|
520
|
-
context.url,
|
|
521
|
-
() => next(),
|
|
522
|
-
(locale) => ({
|
|
523
|
-
locale,
|
|
524
|
-
messages: ui[locale],
|
|
525
|
-
})
|
|
526
|
-
);
|
|
527
|
-
};
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
### `getTranslations<T>(namespace?)`
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
**
|
|
535
|
-
|
|
536
|
-
- `namespace?: string` -
|
|
537
|
-
|
|
538
|
-
**
|
|
539
|
-
|
|
540
|
-
#### `t(key, values?)`
|
|
541
|
-
|
|
542
|
-
- `key: string` -
|
|
543
|
-
- `values?: Record<string, Primitive>` -
|
|
544
|
-
|
|
545
|
-
#### `t.markup(key, options)`
|
|
546
|
-
|
|
547
|
-
- `key: string` -
|
|
548
|
-
- `options` -
|
|
549
|
-
- `Record<string, (chunks: string) => string>` -
|
|
550
|
-
- `{ values?: Record<string, Primitive>, tags: Record<string, (chunks: string) => string> }` - Tags
|
|
551
|
-
|
|
552
|
-
### `getTranslations()` — `astro-intl/react`
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
**
|
|
557
|
-
|
|
558
|
-
### `createGetTranslations(ui, defaultLocale)` — `astro-intl/react`
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
**
|
|
563
|
-
|
|
564
|
-
- `ui: Record<string, Record<string, unknown>>` -
|
|
565
|
-
- `defaultLocale: string` -
|
|
566
|
-
|
|
567
|
-
**
|
|
568
|
-
|
|
569
|
-
### `getTranslations()` — `astro-intl/svelte`
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
**
|
|
574
|
-
|
|
575
|
-
### `createGetTranslations(ui, defaultLocale)` — `astro-intl/svelte`
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
### `renderRichText(segments, options?)` — `astro-intl/svelte`
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
**
|
|
584
|
-
|
|
585
|
-
- `segments: RichSegment[]` -
|
|
586
|
-
- `options.tags?: Record<string, string>` -
|
|
587
|
-
- `options.components?: Record<string, (chunks: string) => string>` -
|
|
588
|
-
|
|
589
|
-
**
|
|
590
|
-
|
|
591
|
-
### `getLocale()`
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
**
|
|
596
|
-
|
|
597
|
-
### `createIntlMiddleware(options)`
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
**
|
|
602
|
-
|
|
603
|
-
- `locales: string[]` -
|
|
604
|
-
- `defaultLocale?: string` -
|
|
605
|
-
- `routes?: RoutesMap` -
|
|
606
|
-
|
|
607
|
-
### `path(routeKey, options?)`
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
**
|
|
612
|
-
|
|
613
|
-
- `routeKey: string` -
|
|
614
|
-
- `options?.locale` -
|
|
615
|
-
- `options?.params` - `Record<string, string>`
|
|
616
|
-
- `options?.encode` -
|
|
617
|
-
|
|
618
|
-
**
|
|
619
|
-
|
|
620
|
-
### `switchLocalePath(currentPath, nextLocale)`
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
**
|
|
625
|
-
|
|
626
|
-
- `currentPath: string | URL` -
|
|
627
|
-
- `nextLocale: string` -
|
|
628
|
-
|
|
629
|
-
**
|
|
630
|
-
|
|
631
|
-
---
|
|
632
|
-
|
|
633
|
-
## 🚀
|
|
634
|
-
|
|
635
|
-
###
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
```bash
|
|
640
|
-
npm run build
|
|
641
|
-
```
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
###
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
```bash
|
|
650
|
-
npm run dev
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
###
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
```bash
|
|
658
|
-
pnpm install
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
## 📦
|
|
664
|
-
|
|
665
|
-
```text
|
|
666
|
-
packages/integration/
|
|
667
|
-
├── src/
|
|
668
|
-
│ ├── adapters/
|
|
669
|
-
│ │ ├── react.ts # React
|
|
670
|
-
│ │ └── svelte.ts # Svelte
|
|
671
|
-
│ ├── core.ts # Barrel — re-
|
|
672
|
-
│ ├── framework-base.ts # parseRichSegments() —
|
|
673
|
-
│ ├── sanitize.ts #
|
|
674
|
-
│ ├── interpolation.ts # {
|
|
675
|
-
│ ├── store.ts #
|
|
676
|
-
│ ├── translations.ts # getTranslations
|
|
677
|
-
│ ├── routing.ts # path(), switchLocalePath() —
|
|
678
|
-
│ ├── middleware.ts # createIntlMiddleware()
|
|
679
|
-
│ ├── index.ts #
|
|
680
|
-
│ └── types/
|
|
681
|
-
│ └── index.ts # TypeScript
|
|
682
|
-
├── dist/ #
|
|
683
|
-
│ ├── *.js #
|
|
684
|
-
│ └── *.d.ts #
|
|
685
|
-
├── package.json
|
|
686
|
-
└── tsconfig.json
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
## 🔧 TypeScript
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
- `module: "Node16"`
|
|
694
|
-
- `declaration: true`
|
|
695
|
-
- Imports
|
|
696
|
-
|
|
697
|
-
## 📝
|
|
698
|
-
|
|
699
|
-
1. **
|
|
700
|
-
2. **dist/
|
|
701
|
-
3.
|
|
1
|
+
# astro-intl
|
|
2
|
+
|
|
3
|
+
Sistema de internacionalización simple y type-safe para Astro.
|
|
4
|
+
|
|
5
|
+
## ✨ Características
|
|
6
|
+
|
|
7
|
+
- 🔒 **Type-safe**: Autocompletado y validación de claves de traducción con TypeScript
|
|
8
|
+
- 🎯 **API simple**: Inspirada en next-intl, fácil de usar
|
|
9
|
+
- ⚛️ **Soporte React**: Adapter dedicado con `t.rich()` para rich text con componentes React. Importa desde `astro-intl/react`
|
|
10
|
+
- 🧡 **Soporte Svelte**: Adapter dedicado con `t.rich()` que retorna segmentos y componente `RichText`. Importa desde `astro-intl/svelte`
|
|
11
|
+
- 🎨 **Markup en traducciones**: Inserta HTML en strings con `t.markup()`
|
|
12
|
+
- 📁 **Namespaces**: Organiza traducciones por secciones
|
|
13
|
+
- 🌐 **Detección automática de locale**: Extrae el idioma desde la URL
|
|
14
|
+
- 🛡️ **Concurrency-safe**: Usa `AsyncLocalStorage` en SSR para aislar requests concurrentes
|
|
15
|
+
- 🌍 **Multi-runtime**: Compatible con Node.js, Cloudflare Workers y Deno
|
|
16
|
+
- ⚙️ **Default locale configurable**: Define tu locale por defecto desde las opciones
|
|
17
|
+
- 🗺️ **Routing localizado**: Define URLs traducidas por locale (`/es/sobre-nosotros` en vez de `/es/about`)
|
|
18
|
+
- 🔄 **Rewrites automáticos**: El middleware reescribe URLs traducidas a rutas canónicas del filesystem
|
|
19
|
+
- 🔗 **Generación de URLs**: `path()` y `switchLocalePath()` para construir y transformar URLs localizadas
|
|
20
|
+
- 📦 **Sub-path imports**: `astro-intl/react`, `astro-intl/svelte`, `astro-intl/routing`, `astro-intl/middleware`
|
|
21
|
+
|
|
22
|
+
## � Migración desde v1 a v2
|
|
23
|
+
|
|
24
|
+
### Breaking changes
|
|
25
|
+
|
|
26
|
+
1. **`getTranslationsReact` ya no se exporta desde `astro-intl`**. Usa `getTranslations` desde `astro-intl/react`:
|
|
27
|
+
|
|
28
|
+
```diff
|
|
29
|
+
- import { getTranslationsReact } from "astro-intl";
|
|
30
|
+
+ import { getTranslations } from "astro-intl/react";
|
|
31
|
+
|
|
32
|
+
- const t = getTranslationsReact();
|
|
33
|
+
+ const t = getTranslations();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
2. **Sub-path imports obligatorios para adapters de framework**:
|
|
37
|
+
- React: `astro-intl/react`
|
|
38
|
+
- Svelte: `astro-intl/svelte`
|
|
39
|
+
|
|
40
|
+
3. Las funciones base de Astro (`getTranslations`, `setRequestLocale`, `getLocale`, etc.) siguen exportándose desde `astro-intl` sin cambios.
|
|
41
|
+
|
|
42
|
+
### Nuevas funcionalidades
|
|
43
|
+
|
|
44
|
+
- **Adapter Svelte** con `t.rich()` y `renderRichText()`
|
|
45
|
+
- **`createGetTranslations` factory** en ambos adapters (React y Svelte) para uso standalone sin store global
|
|
46
|
+
- **`parseRichSegments()`** base agnóstica compartida
|
|
47
|
+
|
|
48
|
+
## �📦 Instalación
|
|
49
|
+
|
|
50
|
+
### Instalación automática (Recomendado)
|
|
51
|
+
|
|
52
|
+
Usa el CLI de Astro para instalar y configurar automáticamente:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx astro add astro-intl
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Este comando:
|
|
59
|
+
|
|
60
|
+
- ✅ Instala el paquete
|
|
61
|
+
- ✅ Agrega la integración a tu `astro.config.mjs`
|
|
62
|
+
- ✅ Configura las dependencias necesarias
|
|
63
|
+
|
|
64
|
+
### Instalación manual
|
|
65
|
+
|
|
66
|
+
Si prefieres instalar manualmente:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install astro-intl
|
|
70
|
+
# o
|
|
71
|
+
pnpm add astro-intl
|
|
72
|
+
# o
|
|
73
|
+
yarn add astro-intl
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Luego agrega la integración en tu `astro.config.mjs`:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import { defineConfig } from "astro/config";
|
|
80
|
+
import astroIntl from "astro-intl";
|
|
81
|
+
|
|
82
|
+
export default defineConfig({
|
|
83
|
+
integrations: [
|
|
84
|
+
astroIntl({
|
|
85
|
+
defaultLocale: "en", // opcional, por defecto es "en"
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## 🎯 Uso
|
|
92
|
+
|
|
93
|
+
### Estructura de archivos de traducción
|
|
94
|
+
|
|
95
|
+
Primero, crea tus archivos de traducción:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// src/i18n/es.json
|
|
99
|
+
{
|
|
100
|
+
"welcome": "Bienvenido",
|
|
101
|
+
"nav": {
|
|
102
|
+
"home": "Inicio",
|
|
103
|
+
"about": "Acerca de"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/i18n/en.json
|
|
108
|
+
{
|
|
109
|
+
"welcome": "Welcome",
|
|
110
|
+
"nav": {
|
|
111
|
+
"home": "Home",
|
|
112
|
+
"about": "About"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/i18n/index.ts
|
|
117
|
+
import es from './es.json';
|
|
118
|
+
import en from './en.json';
|
|
119
|
+
|
|
120
|
+
export const ui = { es, en };
|
|
121
|
+
export type Messages = typeof es;
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### En componentes Astro
|
|
125
|
+
|
|
126
|
+
```astro
|
|
127
|
+
---
|
|
128
|
+
import { setRequestLocale, getTranslations } from 'astro-intl';
|
|
129
|
+
import { ui } from '../i18n';
|
|
130
|
+
|
|
131
|
+
// Configurar el locale para esta petición
|
|
132
|
+
await setRequestLocale(Astro.url, async (locale) => ({
|
|
133
|
+
locale,
|
|
134
|
+
messages: ui[locale as keyof typeof ui]
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
// Obtener función de traducción
|
|
138
|
+
const t = getTranslations();
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
<h1>{t('welcome')}</h1>
|
|
142
|
+
<nav>
|
|
143
|
+
<a href="/">{t('nav.home')}</a>
|
|
144
|
+
<a href="/about">{t('nav.about')}</a>
|
|
145
|
+
</nav>
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Interpolación de variables
|
|
149
|
+
|
|
150
|
+
Usa `{varName}` en tus strings de traducción y pasa un objeto de valores:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
// src/i18n/en.json
|
|
154
|
+
{
|
|
155
|
+
"greeting": "Hello, {name}!",
|
|
156
|
+
"info": "You have {count} items"
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
```astro
|
|
161
|
+
---
|
|
162
|
+
const t = getTranslations();
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
<p>{t('greeting', { name: 'John' })}</p> <!-- "Hello, John!" -->
|
|
166
|
+
<p>{t('info', { count: 5 })}</p> <!-- "You have 5 items" -->
|
|
167
|
+
<p>{t('greeting')}</p> <!-- "Hello, {name}!" (sin valores, placeholder se mantiene) -->
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Los valores aceptados son `string | number | boolean`. Si una variable no se pasa o es `null`/`undefined`, el placeholder `{varName}` se mantiene sin cambios.
|
|
171
|
+
|
|
172
|
+
### Traducciones con markup (HTML en strings)
|
|
173
|
+
|
|
174
|
+
```astro
|
|
175
|
+
---
|
|
176
|
+
// src/i18n/es.json
|
|
177
|
+
// { "terms": "Acepto los <link>términos y condiciones</link>" }
|
|
178
|
+
|
|
179
|
+
const t = getTranslations();
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
<p set:html={t.markup('terms', {
|
|
183
|
+
link: (chunks) => `<a href="/terms">${chunks}</a>`
|
|
184
|
+
})} />
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Markup con interpolación
|
|
188
|
+
|
|
189
|
+
Puedes combinar variables e interpolación de tags usando el formato `{ values, tags }`:
|
|
190
|
+
|
|
191
|
+
```astro
|
|
192
|
+
---
|
|
193
|
+
// src/i18n/en.json
|
|
194
|
+
// { "welcome": "Hello {name}, click <link>here</link> to continue" }
|
|
195
|
+
|
|
196
|
+
const t = getTranslations();
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
<p set:html={t.markup('welcome', {
|
|
200
|
+
values: { name: 'John' },
|
|
201
|
+
tags: {
|
|
202
|
+
link: (chunks) => `<a href="/home">${chunks}</a>`
|
|
203
|
+
}
|
|
204
|
+
})} />
|
|
205
|
+
<!-- "Hello John, click <a href="/home">here</a> to continue" -->
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### En componentes React
|
|
209
|
+
|
|
210
|
+
> **v2**: Importa desde `astro-intl/react` en lugar de `astro-intl`.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
import { getTranslations } from "astro-intl/react";
|
|
214
|
+
|
|
215
|
+
export function MyComponent() {
|
|
216
|
+
const t = getTranslations();
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<div>
|
|
220
|
+
<h1>{t("welcome")}</h1>
|
|
221
|
+
<nav>
|
|
222
|
+
<a href="/">{t("nav.home")}</a>
|
|
223
|
+
</nav>
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### Factory standalone (sin store)
|
|
230
|
+
|
|
231
|
+
Si prefieres pasar los mensajes directamente sin depender del store global:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { createGetTranslations } from "astro-intl/react";
|
|
235
|
+
import { ui } from "../i18n";
|
|
236
|
+
|
|
237
|
+
const getT = createGetTranslations(ui, "en");
|
|
238
|
+
|
|
239
|
+
export function MyComponent({ lang }: { lang: string }) {
|
|
240
|
+
const t = getT(lang, "nav");
|
|
241
|
+
return <a href="/">{t("home")}</a>;
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Traducciones con componentes React (rich text)
|
|
246
|
+
|
|
247
|
+
```tsx
|
|
248
|
+
import { getTranslations } from "astro-intl/react";
|
|
249
|
+
|
|
250
|
+
export function MyComponent() {
|
|
251
|
+
const t = getTranslations();
|
|
252
|
+
|
|
253
|
+
// src/i18n/es.json
|
|
254
|
+
// { "terms": "Acepto los <link>términos y condiciones</link>" }
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<p>
|
|
258
|
+
{t.rich("terms", {
|
|
259
|
+
link: (chunks) => <a href="/terms">{chunks}</a>,
|
|
260
|
+
})}
|
|
261
|
+
</p>
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### En componentes Svelte
|
|
267
|
+
|
|
268
|
+
> **v2**: Nuevo adapter. Importa desde `astro-intl/svelte`.
|
|
269
|
+
|
|
270
|
+
```svelte
|
|
271
|
+
<script>
|
|
272
|
+
import { getTranslations } from 'astro-intl/svelte';
|
|
273
|
+
|
|
274
|
+
const t = getTranslations();
|
|
275
|
+
</script>
|
|
276
|
+
|
|
277
|
+
<h1>{t('welcome')}</h1>
|
|
278
|
+
<nav>
|
|
279
|
+
<a href="/">{t('nav.home')}</a>
|
|
280
|
+
</nav>
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### Rich text en Svelte
|
|
284
|
+
|
|
285
|
+
`t.rich()` retorna un array de `RichSegment[]` que puedes renderizar con `renderRichText()`:
|
|
286
|
+
|
|
287
|
+
```svelte
|
|
288
|
+
<script>
|
|
289
|
+
import { getTranslations, renderRichText } from 'astro-intl/svelte';
|
|
290
|
+
|
|
291
|
+
// { "terms": "Acepto los <link>términos y condiciones</link>" }
|
|
292
|
+
const t = getTranslations();
|
|
293
|
+
const segments = t.rich('terms', ['link']);
|
|
294
|
+
|
|
295
|
+
const html = renderRichText(segments, {
|
|
296
|
+
tags: { link: 'a' }, // renderiza como <a>...</a>
|
|
297
|
+
});
|
|
298
|
+
</script>
|
|
299
|
+
|
|
300
|
+
<p>{@html html}</p>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
También puedes usar funciones personalizadas con `components`:
|
|
304
|
+
|
|
305
|
+
```svelte
|
|
306
|
+
<script>
|
|
307
|
+
import { getTranslations, renderRichText } from 'astro-intl/svelte';
|
|
308
|
+
|
|
309
|
+
const t = getTranslations();
|
|
310
|
+
const segments = t.rich('terms', ['link']);
|
|
311
|
+
|
|
312
|
+
const html = renderRichText(segments, {
|
|
313
|
+
components: {
|
|
314
|
+
link: (chunks) => `<a href="/terms" class="underline">${chunks}</a>`,
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
</script>
|
|
318
|
+
|
|
319
|
+
<p>{@html html}</p>
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Factory standalone en Svelte (sin store)
|
|
323
|
+
|
|
324
|
+
```svelte
|
|
325
|
+
<script>
|
|
326
|
+
import { createGetTranslations } from 'astro-intl/svelte';
|
|
327
|
+
import { ui } from '../i18n';
|
|
328
|
+
|
|
329
|
+
const getT = createGetTranslations(ui, 'en');
|
|
330
|
+
|
|
331
|
+
export let lang;
|
|
332
|
+
const t = getT(lang, 'nav');
|
|
333
|
+
</script>
|
|
334
|
+
|
|
335
|
+
<a href="/">{t('home')}</a>
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Type-safety con TypeScript
|
|
339
|
+
|
|
340
|
+
```astro
|
|
341
|
+
---
|
|
342
|
+
import { setRequestLocale, getTranslations } from 'astro-intl';
|
|
343
|
+
import { ui, type Messages } from '../i18n';
|
|
344
|
+
|
|
345
|
+
await setRequestLocale(Astro.url, async (locale) => ({
|
|
346
|
+
locale,
|
|
347
|
+
messages: ui[locale as keyof typeof ui]
|
|
348
|
+
}));
|
|
349
|
+
|
|
350
|
+
// Tipado fuerte con autocompletado
|
|
351
|
+
const t = getTranslations<Messages>();
|
|
352
|
+
|
|
353
|
+
// TypeScript autocompletará las rutas válidas:
|
|
354
|
+
// t('nav.home') ✓
|
|
355
|
+
// t('nav.invalid') ✗ Error de TypeScript
|
|
356
|
+
---
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### Usar namespaces
|
|
360
|
+
|
|
361
|
+
```astro
|
|
362
|
+
---
|
|
363
|
+
// Obtener solo un namespace específico
|
|
364
|
+
const t = getTranslations<Messages>('nav');
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
<nav>
|
|
368
|
+
<a href="/">{t('home')}</a> <!-- En lugar de t('nav.home') -->
|
|
369
|
+
<a href="/about">{t('about')}</a>
|
|
370
|
+
</nav>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## �️ Routing Localizado
|
|
374
|
+
|
|
375
|
+
### Definir rutas traducidas
|
|
376
|
+
|
|
377
|
+
Crea un mapa de rutas con URLs traducidas por locale:
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
// src/i18n/routing.ts
|
|
381
|
+
export const routing = {
|
|
382
|
+
locales: ["en", "es"],
|
|
383
|
+
defaultLocale: "en",
|
|
384
|
+
routes: {
|
|
385
|
+
home: { en: "/", es: "/" },
|
|
386
|
+
about: { en: "/about", es: "/sobre-nosotros" },
|
|
387
|
+
blog: { en: "/blog/[slug]", es: "/blog/[slug]" },
|
|
388
|
+
shop: { en: "/shop/[category]/[id]", es: "/tienda/[category]/[id]" },
|
|
389
|
+
},
|
|
390
|
+
} as const;
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### Con Middleware (recomendado)
|
|
394
|
+
|
|
395
|
+
Pasa las rutas al middleware. Este reescribe automáticamente URLs traducidas a las rutas canónicas del filesystem:
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// src/middleware.ts
|
|
399
|
+
import "@/i18n/request";
|
|
400
|
+
import { createIntlMiddleware } from "astro-intl/middleware";
|
|
401
|
+
import { routing } from "@/i18n/routing";
|
|
402
|
+
|
|
403
|
+
export const onRequest = createIntlMiddleware(routing);
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Cuando un usuario visita `/es/sobre-nosotros`, el middleware lo reescribe a `/es/about` — que mapea a tu archivo `[lang]/about.astro`. Sin páginas duplicadas.
|
|
407
|
+
|
|
408
|
+
### Sin Middleware
|
|
409
|
+
|
|
410
|
+
Configura las rutas via las opciones de la integración:
|
|
411
|
+
|
|
412
|
+
```js
|
|
413
|
+
// astro.config.mjs
|
|
414
|
+
import { defineConfig } from "astro/config";
|
|
415
|
+
import astroIntl from "astro-intl";
|
|
416
|
+
|
|
417
|
+
export default defineConfig({
|
|
418
|
+
integrations: [
|
|
419
|
+
astroIntl({
|
|
420
|
+
defaultLocale: "en",
|
|
421
|
+
locales: ["en", "es"],
|
|
422
|
+
routes: {
|
|
423
|
+
about: { en: "/about", es: "/sobre-nosotros" },
|
|
424
|
+
},
|
|
425
|
+
}),
|
|
426
|
+
],
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Sin middleware no hay rewrites automáticos. Crea wrappers ligeros para cada ruta traducida:
|
|
431
|
+
|
|
432
|
+
```astro
|
|
433
|
+
---
|
|
434
|
+
// src/pages/[lang]/sobre-nosotros.astro
|
|
435
|
+
export { default } from "./about.astro";
|
|
436
|
+
export { getStaticPaths } from "./about.astro";
|
|
437
|
+
---
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Generar URLs con `path()`
|
|
441
|
+
|
|
442
|
+
```astro
|
|
443
|
+
---
|
|
444
|
+
import { path } from "astro-intl/routing";
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
<a href={path("about")}>About</a>
|
|
448
|
+
<!-- locale "en" → /en/about -->
|
|
449
|
+
<!-- locale "es" → /es/sobre-nosotros -->
|
|
450
|
+
|
|
451
|
+
<a href={path("shop", { locale: "es", params: { category: "ropa", id: "42" } })}>
|
|
452
|
+
Ver producto
|
|
453
|
+
</a>
|
|
454
|
+
<!-- → /es/tienda/ropa/42 -->
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Cambiar locale con `switchLocalePath()`
|
|
458
|
+
|
|
459
|
+
```astro
|
|
460
|
+
---
|
|
461
|
+
import { switchLocalePath } from "astro-intl/routing";
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
<a href={switchLocalePath(Astro.url.pathname, "en")}>English</a>
|
|
465
|
+
<a href={switchLocalePath(Astro.url.pathname, "es")}>Español</a>
|
|
466
|
+
<!-- En /en/about → /es/sobre-nosotros -->
|
|
467
|
+
<!-- En /es/tienda/ropa/42 → /en/shop/ropa/42 -->
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## � API Reference
|
|
471
|
+
|
|
472
|
+
### `astroIntl(options?)`
|
|
473
|
+
|
|
474
|
+
Configura la integración en `astro.config.mjs`.
|
|
475
|
+
|
|
476
|
+
**Opciones:**
|
|
477
|
+
|
|
478
|
+
- `defaultLocale?: string` - Locale por defecto cuando la URL no tiene prefijo de idioma (default: `"en"`)
|
|
479
|
+
- `enabled?: boolean` - Habilitar/deshabilitar la integración (default: `true`)
|
|
480
|
+
- `messages?: MessagesConfig` - Mensajes de traducción estáticos o dinámicos
|
|
481
|
+
- `locales?: string[]` - Lista de locales soportados
|
|
482
|
+
- `routes?: RoutesMap` - Mapa de rutas traducidas por locale
|
|
483
|
+
|
|
484
|
+
### `setRequestLocale(url, getConfig?)`
|
|
485
|
+
|
|
486
|
+
Configura el locale para la petición actual.
|
|
487
|
+
|
|
488
|
+
**Parámetros:**
|
|
489
|
+
|
|
490
|
+
- `url: URL` - El objeto URL de Astro (`Astro.url`)
|
|
491
|
+
- `getConfig?: (locale: string) => RequestConfig | Promise<RequestConfig>` - Función que retorna la configuración
|
|
492
|
+
|
|
493
|
+
**Ejemplo:**
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
await setRequestLocale(Astro.url, async (locale) => ({
|
|
497
|
+
locale,
|
|
498
|
+
messages: ui[locale],
|
|
499
|
+
}));
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### `runWithLocale(url, fn, getConfig?)`
|
|
503
|
+
|
|
504
|
+
Ejecuta una función dentro de un contexto aislado por request. Usa `AsyncLocalStorage` cuando está disponible (Node.js) para evitar race conditions en SSR con requests concurrentes.
|
|
505
|
+
|
|
506
|
+
**Parámetros:**
|
|
507
|
+
|
|
508
|
+
- `url: URL` - El objeto URL de Astro (`Astro.url`)
|
|
509
|
+
- `fn: () => R | Promise<R>` - Función a ejecutar dentro del contexto aislado
|
|
510
|
+
- `getConfig?: GetRequestConfigFn` - Función de configuración opcional
|
|
511
|
+
|
|
512
|
+
**Ejemplo en middleware:**
|
|
513
|
+
|
|
514
|
+
```ts
|
|
515
|
+
// src/middleware.ts
|
|
516
|
+
import { runWithLocale } from "astro-intl";
|
|
517
|
+
|
|
518
|
+
export const onRequest = async (context, next) => {
|
|
519
|
+
return runWithLocale(
|
|
520
|
+
context.url,
|
|
521
|
+
() => next(),
|
|
522
|
+
(locale) => ({
|
|
523
|
+
locale,
|
|
524
|
+
messages: ui[locale],
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
};
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
### `getTranslations<T>(namespace?)`
|
|
531
|
+
|
|
532
|
+
Obtiene la función de traducción para componentes Astro.
|
|
533
|
+
|
|
534
|
+
**Parámetros:**
|
|
535
|
+
|
|
536
|
+
- `namespace?: string` - Namespace opcional para obtener solo un subconjunto de traducciones
|
|
537
|
+
|
|
538
|
+
**Retorna:** Función `t(key, values?)` con método `t.markup(key, tags | { values?, tags })`
|
|
539
|
+
|
|
540
|
+
#### `t(key, values?)`
|
|
541
|
+
|
|
542
|
+
- `key: string` - Clave de traducción (soporta dot notation)
|
|
543
|
+
- `values?: Record<string, Primitive>` - Valores para interpolación `{varName}` (opcional)
|
|
544
|
+
|
|
545
|
+
#### `t.markup(key, options)`
|
|
546
|
+
|
|
547
|
+
- `key: string` - Clave de traducción
|
|
548
|
+
- `options` - Puede ser:
|
|
549
|
+
- `Record<string, (chunks: string) => string>` - Solo tags (backward compatible)
|
|
550
|
+
- `{ values?: Record<string, Primitive>, tags: Record<string, (chunks: string) => string> }` - Tags con interpolación
|
|
551
|
+
|
|
552
|
+
### `getTranslations()` — `astro-intl/react`
|
|
553
|
+
|
|
554
|
+
Obtiene la función de traducción para componentes React (usa el store global).
|
|
555
|
+
|
|
556
|
+
**Retorna:** Función `t(key)` con método `t.rich(key, tags)` que retorna `ReactNode[]`
|
|
557
|
+
|
|
558
|
+
### `createGetTranslations(ui, defaultLocale)` — `astro-intl/react`
|
|
559
|
+
|
|
560
|
+
Factory standalone que no depende del store global. Útil para pasar mensajes directamente.
|
|
561
|
+
|
|
562
|
+
**Parámetros:**
|
|
563
|
+
|
|
564
|
+
- `ui: Record<string, Record<string, unknown>>` - Objeto con todos los mensajes por locale
|
|
565
|
+
- `defaultLocale: string` - Locale por defecto
|
|
566
|
+
|
|
567
|
+
**Retorna:** `(lang, namespace) => t` — función que retorna `t(key)` con `t.rich(key, tags)`
|
|
568
|
+
|
|
569
|
+
### `getTranslations()` — `astro-intl/svelte`
|
|
570
|
+
|
|
571
|
+
Obtiene la función de traducción para componentes Svelte (usa el store global).
|
|
572
|
+
|
|
573
|
+
**Retorna:** Función `t(key)` con método `t.rich(key, tagNames?)` que retorna `RichSegment[]`
|
|
574
|
+
|
|
575
|
+
### `createGetTranslations(ui, defaultLocale)` — `astro-intl/svelte`
|
|
576
|
+
|
|
577
|
+
Factory standalone para Svelte. Misma firma que el de React pero `t.rich()` retorna `RichSegment[]`.
|
|
578
|
+
|
|
579
|
+
### `renderRichText(segments, options?)` — `astro-intl/svelte`
|
|
580
|
+
|
|
581
|
+
Resuelve un array de `RichSegment[]` en un string HTML.
|
|
582
|
+
|
|
583
|
+
**Parámetros:**
|
|
584
|
+
|
|
585
|
+
- `segments: RichSegment[]` - Segmentos retornados por `t.rich()`
|
|
586
|
+
- `options.tags?: Record<string, string>` - Mapea nombre de tag a elemento HTML (ej: `{ link: 'a' }`)
|
|
587
|
+
- `options.components?: Record<string, (chunks: string) => string>` - Funciones personalizadas por tag
|
|
588
|
+
|
|
589
|
+
**Retorna:** `string` - HTML listo para renderizar con `{@html}`
|
|
590
|
+
|
|
591
|
+
### `getLocale()`
|
|
592
|
+
|
|
593
|
+
Obtiene el locale actual configurado.
|
|
594
|
+
|
|
595
|
+
**Retorna:** `string` - El código del locale (ej: `'es'`, `'en'`)
|
|
596
|
+
|
|
597
|
+
### `createIntlMiddleware(options)`
|
|
598
|
+
|
|
599
|
+
Crea un middleware de Astro que llama automáticamente a `setRequestLocale` en cada request. Importar desde `astro-intl/middleware`.
|
|
600
|
+
|
|
601
|
+
**Opciones:**
|
|
602
|
+
|
|
603
|
+
- `locales: string[]` - Lista de locales soportados
|
|
604
|
+
- `defaultLocale?: string` - Locale por defecto (default: `"en"`)
|
|
605
|
+
- `routes?: RoutesMap` - Mapa de rutas traducidas. Cuando se proporciona, el middleware reescribe URLs traducidas a sus rutas canónicas del filesystem
|
|
606
|
+
|
|
607
|
+
### `path(routeKey, options?)`
|
|
608
|
+
|
|
609
|
+
Genera una URL localizada para una ruta nombrada. Importar desde `astro-intl/routing`.
|
|
610
|
+
|
|
611
|
+
**Parámetros:**
|
|
612
|
+
|
|
613
|
+
- `routeKey: string` - Nombre de la ruta (clave del mapa de `routes`)
|
|
614
|
+
- `options?.locale` - Locale destino (default: locale actual)
|
|
615
|
+
- `options?.params` - `Record<string, string>` para sustituir `[param]` en el template
|
|
616
|
+
- `options?.encode` - Codificar params con `encodeURIComponent` (default: `true`)
|
|
617
|
+
|
|
618
|
+
**Retorna:** `string` - URL localizada (ej: `"/es/sobre-nosotros"`)
|
|
619
|
+
|
|
620
|
+
### `switchLocalePath(currentPath, nextLocale)`
|
|
621
|
+
|
|
622
|
+
Convierte la URL actual a su equivalente en otro locale. Importar desde `astro-intl/routing`.
|
|
623
|
+
|
|
624
|
+
**Parámetros:**
|
|
625
|
+
|
|
626
|
+
- `currentPath: string | URL` - Ruta actual (pathname, URL string o URL object)
|
|
627
|
+
- `nextLocale: string` - Locale destino
|
|
628
|
+
|
|
629
|
+
**Retorna:** `string` - URL equivalente en el nuevo locale. Preserva query strings y hashes. Si no matchea ningún template, hace fallback a intercambiar el prefijo del locale.
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
## 🚀 Desarrollo (para contribuidores)
|
|
634
|
+
|
|
635
|
+
### Compilar el paquete
|
|
636
|
+
|
|
637
|
+
Antes de usar el paquete en el playground o en cualquier proyecto, debes compilarlo:
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
npm run build
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
Esto generará los archivos JavaScript y las declaraciones de tipos (`.d.ts`) en la carpeta `dist/`.
|
|
644
|
+
|
|
645
|
+
### Modo desarrollo
|
|
646
|
+
|
|
647
|
+
Para compilar automáticamente cuando hagas cambios:
|
|
648
|
+
|
|
649
|
+
```bash
|
|
650
|
+
npm run dev
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
### Después de compilar
|
|
654
|
+
|
|
655
|
+
Si estás trabajando en un monorepo con pnpm workspaces, después de compilar ejecuta:
|
|
656
|
+
|
|
657
|
+
```bash
|
|
658
|
+
pnpm install
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
Esto actualizará los enlaces simbólicos y los tipos estarán disponibles en los proyectos que usen el paquete.
|
|
662
|
+
|
|
663
|
+
## 📦 Estructura del Paquete
|
|
664
|
+
|
|
665
|
+
```text
|
|
666
|
+
packages/integration/
|
|
667
|
+
├── src/
|
|
668
|
+
│ ├── adapters/
|
|
669
|
+
│ │ ├── react.ts # Adapter React — getTranslations, createGetTranslations, t.rich() → ReactNode[]
|
|
670
|
+
│ │ └── svelte.ts # Adapter Svelte — getTranslations, createGetTranslations, t.rich() → RichSegment[], renderRichText()
|
|
671
|
+
│ ├── core.ts # Barrel — re-exporta todo desde los módulos
|
|
672
|
+
│ ├── framework-base.ts # parseRichSegments() — base agnóstica compartida por React y Svelte
|
|
673
|
+
│ ├── sanitize.ts # Validación de locale, sanitización HTML, escape regex
|
|
674
|
+
│ ├── interpolation.ts # Interpolación {variables}, acceso a valores anidados
|
|
675
|
+
│ ├── store.ts # Estado por request (AsyncLocalStorage + fallback)
|
|
676
|
+
│ ├── translations.ts # getTranslations para componentes Astro
|
|
677
|
+
│ ├── routing.ts # path(), switchLocalePath() — generación de URLs localizadas
|
|
678
|
+
│ ├── middleware.ts # createIntlMiddleware() con rewrites de rutas traducidas
|
|
679
|
+
│ ├── index.ts # Entry point público + integración de Astro
|
|
680
|
+
│ └── types/
|
|
681
|
+
│ └── index.ts # Tipos TypeScript (incluye RoutesMap)
|
|
682
|
+
├── dist/ # Archivos compilados (generados)
|
|
683
|
+
│ ├── *.js # JavaScript compilado
|
|
684
|
+
│ └── *.d.ts # Declaraciones de tipos
|
|
685
|
+
├── package.json
|
|
686
|
+
└── tsconfig.json
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
## 🔧 Configuración TypeScript
|
|
690
|
+
|
|
691
|
+
El paquete usa:
|
|
692
|
+
|
|
693
|
+
- `module: "Node16"` para soporte ESM completo
|
|
694
|
+
- `declaration: true` para generar archivos `.d.ts`
|
|
695
|
+
- Imports con extensión `.js` para compatibilidad ESM
|
|
696
|
+
|
|
697
|
+
## 📝 Notas Importantes
|
|
698
|
+
|
|
699
|
+
1. **Siempre compila antes de probar**: Los cambios en `src/` no se reflejan hasta que ejecutes `npm run build`
|
|
700
|
+
2. **Archivos dist/ en .gitignore**: Los archivos compilados no se suben a git, se generan en cada instalación
|
|
701
|
+
3. **Extensiones .js en imports**: Aunque el código fuente es TypeScript, los imports deben usar `.js` para compatibilidad con Node16/ESM
|