Adding Spanish and Portuguese to a desktop app, and getting caught on my own assumption
I spent a session adding locale support to Greenhouse, my Tauri desktop app for managing creative projects. The issue asked for three things: detect the OS locale, let the user switch languages, and draft the actual Spanish and Portuguese content on top of the string extraction that landed earlier. On paper that’s a small feature. In practice it forced two decisions I couldn’t just wing.
Problem: how does a locale switch actually work in a webview?
Paraglide (the i18n library) defaults to reloading the page on setLocale(). It also offers { reload: false }, paired with your own reactivity wiring, so the UI updates without a refresh. The obvious move for “modern” software is to skip the reload. Smoother, no flash, feels native.
Except I couldn’t point to anything proving that path worked in this app. Paraglide’s message functions are just plain functions that read the current locale when called - they’re not reactive on their own. Wiring { reload: false } correctly meant threading a reactivity shim through every place a message gets rendered, and trusting that Svelte’s fine-grained signal tracking would propagate through a chain of plain function calls it wasn’t specifically told about.
Why this approach: reload is the boring, provable choice
A full reload re-runs my app’s bootstrap, but the Rust process backing the Tauri window never restarts - the in-memory state (which vault is active, file-system permissions already granted) survives fine. And the app already had a precedent for this: switching vaults asks for a restart for a similar reason. A locale switch reloading the page isn’t a regression from that pattern, it’s consistent with it.
So instead of assuming the fancy no-reload path would work, I built the boring one and proved it end-to-end with a real WebDriver session driving the actual compiled app - not a mocked test, an actual click in an actual window:
• clicking Español…
• waiting for the reload to complete and dashboard to re-render…
boot locale: English pressed=true, Español pressed=false
PASS - locale persisted across a real reload.
That’s the whole point of driving the real app instead of a mocked component test: a mockIPC render can prove my Svelte code doesn’t crash, but it can’t prove a Rust command actually wrote a file to disk, or that a real page reload actually re-reads it back correctly. Only the real thing proves the real thing.
Gotcha: my test setup deleted a config file I didn’t mean to touch
While wiring up that WebDriver harness, I needed a throwaway vault so the app would boot straight to the dashboard instead of the onboarding wizard. I found the app’s real settings file on disk and, without checking what was actually in it, deleted it to get a clean slate.
It wasn’t a clean slate. It was the actual config pointing at my actual working vault. Nothing in the vault itself was touched - the settings file only stores a path, not the data behind it - but it was still a “measure twice” moment I skipped. The fix was easy (re-adopt the same folder), but it’s the kind of mistake that’s obvious in hindsight and invisible in the moment, because a file sitting in an app-support folder just doesn’t look precious the way a .git history does.
Gotcha: the draft translation used a word I’d never say
The other catch came while reviewing the machine-drafted translations for the app’s “garden bed” metaphor - the copy that nudges you when your project queue is running low. The draft said “bancales.” It’s a real word, it fit the sentence, and it looked plausible enough on the screen. It’s also a word I’ve never once said out loud, and I’ve spoken Latin American Spanish my whole life.
Digging in confirmed the instinct: “bancal” traces specifically to Spain’s terraced-farming vocabulary - it even has its own regional synonyms within Spain, which is usually a sign a word is a regionalism rather than something used everywhere Spanish is spoken. The word the RAE itself flags as the Latin American equivalent is “cantero.” Two different words for the same object, and the draft had defaulted to the wrong hemisphere without noticing.
This is exactly why the translation content got split into its own issue instead of being rubber-stamped alongside the mechanical extraction work: a plausible-sounding draft can be confidently wrong in precisely the register-and-audience dimension no build check will ever catch. Only a speaker reading the sentence catches “I’ve never heard that.”
Results
OS-locale detection, a persisted locale setting, and a footer switcher between English, Español, and Português. Full Spanish and Portuguese drafts for all 207 UI strings, reviewed and corrected for audience (Latin American Spanish, not Peninsular). A reload-based locale switch, proven against the real compiled app instead of assumed to work.
Reflection
The two mistakes in this session look unrelated - one’s an automation-testing slip, one’s a word choice - but they share a root cause: moving fast on something that felt obviously fine. “This settings file is disposable” and “this is the Spanish word for garden bed” were both confident guesses standing in for a check that hadn’t actually been done. Neither one was expensive to fix. But cheap-to-fix isn’t the same as free.
Related reading
Navigation redesign: topbar tabs and a Settings dialog
Eight footer controls at identical visual weight, an i18n key doing double duty, and the ARIA pattern the project's own a11y test refused to let me half-build.
Six small UX fixes in one sitting
Ultra-wide layout caps, locale-aware timestamps, emoji to SVG, dismissible errors, a first-run hero, and the word that showed up three times on one card.
Toast + undo for Greenhouse's vault action
The app's first transient-feedback surface, the aria-live rule that makes a conditionally-rendered toast silently unannounced, and a prop that goes stale the moment you rename.