@edgekits/astro-i18n 0.1.0 → 0.1.2

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 CHANGED
@@ -4,7 +4,26 @@ 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.1.0 — unreleased
7
+ ## 0.1.2 — 2026-10-13
8
+
9
+ ### Fixed
10
+
11
+ - `package.json` is normalised (`bin` path without `./`, `repository.url` in the `git+https://….git` form), so `npm publish` prints no warnings.
12
+ - The build removes `dist/` before compiling, so a deleted source file can no longer ship as a stale `dist/*.js`.
13
+
14
+ ## 0.1.1 — 2026-10-13
15
+
16
+ ### Changed
17
+
18
+ - `countryCode` in `languages` is optional. `LanguageSwitcher` renders the `flag` component only for a locale that has one; the item then shows the name alone.
19
+ - The `astro-i18n` skill asks whether to show flags (each locale's `countryCode`, SVGs from the `flag-icons` package or the project's own) and moves the project's pages under `src/pages/[lang]/` during adoption.
20
+
21
+ ### Added
22
+
23
+ - "Using the skill": a user guide for the `astro-i18n` skill, the same section in this README and in the plugin's.
24
+ - A `prepublishOnly` script that builds `dist/` before `npm publish`.
25
+
26
+ ## 0.1.0 — 2026-10-12
8
27
 
9
28
  ### Added
10
29
 
package/README.md CHANGED
@@ -73,18 +73,22 @@ export default defineI18nConfig({
73
73
  })
74
74
  ```
75
75
 
76
- | Field | Required | Default | Meaning |
77
- | -------------------------- | -------- | ------------ | ------------------------------------------------------------------------ |
78
- | `languages` | yes | | `{ [locale]: { countryCode, name } }`; `countryCode` is for your flag UI |
79
- | `defaultLocale` | yes | | one of `languages`; its JSON files define the schema |
80
- | `cookie` | yes | | `{ name, maxAge }` (seconds) of the language cookie |
81
- | `keyPrefix` | yes | | KV keys are `<keyPrefix>:<namespace>:<locale>` |
82
- | `missingTranslationBanner` | yes | | enables `MissingTranslationBanner` |
83
- | `routing` | yes | | `'prefix'` |
84
- | `localesDir` | no | `'locales'` | where the CLI reads `<locale>/<namespace>.json` |
85
- | `generatedDir` | no | `'src/i18n'` | where the CLI writes `runtime-constants.ts` and `fallbacks.generated.ts` |
86
- | `countryToLocale` | no | none | geo-IP step of the locale fallback (see Reference) |
87
- | `markdownLocale` | no | none | the locale `.md` endpoints are served in (see Reference) |
76
+ | Field | Required | Default | Meaning |
77
+ | -------------------------- | -------- | ------------ | -------------------------------------------------------------------------------------------------------- |
78
+ | `languages` | yes | | `{ [locale]: { name, countryCode? } }`; `countryCode` is optional: the code your flag component receives |
79
+ | `defaultLocale` | yes | | one of `languages`; its JSON files define the schema |
80
+ | `cookie` | yes | | `{ name, maxAge }` (seconds) of the language cookie |
81
+ | `keyPrefix` | yes | | KV keys are `<keyPrefix>:<namespace>:<locale>` |
82
+ | `missingTranslationBanner` | yes | | enables `MissingTranslationBanner` |
83
+ | `routing` | yes | | `'prefix'` |
84
+ | `localesDir` | no | `'locales'` | where the CLI reads `<locale>/<namespace>.json` |
85
+ | `generatedDir` | no | `'src/i18n'` | where the CLI writes `runtime-constants.ts` and `fallbacks.generated.ts` |
86
+ | `countryToLocale` | no | none | geo-IP step of the locale fallback (see Reference) |
87
+ | `markdownLocale` | no | none | the locale `.md` endpoints are served in (see Reference) |
88
+
89
+ Locale codes (the keys of `languages`) are lowercase everywhere: the config, the `locales/`
90
+ folders and the URLs (`pt-br`, not `pt-BR`). `toIetfTag()` from `./format` gives the IETF form
91
+ (`pt-BR`) where one is needed, such as `<html lang>` and hreflang.
88
92
 
89
93
  The config is validated when it loads: an empty `languages`, a `defaultLocale` or
90
94
  `markdownLocale` outside it, or a bad `countryToLocale` entry throws with a message naming the
@@ -379,6 +383,265 @@ language-switcher > div {
379
383
  }
380
384
  ```
381
385
 
386
+ #### Flags
387
+
388
+ The package ships no flag images. To show flags in the switcher, pass your own Astro component
389
+ as `flag`: each item renders it as `<Flag code={countryCode} size={16} />` before the language
390
+ name. A locale without a `countryCode` gets the name alone, and so does every locale when you
391
+ pass no `flag`. A recipe that copies only the SVGs your locales use from the `flag-icons` npm
392
+ package is under [Flags in the switcher](#flags-in-the-switcher) in "Using the skill".
393
+
394
+ <!-- skill-guide:start -->
395
+
396
+ ## Using the skill
397
+
398
+ `astro-i18n` is a free agent skill that sets up `@edgekits/astro-i18n` in your project and keeps
399
+ working with it afterwards. It runs in Claude Code as a plugin, and in Cursor, OpenCode or any other
400
+ coding agent that reads `SKILL.md`. Its source is in
401
+ [`plugins/astro-i18n`](https://github.com/EdgeKits/astro-i18n/tree/main/plugins/astro-i18n).
402
+
403
+ ### What it does
404
+
405
+ The skill adopts `@edgekits/astro-i18n` in an existing Astro project that runs on Cloudflare
406
+ Workers through `@astrojs/cloudflare`. It asks the questions only you can answer, follows the
407
+ package README in order, and checks the result against a checklist. After the setup it handles
408
+ the follow-up work: new keys, locales and namespaces, production copy, localized forms and
409
+ multilingual SEO.
410
+
411
+ It does not create an Astro project, and it stops if the project has no Cloudflare adapter: the
412
+ package depends on Workers KV. It copies no engine code into your project; everything it writes
413
+ uses the package's exports.
414
+
415
+ What changes in your project during the setup:
416
+
417
+ - dependencies: `@edgekits/astro-i18n`, and `flag-icons` as a dev dependency if you want flags;
418
+ - `astro.config`: the `edgekitsI18n()` integration;
419
+ - `wrangler.jsonc`: a `TRANSLATIONS` KV binding;
420
+ - new files: `edgekits-i18n.config.ts`, `locales/<locale>/*.json`, the generated files, and a
421
+ `Flag.astro` with its SVGs;
422
+ - `src/pages/`: your pages move under `src/pages/[lang]/`;
423
+ - your base layout: `<html lang>` from the request's locale, hreflang tags, the language
424
+ switcher.
425
+
426
+ ### Installing it
427
+
428
+ In Claude Code, add the EdgeKits marketplace once, then install the plugin:
429
+
430
+ ```
431
+ /plugin marketplace add EdgeKits/skills
432
+ /plugin install astro-i18n@edgekits
433
+ ```
434
+
435
+ That installs it for you, in every project. To set it up for everyone working on one
436
+ repository, run these in the repository and commit `.claude/settings.json`:
437
+
438
+ ```sh
439
+ claude plugin marketplace add EdgeKits/skills --scope project
440
+ claude plugin install astro-i18n@edgekits --scope project
441
+ ```
442
+
443
+ The first command declares the EdgeKits marketplace in `.claude/settings.json`; the second
444
+ turns the plugin on there, under `enabledPlugins` (`"astro-i18n@edgekits": true`). Claude Code
445
+ honours those entries for a teammate once they trust the folder, but it does not download the
446
+ plugin for them: each teammate runs the same `claude plugin install … --scope project` once.
447
+
448
+ Any other agent, with the [skills CLI](https://skills.sh):
449
+
450
+ ```sh
451
+ npx skills add EdgeKits/astro-i18n --skill astro-i18n
452
+ ```
453
+
454
+ ### Prerequisites
455
+
456
+ - An Astro `^7.3` project with `output: 'server'` and the `@astrojs/cloudflare` adapter `^14.3`.
457
+ For a new site: `npm create astro@latest`, then `npx astro add cloudflare`, then set
458
+ `output: 'server'` in `astro.config`.
459
+ - wrangler `^4.125` installed in the project (`npm i -D wrangler`) and a `wrangler.jsonc`. The
460
+ package's CLI runs the project's own wrangler, never a global one.
461
+ - A Cloudflare account. Local development needs none until you create the real KV namespace.
462
+ - For `edgekits-i18n migrate` (pushing copy to production): the site on a proxied Cloudflare
463
+ zone (orange cloud), that zone's ID, and an API token with the Zone › Cache Purge permission.
464
+
465
+ ### First run on a fresh site
466
+
467
+ #### What to say
468
+
469
+ Open the project in your agent and ask for the result you want, for example:
470
+
471
+ - "Add i18n to this site: English as the default, German and Brazilian Portuguese."
472
+ - "Make the site multilingual with en and es, and send visitors from Spain and Mexico to
473
+ Spanish."
474
+ - "Install @edgekits/astro-i18n and set it up."
475
+
476
+ #### The questions it asks
477
+
478
+ The skill asks before it writes any code, and waits for the answers:
479
+
480
+ | Question | Example answer |
481
+ | ---------------------------------------------------------------------------- | --------------------------------------------- |
482
+ | Which locales, and which is the default? | `en` (default), `de`, `pt-br` |
483
+ | Does the site serve markdown endpoints (`/blog/x.md`)? In which locale? | No (or: yes, `en`) |
484
+ | Should a visitor's country pick the locale when nothing else does? How? | `DE`, `AT`, `CH` → `de`; `BR` → `pt-br` |
485
+ | Middleware: registered by the integration (`auto`), or in your own sequence? | `auto` |
486
+ | Flags in the language switcher? | Yes: `en` → `us`, `de` → `de`, `pt-br` → `br` |
487
+
488
+ For flags, a regional locale takes its region (`pt-br` → `br`, `en-gb` → `gb`), and for a
489
+ language-only locale the skill proposes the obvious country (`en` → `us`) for you to confirm. It
490
+ shows the full list back before it writes the config. If you have no preference on any of
491
+ these, it uses the defaults: no geo table, no markdown locale, `auto` middleware, flags on.
492
+
493
+ #### What it creates
494
+
495
+ - `edgekits-i18n.config.ts` with your answers, and the integration in `astro.config`.
496
+ - `locales/<default>/common.json` and `messages.json` (the banner texts and form error codes),
497
+ and the same files for each other locale.
498
+ - The generated files, written by `npx edgekits-i18n seed`: `i18n-data.json`,
499
+ `src/i18n.generated.d.ts`, `src/i18n/runtime-constants.ts`, `src/i18n/fallbacks.generated.ts`.
500
+ Commit them.
501
+ - The route layout: `src/pages/about.astro` becomes `src/pages/[lang]/about.astro`, and so on.
502
+ The root `index.astro` moves too, since the middleware redirects `/` to `/<locale>/`.
503
+ Endpoints that are not localized (`robots.txt`, `sitemap.xml`, `/api/*`) and `public/` stay
504
+ where they are.
505
+ - `src/components/Flag.astro` and one SVG per flag in `src/assets/icons/flags/`, if you chose
506
+ flags (the recipe is below).
507
+ - A KV namespace on your Cloudflare account (`wrangler kv namespace create`). The skill asks
508
+ before it runs that command. Until then, local development works with a placeholder id of 32
509
+ hexadecimal characters, such as `00000000000000000000000000000000`: the CLI checks the id's
510
+ shape before it runs wrangler.
511
+
512
+ #### Flags in the switcher
513
+
514
+ The package ships no flag images; `LanguageSwitcher` takes your flag component as its `flag`
515
+ prop and passes it each locale's `countryCode`. The skill's recipe, which you can also follow by
516
+ hand:
517
+
518
+ 1. `npm i -D flag-icons` (MIT).
519
+ 2. Copy the SVG of each configured `countryCode`, and only those, into
520
+ `src/assets/icons/flags/`, and commit them:
521
+
522
+ ```sh
523
+ mkdir -p src/assets/icons/flags
524
+ for code in us de br; do cp node_modules/flag-icons/flags/4x3/$code.svg src/assets/icons/flags/; done
525
+ ```
526
+
527
+ 3. Add `src/components/Flag.astro`:
528
+
529
+ ```astro
530
+ ---
531
+ import type { SvgComponent } from 'astro/types'
532
+
533
+ interface Props {
534
+ code: string
535
+ size?: number
536
+ }
537
+ const { code, size = 16 } = Astro.props
538
+
539
+ const flags = import.meta.glob<SvgComponent>('../assets/icons/flags/*.svg', { eager: true, import: 'default' })
540
+ const Svg = flags[`../assets/icons/flags/${code}.svg`]
541
+ ---
542
+
543
+ {Svg && <Svg width={(size * 4) / 3} height={size} aria-hidden="true" />}
544
+ ```
545
+
546
+ 4. Pass it to the switcher: `<LanguageSwitcher flag={Flag} />`.
547
+
548
+ A locale without a `countryCode`, or without an SVG, shows its name alone. The flag belongs to
549
+ the `countryCode`, not the language, so `pt` and `pt-br` can show different flags.
550
+
551
+ #### How it checks the result
552
+
553
+ Before it reports back, the skill runs its adoption checklist: the config matches your answers
554
+ and every locale code is lowercase; `seed` writes the generated files and leaves
555
+ `.i18n-hashes.json` alone; `astro check` passes; under `astro dev`, `/` redirects to a locale,
556
+ each locale renders its own copy (or the default locale's, if it has no JSON yet), an unknown
557
+ first segment such as `/zz/` is prefixed rather than crashing, `<html lang>` reads `pt-BR` on a
558
+ `pt-br` page, hreflang tags cover every locale plus `x-default`, and the switcher changes the
559
+ URL and sets the language cookie. It also checks that no page is left at `src/pages/*.astro`
560
+ and that every `countryCode` has its SVG.
561
+
562
+ ### Everyday work
563
+
564
+ Ask in plain words: "add a key for the footer note", "add French", "add a `pricing`
565
+ namespace", "change the hero title in production". What the skill does:
566
+
567
+ - **A key or a value.** It adds a new key to the default locale first, since the default
568
+ locale's JSON is the type schema, then to the other locales, and runs `npx edgekits-i18n
569
+ seed`. A key that exists only in another locale is not typed.
570
+ - **A locale.** It adds the locale to `languages` (and its SVG, with flags on), optionally
571
+ writes `locales/<new>/`, runs `seed` and restarts the dev server. A locale without JSON is
572
+ routable already: it shows the default locale's copy and the missing-translation banner.
573
+ - **A namespace.** It creates `locales/<default>/<namespace>.json` and the same file in each
574
+ other locale, runs `seed`, and fetches the namespace only on the pages that render it.
575
+ - **Production copy.** Edit the JSON, then: `seed` and check it locally; run the project's
576
+ checks (`astro check`, tests, build); `npx edgekits-i18n migrate`, which pushes to production
577
+ KV and purges only the changed namespaces; commit the JSON together with
578
+ `.i18n-hashes.json`. No redeploy is needed unless the config or code changed too.
579
+
580
+ `.i18n-hashes.json` records what production KV holds. Only a successful `migrate` writes it,
581
+ it is committed right after, and nothing else edits it. `migrate` refuses to run while that
582
+ file has uncommitted changes.
583
+
584
+ ### Forms and SEO
585
+
586
+ **Forms.** Ask for "a localized contact form with name, email and message", or "localize this
587
+ form". The skill asks for the fields and their rules, where the form submits (an Astro Action
588
+ by default) and where it renders. What comes back: stable error codes (`INVALID_EMAIL`) with a
589
+ message for every code in every locale's `messages.json`; a server schema that emits the codes;
590
+ an Astro Action; and a `.astro` form with an inline script. The browser side uses no framework
591
+ and no zod: it validates with a small hand-written mirror of the server schema, so the form
592
+ works with a strict Content Security Policy and without JavaScript.
593
+
594
+ **SEO.** Ask for "hreflang tags", "a sitemap for every locale", "an RSS feed per locale",
595
+ "`llms.txt`" or "`robots.txt` that blocks staging". The skill writes them as your project's
596
+ code, using the package's locale constants and helpers: a `SeoHreflangs` component and
597
+ `<html lang>` in the base layout, `sitemap.xml`, `[lang]/rss.xml`, `llms.txt` and `robots.txt`
598
+ endpoints.
599
+
600
+ ### Rules the agent follows
601
+
602
+ - Locale codes are lowercase everywhere: config keys, `locales/` folders, URLs (`pt-br`). Only
603
+ `toIetfTag()` produces `pt-BR`, for `<html lang>` and hreflang.
604
+ - Generated files (`*.generated.*`, `runtime-constants.ts`, `i18n-data.json`) come from the CLI
605
+ and are never edited by hand.
606
+ - `@edgekits/astro-i18n/runtime` and `/middleware` run only in workerd. Anything Node loads
607
+ (`astro.config`, `src/content.config.ts`, unit tests) uses the Node-safe `/config` and
608
+ `/format` entries instead.
609
+ - No engine code is copied into the project. If the package lacks something, the skill says so
610
+ rather than re-implementing it.
611
+ - Secrets (`.dev.vars`, `.env*`, API tokens) never enter git, and anything that touches your
612
+ Cloudflare account is asked first.
613
+
614
+ ### Troubleshooting
615
+
616
+ - **Edited translations do not show under `astro dev`.** The local Cache API keeps the first
617
+ value it served. Put `I18N_CACHE=off` in `.dev.vars`, restart the dev server, run `seed`.
618
+ - **A new key is a type error.** It was added only to a non-default locale, or the CLI has not
619
+ run since. Add it to the default locale and run `seed` (or `bundle`); if the editor still
620
+ shows the error, restart its TypeScript server.
621
+ - **`Cannot find module 'cloudflare:workers'` in `tsc`.** Run `npx wrangler types`.
622
+ - **`migrate` refuses to run.** `.i18n-hashes.json` has uncommitted changes. If the last
623
+ `migrate` wrote it, commit it. If anything else changed it, discard the change with
624
+ `git checkout .i18n-hashes.json` and run `migrate` again. Never edit or delete it to get
625
+ past the check.
626
+ - **`migrate` updated KV but the site shows old copy.** The purge was skipped: the zone is not
627
+ proxied, or the zone ID or token is missing. Fix it and run `migrate` again; it retries the
628
+ same purge.
629
+ - **Windows.** Local KV (`seed`) can fail with "Network connection lost" or "internal error"
630
+ when the project sits in a deep folder: the local Workers runtime hits the path length
631
+ limit, so keep the project at a short path. Stopping `astro dev` can leave `workerd.exe`
632
+ running and holding `.wrangler/state`; end it before the next `seed`. In PowerShell, copy
633
+ the flags with
634
+ `foreach ($c in 'us','de','br') { Copy-Item node_modules/flag-icons/flags/4x3/$c.svg src/assets/icons/flags/ }`.
635
+
636
+ ### Privacy
637
+
638
+ This plugin collects, stores and sends no data. It contains no code, no hooks and no MCP servers, only
639
+ instructions (`SKILL.md`, its reference pages and two starter translation files). The commands it asks your agent
640
+ to run (`npm install`, `wrangler`, the package's `edgekits-i18n` CLI) talk to npm and to your own Cloudflare account
641
+ under their own terms. EdgeKits runs no service behind this plugin.
642
+
643
+ <!-- skill-guide:end -->
644
+
382
645
  ## How it works
383
646
 
384
647
  A request reaches the middleware, which settles the URL's locale (redirecting when there is
package/dist/types.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * under plain Node.
5
5
  */
6
6
  export type I18nLanguage = {
7
- countryCode: string;
7
+ countryCode?: string;
8
8
  name: string;
9
9
  };
10
10
  export type I18nConfig = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgekits/astro-i18n",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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,7 +16,7 @@
16
16
  "author": "Gary Stupak",
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "https://github.com/EdgeKits/astro-i18n"
19
+ "url": "git+https://github.com/EdgeKits/astro-i18n.git"
20
20
  },
21
21
  "homepage": "https://github.com/EdgeKits/astro-i18n#readme",
22
22
  "bugs": {
@@ -38,7 +38,7 @@
38
38
  "./package.json": "./package.json"
39
39
  },
40
40
  "bin": {
41
- "edgekits-i18n": "./dist/cli/index.js"
41
+ "edgekits-i18n": "dist/cli/index.js"
42
42
  },
43
43
  "files": [
44
44
  "dist",
@@ -51,7 +51,8 @@
51
51
  "access": "public"
52
52
  },
53
53
  "scripts": {
54
- "build": "tsc -p tsconfig.build.json",
54
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json",
55
+ "prepublishOnly": "npm run build",
55
56
  "test": "node --import ./test/register.mjs --test test/**/*.test.ts",
56
57
  "typecheck": "tsc --noEmit",
57
58
  "format": "prettier --write .",
@@ -5,7 +5,8 @@ import type { Locale } from '../constants'
5
5
 
6
6
  interface Props {
7
7
  // The project's flag component (an Astro component reference), passed in so the
8
- // engine imports no project UI. Without it, items render label-only.
8
+ // engine imports no project UI. It receives the locale's `countryCode`; without the
9
+ // component, or for a locale without a `countryCode`, the item renders label-only.
9
10
  flag?: (props: { code: string; size?: number }) => any
10
11
  // The project's icons for the trigger button and the active item (Astro component
11
12
  // references, like `flag`). Without them, the minimal inline defaults below render.
@@ -76,7 +77,7 @@ const { uiLocale: currentLang } = Astro.locals
76
77
  disabled={isActive}
77
78
  >
78
79
  <span class={classes.itemLabel}>
79
- {Flag && <Flag code={flagCode} size={16} />}
80
+ {Flag && flagCode && <Flag code={flagCode} size={16} />}
80
81
  {label.name}
81
82
  </span>
82
83
  {isActive &&
package/src/types.ts CHANGED
@@ -4,7 +4,9 @@
4
4
  * under plain Node.
5
5
  */
6
6
 
7
- export type I18nLanguage = { countryCode: string; name: string }
7
+ // `countryCode` is what the switcher's `flag` component receives; without it the item
8
+ // shows the name alone.
9
+ export type I18nLanguage = { countryCode?: string; name: string }
8
10
 
9
11
  export type I18nConfig = {
10
12
  // The only source of truth for languages: the keys are the locales.