Kristopher Baker iOS roots · Product systems · AI-assisted workflows
← Aoede

shipped · 2026.06.07 · 2 min read

Furigana That Matches the Voice

Aoede can now show furigana, the small kana readings printed above kanji, toggled from a toolbar button that only appears for Japanese. The part I care about is that the furigana matches the voice. Both the printed reading and the spoken one come from OpenJTalk, so when the narrator says わたし for 私, that is exactly what you see above the kanji, contextual particle readings included. If the OpenJTalk dictionary isn't present, the furigana falls back to CFStringTokenizer, the same fallback the audio uses.

I have a sibling app, Aside, that already does furigana, so I started by reaching for its logic. The reading code ported over cleanly: tokenize, convert to hiragana, trim okurigana, split mixed kanji runs. The rendering did not. Aside draws ruby with a single CTRubyAnnotation in one monolithic text block, and Aoede's whole reading experience is a karaoke highlight that slides word by word across the text. A monolithic block gives the highlight nowhere to land. So I rebuilt the rendering per word in SwiftUI: kana stacked over each kanji run, with an invisible kana lane over the plain runs so every line keeps the same baseline. The highlight survives, now gliding under the furigana instead of through it.

One small bug was worth the detour. When a kanji run is about as wide as its reading (物凄 is roughly as wide as ものすご), the kana-measured column could come up a hair short and SwiftUI would truncate the kanji itself to a single "…". Pinning the base run to its own ideal width, not just the kana lane's, fixed it. Furigana is a small feature on paper, but when you're reading in a language you're still learning, seeing the reading you're hearing is most of the point.