diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..d84754c
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,28 @@
+# Build output and caches
+dist/
+.astro/
+coverage/
+
+# Tooling artifacts
+playwright-report/
+test-results/
+
+# Generated files (regenerated by scripts — don't fight their formatting)
+src/data/maps-catalogue.json
+package-lock.json
+public/maps/
+
+# Legacy/reference content predating the Astro migration
+_legacy-content/
+docs/
+posts/
+map/
+TODO.complete/
+
+# prettier-plugin-astro breaks text↔tag line boundaries inside prose,
+# and Astro trims those breaks (rendered spaces vanish — see the
+# whitespace-collapse guard in test/site.test.ts). paragraphs with
+# text↔tag boundaries (e.g. compare.astro's hero, the map page's
+# preview deck) stay hand-wrapped — prettier-ignore corrupts files.
+src/pages/compare.astro
+src/pages/maps/\[systemCode\].astro
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..8910d9f
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,5 @@
+{
+ "semi": false,
+ "printWidth": 100,
+ "plugins": ["prettier-plugin-astro"]
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..25eeb30
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,47 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this is
+
+interscript.org v2 — the Interscript website, rebuilt on Astro 7 (branch `astro-migration`). It replaces the legacy react-static site that still lives on the `main` branch — consult `main` (and `TODO.complete/`) as the reference for feature parity. Content claims must be factual: do not fabricate partner/authority/endorsement statements.
+
+## Commands
+
+```bash
+npm run dev # dev server (localhost:4321; bumps port if taken — e2e uses 4322)
+npm run build # static build to dist/
+npm run preview # serve the build
+
+npm run check # astro check (TypeScript across .astro/.ts/.vue)
+npm test # vitest run (unit tests in test/)
+npx vitest run test/site.test.ts # single test file
+npm run test:e2e # Playwright (e2e/); local runs need `npm run dev` on 4322
+npm run lint # eslint
+npm run format # prettier --write .
+npm run generate:catalogue # regenerate src/data/maps-catalogue.json from .isc sources
+```
+
+Dependency note: `interscript-ts` is a `file:` dependency on the sibling checkout `../interscript-ts`, and `npm run generate:catalogue` reads `../maps/maps/*.isc` — both sibling repos must exist locally.
+
+## Architecture
+
+- **Rendering**: Astro `output: "static"` + `@astrojs/node` adapter. Pages under `src/pages/` are prerendered by default; the real-time routes (`src/pages/api/{detect,systems,transliterate,transliterate/batch}.ts`) opt out with `export const prerender = false` and run `interscript-ts` server-side.
+- **Interactive islands**: Vue 3 (`@astrojs/vue`) for tools — `MapExplorer`, `CompareMode`, `BatchProcessor`, `DiffViewer`, `DetectPanel`, `MarcTool`, `SubtitlesProcessor`, etc. Static pages are plain `.astro` importing these islands.
+- **Map data**: ~289 compact `.isc` maps in `public/maps/`. The browsable catalogue `src/data/maps-catalogue.json` is generated by `scripts/generate-catalogue.mjs` (commit the regenerated file when maps change).
+- **Map loading strategies** (why two modules): browser runtimes use `src/scripts/map-strategies.ts` — ISC files first, compiled-JSON HTTP fallback only for `.iml` libraries (posix, unicode, var-Cyrl, var-kor) which have no ISC form. The SSR API routes use `src/lib/server-map-strategies.ts` — same order but ISC loads from the filesystem, no HTTP roundtrip. Keep both in sync.
+- **Transliteration worker**: `src/scripts/transliteration-worker.ts` runs interscript-ts off the main thread; `worker-client.ts` is the typed RPC client. Vite bundles the worker via `new Worker(new URL(...), { type: "module" })`.
+- **Design system**: single source of truth in `src/styles/global.css` — Tailwind 4 CSS-first `@theme` tokens (colors, fonts, type scale, spacing) plus component classes (`.btn`, `.card`, `.prose`, `.eyebrow`). All pages consume these tokens; don't hardcode hex values. `src/layouts/Base.astro` owns the header nav / drawer / footer and carries its scoped styles inline.
+- **Content**: AsciiDoc (`src/content/docs/`, `src/content/blog/`) loaded by `src/content/loaders/asciidoc.ts` (manual frontmatter parse + @asciidoctor/core), rendered into `.prose` wrappers.
+- **Theme**: light/dark via `data-theme` on ``; tokens flip in `src/styles/global.css`. A service worker (`public/sw.js`) provides offline access.
+
+## Testing layout
+
+- `test/*.test.ts` — vitest + happy-dom; covers pages, components, catalogue integrity, API endpoints.
+- `e2e/*.spec.ts` — Playwright, Chromium only. In CI the config starts `npm run dev` itself; locally run the dev server on port 4322 first (config `baseURL`).
+
+## Conventions
+
+- Conventional Commits (`feat:`, `fix:`, `docs:`, …). All changes go through PRs to `main` (branch `astro-migration` is the active migration branch).
+- Lint: `eslint.config.mjs` (ESLint 10 flat config; TS + Astro + Vue). Prettier: `.prettierrc` (no semicolons, 100 cols, astro plugin), `.prettierignore` for generated/legacy paths.
+- **Astro whitespace trap:** Astro _trims_ the space at a text↔inline-tag line break — `Backed by\n` renders as `byinterscript-ts`. `prettier-plugin-astro` reflows prose and will create such breaks; `test/site.test.ts` has a guard that fails on them. `prettier-ignore` comments corrupt `.astro` files — instead, hand-wrap the paragraph at text-only boundaries and add the file to `.prettierignore` (see `src/pages/compare.astro`).
diff --git a/e2e/compare.spec.ts b/e2e/compare.spec.ts
index 45638dc..1cc8bff 100644
--- a/e2e/compare.spec.ts
+++ b/e2e/compare.spec.ts
@@ -20,7 +20,12 @@ test.describe("compare systems", () => {
test("has system selectors", async ({ page }) => {
await page.goto("/compare")
const selects = page.locator("select")
- if (await selects.first().isVisible({ timeout: 3000 }).catch(() => false)) {
+ if (
+ await selects
+ .first()
+ .isVisible({ timeout: 3000 })
+ .catch(() => false)
+ ) {
const count = await selects.count()
expect(count).toBeGreaterThanOrEqual(1)
}
diff --git a/e2e/demo-edge-cases.spec.ts b/e2e/demo-edge-cases.spec.ts
index 0f9cc06..991ca6b 100644
--- a/e2e/demo-edge-cases.spec.ts
+++ b/e2e/demo-edge-cases.spec.ts
@@ -12,6 +12,9 @@ import { test, expect, type Page } from "@playwright/test"
async function ready(page: Page) {
await expect(page.locator(".rail-status")).toContainText("ready", { timeout: 20000 })
+ // The page default is the first catalogue entry (Chinese); these tests
+ // assert Cyrillic → Latin, so pin a Cyrillic system.
+ await page.locator("select.field-input").selectOption("odni-rus-Cyrl-Latn-2015")
}
test.describe("demo edge cases", () => {
diff --git a/e2e/demo.spec.ts b/e2e/demo.spec.ts
index 0188d97..f744609 100644
--- a/e2e/demo.spec.ts
+++ b/e2e/demo.spec.ts
@@ -21,13 +21,14 @@ test.describe("demo page — transliteration explorer", () => {
const select = page.locator("select.field-input")
await expect(select).toBeVisible()
const options = select.locator("option")
- await expect(options).toHaveCount(5)
+ await expect(options).toHaveCount(289)
+ await expect(select.locator("optgroup").first()).toHaveAttribute("label", "ACADSIN")
})
- test("default system is BGN/PCGN Ukrainian", async ({ page }) => {
+ test("default system is the first catalogue entry", async ({ page }) => {
await page.goto("/demo")
const select = page.locator("select.field-input")
- await expect(select).toHaveValue("bgnpcgn-ukr-Cyrl-Latn-2019")
+ await expect(select).toHaveValue("acadsin-zho-Hani-Latn-2002")
})
test("transliterates Ukrainian Cyrillic to Latin", async ({ page }) => {
@@ -37,6 +38,8 @@ test.describe("demo page — transliteration explorer", () => {
const status = page.locator(".rail-status")
await expect(status).toContainText("ready", { timeout: 20000 })
+ await page.locator("select.field-input").selectOption("bgnpcgn-ukr-Cyrl-Latn-2019")
+
// Type Cyrillic input
const textarea = page.locator("textarea.pane-body")
await textarea.fill("Антон")
@@ -158,6 +161,7 @@ test.describe("demo modes — API vs in-browser", () => {
const status = page.locator(".rail-status")
await expect(status).toContainText("ready", { timeout: 30000 })
+ await page.locator("select.field-input").selectOption("odni-rus-Cyrl-Latn-2015")
await page.locator("textarea.pane-body").fill("Антон")
await expect(page.locator(".pane-result")).toContainText("Anton", { timeout: 10000 })
})
@@ -182,6 +186,7 @@ test.describe("demo modes — API vs in-browser", () => {
// API mode first
const status = page.locator(".rail-status")
await expect(status).toContainText("ready", { timeout: 30000 })
+ await page.locator("select.field-input").selectOption("bgnpcgn-ukr-Cyrl-Latn-2019")
await page.locator("textarea.pane-body").fill("Київ")
await expect(page.locator(".pane-result")).toContainText("Kyiv", { timeout: 10000 })
const apiOutput = await page.locator(".pane-result").textContent()
diff --git a/e2e/maps.spec.ts b/e2e/maps.spec.ts
index 48ea2b6..364f052 100644
--- a/e2e/maps.spec.ts
+++ b/e2e/maps.spec.ts
@@ -22,7 +22,9 @@ test.describe("maps browser", () => {
test("has search or filter controls", async ({ page }) => {
await page.goto("/maps")
// Look for search input, select, or filter buttons
- const controls = page.locator("input[type='search'], input[placeholder*='search' i], select, button")
+ const controls = page.locator(
+ "input[type='search'], input[placeholder*='search' i], select, button",
+ )
await expect(controls.first()).toBeVisible({ timeout: 5000 })
})
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..a54ab2d
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,61 @@
+// @ts-check
+import js from "@eslint/js"
+import tseslint from "typescript-eslint"
+import astro from "eslint-plugin-astro"
+import vue from "eslint-plugin-vue"
+import prettierConfig from "eslint-config-prettier"
+import globals from "globals"
+
+export default tseslint.config(
+ {
+ ignores: [
+ "dist/**",
+ ".astro/**",
+ "coverage/**",
+ "playwright-report/**",
+ "test-results/**",
+ "public/**",
+ "_legacy-content/**",
+ "docs/**",
+ "posts/**",
+ "map/**",
+ "TODO.complete/**",
+ ],
+ },
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ ...astro.configs.recommended,
+ vue.configs["flat/essential"],
+ prettierConfig,
+ {
+ languageOptions: {
+ globals: { ...globals.browser, ...globals.node },
+ },
+ rules: {
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ {
+ argsIgnorePattern: "^_",
+ varsIgnorePattern: "^_",
+ caughtErrorsIgnorePattern: "^_",
+ },
+ ],
+ // The ISC JSON IR and catalogue entries are untyped by design.
+ "@typescript-eslint/no-explicit-any": "off",
+ },
+ },
+ {
+ files: ["**/*.vue"],
+ languageOptions: {
+ parserOptions: { parser: tseslint.parser },
+ },
+ },
+ {
+ files: ["**/*.astro", "**/*.ts"],
+ rules: {
+ // tsc (astro check) owns undefined-variable detection; the
+ // Astro frontmatter processor trips no-undef on TS globals.
+ "no-undef": "off",
+ },
+ },
+)
diff --git a/package.json b/package.json
index 7c82c60..8f241d0 100644
--- a/package.json
+++ b/package.json
@@ -47,12 +47,15 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-astro": "^1.0.0",
+ "eslint-plugin-vue": "^10.10.0",
+ "globals": "^17.11.0",
"happy-dom": "^20.11.1",
"playwright": "^1.62.1",
"prettier": "^3.9.0",
"prettier-plugin-astro": "^0.14.0",
"tsx": "^4.23.7",
"typescript": "^5.6.0",
+ "typescript-eslint": "^8.67.0",
"vitest": "^4.1.10"
}
}
diff --git a/public/img/partners/bas.png b/public/img/partners/bas.png
new file mode 100644
index 0000000..0368452
Binary files /dev/null and b/public/img/partners/bas.png differ
diff --git a/public/img/partners/bgn.png b/public/img/partners/bgn.png
new file mode 100644
index 0000000..3bf4852
Binary files /dev/null and b/public/img/partners/bgn.png differ
diff --git a/public/img/partners/calconnect.svg b/public/img/partners/calconnect.svg
new file mode 100644
index 0000000..2a84e1d
--- /dev/null
+++ b/public/img/partners/calconnect.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/img/partners/icao.ico b/public/img/partners/icao.ico
new file mode 100644
index 0000000..cee6d33
Binary files /dev/null and b/public/img/partners/icao.ico differ
diff --git a/public/img/partners/icao.png b/public/img/partners/icao.png
new file mode 100644
index 0000000..fc8bdee
--- /dev/null
+++ b/public/img/partners/icao.png
@@ -0,0 +1 @@
+404 Not Found
Not Found
The requested URL "https://www.icao.int/sites/default/files/ICAO_LOGO.png" was not found on this server.
\ No newline at end of file
diff --git a/public/img/partners/ogc.svg b/public/img/partners/ogc.svg
new file mode 100644
index 0000000..4a56d2b
--- /dev/null
+++ b/public/img/partners/ogc.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/public/img/partners/ungegn.png b/public/img/partners/ungegn.png
new file mode 100644
index 0000000..a48f41d
Binary files /dev/null and b/public/img/partners/ungegn.png differ
diff --git a/public/offline.html b/public/offline.html
index 1d5b0c9..48e6bff 100644
--- a/public/offline.html
+++ b/public/offline.html
@@ -50,9 +50,8 @@
You're offline.
- The Interscript page you wanted isn't cached yet. Pages and map
- data you've visited before are available — try one of those, or
- reconnect to load the page fresh.
+ The Interscript page you wanted isn't cached yet. Pages and map data you've visited before
+ are available — try one of those, or reconnect to load the page fresh.
- Same input, different romanization systems. Each authority publishes
- its own rules — Interscript encodes them as comparable, runnable maps
- so you can see the differences at a glance.
+ Same input, different romanization systems. Each authority publishes its own rules —
+ Interscript encodes them as comparable, runnable maps so you can see the differences at a
+ glance.
- The detector transliterates your source text through every system
- in the family, then ranks by Levenshtein distance between each
- output and your observed romanization. Distance 0 means the
+ The detector transliterates your source text through every system in the family, then ranks by
+ Levenshtein distance between each output and your observed romanization. Distance 0 means the
system produced your observed output exactly.
Live preview not available for this system.
- Only a curated subset of maps is shipped to the browser.
- To run this system locally:
+ This map has no browser-bundled ISC source. To run it locally:
- Open-source transliteration systems from BGN/PCGN, ISO, UN,
- ALA-LC, ODNI, and other authorities.
+ Open-source transliteration systems from BGN/PCGN, ISO, UN, ALA-LC, ODNI, and other
+ authorities.