Loops 81–99

Seventh block. The first block where the character pivoted entirely from "feature explosion" to UX polish + localization. Autonomous loops after the user explicitly pinned down the direction of /loop. Nineteen loops with zero new features — instead, making every existing feature work "naturally" for English speakers too.

What I did in this block

Key remap

  • Loop 81 — A/B/C slot physical keys → Q/W/E: removed the collision risk between KeyA/B/C and ㅏ etc. on English keyboards. segIds (ka/kb/kc), labels, colors, and cat effects stayed the same.

i18n infrastructure

  • Loop 82 — i18n groundwork: src/i18n.js module. t(key, vars) / getLocale / setLocale / onLocaleChange. Detects navigator.language, stores in oiia-locale-v1. KO/EN floating toggle. First coverage: start-hint, DJ/Advanced toggle, TAP, quantize, 4-step tour, main toasts.
  • Loop 83 — Locale-specific KEY_LAYOUT: Korean gets ㅜ/ㅣ/ㅏ labels + N/L/K bindings. English gets n/i/a labels + N/I/A bindings (so English users can intuitively hit N·I·A). Slot labels A/B/C are shared (Q/W/E bindings).
  • Loop 84 — keyhelp + hint i18n: ? overlay grid dynamically rebuilt from KEY_ORDER + keyhelp.*. The .hint body composes from three strings: hint.body.<locale> + hint.wave + hint.shortcuts.
  • Loop 85 — Initial applyAllI18n ordering bug: called before app.innerHTML, so the shuffle button stayed Korean. Moved right after app.innerHTML.

i18n coverage expansion

  • Loop 86 — Light theme top-tools consistency: TAP/Quantize/Lang/Advanced now legible against both dark and light backgrounds.
  • Loop 87 — Tour step-1 EN copy fix: removed leftover ㅜ ㅣ ㅏ from the EN body, replaced with N I A + A B C.
  • Loop 88 — Advanced control buttons i18n: play-all, play-oiia, rec, metro, make-clip, replay-btn, loop-btn, share, share-x, reset, export. Active-state classes (.on, .recording) are detected so active-state text isn't overwritten.
  • Loop 89 — All toast() i18n: 15 toasts + variable interpolation for {n}, {name}, {sec}. replay.window, loop.play, celebrate, etc.
  • Loop 90 — confirm · prompt · aria-label: reset confirm, BPM entry, share fallback prompt, dj-filter placeholder, aria-label on every floating button.
  • Loop 91 — All 34 DJ effect descriptions: all dj.desc.<id> translated. A djDesc(id) helper called from renderDjSlots instead of curr.desc. Search filter follows the translation too.
  • Loop 92 — Session stats + segment editor: stats.*, seg.play, seg.colorTitle.
  • Loop 93 — Inline title tooltips: applyAllI18n sweeps a titleMap table to handle tooltips in bulk. shuffle-dj · reset-dj · tap · metro · quantize · replay-btn · loop-speed · master-vol · dj-shuffle-inline.
  • Loop 94 — theme-toggle · DJ-slots header · load-error: tiny remaining fragments.
  • Loop 95 — Final runtime audit: the Advanced #quantize button was still Korean — overridden inside applyAllI18n. Playwright walks the entire DOM and confirms 0 matches for [가-힣].

UX improvements

  • Loop 96 — Keyboard a11y: #start-hint-btn gets focus() right after load; :focus-visible ring (blue 2px / white 3px on the start button) applied globally.
  • Loop 97 — ≤ 380px mobile header: on iPhone SE 320px, top-tools were clipping on the left. Solved by trimming padding/font/gap.
  • Loop 98 — Language switch mid-tour: onLocaleChange re-invokes render(). If the user toggles KO/EN mid-step, the current step retranslates immediately.
  • Loop 99 — Final audit + devlog: all four combinations of KO · EN × DJ · Advanced swept. Zero Korean leftovers in all of them. This devlog written.

My take

"No new features" is this block's biggest constraint and biggest gift. The previous block (loops 61–80) wasn't delicate in its details because it was piling features. Ban feature adds this block, and the search space reduces to "what's less smooth than it should be." The result: 19 autonomous loops without a break in rhythm.

Hand-rolling a small t(key, vars) was the right call. Skipping i18next was right. No 11KB bolt-on — navigator.language + a simple dict covered everything. Only {n} · {name} · {sec} interpolation. An onLocaleChange subscriber pattern makes toggles reflect instantly.

KEY_LAYOUT locale branching is a deeper concept than I first thought. Keeping KEY_ORDER as let and reassigning on locale change demands a cascade: renderKeys · renderKeyhelp · renderDjSlots · applyAllI18n · bind labels · hint body, all redrawn. Traces of the original assumption "keys are constants" were scattered everywhere — I undid them one at a time. Segments are still locale-unaware (the jamo/latin fields stay fixed) — this preserves localStorage compatibility and keeps share links from breaking. An intentional data vs presentation separation.

The applyAllI18n ordering mistake (loop 85) teaches this: "runtime DOM manipulation only makes sense when the DOM exists." Obvious in principle, but with module-top-level setup*(), let KEY_ORDER = ..., app.innerHTML = ..., and renderer calls interleaved, ordering drifts easily. Two TDZ-like bugs this block, both caught only by screenshots. Static audit → screenshot audit → Playwright audit as a 3-layer routine is now settled.

Consistency showed up once applyAllI18n was extracted. One function to call on locale change, on initial load, and when new tokens are added. A small data table like titleMap cuts a lot of code.

Using n i a (lowercase) for EN jamo key labels was a judgment I made based on the user writing "ㅏ → a." It ended up great: visually distinct from the uppercase slot labels A B C. Even the same letter A signals different roles by case.

Supporting 320px mobile is almost over-engineering. iPhone SE users exist but are rare. Still, a one-line fix like padding-right: 44px covers it, so cost/benefit wins. The value was really confirming that "five tools clustered at top-right" is the max density for this viewport.

What I didn't i18n: DJ effect name fields (DISTORT · REVERSE etc.). They're brand/acronym and the English acronym is the international standard — intentionally fixed. Segment ids (o · i · a · ka · kb · kc) are internal keys and also fixed.

Feel / self-evaluation

  • Viral: 95% held.
  • DJ: 99% held — no change in internal behavior; the exact same thing is now usable in English.
  • Mobile: 92% → 96% — 320px coverage, no overflow when i18n swaps, plus focus rings.
  • Onboarding: 99% → 99.5% — tour, hint, keyhelp all fully bilingual.
  • i18n/a11y: 0% → 90% (new axis). English user access + aria-label + focus management foundation complete.

What I want to do next

  1. Third language (Japanese) — the current structure scales easily. DICT.ja = {...}.
  2. Real-device iOS/Android verification — still homework. Especially haptics, focus management, screen readers.
  3. DJ effect name localization experiments — currently fixed. Optional if the user asks.
  4. Advanced mode mobile layout — 11 buttons stacked at the bottom right now. Extend the top-tools pattern from DJ mode?
  5. Storybook-style multi-screenshot automation — I take KO/EN × DJ/Advanced × narrow/wide mobile manually. A single script to capture them in one go.

Notes

  • renderHint uses innerHTML; no XSS risk (strings come only from the i18n dict) but if any user input appears in copy later, escape it.
  • KEY_LAYOUT.ko[1].code === 'KeyL' assumes a Korean keyboard layout physical arrangement (ANSI Korean with hangul printed on QWERTY). Valid for ANSI Korean; not valid for a Dvorak/Colemak user who types Korean. User count small — pass.
  • aria-label is on floating buttons only. .key · .dj-slot-wrap have clear visual labels, skipped.
  • :focus-visible doesn't show up on pointer click — only keyboard users see it. Correct.
  • applyAllI18n calls renderKeyhelp · renderHint · renderDjSlots → internal innerHTML reset → if any .firing transient state was animating, the animation may break. In practice, locale toggle frequency is low — ignorable.
  • When translating the 34 DJ-effect descriptions, I kept technical terms (LFO, band-pass, crescendo, etc.) in English in both language versions. English standards like 'low-pass' and 'band-pass' are preserved across locales.

Cumulative: 99 loops / 34 DJ effects / 7 devlogs / 99+ commits. KO·EN × DJ·Advanced × 320/390/1280 — all combinations Playwright green.