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.jsmodule.t(key, vars)/getLocale/setLocale/onLocaleChange. Detectsnavigator.language, stores inoiia-locale-v1.KO/ENfloating 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/Kbindings. English getsn/i/alabels +N/I/Abindings (so English users can intuitively hit N·I·A). Slot labelsA/B/Care shared (Q/W/E bindings). - Loop 84 — keyhelp + hint i18n:
?overlay grid dynamically rebuilt fromKEY_ORDER+keyhelp.*. The.hintbody composes from three strings:hint.body.<locale>+hint.wave+hint.shortcuts. - Loop 85 — Initial
applyAllI18nordering bug: called beforeapp.innerHTML, so the shuffle button stayed Korean. Moved right afterapp.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 withN 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-filterplaceholder,aria-labelon every floating button. - Loop 91 — All 34 DJ effect descriptions: all
dj.desc.<id>translated. AdjDesc(id)helper called fromrenderDjSlotsinstead ofcurr.desc. Search filter follows the translation too. - Loop 92 — Session stats + segment editor:
stats.*,seg.play,seg.colorTitle. - Loop 93 — Inline title tooltips:
applyAllI18nsweeps atitleMaptable 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
#quantizebutton was still Korean — overridden insideapplyAllI18n. Playwright walks the entire DOM and confirms 0 matches for[가-힣].
UX improvements
- Loop 96 — Keyboard a11y:
#start-hint-btngetsfocus()right after load;:focus-visiblering (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:
onLocaleChangere-invokesrender(). 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
- Third language (Japanese) — the current structure scales easily.
DICT.ja = {...}. - Real-device iOS/Android verification — still homework. Especially haptics, focus management, screen readers.
- DJ effect
namelocalization experiments — currently fixed. Optional if the user asks. - Advanced mode mobile layout — 11 buttons stacked at the bottom right now. Extend the top-tools pattern from DJ mode?
- 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
renderHintusesinnerHTML; 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-labelis on floating buttons only..key·.dj-slot-wraphave clear visual labels, skipped.:focus-visibledoesn't show up on pointer click — only keyboard users see it. Correct.applyAllI18ncallsrenderKeyhelp·renderHint·renderDjSlots→ internalinnerHTMLreset → if any.firingtransient 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.