Skip to content

Commit ffefd42

Browse files
committed
fix: live mosaic + ISO data + broken legacy pages
Three intertwined fixes: 1. Script mosaic was showing (error) on every cell Root cause: components used import.meta.glob("/maps/*.json") to preload map IR, but Vite's glob doesn't see files in public/ in dev (only source files). The strategy was registered with an empty map dictionary, so every transliterate() call threw "Map dependency missing". Fix: switched ScriptMosaic, MapExplorer, MapPreview, MapCatalogue, HeroMorph, QuickBox to use fetch() at runtime, walking transitive dependencies in two passes so multi-stage pipelines resolve. Also corrected six system codes in the mosaic that never existed (un-hin-Deva-Latn-1972 -> un-hin-Deva-Latn-2016, etc.). 2. /about, /docs, /blog rendered without styles Root cause: those pages used legacy design tokens (--muted, --text, --border, --accent, --bg, --surface) that no longer exist after the redesign. Fix: bulk-migrated every legacy token to the new design system (--color-stone, --color-ink, --color-rule, --color-brand, etc.). 3. Wired in @iso24229/iso15924-data and @iso24229/iso639-data Every transliteration system carries ISO 15924 script codes (source + destination) and ISO 639 language info (or multiple for cross-language systems). New src/data/iso.ts resolves codes to human names at build time: - scriptName("Cyrl") -> "Cyrillic" - scriptNumber("Cyrl") -> 220 - languageName("eng") -> "English" - parseLanguageField("iso-639-2:eng|iso-639-2:fr") -> "English / French" Map detail pages now show "Cyrillic · 220" instead of bare "Cyrl", and language fields render as "English / French" instead of raw "iso-639-2:eng|iso-639-2:fr". Tests: - Added test/iso-data.test.ts (10 tests covering script + lang resolution) - Updated script-mosaic.test.ts to mock fetch() - All 129 tests pass
1 parent e172ea9 commit ffefd42

18 files changed

Lines changed: 358 additions & 76 deletions

package-lock.json

Lines changed: 18 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
"@fontsource-variable/inter-tight": "^5.3.0",
2626
"@fontsource-variable/newsreader": "^5.3.0",
2727
"@fontsource/jetbrains-mono": "^5.3.0",
28+
"@iso24229/iso15924-data": "^0.2.0",
29+
"@iso24229/iso639-data": "^0.2.0",
2830
"@tailwindcss/vite": "^4.0.0",
2931
"astro": "^7.0.0",
3032
"interscript-ts": "file:../interscript-ts",

src/components/HeroMorph.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,18 @@ async function ensureEngine() {
4848
if (transliterateFn) return
4949
try {
5050
const mod = await import("interscript-ts")
51-
const modules = import.meta.glob("/maps/*.json", { eager: true, as: "raw" })
51+
const wanted = new Set<string>(morphs.map((m) => m.system))
5252
const maps: Record<string, unknown> = {}
53-
for (const [path, raw] of Object.entries(modules)) {
54-
const code = path.match(/\/maps\/(.+)\.json$/)?.[1]
55-
if (code) maps[code] = JSON.parse(raw as string)
53+
const fetchOne = async (code: string) => {
54+
if (maps[code]) return
55+
const res = await fetch(`/maps/${code}.json`)
56+
if (!res.ok) return
57+
const json = (await res.json()) as { dependencies?: string[] }
58+
maps[code] = json
59+
for (const dep of json.dependencies ?? []) wanted.add(dep)
5660
}
61+
for (const code of wanted) await fetchOne(code)
62+
for (const code of [...wanted]) await fetchOne(code)
5763
mod.reset()
5864
mod.configure({ strategies: [mod.bundledStrategy(maps)] })
5965
transliterateFn = mod.transliterate

src/components/MapExplorer.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@ async function ensureEngine() {
2121
engine.value = "loading"
2222
try {
2323
const mod = await import("interscript-ts")
24-
const modules = import.meta.glob("/maps/*.json", { eager: true, as: "raw" })
24+
// fetch() the systems we need + their transitive deps. import.meta.glob
25+
// doesn't see files under public/ in dev.
26+
const wanted = new Set<string>(props.systems.map((s) => s.code))
2527
const maps: Record<string, unknown> = {}
26-
for (const [path, raw] of Object.entries(modules)) {
27-
const code = path.match(/\/maps\/(.+)\.json$/)?.[1]
28-
if (code) maps[code] = JSON.parse(raw as string)
28+
const fetchOne = async (code: string) => {
29+
if (maps[code]) return
30+
const res = await fetch(`/maps/${code}.json`)
31+
if (!res.ok) return
32+
const json = (await res.json()) as { dependencies?: string[] }
33+
maps[code] = json
34+
for (const dep of json.dependencies ?? []) wanted.add(dep)
2935
}
36+
for (const code of wanted) await fetchOne(code)
37+
for (const code of [...wanted]) await fetchOne(code)
3038
mod.reset()
3139
mod.configure({ strategies: [mod.bundledStrategy(maps)] })
3240
transliterateFn = mod.transliterate

src/components/MapPreview.vue

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,20 @@ async function ensureEngine() {
3838
engine.value = "loading"
3939
try {
4040
const mod = await import("interscript-ts")
41-
const modules = import.meta.glob("/maps/*.json", { eager: true, as: "raw" })
41+
// Fetch this system + its transitive deps. import.meta.glob can't
42+
// see public/ files in dev.
43+
const wanted = new Set<string>([props.systemCode])
4244
const maps: Record<string, unknown> = {}
43-
for (const [path, raw] of Object.entries(modules)) {
44-
const code = path.match(/\/maps\/(.+)\.json$/)?.[1]
45-
if (code) maps[code] = JSON.parse(raw as string)
45+
const fetchOne = async (code: string) => {
46+
if (maps[code]) return
47+
const res = await fetch(`/maps/${code}.json`)
48+
if (!res.ok) return
49+
const json = (await res.json()) as { dependencies?: string[] }
50+
maps[code] = json
51+
for (const dep of json.dependencies ?? []) wanted.add(dep)
4652
}
53+
for (const code of wanted) await fetchOne(code)
54+
for (const code of [...wanted]) await fetchOne(code)
4755
mod.reset()
4856
mod.configure({ strategies: [mod.bundledStrategy(maps)] })
4957
transliterateFn = mod.transliterate

src/components/QuickBox.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,18 @@ async function ensureEngine() {
4040
if (transliterateFn) return
4141
try {
4242
const mod = await import("interscript-ts")
43-
const modules = import.meta.glob("/maps/*.json", { eager: true, as: "raw" })
43+
const wanted = new Set<string>(systems.map((s) => s.code))
4444
const maps: Record<string, unknown> = {}
45-
for (const [path, raw] of Object.entries(modules)) {
46-
const code = path.match(/\/maps\/(.+)\.json$/)?.[1]
47-
if (code) maps[code] = JSON.parse(raw as string)
45+
const fetchOne = async (code: string) => {
46+
if (maps[code]) return
47+
const res = await fetch(`/maps/${code}.json`)
48+
if (!res.ok) return
49+
const json = (await res.json()) as { dependencies?: string[] }
50+
maps[code] = json
51+
for (const dep of json.dependencies ?? []) wanted.add(dep)
4852
}
53+
for (const code of wanted) await fetchOne(code)
54+
for (const code of [...wanted]) await fetchOne(code)
4955
mod.reset()
5056
mod.configure({ strategies: [mod.bundledStrategy(maps)] })
5157
transliterateFn = mod.transliterate

src/components/ScriptMosaic.vue

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ const cells: Cell[] = [
6161
id: "devanagari",
6262
script: "Devanagari",
6363
transforms: [
64-
{ system: "un-hin-Deva-Latn-1972", input: "महात्मा", authority: "UN", note: "Hindi · 1972" },
65-
{ system: "alalc-mar-Deva-Latn-1997", input: "मुंबई", authority: "ALA-LC", note: "Marathi · 1997" },
64+
{ system: "un-hin-Deva-Latn-2016", input: "महात्मा", authority: "UN", note: "Hindi · 2016" },
65+
{ system: "alalc-hin-Deva-Latn-2011", input: "मुंबई", authority: "ALA-LC", note: "Hindi · 2011" },
6666
{ system: "iso-hin-Deva-Latn-15919-2001", input: "फिलिपींस", authority: "ISO", note: "ISO 15919" },
6767
],
6868
},
@@ -71,8 +71,8 @@ const cells: Cell[] = [
7171
script: "Han",
7272
transforms: [
7373
{ system: "acadsin-zho-Hani-Latn-2002", input: "台北", authority: "Academia Sinica", note: "Tongyong · 2002" },
74-
{ system: "bgnpcgn-zho-Hani-Latn-1979", input: "北京", authority: "BGN/PCGN", note: "Hanyu Pinyin · 1979" },
75-
{ system: "iso-zho-Hani-Latn-1996", input: "香港", authority: "ISO", note: "ISO 7098 · 1996" },
74+
{ system: "bgnpcgn-zho-Hans-Latn-1979", input: "北京", authority: "BGN/PCGN", note: "Hanyu Pinyin · 1979" },
75+
{ system: "sac-zho-Hans-Latn-1979", input: "香港", authority: "SAC", note: "Hans · 1979" },
7676
],
7777
},
7878
{
@@ -87,9 +87,9 @@ const cells: Cell[] = [
8787
id: "greek",
8888
script: "Greek",
8989
transforms: [
90-
{ system: "iso-grc-Grek-Latn-843-1997", input: "Αθήνα", authority: "ISO", note: "Greek · 843/1997" },
91-
{ system: "alalc-grc-Grek-Latn-1997", input: "Θεσσαλονίκη", authority: "ALA-LC", note: "Greek · 1997" },
92-
{ system: "bgnpcgn-grc-Grek-Latn-1962", input: "Ελλάδα", authority: "BGN/PCGN", note: "Greek · 1962" },
90+
{ system: "iso-ell-Grek-Latn-843-1997-t1", input: "Αθήνα", authority: "ISO", note: "Greek · 843/1997" },
91+
{ system: "alalc-ell-Grek-Latn-1997", input: "Θεσσαλονίκη", authority: "ALA-LC", note: "Greek · 1997" },
92+
{ system: "bgnpcgn-ell-Grek-Latn-1962", input: "Ελλάδα", authority: "BGN/PCGN", note: "Greek · 1962" },
9393
],
9494
},
9595
]
@@ -105,12 +105,30 @@ async function ensureEngine() {
105105
if (transliterateFn) return
106106
try {
107107
const mod = await import("interscript-ts")
108-
const modules = import.meta.glob("/maps/*.json", { eager: true, as: "raw" })
108+
// Collect every system code referenced by the mosaic, plus their
109+
// transitive dependencies. fetch() each one — import.meta.glob
110+
// doesn't see files under public/ in dev.
111+
const wanted = new Set<string>()
112+
for (const cell of cells) {
113+
for (const tf of cell.transforms) {
114+
wanted.add(tf.system)
115+
}
116+
}
109117
const maps: Record<string, unknown> = {}
110-
for (const [path, raw] of Object.entries(modules)) {
111-
const code = path.match(/\/maps\/(.+)\.json$/)?.[1]
112-
if (code) maps[code] = JSON.parse(raw as string)
118+
// Fetch in two passes: first the systems themselves, then any
119+
// transitive deps they declare (so multi-stage pipelines resolve).
120+
const fetchOne = async (code: string) => {
121+
if (maps[code]) return
122+
const res = await fetch(`/maps/${code}.json`)
123+
if (!res.ok) return
124+
const json = (await res.json()) as { dependencies?: string[] }
125+
maps[code] = json
126+
for (const dep of json.dependencies ?? []) wanted.add(dep)
113127
}
128+
for (const code of wanted) await fetchOne(code)
129+
// Second pass for any deps discovered above.
130+
for (const code of [...wanted]) await fetchOne(code)
131+
114132
mod.reset()
115133
mod.configure({ strategies: [mod.bundledStrategy(maps)] })
116134
transliterateFn = mod.transliterate

src/data/iso.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Build-time helpers for ISO 15924 (script) and ISO 639 (language)
3+
* code resolution. Powered by @iso24229/iso15924-data and
4+
* @iso24229/iso639-data — the canonical ISO registries, kept in sync
5+
* with the upstream sources via npm.
6+
*
7+
* Server-only: imports `node:fs`. Used in Astro frontmatter, not in
8+
* browser islands.
9+
*/
10+
11+
import { codes as scriptCodes } from "@iso24229/iso15924-data"
12+
import { parts as langParts } from "@iso24229/iso639-data"
13+
14+
const langIndex = new Map<string, string>()
15+
for (const part of ["639-1", "639-2", "639-3", "639-5"] as const) {
16+
for (const [code, entry] of Object.entries(langParts[part] ?? {})) {
17+
const name = (entry as { name?: { en?: string } }).name?.en
18+
if (name && !langIndex.has(code)) langIndex.set(code, name)
19+
}
20+
}
21+
22+
const scriptIndex = new Map<string, { name: string; number: string | number }>()
23+
for (const [code, entry] of Object.entries(scriptCodes)) {
24+
const name = (entry as { name?: { en?: string }; pva?: string }).name?.en
25+
?? (entry as { pva?: string }).pva
26+
?? code
27+
scriptIndex.set(code, { name, number: (entry as { number: string | number }).number })
28+
}
29+
30+
/**
31+
* Resolve an ISO 15924 script code (e.g. "Cyrl", "Arab") to its
32+
* English display name. Falls back to the code itself if unknown.
33+
*/
34+
export function scriptName(code: string): string {
35+
return scriptIndex.get(code)?.name ?? code
36+
}
37+
38+
/**
39+
* Resolve an ISO 15924 script code to its numeric identifier.
40+
*/
41+
export function scriptNumber(code: string): string | number | undefined {
42+
return scriptIndex.get(code)?.number
43+
}
44+
45+
/**
46+
* Resolve an ISO 639 language code (any of 639-1/2/3/5 — e.g. "en",
47+
* "eng", "deu", "ger") to its English display name.
48+
*/
49+
export function languageName(code: string): string {
50+
return langIndex.get(code) ?? code
51+
}
52+
53+
/**
54+
* Parse a catalogue `language` field which may take the form
55+
* `iso-639-2:eng` or `iso-639-2:eng|iso-639-2:en` (cross-language).
56+
* Returns the resolved display name(s), comma-separated.
57+
*/
58+
export function parseLanguageField(field: string | undefined): string {
59+
if (!field) return ""
60+
const parts = field.split("|").map((p) => p.trim())
61+
const names = parts
62+
.map((p) => {
63+
const m = p.match(/^iso-639-\d:(.+)$/)
64+
if (!m) return p
65+
return languageName(m[1]!)
66+
})
67+
.filter(Boolean)
68+
return names.join(" / ")
69+
}
70+
71+
export interface ScriptInfo {
72+
code: string
73+
name: string
74+
number?: string | number
75+
}
76+
77+
export interface LanguageInfo {
78+
code: string
79+
name: string
80+
}
81+
82+
/** All known script codes — used to populate filter facets. */
83+
export function allScripts(): ScriptInfo[] {
84+
return [...scriptIndex.entries()]
85+
.map(([code, { name, number }]) => ({ code, name, number }))
86+
.sort((a, b) => a.code.localeCompare(b.code))
87+
}

src/pages/404.astro

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import Base from "../layouts/Base.astro"
2929
h1 {
3030
font-size: clamp(4rem, 12vw, 8rem);
3131
margin: 0;
32-
color: var(--accent);
32+
color: var(--color-brand);
3333
letter-spacing: -0.05em;
3434
}
3535
.lead {
@@ -45,13 +45,13 @@ import Base from "../layouts/Base.astro"
4545
}
4646
.suggestions li {
4747
padding: 0.5rem 0;
48-
border-bottom: 1px solid var(--border);
48+
border-bottom: 1px solid var(--color-rule);
4949
}
5050
.search-hint {
51-
color: var(--muted);
51+
color: var(--color-stone);
5252
margin-top: 2rem;
5353
}
5454
a {
55-
color: var(--accent);
55+
color: var(--color-brand);
5656
}
5757
</style>

src/pages/about.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ import Base from "../layouts/Base.astro"
7070
}
7171
p {
7272
line-height: 1.7;
73-
color: var(--text);
73+
color: var(--color-ink);
7474
}
7575
a {
76-
color: var(--accent);
76+
color: var(--color-brand);
7777
}
7878
</style>

0 commit comments

Comments
 (0)