Blog
7 min read
Thucde.dev now speaks two languages
Actually... maybe more later. The app now has a real i18n foundation.

Sometimes I start with something that sounds tiny, and somehow it turns into a whole renovation.
This time, it was Thucde.dev.
At first, I only had one simple thought: if a post has both Vietnamese and English versions, can I add a button so readers can switch between them?
Sounds simple, right? One button.
VI. EN. Tap, switch, done.But the deeper I went, the clearer it became: doing bilingual support properly is not just about translating text. It touches how the app understands content, how URLs are shaped, how search and listings avoid duplicates, how metadata talks to Google, how RSS behaves, and even how production gets deployed.
In other words, Thucde.dev was not just “translated into English.” It had to learn how language works across the app.
From the outside, the most visible change is
/vi and /en. Under the hood, almost everything got touched: posts, dashboard, editor, search, feedback, sitemap, RSS, release notes, backend indexes, tests, and the deploy flow.It started as a language switcher. It ended up as a foundation upgrade.
Bilingual is not just translated textBilingual is not just translated text
Bilingual is not just translated textBilingual is not just translated text
The part I liked most about this work is that it forced me to rethink what “a post” actually means.
If one article has a Vietnamese version and an English version, is that one post or two? Keeping both languages inside one object sounds easier at first, but it gets messy for URLs, SEO, and metadata. Splitting them into separate posts works better for the web, but then they need to stay connected somehow.
The final model Thucde.dev landed on is: each translation is its own post, but all versions belong to the same translation group.
So a post can have:
txt
/vi/{slug} /en/{slug}
Two URLs, two language versions, two separate metadata sets. But meaning-wise, they are still the same article.
That keeps search and listings from showing duplicate content, while each version still gets a clean URL for sharing, indexing, previews, and reading in the right language.
That was the moment i18n stopped feeling like a UI layer. It became part of the data model.
The website needs to know which language it is speakingThe website needs to know which language it is speaking
The website needs to know which language it is speakingThe website needs to know which language it is speaking
When I added
/vi and /en, I did not want them to be decorative prefixes. Locale had to become a real part of the app.If someone is browsing in English, they should stay in that English space when they open a category, dashboard, post, search page, or release note. If they switch to Vietnamese, the app should keep their current context instead of throwing them out of the flow.
Tiny things like preserving query strings, rendering the right
html lang, keeping canonical URLs locale-aware, and preventing small links from falling back to old routes do not sound dramatic. But they decide whether the app feels real.A bilingual app where only the homepage is bilingual is not enough. The hard part is making the whole experience stay coherent.
SEO has to understand bilingual content tooSEO has to understand bilingual content too
SEO has to understand bilingual content tooSEO has to understand bilingual content too
Readers see the content. But the web also has Google, crawlers, RSS readers, social preview bots, browsers, parsers, and a whole bunch of systems that are not exactly human.
So when Thucde.dev has Vietnamese and English versions of the same post, the app needs to tell those systems clearly: these are language variants of one piece of content, not two accidental duplicates.
That is why this release touched
canonical, hreflang, Open Graph locale, JSON-LD, sitemap, RSS, and robots.txt.This part is not flashy. Most readers will never notice it. But to me, it is a sign that the app is maturing: it does not only render correctly for people, it also describes itself correctly to the rest of the web.
A nicer house is good. Clear paperwork is good too.
While I was there, I upgraded the foundation tooWhile I was there, I upgraded the foundation too
While I was there, I upgraded the foundation tooWhile I was there, I upgraded the foundation too
While working on
i18n, I also upgraded quite a bit of what sits underneath.The frontend is now on
Next.js 16.3 (preview), React 19, and TypeScript 6. Some older UI pieces were cleaned up, types got stricter, builds got faster, navigation got smoother, and a few production-only annoyances were fixed along the way.The backend had to follow too: posts now understand translations, translation drafts, source locale, grouping, duplicate-safe indexes, locale-aware feed, and locale-aware sitemap behavior. Security, caching, release notes, and deploy flow also got touched.
In normal-person words: I meant to fix the door, then opened the wall and decided the wiring and plumbing deserved attention too.
More tiring, yes. Worth it, also yes. 😋
Production tells the truthProduction tells the truth
Production tells the truthProduction tells the truth
One memorable part of this release was when everything looked fine locally, but I still could not call it done.
Tests passed. Build passed. Local smoke tests looked good. But production still had moments where
/vi and /en returned 200 while rendering 404 content. Some posts had the right canonical URL but the wrong html lang or og:locale.That was the familiar reminder: local being right does not automatically mean production is right.
This release only felt complete after database indexes were synced, frontend and backend were deployed, production routes were smoked, metadata was checked again, sitemap/feed/robots were working, and even smaller things like OG image generation were fixed.
It sounds exhausting, but that is what made the story feel real. Building an app is not always a string of “wow” moments. A lot of it is reading logs, clearing cache, checking ports, rerunning tests, waiting for deploys, and wondering, “wait, why does it work locally but not in prod?”
Building with AI, but still holding the wheelBuilding with AI, but still holding the wheel
Building with AI, but still holding the wheelBuilding with AI, but still holding the wheel
I also used AI quite a lot during this build.
Not in the “AI did everything for me” sense. It felt more like having a few assistants nearby: checking lists, spotting easy-to-miss edges, rebuilding context after several days, and reminding me to verify things I should not trust by vibes alone.
I still decide where Thucde.dev should go, what is worth doing, and what the experience should feel like. AI just helps me go deeper into layers that one small change can touch: routing, metadata, sitemap, feed, deploy, and production smoke tests.
The part I like is that it does not take the wheel. It feels more like a group in the passenger seat: opening the map, checking the mirrors, reminding me of the checklist, and occasionally going “hey, something looks off here.”
txt
Window: Jun 11 → Jun 30, 2026 Commits: 319 across root/frontend/backend App commits: 219 in frontend + backend Files touched: at least 874 Code churn: at least +74,600 / -41,194 lines Active commit days: 11
Thucde.dev is growing upThucde.dev is growing up
Thucde.dev is growing upThucde.dev is growing up
After this release, Thucde.dev can do something it could not properly do before: live in two languages.
I can write a post in Vietnamese, then create the English version later. The two versions can stay connected, while still having their own URL, metadata, and preview. Readers can switch languages inside the post. Search and listings do not duplicate content. Sitemap and RSS understand locale. Release notes are starting to feel more like a public changelog.
But the part I like most is not any single feature.
I like the feeling that Thucde.dev is slowly becoming a place that can grow with me.
It is still a personal site. Still a place where I write, experiment, break things, fix things, and learn. But underneath, it now has more of the qualities of a serious product: clearer routes, more careful deploys, more tests, better metadata, and a build workflow with more discipline.
Thucde.dev now speaks two languages.
More importantly, it is learning how to become a more
trustworthy app.
