Building the Input/Output UI in Astro
Romaji to Kana Converter: Astro
Chapter 4 · Building the Input/Output UI in Astro
Chapters 2 and 3 built a complete, correctly-tested conversion engine with zero Astro dependency anywhere in
it. This chapter gives src/lib/convert.ts its first real caller — replacing Chapter 1's own
placeholder page with an actual input box and live kana output, built entirely with a plain client-side
script and no framework, no island, and no extra dependency installed.
A Plain <script> Tag Is All This Needs
Astro processes an ordinary <script> tag — one with no attributes beyond an optional
src — through a real, specific pipeline, verified directly against Astro's own current
documentation rather than assumed:
- It's bundled by Vite. The script becomes a real ES module, deduplicated automatically if the same component ever renders more than once on a page, and small scripts get inlined into the HTML automatically.
- It supports real ES module imports. A default script can import both local files from
src/and npm packages — which is exactly what lets this page pull inconvertfromsrc/lib/convert.tswith an ordinary relative import. - It's TypeScript by default. Astro's own documentation states this plainly — no extra
config, no
lang="ts"attribute, nothing to opt into.
The one thing that turns all of this off is the is:inline directive — Astro's own documentation
is direct about what it does: a script marked is:inline is rendered into the HTML exactly as
written, with no TypeScript support and no import resolution at all. That's the right choice for an external
CDN script or something already sitting in public/; it's the wrong choice here, since this
page's whole script depends on importing a real local TypeScript module.
The Page Markup
A text input, and two separate output areas — one per script, matching how convert() has
returned both since Chapter 2:
No Astro frontmatter fence is needed anywhere in this file — nothing about this page runs at build time or request time on the server at all. Every part of what makes this page actually work happens in the browser, after the static HTML above has already loaded.
Wiring the Script
Imported directly, called on every keystroke, with the two output elements updated from the exact same
{ hiragana, katakana } shape convert() has returned since Chapter 2:
Running npm run dev and typing into the input box updates both output areas on every single
keystroke — no submit button, no page reload, and (per the section above) no bundler configuration of any
kind beyond what a default Astro scaffold already provides.
A Real, Honest Finding: Converting Live, Mid-Token
Converting on every keystroke means the engine is regularly asked to convert a string that isn't a complete
word yet — and Chapter 2's own longest-to-shortest tokenizer has a real, predictable answer for that:
whatever hasn't matched a real token yet passes through untouched. Typing the word kya
(客-adjacent syllable きゃ) one letter at a time, and calling convert() after each one, produces
exactly this, verified directly against real output:
kya is still incomplete, the tokenizer correctly has no
valid match for "k" or "ky" on their own, so both letters pass straight through as
raw Latin text — exactly the same "no match, emit the raw character" fallback Chapter 2 built for genuinely
unrecognized input. The instant the third letter completes a real token, the output snaps directly from raw
Latin to the correct kana in one keystroke. This is the algorithm working exactly as specified, not a
glitch — but a first-time user watching text flicker between "unconverted" and "converted" while typing a
yōon syllable could easily read it as broken if this behavior isn't expected going in.
こんにちは shows the same effect on a larger scale: since convert() checks the
lexicalized-exception dictionary against the whole word, typing "konnichiw" — one
letter short of the full greeting — produces the ordinary tokenized-but-uncorrected output, and only the
final "a" completes the exact string the dictionary is keyed on, snapping the whole output to
こんにちは in a single keystroke.
convert() is
a plain, synchronous, entirely local function call — there's no network request to rate-limit, and no
latency for a fast typist to outrun. A server-backed version of this exact same feature would have a real
reason to debounce every keystroke into a single delayed request instead of firing one per letter — that's
precisely the kind of tradeoff Chapter 5's own client-vs-server comparison exists to measure directly,
rather than this chapter simply declaring one approach better.
Trying It End to End
With the script wired in, typing each of the words already verified across Chapters 2 and 3 reproduces the
exact same output live in the browser that node already confirmed on the command line:
| Typed | Hiragana output | Katakana output |
|---|---|---|
sushi | すし | スシ |
gakusei | がくせい | ガクセイ |
konnichiwa | こんにちは | コンニチハ |
Nothing about this UI depends on where the conversion logic actually runs — convert() is
called the same way whether it's the local function this chapter imports directly, or (per Chapter 5) a
fetch call to a real endpoint returning the identical shape. That's deliberate: this chapter builds the UI
around a stable interface, so the comparison in Chapter 5 can swap out what's behind it without touching
the markup or the event listener at all.
Hands-On Exercises
Build index.astro exactly as this chapter describes, run npm run dev, and type "kya" into the input box one letter at a time. Confirm you see the same live intermediate output this chapter verified (k, then ky, then きゃ) before writing down what you observed.
📄 View solutionAdd a temporary console.log inside the input event handler, logging the raw input value and the converted hiragana/katakana on every keystroke. Open the browser console and confirm it fires exactly once per keystroke — not more, not less — while typing a full word.
📄 View solutionIn your own words, explain why this page's on-input conversion needs no debouncing at all, and name specifically what would have to change about where the conversion logic runs for debouncing to genuinely become necessary.
📄 View solutionChapter 4 Quick Reference
- index.astro — a plain input box plus two output areas, no frontmatter needed since nothing runs server-side here
- A default <script> tag — bundled by Vite, TypeScript by default, real ES module imports supported, verified against Astro's own current documentation
- is:inline — the one thing that turns all of that off; the wrong choice here, since this page depends on importing a real local module
- Live conversion, verified — an incomplete multi-character token (k, ky) passes through as raw Latin text until the full token completes, then snaps to the correct kana in one keystroke — expected, not a bug
- No debouncing needed — convert() is synchronous and entirely local, with no network request to rate-limit
- Stable interface — the UI calls convert() the same way regardless of where the logic behind it actually runs, setting up Chapter 5's own comparison cleanly
- Next chapter: Client-Side vs. Server-Side Conversion — building a real server-rendered alternative and measuring it directly against this one