@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +87 -23
  3. package/dist/integration/index.d.ts +25 -4
  4. package/dist/integration/index.js +35 -9
  5. package/dist/integration/vite-plugin.d.ts +1 -1
  6. package/dist/types.d.ts +7 -55
  7. package/dist/types.js +1 -44
  8. package/package.json +7 -17
  9. package/src/components/LanguageSwitcher.astro +89 -93
  10. package/src/config.entry.ts +5 -36
  11. package/src/constants.ts +5 -3
  12. package/src/cookie-storage.ts +19 -25
  13. package/src/fetcher.ts +3 -304
  14. package/src/format.ts +3 -107
  15. package/src/instance.ts +44 -0
  16. package/src/integration/index.ts +38 -9
  17. package/src/integration/vite-plugin.ts +1 -1
  18. package/src/middleware/i18n.ts +232 -114
  19. package/src/resolve-locale.ts +7 -39
  20. package/src/runtime.ts +2 -2
  21. package/src/schema.ts +3 -11
  22. package/src/translations-keys.ts +7 -38
  23. package/src/types.ts +16 -116
  24. package/dist/cli/generate.d.ts +0 -50
  25. package/dist/cli/generate.js +0 -160
  26. package/dist/cli/hashes.d.ts +0 -19
  27. package/dist/cli/hashes.js +0 -42
  28. package/dist/cli/index.d.ts +0 -2
  29. package/dist/cli/index.js +0 -372
  30. package/dist/cli/key-prefix-check.d.ts +0 -6
  31. package/dist/cli/key-prefix-check.js +0 -27
  32. package/dist/cli/load-config.d.ts +0 -7
  33. package/dist/cli/load-config.js +0 -19
  34. package/dist/cli/purge.d.ts +0 -29
  35. package/dist/cli/purge.js +0 -94
  36. package/dist/cli/wrangler.d.ts +0 -10
  37. package/dist/cli/wrangler.js +0 -71
  38. package/dist/cli/zone.d.ts +0 -21
  39. package/dist/cli/zone.js +0 -42
  40. package/dist/key-format.d.ts +0 -8
  41. package/dist/key-format.js +0 -15
  42. package/src/accept-language.ts +0 -56
  43. package/src/assemble.ts +0 -29
  44. package/src/cli/generate.ts +0 -205
  45. package/src/cli/hashes.ts +0 -66
  46. package/src/cli/index.ts +0 -472
  47. package/src/cli/key-prefix-check.ts +0 -31
  48. package/src/cli/load-config.ts +0 -26
  49. package/src/cli/purge.ts +0 -122
  50. package/src/cli/wrangler.ts +0 -96
  51. package/src/cli/zone.ts +0 -64
  52. package/src/deep-merge.ts +0 -38
  53. package/src/key-format.ts +0 -17
  54. package/src/locale-fallback.ts +0 -58
  55. package/src/markdown-path.ts +0 -31
  56. 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/). The only routing there is.
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 | Meaning |
78
- | -------------------------- | -------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
79
- | `languages` | yes | | `{ [locale]: { name, countryCode? } }`; `countryCode` is optional: the code your flag component receives |
80
- | `defaultLocale` | yes | | one of `languages`; its JSON files define the schema |
81
- | `cookie` | yes | | `{ name, maxAge }` (seconds) of the language cookie |
82
- | `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)) |
83
- | `missingTranslationBanner` | yes | | enables `MissingTranslationBanner` |
84
- | `routing` | yes | | `'prefix'` |
85
- | `localesDir` | no | `'locales'` | where the CLI reads `<locale>/<namespace>.json` |
86
- | `generatedDir` | no | `'src/i18n'` | where the CLI writes `runtime-constants.ts` and `fallbacks.generated.ts` |
87
- | `countryToLocale` | no | none | geo-IP step of the locale fallback (see Reference) |
88
- | `markdownLocale` | no | none | the locale `.md` endpoints are served in (see Reference) |
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 keep your own
122
- block, its `locales` must include every configured locale and its `defaultLocale` must match,
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
- The middleware redirects a URL without a locale (`/`, `/about/`) to a localized one and sets
140
- `Astro.locals.uiLocale` (the URL's locale) and `Astro.locals.translationLocale` (the locale to
141
- fetch translations for) on every request.
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/astro-i18n/tree/main/plugins/astro-i18n).
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/astro-i18n --skill astro-i18n
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, since the middleware redirects `/` to `/<locale>/`.
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 '../types.ts';
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; every URL locale-prefixed (the middleware redirects `/` itself, so Astro
23
- * does not). Complete, `fallbackType` included: Astro does not re-validate what
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 "../types.js";
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 = defineI18nConfig(config);
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
- // Each entry that exports `I18nLocales` from its own declaration: the root entry's comes
49
- // from dist/types.d.ts, the runtime entry's from the shipped source (src/types.ts).
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; every URL locale-prefixed (the middleware redirects `/` itself, so Astro
75
- * does not). Complete, `fallbackType` included: Astro does not re-validate what
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`) and share the default locale.
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 '../types.ts';
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
- * 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.
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
- export type I18nLanguage = {
7
- countryCode?: string;
8
- name: string;
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.1.8",
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/astro-i18n.git"
19
+ "url": "git+https://github.com/EdgeKits/i18n.git",
20
+ "directory": "packages/astro"
20
21
  },
21
- "homepage": "https://github.com/EdgeKits/astro-i18n#readme",
22
+ "homepage": "https://github.com/EdgeKits/i18n#readme",
22
23
  "bugs": {
23
- "url": "https://github.com/EdgeKits/astro-i18n/issues"
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
- "jiti": "^2.7.0",
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
- "@cloudflare/workers-types": "^5.20261011.1",
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
  }