Code › tail-villain
The Language Switcher Was Not the Problem
What bilingual support revealed about local UI state, account preferences, and dashboard structure
I clicked the language switcher, and the screen looked like nothing had changed.
The feature was not completely broken. State was changing, and some components were reading the new locale. The problem was that hardcoded strings still lived across the page. Users do not inspect internal state. If they click a button and the visible text stays the same, the feature feels broken.
That day’s work was supposed to be about supporting Korean and English. In practice, adding translation files was the smaller part. The harder part was deciding what kind of state language actually was. Some language changes are temporary UI state. Some are account preferences that should persist.
I had been experimenting with a stronger RPG-flavored UI in a separate worktree.
Tail Villain has villain interviewers, a dark visual tone, and enough personality to support that direction. But merging that experiment directly into the main product would have been risky. Reusable layout and bilingual infrastructure could move over. The whole voice of the experiment should not.
So I preserved the experiment separately and ported only the reusable infrastructure into main: the language provider, dictionaries, and switcher pieces. The copy itself stayed aligned with the existing product tone. Without that boundary, a bilingual support task could quietly turn into a product voice rewrite.
In code, the difference can look small. In the product, it is not. Moving infrastructure is not the same as moving the product’s voice.
The first issue was the responsibility of the language switcher.
A header switcher is good for changing the visible UI quickly. A user may want to check how the English UI looks, or switch language temporarily during a session. That is local UI state.
The account’s preferred language is different. It decides what language should be used the next time the user comes back. That belongs in profile settings, where the user expects persistent account defaults to live.
At first, those two responsibilities were coupled. Changing the global switcher could also update the stored account preference. That would create a strange experience later. If someone only switched to English for a moment, they would not expect their default language to change on the next visit.
So I split the responsibilities. The header switcher changes local UI language. The profile settings dialog changes the persisted account preference. Both look like “changing language,” but one is the state of the current screen and the other is a user setting.
Where state is stored becomes part of what that state means.
The reason the switcher looked broken came from the same kind of boundary problem.
If state changes but most of the page does not react, users experience that as failure. Bilingual support was not finished by creating a dictionary. The dashboard, roadmap creation, roadmap detail, interview start flow, and interview session all had to read the same language state.
The interview screen is especially sensitive to this. Tail Villain is not just a settings page. It is a product where users practice under pressure. If the question, buttons, and feedback appear in mixed languages, the pressure stops feeling intentional and starts feeling like a bug.
So I went through the main logged-in flow and removed hardcoded strings. Perfect translation quality was not the first target. The first target was making the UI visibly react when the user changed language.
Invisible state changes are not a feature. To the user, the screen has to change.
I also changed the dashboard structure during the same pass.
The old dashboard mixed roadmap inventory, progress, and next actions on one screen. That was tolerable early on, but as the product gained more data, the page lost its job. After login, the most important question is not “how many roadmaps do I have?” It is closer to “what should I do next?”
So /dashboard stayed as the overview, and roadmap management moved to /dashboard/roadmaps. The dashboard became the place for progress, review-now items, weak topics, and next-step guidance. The roadmap library became the place to manage roadmaps.
Once the screens had clearer jobs, the layout felt less forced. Some of the awkward right-rail stretching and sticky-scroll behavior looked like CSS problems at first. But if one page is trying to do too many things, layout issues are often information architecture issues in disguise.
Progress bars and a radar chart came in through that same lens. I did not add visualization because there was a chart component to use. I added it because the dashboard needed to show status at a glance: roadmap coverage, topic completion, practice coverage, review health, and interview performance.
The lesson from that work was simple enough.
Bilingual support was not just a translation problem. It was a state problem. I had to decide where language changes happen, where they persist, and which screens must react to them. If that boundary is unclear, users think the switcher is broken while I stare at logs saying the state changed correctly.
The dashboard had a similar problem. Sometimes a screen feels noisy not because the components are ugly, but because the screen has too many responsibilities. Cards and charts cannot fix a page that does not know what question it is answering.
Tail Villain needs villain interviewers that ask sharp questions. But before that, users should not get lost in the product. Whether it was the language switcher or the dashboard, the underlying problem was the same: showing product state in a form the user can understand.