@edgekits/astro-i18n 0.1.8 → 0.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/CHANGELOG.md +22 -0
- package/README.md +87 -23
- package/dist/integration/index.d.ts +25 -4
- package/dist/integration/index.js +35 -9
- package/dist/integration/vite-plugin.d.ts +1 -1
- package/dist/types.d.ts +7 -55
- package/dist/types.js +1 -44
- package/package.json +7 -17
- package/src/components/LanguageSwitcher.astro +89 -93
- package/src/config.entry.ts +5 -36
- package/src/constants.ts +5 -3
- package/src/cookie-storage.ts +19 -25
- package/src/fetcher.ts +3 -304
- package/src/format.ts +3 -107
- package/src/instance.ts +44 -0
- package/src/integration/index.ts +38 -9
- package/src/integration/vite-plugin.ts +1 -1
- package/src/middleware/i18n.ts +232 -114
- package/src/resolve-locale.ts +7 -39
- package/src/runtime.ts +2 -2
- package/src/schema.ts +3 -11
- package/src/translations-keys.ts +7 -38
- package/src/types.ts +16 -116
- package/dist/cli/generate.d.ts +0 -50
- package/dist/cli/generate.js +0 -160
- package/dist/cli/hashes.d.ts +0 -19
- package/dist/cli/hashes.js +0 -42
- package/dist/cli/index.d.ts +0 -2
- package/dist/cli/index.js +0 -372
- package/dist/cli/key-prefix-check.d.ts +0 -6
- package/dist/cli/key-prefix-check.js +0 -27
- package/dist/cli/load-config.d.ts +0 -7
- package/dist/cli/load-config.js +0 -19
- package/dist/cli/purge.d.ts +0 -29
- package/dist/cli/purge.js +0 -94
- package/dist/cli/wrangler.d.ts +0 -10
- package/dist/cli/wrangler.js +0 -71
- package/dist/cli/zone.d.ts +0 -21
- package/dist/cli/zone.js +0 -42
- package/dist/key-format.d.ts +0 -8
- package/dist/key-format.js +0 -15
- package/src/accept-language.ts +0 -56
- package/src/assemble.ts +0 -29
- package/src/cli/generate.ts +0 -205
- package/src/cli/hashes.ts +0 -66
- package/src/cli/index.ts +0 -472
- package/src/cli/key-prefix-check.ts +0 -31
- package/src/cli/load-config.ts +0 -26
- package/src/cli/purge.ts +0 -122
- package/src/cli/wrangler.ts +0 -96
- package/src/cli/zone.ts +0 -64
- package/src/deep-merge.ts +0 -38
- package/src/key-format.ts +0 -17
- package/src/locale-fallback.ts +0 -58
- package/src/markdown-path.ts +0 -31
- package/src/parse-boolean.ts +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to this package are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the package uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.2.0 — 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- The package is an adapter over [`@edgekits/i18n-core`](https://github.com/EdgeKits/i18n/tree/main/packages/core#readme), the framework-free engine extracted from it. Same entries, same exports, same generated files. The `edgekits-i18n` command now ships with the core and reaches your project's `.bin` through this package's dependency; `jiti` and `jsonc-parser` are no longer direct dependencies of this package. Config validation errors are prefixed `[@edgekits/i18n-core]`.
|
|
12
|
+
- The switcher's client script builds its target URL through the routing mode, so the one script Astro inlines per page changes.
|
|
13
|
+
- A project's own Astro `i18n` block must keep `prefixDefaultLocale: true`, in both routing modes (the build stops otherwise): Astro checks the URL after the middleware, on the rewritten `/<defaultLocale>/…` path, and with `false` it would answer 404 there.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `routing: 'prefix-except-default'`: the default locale at the root (`/`, `/about/`), the other locales under their prefix (`/de/about/`). It is the default when `routing` is left out, so new projects get it. Configs that say `routing: 'prefix'` are unchanged. See README "Routing modes".
|
|
18
|
+
- `localizedPath` in `/runtime` (and `./format`): the path of a page in a locale for either mode, for links, canonical, hreflang and sitemaps.
|
|
19
|
+
- `<language-switcher>` carries `data-routing` and `data-default-locale`.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- After a failed KV read, the fallback text is no longer written to the edge cache for the requested locale. It was cached for a year, so one KV error could leave a locale showing default-locale text until the next `migrate` purged it.
|
|
24
|
+
|
|
25
|
+
### Docs
|
|
26
|
+
|
|
27
|
+
- The skill asks the routing question, its SEO references build links, canonical, hreflang and the sitemap with `localizedPath`, and the plugin ships a second skill, `tanstack-i18n`.
|
|
28
|
+
|
|
7
29
|
## 0.1.8 — 2026-10-18
|
|
8
30
|
|
|
9
31
|
### Added
|
package/README.md
CHANGED
|
@@ -69,23 +69,24 @@ export default defineI18nConfig({
|
|
|
69
69
|
keyPrefix: 'example.com',
|
|
70
70
|
// Whether MissingTranslationBanner may render.
|
|
71
71
|
missingTranslationBanner: true,
|
|
72
|
-
// Every URL starts with its locale (/en/, /de/about/).
|
|
72
|
+
// Every URL starts with its locale (/en/, /de/about/). Leave it out for the default,
|
|
73
|
+
// 'prefix-except-default': the default locale at the root (see Routing modes).
|
|
73
74
|
routing: 'prefix',
|
|
74
75
|
})
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
| Field | Required | Default
|
|
78
|
-
| -------------------------- | -------- |
|
|
79
|
-
| `languages` | yes |
|
|
80
|
-
| `defaultLocale` | yes |
|
|
81
|
-
| `cookie` | yes |
|
|
82
|
-
| `keyPrefix` | yes |
|
|
83
|
-
| `missingTranslationBanner` | yes |
|
|
84
|
-
| `routing` |
|
|
85
|
-
| `localesDir` | no | `'locales'`
|
|
86
|
-
| `generatedDir` | no | `'src/i18n'`
|
|
87
|
-
| `countryToLocale` | no | none
|
|
88
|
-
| `markdownLocale` | no | none
|
|
78
|
+
| Field | Required | Default | Meaning |
|
|
79
|
+
| -------------------------- | -------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `languages` | yes | | `{ [locale]: { name, countryCode? } }`; `countryCode` is optional: the code your flag component receives |
|
|
81
|
+
| `defaultLocale` | yes | | one of `languages`; its JSON files define the schema |
|
|
82
|
+
| `cookie` | yes | | `{ name, maxAge }` (seconds) of the language cookie |
|
|
83
|
+
| `keyPrefix` | yes | | a hostname of your zone, or a unique name without a dot: KV keys are `<keyPrefix>:<namespace>:<locale>` and every cache URL is `https://<keyPrefix>/…` (see [Deployment targets](#deployment-targets)) |
|
|
84
|
+
| `missingTranslationBanner` | yes | | enables `MissingTranslationBanner` |
|
|
85
|
+
| `routing` | no | `'prefix-except-default'` | `'prefix-except-default'`: the default locale at the root (`/about/`), the others prefixed (`/de/about/`); `'prefix'`: every URL starts with its locale (`/en/about/`). See [Routing modes](#routing-modes) |
|
|
86
|
+
| `localesDir` | no | `'locales'` | where the CLI reads `<locale>/<namespace>.json` |
|
|
87
|
+
| `generatedDir` | no | `'src/i18n'` | where the CLI writes `runtime-constants.ts` and `fallbacks.generated.ts` |
|
|
88
|
+
| `countryToLocale` | no | none | geo-IP step of the locale fallback (see Reference) |
|
|
89
|
+
| `markdownLocale` | no | none | the locale `.md` endpoints are served in (see Reference) |
|
|
89
90
|
|
|
90
91
|
Locale codes (the keys of `languages`) are lowercase everywhere: the config, the `locales/`
|
|
91
92
|
folders and the URLs (`pt-br`, not `pt-BR`). `toIetfTag()` from `./format` gives the IETF form
|
|
@@ -117,10 +118,10 @@ The integration sets up:
|
|
|
117
118
|
- a Vite plugin serving the config and your generated files to the runtime as virtual modules;
|
|
118
119
|
- `vite.ssr.noExternal` for the package, whose runtime ships as source;
|
|
119
120
|
- Astro's `i18n` block, when `astro.config` has none: `locales` from `languages`, your
|
|
120
|
-
`defaultLocale`, `routing: { prefixDefaultLocale: true, redirectToDefaultLocale: false }
|
|
121
|
-
Astro needs it to set `Astro.currentLocale`, which `resolveLocale` reads. If you
|
|
122
|
-
block, its `locales` must include every configured locale
|
|
123
|
-
or the build stops with a message saying which;
|
|
121
|
+
`defaultLocale`, `routing: { prefixDefaultLocale: true, redirectToDefaultLocale: false }` in
|
|
122
|
+
both modes. Astro needs it to set `Astro.currentLocale`, which `resolveLocale` reads. If you
|
|
123
|
+
keep your own block, its `locales` must include every configured locale, its `defaultLocale`
|
|
124
|
+
must match, and its `routing` must be that one, or the build stops with a message saying which;
|
|
124
125
|
- the `Locale` type: the union of your locales, for the package's exports;
|
|
125
126
|
- type declarations for its virtual modules, so `tsc` and `astro check` accept the package's
|
|
126
127
|
runtime source in your program;
|
|
@@ -136,9 +137,10 @@ The integration sets up:
|
|
|
136
137
|
export const onRequest = sequence(i18n, yourOwnMiddleware)
|
|
137
138
|
```
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
`
|
|
141
|
-
|
|
140
|
+
In `'prefix'` the middleware redirects a URL without a locale (`/`, `/about/`) to a localized
|
|
141
|
+
one; in `'prefix-except-default'` such a URL is a default-locale page (see
|
|
142
|
+
[Routing modes](#routing-modes)). Either way it sets `Astro.locals.uiLocale` (the URL's locale)
|
|
143
|
+
and `Astro.locals.translationLocale` (the locale to fetch translations for) on every request.
|
|
142
144
|
|
|
143
145
|
### 5. Write translations
|
|
144
146
|
|
|
@@ -174,6 +176,9 @@ its first element, and a locale that has the list replaces it whole.
|
|
|
174
176
|
npx edgekits-i18n seed
|
|
175
177
|
```
|
|
176
178
|
|
|
179
|
+
The `edgekits-i18n` command comes from `@edgekits/i18n-core`, which this package depends on, so
|
|
180
|
+
it is in your project's `node_modules/.bin` with no extra install.
|
|
181
|
+
|
|
177
182
|
| Command | Writes | Then |
|
|
178
183
|
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
179
184
|
| `edgekits-i18n bundle` | `i18n-data.json` (the KV payload), `src/i18n.generated.d.ts` (the schema), `<generatedDir>/runtime-constants.ts` | nothing else |
|
|
@@ -306,6 +311,56 @@ export default defineI18nConfig({
|
|
|
306
311
|
Use it when your markdown endpoints exist in one locale only. Without it, the package assumes
|
|
307
312
|
nothing about them.
|
|
308
313
|
|
|
314
|
+
In `'prefix-except-default'` the default locale has no prefix here either: with
|
|
315
|
+
`markdownLocale: 'en'` (the default locale) the canonical form is `/blog/x.md`, and
|
|
316
|
+
`/en/blog/x.md` redirects to it. A `.md` path without a locale segment is served from
|
|
317
|
+
`src/pages/[lang]/` as the default locale's, like any root page.
|
|
318
|
+
|
|
319
|
+
### Routing modes
|
|
320
|
+
|
|
321
|
+
`routing` decides where the default locale lives. Pages stay in `src/pages/[lang]/…` in both.
|
|
322
|
+
|
|
323
|
+
- `'prefix-except-default'` (the default when `routing` is left out): the default locale at the
|
|
324
|
+
root (`/`, `/about/`), every other locale under its prefix (`/de/about/`). The middleware
|
|
325
|
+
serves a root URL by rewriting it internally to `/<defaultLocale>/…`, so the URL stays;
|
|
326
|
+
`/en/about/` redirects permanently to `/about/`. A root URL consults neither Accept-Language
|
|
327
|
+
nor the country: only a lang cookie for another locale (the visitor chose it in the switcher)
|
|
328
|
+
redirects, temporarily, to that locale.
|
|
329
|
+
- `'prefix'`: every URL starts with its locale (`/en/about/`); `/` and a URL without a locale
|
|
330
|
+
redirect to the visitor's locale (cookie, Accept-Language, country, default). Projects that
|
|
331
|
+
set `routing: 'prefix'` keep exactly this by leaving their config alone.
|
|
332
|
+
|
|
333
|
+
In `'prefix-except-default'`, build canonical and hreflang URLs from `Astro.originPathname`
|
|
334
|
+
(the URL the visitor requested; `Astro.url` is the rewritten `/<defaultLocale>/…`) through
|
|
335
|
+
`localizedPath` from `/runtime`. Astro's own `astro:i18n` URL helpers prefix the default locale
|
|
336
|
+
here, so `localizedPath` is the one to use:
|
|
337
|
+
|
|
338
|
+
```astro
|
|
339
|
+
---
|
|
340
|
+
import { DEFAULT_LOCALE, I18N_CONFIG, SUPPORTED_LOCALES, localizedPath, toIetfTag } from '@edgekits/astro-i18n/runtime'
|
|
341
|
+
|
|
342
|
+
const options = {
|
|
343
|
+
routing: I18N_CONFIG.routing,
|
|
344
|
+
defaultLocale: DEFAULT_LOCALE,
|
|
345
|
+
isLocale: (segment: string) => (SUPPORTED_LOCALES as readonly string[]).includes(segment),
|
|
346
|
+
}
|
|
347
|
+
const path = Astro.originPathname
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
<link rel="canonical" href={new URL(localizedPath(Astro.locals.uiLocale, path, options), Astro.site)} />
|
|
351
|
+
{SUPPORTED_LOCALES.map((locale) => <link rel="alternate" hreflang={toIetfTag(locale)} href={new URL(localizedPath(locale, path, options), Astro.site)} />)}
|
|
352
|
+
<link rel="alternate" hreflang="x-default" href={new URL(localizedPath(DEFAULT_LOCALE, path, options), Astro.site)} />
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Two limits:
|
|
356
|
+
|
|
357
|
+
- A root path whose first segment equals a locale code is read as that locale: a default-locale
|
|
358
|
+
page at `/de/` (a content slug `de`) cannot exist next to the German site.
|
|
359
|
+
- A shared HTML cache in front of the site must key root paths on the lang cookie, or skip
|
|
360
|
+
caching requests that carry one: the same URL renders for a cookie-less visitor and
|
|
361
|
+
redirects one whose cookie names another locale. Under `'prefix'`, `/en/` is in the same
|
|
362
|
+
situation: it answers a visitor whose cookie names another locale with a `Set-Cookie`.
|
|
363
|
+
|
|
309
364
|
### Styling the components
|
|
310
365
|
|
|
311
366
|
`LanguageSwitcher` and `MissingTranslationBanner` are headless: they carry structure, behaviour
|
|
@@ -438,7 +493,7 @@ per render.
|
|
|
438
493
|
`astro-i18n` is a free agent skill that sets up `@edgekits/astro-i18n` in your project and keeps
|
|
439
494
|
working with it afterwards. It runs in Claude Code as a plugin, and in Cursor, OpenCode or any other
|
|
440
495
|
coding agent that reads `SKILL.md`. Its source is in
|
|
441
|
-
[`plugins/astro-i18n`](https://github.com/EdgeKits/
|
|
496
|
+
[`plugins/astro-i18n`](https://github.com/EdgeKits/i18n/tree/main/plugins/astro-i18n).
|
|
442
497
|
|
|
443
498
|
### What it does
|
|
444
499
|
|
|
@@ -510,7 +565,7 @@ folder. In both cases the plugin itself is still installed once per contributor
|
|
|
510
565
|
Any other agent, with the [skills CLI](https://skills.sh):
|
|
511
566
|
|
|
512
567
|
```sh
|
|
513
|
-
npx skills add EdgeKits/
|
|
568
|
+
npx skills add EdgeKits/i18n --skill astro-i18n
|
|
514
569
|
```
|
|
515
570
|
|
|
516
571
|
### Prerequisites
|
|
@@ -551,12 +606,18 @@ The skill asks before it writes any code, and waits for the answers:
|
|
|
551
606
|
| Should a visitor's country pick the locale when nothing else does? How? | `DE`, `AT`, `CH` → `de`; `BR` → `pt-br` |
|
|
552
607
|
| Middleware: registered by the integration (`auto`), or in your own sequence? | `auto` |
|
|
553
608
|
| Flags in the language switcher? | Yes: `en` → `us`, `de` → `de`, `pt-br` → `br` |
|
|
609
|
+
| Routing: the default locale at the root, or every locale prefixed? | At the root |
|
|
554
610
|
|
|
555
611
|
For flags, a regional locale takes its region (`pt-br` → `br`, `en-gb` → `gb`), and for a
|
|
556
612
|
language-only locale the skill proposes the obvious country (`en` → `us`) for you to confirm. It
|
|
557
613
|
shows the full list back before it writes the config. If you have no preference on any of
|
|
558
614
|
these, it uses the defaults: no geo table, no markdown locale, `auto` middleware, flags on.
|
|
559
615
|
|
|
616
|
+
For routing, the default locale at the root (`/`, `/about/`; other locales prefixed) is
|
|
617
|
+
`routing: 'prefix-except-default'`, the default; every locale prefixed (`/en/about/`) is
|
|
618
|
+
`routing: 'prefix'`. The skill asks only with two or more locales; with one, the root mode is
|
|
619
|
+
the only sensible one and needs no question.
|
|
620
|
+
|
|
560
621
|
#### What it creates
|
|
561
622
|
|
|
562
623
|
- `edgekits-i18n.config.ts` with your answers, and the integration in `astro.config`.
|
|
@@ -566,11 +627,14 @@ these, it uses the defaults: no geo table, no markdown locale, `auto` middleware
|
|
|
566
627
|
`src/i18n.generated.d.ts`, `src/i18n/runtime-constants.ts`, `src/i18n/fallbacks.generated.ts`.
|
|
567
628
|
Commit them.
|
|
568
629
|
- The route layout: `src/pages/about.astro` becomes `src/pages/[lang]/about.astro`, and so on.
|
|
569
|
-
The root `index.astro` moves too
|
|
630
|
+
The root `index.astro` moves too: in `'prefix'` the middleware redirects `/` to
|
|
631
|
+
`/<locale>/`, and in `'prefix-except-default'` it serves the default locale's page there.
|
|
570
632
|
Endpoints that are not localized (`robots.txt`, `sitemap.xml`, `/api/*`) and `public/` stay
|
|
571
633
|
where they are.
|
|
572
634
|
- `src/components/Flag.astro` and one SVG per flag in `public/flags/`, if you chose flags (the
|
|
573
635
|
recipe is below).
|
|
636
|
+
- The language switcher and the hreflang tags work in both routing modes: the hreflang
|
|
637
|
+
component builds its URLs with `localizedPath`, which knows the mode.
|
|
574
638
|
- A KV namespace on your Cloudflare account (`wrangler kv namespace create`). The skill asks
|
|
575
639
|
before it runs that command. Until then, local development works with a placeholder id of 32
|
|
576
640
|
hexadecimal characters, such as `00000000000000000000000000000000`: the CLI checks the id's
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AstroConfig, AstroIntegration } from 'astro';
|
|
2
|
-
import { type I18nConfig } from '
|
|
2
|
+
import { type I18nConfig, type I18nRouting } from '@edgekits/i18n-core';
|
|
3
3
|
export type EdgekitsI18nOptions = {
|
|
4
4
|
middleware?: 'auto' | 'manual';
|
|
5
5
|
};
|
|
@@ -10,6 +10,14 @@ export declare const PACKAGE_NAME = "@edgekits/astro-i18n";
|
|
|
10
10
|
* plugin's virtual modules.
|
|
11
11
|
*/
|
|
12
12
|
export declare function edgekitsI18n(config: I18nConfig, options?: EdgekitsI18nOptions): AstroIntegration;
|
|
13
|
+
/**
|
|
14
|
+
* The validated config with `routing` resolved: absent means 'prefix-except-default', the
|
|
15
|
+
* mode for new projects. Resolved here, before the Vite plugin serialises it, so the
|
|
16
|
+
* virtual config (and `I18N_CONFIG`) always carries the mode the middleware runs.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveConfig(config: I18nConfig): I18nConfig & {
|
|
19
|
+
routing: I18nRouting;
|
|
20
|
+
};
|
|
13
21
|
/**
|
|
14
22
|
* Augments `I18nLocales` with the configured locales, so `Locale` narrows from
|
|
15
23
|
* `string` to their union in the project, through both entries. The `export {}` keeps
|
|
@@ -19,8 +27,21 @@ export declare function edgekitsI18n(config: I18nConfig, options?: EdgekitsI18nO
|
|
|
19
27
|
export declare function localesDeclaration(locales: readonly string[]): string;
|
|
20
28
|
/**
|
|
21
29
|
* The Astro `i18n` block set when the project has none: locales and default from the
|
|
22
|
-
* config;
|
|
23
|
-
* does not). Complete, `fallbackType` included: Astro does not
|
|
24
|
-
* `updateConfig` merges, so its schema defaults never fill a partial block.
|
|
30
|
+
* config; routed as locale-prefixed in both modes (the middleware redirects `/` itself in
|
|
31
|
+
* 'prefix', so Astro does not). Complete, `fallbackType` included: Astro does not
|
|
32
|
+
* re-validate what `updateConfig` merges, so its schema defaults never fill a partial block.
|
|
33
|
+
*
|
|
34
|
+
* `prefixDefaultLocale: true` in 'prefix-except-default' too: Astro's own i18n check runs
|
|
35
|
+
* after the middleware, on the rewritten URL (astro/dist/core/routing/handler.js:128-130),
|
|
36
|
+
* and with `prefixDefaultLocale: false` the strategy `pathname-prefix-other-locales`
|
|
37
|
+
* answers notFound to any path carrying the default locale (astro/dist/i18n/router.js:
|
|
38
|
+
* 104-121) — the rewritten `/<default>/about/` is exactly that. With `true` the check
|
|
39
|
+
* passes and `Astro.currentLocale` works on root pages.
|
|
25
40
|
*/
|
|
26
41
|
export declare function astroI18n(config: I18nConfig): NonNullable<AstroConfig['i18n']>;
|
|
42
|
+
/**
|
|
43
|
+
* A project's own Astro `i18n` block must cover every configured locale (as a string
|
|
44
|
+
* entry, a `path` or one of its `codes`), share the default locale, and route as
|
|
45
|
+
* `astroI18n` does (`prefixDefaultLocale: true`, `redirectToDefaultLocale: false`).
|
|
46
|
+
*/
|
|
47
|
+
export declare function assertAstroI18n(block: NonNullable<AstroConfig['i18n']>, config: I18nConfig): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_GENERATED_DIR, defineI18nConfig } from
|
|
1
|
+
import { DEFAULT_GENERATED_DIR, defineI18nConfig } from '@edgekits/i18n-core';
|
|
2
2
|
import { i18nVitePlugin } from "./vite-plugin.js";
|
|
3
3
|
import { virtualModulesDeclaration } from "./virtual-types.js";
|
|
4
4
|
const MIDDLEWARE_MODES = ['auto', 'manual'];
|
|
@@ -9,7 +9,7 @@ export const PACKAGE_NAME = '@edgekits/astro-i18n';
|
|
|
9
9
|
* plugin's virtual modules.
|
|
10
10
|
*/
|
|
11
11
|
export function edgekitsI18n(config, options = {}) {
|
|
12
|
-
const i18nConfig =
|
|
12
|
+
const i18nConfig = resolveConfig(config);
|
|
13
13
|
const middleware = options.middleware ?? 'auto';
|
|
14
14
|
if (!MIDDLEWARE_MODES.includes(middleware)) {
|
|
15
15
|
throw new Error(`[@edgekits/astro-i18n] \`middleware\` "${String(middleware)}" is not supported; use 'auto' or 'manual'.`);
|
|
@@ -45,8 +45,18 @@ export function edgekitsI18n(config, options = {}) {
|
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
/**
|
|
49
|
+
* The validated config with `routing` resolved: absent means 'prefix-except-default', the
|
|
50
|
+
* mode for new projects. Resolved here, before the Vite plugin serialises it, so the
|
|
51
|
+
* virtual config (and `I18N_CONFIG`) always carries the mode the middleware runs.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveConfig(config) {
|
|
54
|
+
const validated = defineI18nConfig(config);
|
|
55
|
+
return { ...validated, routing: validated.routing ?? 'prefix-except-default' };
|
|
56
|
+
}
|
|
57
|
+
// Each entry that exports `I18nLocales`: both re-export the core's interface (root through
|
|
58
|
+
// dist/types.d.ts, runtime through the shipped src/types.ts), so either augmentation
|
|
59
|
+
// merges into it; both are kept so neither entry depends on how the other resolves.
|
|
50
60
|
const LOCALE_MODULES = ['@edgekits/astro-i18n', '@edgekits/astro-i18n/runtime'];
|
|
51
61
|
/**
|
|
52
62
|
* Augments `I18nLocales` with the configured locales, so `Locale` narrows from
|
|
@@ -71,9 +81,16 @@ export function localesDeclaration(locales) {
|
|
|
71
81
|
}
|
|
72
82
|
/**
|
|
73
83
|
* The Astro `i18n` block set when the project has none: locales and default from the
|
|
74
|
-
* config;
|
|
75
|
-
* does not). Complete, `fallbackType` included: Astro does not
|
|
76
|
-
* `updateConfig` merges, so its schema defaults never fill a partial block.
|
|
84
|
+
* config; routed as locale-prefixed in both modes (the middleware redirects `/` itself in
|
|
85
|
+
* 'prefix', so Astro does not). Complete, `fallbackType` included: Astro does not
|
|
86
|
+
* re-validate what `updateConfig` merges, so its schema defaults never fill a partial block.
|
|
87
|
+
*
|
|
88
|
+
* `prefixDefaultLocale: true` in 'prefix-except-default' too: Astro's own i18n check runs
|
|
89
|
+
* after the middleware, on the rewritten URL (astro/dist/core/routing/handler.js:128-130),
|
|
90
|
+
* and with `prefixDefaultLocale: false` the strategy `pathname-prefix-other-locales`
|
|
91
|
+
* answers notFound to any path carrying the default locale (astro/dist/i18n/router.js:
|
|
92
|
+
* 104-121) — the rewritten `/<default>/about/` is exactly that. With `true` the check
|
|
93
|
+
* passes and `Astro.currentLocale` works on root pages.
|
|
77
94
|
*/
|
|
78
95
|
export function astroI18n(config) {
|
|
79
96
|
return {
|
|
@@ -84,9 +101,10 @@ export function astroI18n(config) {
|
|
|
84
101
|
}
|
|
85
102
|
/**
|
|
86
103
|
* A project's own Astro `i18n` block must cover every configured locale (as a string
|
|
87
|
-
* entry, a `path` or one of its `codes`)
|
|
104
|
+
* entry, a `path` or one of its `codes`), share the default locale, and route as
|
|
105
|
+
* `astroI18n` does (`prefixDefaultLocale: true`, `redirectToDefaultLocale: false`).
|
|
88
106
|
*/
|
|
89
|
-
function assertAstroI18n(block, config) {
|
|
107
|
+
export function assertAstroI18n(block, config) {
|
|
90
108
|
const declared = new Set(block.locales.flatMap((entry) => (typeof entry === 'string' ? [entry] : [entry.path, ...entry.codes])));
|
|
91
109
|
const missing = Object.keys(config.languages).filter((locale) => !declared.has(locale));
|
|
92
110
|
if (missing.length > 0) {
|
|
@@ -97,6 +115,14 @@ function assertAstroI18n(block, config) {
|
|
|
97
115
|
throw new Error(`[@edgekits/astro-i18n] Astro \`i18n.defaultLocale\` "${block.defaultLocale}" differs from \`defaultLocale\` "${config.defaultLocale}". ` +
|
|
98
116
|
'Make them equal, or remove the `i18n` block from astro.config and the integration sets it for you.');
|
|
99
117
|
}
|
|
118
|
+
// Absent values are Astro's schema defaults (both false): `redirectToDefaultLocale` left
|
|
119
|
+
// out is fine, `prefixDefaultLocale` left out is not.
|
|
120
|
+
const routing = typeof block.routing === 'object' ? block.routing : undefined;
|
|
121
|
+
if (routing?.prefixDefaultLocale !== true || routing.redirectToDefaultLocale === true) {
|
|
122
|
+
throw new Error('[@edgekits/astro-i18n] Astro `i18n.routing` must set `prefixDefaultLocale: true` and `redirectToDefaultLocale: false` ' +
|
|
123
|
+
`(found ${JSON.stringify(block.routing)}): the middleware handles the default locale itself, and Astro's ` +
|
|
124
|
+
'own check would otherwise answer 404 or redirect the pages it serves. Set them, or remove the `i18n` block from astro.config and the integration sets it for you.');
|
|
125
|
+
}
|
|
100
126
|
}
|
|
101
127
|
function trimSlashes(path) {
|
|
102
128
|
return path.replace(/^\/+|\/+$/g, '');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AstroConfig } from 'astro';
|
|
2
|
-
import type { I18nConfig } from '
|
|
2
|
+
import type { I18nConfig } from '@edgekits/i18n-core';
|
|
3
3
|
type VitePlugin = NonNullable<NonNullable<AstroConfig['vite']>['plugins']>[number];
|
|
4
4
|
export declare const CONFIG_ID = "virtual:@edgekits/astro-i18n/config";
|
|
5
5
|
export declare const FALLBACKS_ID = "virtual:@edgekits/astro-i18n/fallbacks";
|
package/dist/types.d.ts
CHANGED
|
@@ -1,60 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* The core's public types and config helpers, re-exported for this package's entries
|
|
3
|
+
* (root, `./config`, `./runtime`), plus what only Astro needs: the `App.Locals` the
|
|
4
|
+
* middleware fills. `I18nLocales` is the core's interface: augmenting it through either
|
|
5
|
+
* entry (the integration's injected `locales.d.ts`) merges into that one declaration.
|
|
5
6
|
*/
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
};
|
|
10
|
-
export type I18nConfig = {
|
|
11
|
-
languages: Record<string, I18nLanguage>;
|
|
12
|
-
defaultLocale: string;
|
|
13
|
-
cookie: {
|
|
14
|
-
name: string;
|
|
15
|
-
maxAge: number;
|
|
16
|
-
};
|
|
17
|
-
keyPrefix: string;
|
|
18
|
-
missingTranslationBanner: boolean;
|
|
19
|
-
routing: 'prefix';
|
|
20
|
-
generatedDir?: string;
|
|
21
|
-
localesDir?: string;
|
|
22
|
-
countryToLocale?: Record<string, string>;
|
|
23
|
-
markdownLocale?: string;
|
|
24
|
-
};
|
|
25
|
-
export declare const DEFAULT_GENERATED_DIR = "src/i18n";
|
|
26
|
-
export declare const DEFAULT_LOCALES_DIR = "locales";
|
|
27
|
-
/** The locale keys of a config's `languages`: a literal union for a literal config. */
|
|
28
|
-
export type LocaleOf<C extends I18nConfig> = Extract<keyof C['languages'], string>;
|
|
29
|
-
/**
|
|
30
|
-
* Validates and returns the config unchanged. Throws at config time on an empty
|
|
31
|
-
* `languages`, a `defaultLocale` or `markdownLocale` that is not one of its keys, an
|
|
32
|
-
* unknown `routing`, or a `countryToLocale` entry whose key is not an uppercase country
|
|
33
|
-
* code or whose value is not one of `languages` (also a type error for a literal config).
|
|
34
|
-
*/
|
|
35
|
-
export declare function defineI18nConfig<const C extends I18nConfig>(config: C & {
|
|
36
|
-
countryToLocale?: Record<string, LocaleOf<C>>;
|
|
37
|
-
markdownLocale?: LocaleOf<C>;
|
|
38
|
-
}): C;
|
|
39
|
-
/**
|
|
40
|
-
* The project's locales, as keys. Empty here; the integration augments it from the
|
|
41
|
-
* config's `languages` (`injectTypes` at `astro:config:done`).
|
|
42
|
-
*/
|
|
43
|
-
export interface I18nLocales {
|
|
44
|
-
}
|
|
45
|
-
type LocaleKey = Extract<keyof I18nLocales, string>;
|
|
46
|
-
export type Locale = [LocaleKey] extends [never] ? string : LocaleKey;
|
|
47
|
-
/** The Worker bindings the engine reads from `cloudflare:workers`. */
|
|
48
|
-
export interface I18nBindings {
|
|
49
|
-
TRANSLATIONS: KVNamespace;
|
|
50
|
-
I18N_CACHE?: string;
|
|
51
|
-
DEBUG_I18N?: string;
|
|
52
|
-
}
|
|
7
|
+
import type { Locale } from '@edgekits/i18n-core';
|
|
8
|
+
export { DEFAULT_GENERATED_DIR, defineI18nConfig, localesOf } from '@edgekits/i18n-core';
|
|
9
|
+
export type { I18nBindings, I18nConfig, I18nLanguage, I18nLocales, Locale, LocaleOf, LocalesOf, } from '@edgekits/i18n-core';
|
|
53
10
|
declare global {
|
|
54
|
-
namespace I18n {
|
|
55
|
-
interface Schema {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
11
|
namespace App {
|
|
59
12
|
interface Locals {
|
|
60
13
|
uiLocale: Locale;
|
|
@@ -63,4 +16,3 @@ declare global {
|
|
|
63
16
|
}
|
|
64
17
|
}
|
|
65
18
|
}
|
|
66
|
-
export {};
|
package/dist/types.js
CHANGED
|
@@ -1,44 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Public types of the package and the config validator. Pure: no virtual modules,
|
|
3
|
-
* no `cloudflare:workers`, so the consumer's `astro.config` and the CLI can import it
|
|
4
|
-
* under plain Node.
|
|
5
|
-
*/
|
|
6
|
-
export const DEFAULT_GENERATED_DIR = 'src/i18n';
|
|
7
|
-
export const DEFAULT_LOCALES_DIR = 'locales';
|
|
8
|
-
const COUNTRY_CODE = /^[A-Z]{2}$/;
|
|
9
|
-
/**
|
|
10
|
-
* Validates and returns the config unchanged. Throws at config time on an empty
|
|
11
|
-
* `languages`, a `defaultLocale` or `markdownLocale` that is not one of its keys, an
|
|
12
|
-
* unknown `routing`, or a `countryToLocale` entry whose key is not an uppercase country
|
|
13
|
-
* code or whose value is not one of `languages` (also a type error for a literal config).
|
|
14
|
-
*/
|
|
15
|
-
export function defineI18nConfig(config) {
|
|
16
|
-
const locales = Object.keys(config.languages ?? {});
|
|
17
|
-
if (locales.length === 0) {
|
|
18
|
-
throw new Error('[@edgekits/astro-i18n] `languages` must define at least one locale.');
|
|
19
|
-
}
|
|
20
|
-
// Lowercase everywhere (config, `locales/` folders, URLs); `toIetfTag` relies on it.
|
|
21
|
-
for (const locale of locales) {
|
|
22
|
-
if (locale !== locale.toLowerCase()) {
|
|
23
|
-
throw new Error(`[@edgekits/astro-i18n] \`languages\` key "${locale}" is not lowercase (use "${locale.toLowerCase()}").`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
if (!locales.includes(config.defaultLocale)) {
|
|
27
|
-
throw new Error(`[@edgekits/astro-i18n] \`defaultLocale\` "${config.defaultLocale}" is not one of \`languages\`: ${locales.join(', ')}.`);
|
|
28
|
-
}
|
|
29
|
-
if (config.markdownLocale !== undefined && !locales.includes(config.markdownLocale)) {
|
|
30
|
-
throw new Error(`[@edgekits/astro-i18n] \`markdownLocale\` "${config.markdownLocale}" is not one of \`languages\`: ${locales.join(', ')}.`);
|
|
31
|
-
}
|
|
32
|
-
if (config.routing !== 'prefix') {
|
|
33
|
-
throw new Error(`[@edgekits/astro-i18n] \`routing\` "${String(config.routing)}" is not supported; the only routing is 'prefix'.`);
|
|
34
|
-
}
|
|
35
|
-
for (const [country, locale] of Object.entries(config.countryToLocale ?? {})) {
|
|
36
|
-
if (!COUNTRY_CODE.test(country)) {
|
|
37
|
-
throw new Error(`[@edgekits/astro-i18n] \`countryToLocale\` key "${country}" is not an uppercase ISO 3166-1 alpha-2 country code.`);
|
|
38
|
-
}
|
|
39
|
-
if (!locales.includes(locale)) {
|
|
40
|
-
throw new Error(`[@edgekits/astro-i18n] \`countryToLocale\` maps "${country}" to "${locale}", which is not one of \`languages\`: ${locales.join(', ')}.`);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return config;
|
|
44
|
-
}
|
|
1
|
+
export { DEFAULT_GENERATED_DIR, defineI18nConfig, localesOf } from '@edgekits/i18n-core';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edgekits/astro-i18n",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Edge-native i18n for Astro on Cloudflare Workers: translations in Workers KV, a per-namespace edge cache invalidated by the Purge API, and typed keys.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
"author": "Gary Stupak",
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/EdgeKits/
|
|
19
|
+
"url": "git+https://github.com/EdgeKits/i18n.git",
|
|
20
|
+
"directory": "packages/astro"
|
|
20
21
|
},
|
|
21
|
-
"homepage": "https://github.com/EdgeKits/
|
|
22
|
+
"homepage": "https://github.com/EdgeKits/i18n#readme",
|
|
22
23
|
"bugs": {
|
|
23
|
-
"url": "https://github.com/EdgeKits/
|
|
24
|
+
"url": "https://github.com/EdgeKits/i18n/issues"
|
|
24
25
|
},
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=22"
|
|
@@ -37,9 +38,6 @@
|
|
|
37
38
|
"./components/*": "./src/components/*.astro",
|
|
38
39
|
"./package.json": "./package.json"
|
|
39
40
|
},
|
|
40
|
-
"bin": {
|
|
41
|
-
"edgekits-i18n": "dist/cli/index.js"
|
|
42
|
-
},
|
|
43
41
|
"files": [
|
|
44
42
|
"dist",
|
|
45
43
|
"src",
|
|
@@ -59,8 +57,7 @@
|
|
|
59
57
|
"format:check": "prettier --check ."
|
|
60
58
|
},
|
|
61
59
|
"dependencies": {
|
|
62
|
-
"
|
|
63
|
-
"jsonc-parser": "^3.3.1"
|
|
60
|
+
"@edgekits/i18n-core": "0.2.0"
|
|
64
61
|
},
|
|
65
62
|
"peerDependencies": {
|
|
66
63
|
"@astrojs/cloudflare": "^14.3.0",
|
|
@@ -68,13 +65,6 @@
|
|
|
68
65
|
"wrangler": "^4.125.0"
|
|
69
66
|
},
|
|
70
67
|
"devDependencies": {
|
|
71
|
-
"
|
|
72
|
-
"@types/node": "^25.9.1",
|
|
73
|
-
"astro": "^7.3.8",
|
|
74
|
-
"prettier": "3.9.4",
|
|
75
|
-
"prettier-plugin-astro": "0.14.1",
|
|
76
|
-
"prettier-plugin-tailwindcss": "0.8.0",
|
|
77
|
-
"tsx": "^4.23.12",
|
|
78
|
-
"typescript": "~5.9.3"
|
|
68
|
+
"astro": "^7.3.8"
|
|
79
69
|
}
|
|
80
70
|
}
|