astro-intl 2.0.3 → 2.2.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 CHANGED
@@ -1,701 +1,701 @@
1
- # astro-intl
2
-
3
- Simple and type-safe internationalization system for Astro.
4
-
5
- ## ✨ Features
6
-
7
- - 🔒 **Type-safe**: Autocompletion and validation of translation keys with TypeScript
8
- - 🎯 **Simple API**: Inspired by next-intl, easy to use
9
- - ⚛️ **React support**: Dedicated adapter with `t.rich()` for rich text with React components. Import from `astro-intl/react`
10
- - 🧡 **Svelte support**: Dedicated adapter with `t.rich()` that returns segments and `RichText` component. Import from `astro-intl/svelte`
11
- - 🎨 **Markup in translations**: Insert HTML in strings with `t.markup()`
12
- - 📁 **Namespaces**: Organize translations by sections
13
- - 🌐 **Automatic locale detection**: Extracts the language from the URL
14
- - 🛡️ **Concurrency-safe**: Uses `AsyncLocalStorage` in SSR to isolate concurrent requests
15
- - 🌍 **Multi-runtime**: Compatible with Node.js, Cloudflare Workers and Deno
16
- - ⚙️ **Configurable default locale**: Define your default locale from options
17
- - 🗺️ **Localized routing**: Define translated URLs per locale (`/es/sobre-nosotros` instead of `/es/about`)
18
- - 🔄 **Automatic rewrites**: Middleware rewrites translated URLs to canonical filesystem routes
19
- - 🔗 **URL generation**: `path()` and `switchLocalePath()` to build and transform localized URLs
20
- - 📦 **Sub-path imports**: `astro-intl/react`, `astro-intl/svelte`, `astro-intl/routing`, `astro-intl/middleware`
21
-
22
- ## 🔄 Migration from v1 to v2
23
-
24
- ### Breaking changes
25
-
26
- 1. **`getTranslationsReact` is no longer exported from `astro-intl`**. Use `getTranslations` from `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 required for framework adapters**:
37
- - React: `astro-intl/react`
38
- - Svelte: `astro-intl/svelte`
39
-
40
- 3. Base Astro functions (`getTranslations`, `setRequestLocale`, `getLocale`, etc.) continue to be exported from `astro-intl` without changes.
41
-
42
- ### New features
43
-
44
- - **Svelte adapter** with `t.rich()` and `renderRichText()`
45
- - **`createGetTranslations` factory** in both adapters (React and Svelte) for standalone use without global store
46
- - **`parseRichSegments()`** shared framework-agnostic base
47
-
48
- ## 📦 Installation
49
-
50
- ### Automatic installation (Recommended)
51
-
52
- Use the Astro CLI to install and configure automatically:
53
-
54
- ```bash
55
- npx astro add astro-intl
56
- ```
57
-
58
- This command:
59
-
60
- - ✅ Installs the package
61
- - ✅ Adds the integration to your `astro.config.mjs`
62
- - ✅ Configures necessary dependencies
63
-
64
- ### Manual installation
65
-
66
- If you prefer to install manually:
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
- Then add the integration in your `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", // optional, defaults to "en"
86
- }),
87
- ],
88
- });
89
- ```
90
-
91
- ## 🎯 Usage
92
-
93
- ### Translation file structure
94
-
95
- First, create your translation files:
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
- ### In Astro components
125
-
126
- ```astro
127
- ---
128
- import { setRequestLocale, getTranslations } from 'astro-intl';
129
- import { ui } from '../i18n';
130
-
131
- // Configure the locale for this request
132
- await setRequestLocale(Astro.url, async (locale) => ({
133
- locale,
134
- messages: ui[locale as keyof typeof ui]
135
- }));
136
-
137
- // Get translation function
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
- ### Variable interpolation
149
-
150
- Use `{varName}` in your translation strings and pass an object of values:
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}!" (without values, placeholder remains) -->
168
- ```
169
-
170
- Accepted values are `string | number | boolean`. If a variable is not passed or is `null`/`undefined`, the placeholder `{varName}` remains unchanged.
171
-
172
- ### Translations with markup (HTML in 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 with interpolation
188
-
189
- You can combine variables and tag interpolation using the `{ values, tags }` format:
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
- ### In React components
209
-
210
- > **v2**: Import from `astro-intl/react` instead of `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
- #### Standalone factory (without store)
230
-
231
- If you prefer to pass messages directly without depending on the global store:
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
- ### Translations with React components (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
- ### In Svelte components
267
-
268
- > **v2**: New adapter. Import from `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 in Svelte
284
-
285
- `t.rich()` returns an array of `RichSegment[]` that you can render with `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' }, // renders as <a>...</a>
297
- });
298
- </script>
299
-
300
- <p>{@html html}</p>
301
- ```
302
-
303
- You can also use custom functions with `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
- #### Standalone factory in Svelte (without 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 with 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
- // Strong typing with autocompletion
351
- const t = getTranslations<Messages>();
352
-
353
- // TypeScript will autocomplete valid paths:
354
- // t('nav.home') ✓
355
- // t('nav.invalid') ✗ TypeScript error
356
- ---
357
- ```
358
-
359
- ### Using namespaces
360
-
361
- ```astro
362
- ---
363
- // Get only a specific namespace
364
- const t = getTranslations<Messages>('nav');
365
- ---
366
-
367
- <nav>
368
- <a href="/">{t('home')}</a> <!-- Instead of t('nav.home') -->
369
- <a href="/about">{t('about')}</a>
370
- </nav>
371
- ```
372
-
373
- ## 🗺️ Localized Routing
374
-
375
- ### Define translated routes
376
-
377
- Create a route map with translated URLs per 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
- ### With Middleware (recommended)
394
-
395
- Pass the routes to the middleware. It automatically rewrites translated URLs to canonical filesystem routes:
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
- When a user visits `/es/sobre-nosotros`, the middleware rewrites it to `/es/about` — which maps to your `[lang]/about.astro` file. No duplicate pages.
407
-
408
- ### Without Middleware
409
-
410
- Configure routes via integration options:
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
- Without middleware there are no automatic rewrites. Create lightweight wrappers for each translated route:
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
- ### Generate URLs with `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
- View product
453
- </a>
454
- <!-- → /es/tienda/ropa/42 -->
455
- ```
456
-
457
- ### Switch locale with `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
- <!-- On /en/about → /es/sobre-nosotros -->
467
- <!-- On /es/tienda/ropa/42 → /en/shop/ropa/42 -->
468
- ```
469
-
470
- ## 📚 API Reference
471
-
472
- ### `astroIntl(options?)`
473
-
474
- Configures the integration in `astro.config.mjs`.
475
-
476
- **Options:**
477
-
478
- - `defaultLocale?: string` - Default locale when the URL has no language prefix (default: `"en"`)
479
- - `enabled?: boolean` - Enable/disable the integration (default: `true`)
480
- - `messages?: MessagesConfig` - Static or dynamic translation messages
481
- - `locales?: string[]` - List of supported locales
482
- - `routes?: RoutesMap` - Map of translated routes per locale
483
-
484
- ### `setRequestLocale(url, getConfig?)`
485
-
486
- Configures the locale for the current request.
487
-
488
- **Parameters:**
489
-
490
- - `url: URL` - The Astro URL object (`Astro.url`)
491
- - `getConfig?: (locale: string) => RequestConfig | Promise<RequestConfig>` - Function that returns the configuration
492
-
493
- **Example:**
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
- Executes a function within a request-isolated context. Uses `AsyncLocalStorage` when available (Node.js) to avoid race conditions in SSR with concurrent requests.
505
-
506
- **Parameters:**
507
-
508
- - `url: URL` - The Astro URL object (`Astro.url`)
509
- - `fn: () => R | Promise<R>` - Function to execute within the isolated context
510
- - `getConfig?: GetRequestConfigFn` - Optional configuration function
511
-
512
- **Example in 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
- Gets the translation function for Astro components.
533
-
534
- **Parameters:**
535
-
536
- - `namespace?: string` - Optional namespace to get only a subset of translations
537
-
538
- **Returns:** Function `t(key, values?)` with method `t.markup(key, tags | { values?, tags })`
539
-
540
- #### `t(key, values?)`
541
-
542
- - `key: string` - Translation key (supports dot notation)
543
- - `values?: Record<string, Primitive>` - Values for `{varName}` interpolation (optional)
544
-
545
- #### `t.markup(key, options)`
546
-
547
- - `key: string` - Translation key
548
- - `options` - Can be:
549
- - `Record<string, (chunks: string) => string>` - Tags only (backward compatible)
550
- - `{ values?: Record<string, Primitive>, tags: Record<string, (chunks: string) => string> }` - Tags with interpolation
551
-
552
- ### `getTranslations()` — `astro-intl/react`
553
-
554
- Gets the translation function for React components (uses the global store).
555
-
556
- **Returns:** Function `t(key)` with method `t.rich(key, tags)` that returns `ReactNode[]`
557
-
558
- ### `createGetTranslations(ui, defaultLocale)` — `astro-intl/react`
559
-
560
- Standalone factory that doesn't depend on the global store. Useful for passing messages directly.
561
-
562
- **Parameters:**
563
-
564
- - `ui: Record<string, Record<string, unknown>>` - Object with all messages per locale
565
- - `defaultLocale: string` - Default locale
566
-
567
- **Returns:** `(lang, namespace) => t` — function that returns `t(key)` with `t.rich(key, tags)`
568
-
569
- ### `getTranslations()` — `astro-intl/svelte`
570
-
571
- Gets the translation function for Svelte components (uses the global store).
572
-
573
- **Returns:** Function `t(key)` with method `t.rich(key, tagNames?)` that returns `RichSegment[]`
574
-
575
- ### `createGetTranslations(ui, defaultLocale)` — `astro-intl/svelte`
576
-
577
- Standalone factory for Svelte. Same signature as React but `t.rich()` returns `RichSegment[]`.
578
-
579
- ### `renderRichText(segments, options?)` — `astro-intl/svelte`
580
-
581
- Resolves an array of `RichSegment[]` into an HTML string.
582
-
583
- **Parameters:**
584
-
585
- - `segments: RichSegment[]` - Segments returned by `t.rich()`
586
- - `options.tags?: Record<string, string>` - Maps tag name to HTML element (e.g., `{ link: 'a' }`)
587
- - `options.components?: Record<string, (chunks: string) => string>` - Custom functions per tag
588
-
589
- **Returns:** `string` - HTML ready to render with `{@html}`
590
-
591
- ### `getLocale()`
592
-
593
- Gets the currently configured locale.
594
-
595
- **Returns:** `string` - The locale code (e.g., `'es'`, `'en'`)
596
-
597
- ### `createIntlMiddleware(options)`
598
-
599
- Creates an Astro middleware that automatically calls `setRequestLocale` on each request. Import from `astro-intl/middleware`.
600
-
601
- **Options:**
602
-
603
- - `locales: string[]` - List of supported locales
604
- - `defaultLocale?: string` - Default locale (default: `"en"`)
605
- - `routes?: RoutesMap` - Map of translated routes. When provided, the middleware rewrites translated URLs to their canonical filesystem routes
606
-
607
- ### `path(routeKey, options?)`
608
-
609
- Generates a localized URL for a named route. Import from `astro-intl/routing`.
610
-
611
- **Parameters:**
612
-
613
- - `routeKey: string` - Route name (key from the `routes` map)
614
- - `options?.locale` - Target locale (default: current locale)
615
- - `options?.params` - `Record<string, string>` to substitute `[param]` in the template
616
- - `options?.encode` - Encode params with `encodeURIComponent` (default: `true`)
617
-
618
- **Returns:** `string` - Localized URL (e.g., `"/es/sobre-nosotros"`)
619
-
620
- ### `switchLocalePath(currentPath, nextLocale)`
621
-
622
- Converts the current URL to its equivalent in another locale. Import from `astro-intl/routing`.
623
-
624
- **Parameters:**
625
-
626
- - `currentPath: string | URL` - Current path (pathname, URL string or URL object)
627
- - `nextLocale: string` - Target locale
628
-
629
- **Returns:** `string` - Equivalent URL in the new locale. Preserves query strings and hashes. If no template matches, falls back to swapping the locale prefix.
630
-
631
- ---
632
-
633
- ## 🚀 Development (for contributors)
634
-
635
- ### Build the package
636
-
637
- Before using the package in the playground or any project, you must build it:
638
-
639
- ```bash
640
- npm run build
641
- ```
642
-
643
- This will generate the JavaScript files and type declarations (`.d.ts`) in the `dist/` folder.
644
-
645
- ### Development mode
646
-
647
- To automatically compile when you make changes:
648
-
649
- ```bash
650
- npm run dev
651
- ```
652
-
653
- ### After building
654
-
655
- If you're working in a monorepo with pnpm workspaces, after building run:
656
-
657
- ```bash
658
- pnpm install
659
- ```
660
-
661
- This will update the symbolic links and types will be available in projects that use the package.
662
-
663
- ## 📦 Package Structure
664
-
665
- ```text
666
- packages/integration/
667
- ├── src/
668
- │ ├── adapters/
669
- │ │ ├── react.ts # React Adapter — getTranslations, createGetTranslations, t.rich() → ReactNode[]
670
- │ │ └── svelte.ts # Svelte Adapter — getTranslations, createGetTranslations, t.rich() → RichSegment[], renderRichText()
671
- │ ├── core.ts # Barrel — re-exports everything from modules
672
- │ ├── framework-base.ts # parseRichSegments() — framework-agnostic base shared by React and Svelte
673
- │ ├── sanitize.ts # Locale validation, HTML sanitization, regex escape
674
- │ ├── interpolation.ts # {variable} interpolation, nested value access
675
- │ ├── store.ts # Per-request state (AsyncLocalStorage + fallback)
676
- │ ├── translations.ts # getTranslations for Astro components
677
- │ ├── routing.ts # path(), switchLocalePath() — localized URL generation
678
- │ ├── middleware.ts # createIntlMiddleware() with translated route rewrites
679
- │ ├── index.ts # Public entry point + Astro integration
680
- │ └── types/
681
- │ └── index.ts # TypeScript types (includes RoutesMap)
682
- ├── dist/ # Compiled files (generated)
683
- │ ├── *.js # Compiled JavaScript
684
- │ └── *.d.ts # Type declarations
685
- ├── package.json
686
- └── tsconfig.json
687
- ```
688
-
689
- ## 🔧 TypeScript Configuration
690
-
691
- The package uses:
692
-
693
- - `module: "Node16"` for full ESM support
694
- - `declaration: true` to generate `.d.ts` files
695
- - Imports with `.js` extension for ESM compatibility
696
-
697
- ## 📝 Important Notes
698
-
699
- 1. **Always build before testing**: Changes in `src/` are not reflected until you run `npm run build`
700
- 2. **dist/ files in .gitignore**: Compiled files are not uploaded to git, they are generated on each installation
701
- 3. **.js extensions in imports**: Although the source code is TypeScript, imports must use `.js` for Node16/ESM compatibility
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