// TL;DR
On 30 January 2026 the RS Award remake got a new design system with motion primitives and a Thai UI in the same milestone. That pairing was deliberate: Thai is a typography constraint before it is a translation job — no spaces between words, marks stacking above and below the baseline, and no italic tradition — so a component kit whose defaults were picked for English breaks in four places at once. The cost of batching them was that I could no longer attribute a regression to one change or the other.
On 30 January 2026 the RS Award remake got two things on the same day: a new design system with motion primitives, and a Thai user interface. RS Award is my family's plaque and award catalog — RUAMSUK PLATING LIMITED PARTNERSHIP has been making and plating awards in Pathum Thani since 2006 — and I had started remaking its 2022 WordPress build in Next.js eight weeks earlier, on 4 December 2025.
Two days before this milestone the data layer had moved from Prisma/Postgres to MongoDB, mid-build. My full-time job at that point was somewhere else entirely: the career timeline on this site puts me at Jasmine Technology Solution from July 2025, with no role at the family company again until the CTO one in August 2026. The remake carried on through that gap anyway, which is the honest shape of a lot of family-company work — no title on it, no sprint board, just the site that sells the plaques.
//Thai is a typography constraint, not a translation layer
The naive model of localization is that strings go out to be translated and come back longer. Thai punishes that model immediately, because the script itself disagrees with the assumptions baked into a Latin-first component kit. Four of those assumptions broke on the same afternoon.
- Thai does not put spaces between words. A run of text is one uninterrupted token as far as a naive line-breaker is concerned, so the browser either breaks anywhere or refuses to break at all — and a long product name pushes a card out of its own grid.
- Vowels and tone marks stack. They sit above and below the consonant, so a line-height chosen to look tight in English clips the marks against the line above.
- There is no italic tradition. Ask a browser for italic Thai and it synthesizes a slant that reads as a rendering fault, not as emphasis.
- A font that looks fine in the picker may have no Thai coverage at all — the fallback silently substitutes another face, and suddenly two typefaces are sharing one paragraph.
None of that is fixable string by string. It is fixable exactly once, in the tokens the whole system reads from — which is why replacing the design system and localizing to Thai were not two projects that happened to collide. They were one project.
:root {
--font-sans: "Noto Sans Thai", system-ui, sans-serif;
--leading-body: 1.75; /* a Latin-tuned 1.5 clips Thai tone marks */
}
[lang="th"] {
line-height: var(--leading-body);
line-break: loose;
overflow-wrap: anywhere;
font-style: normal; /* never synthesize italic Thai */
}The italic rule is the one worth arguing about. Dropping a style at the token level is heavy-handed — some day a component will genuinely want emphasis and find the door locked. I took that trade because the alternative is worse: a synthesized slant on Thai text looks like a bug to a Thai buyer, and the buyer never files the report. Emphasis moved to weight and colour, which both scripts render honestly.
//Motion primitives on a catalog nobody visits to be entertained
The new design system brought motion primitives with it, and motion is the part of a design system that is easiest to overspend. A plaque and award catalog has one job: get a buyer from a search result to a product page to a quote conversation. Custom awards are quoted, not carted, so every animation sits directly between the visitor and a phone call.
So the motion budget was deliberately small — entrance transitions on cards and page-level fades, nothing that delays a click — and everything gated on the user preference rather than my taste.
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}//Two variables, one milestone
Here is the part I would defend least. Changing the design system and the language of the interface in a single milestone means that when something looks wrong afterwards, you cannot say which change did it. That is a real cost and I paid it — a few layout oddities in the first pass could plausibly have come from either side, and I had to bisect them by hand instead of by history.
I did it anyway for a boring reason: both changes touched the same surface. Every component in the catalog needed its typography revisited for Thai, and every component was being restyled by the new system regardless. Splitting the milestone would have meant restyling the entire catalog twice, alone, in evenings that were already borrowed from a full-time job somewhere else. With a reviewer or a QA pass, I would split it. Solo, on a build that was still changing shape weekly, batching won.
That pace is the other thing worth being honest about. A data-layer migration on Wednesday and a design-system replacement on Friday is not discipline, it is a small project with exactly one person in it and no coordination cost. It works at this size. It stops working the moment a second person has to know what changed.
The words themselves were never mine to invent. The product vocabulary on those pages is what the company has used with Thai buyers since 2006, and my job was narrower than it first looked: stop the layout from mangling language that already worked. Eleven days later the same site got its product pages, structured data and client-side search — but none of that would have been worth shipping on a page that clipped its own tone marks.
// What I'd Do Differently
- If I ran it again I would land the design system first, verify it, then do the Thai pass — not because batching was wrong for a solo build, but because I traded away the ability to say which change caused what.
- Typography tokens are the cheapest place to hold a language. Line box, break policy, font stack and an emphasis rule fixed more Thai layout bugs than any component-level patch did.
- Motion primitives arrive with a design system whether you asked for them or not. Deciding the budget before adopting them is much easier than trimming them back afterwards.
- The existing Thai copy was an asset, not a starting point to improve on. Twenty years of selling awards produced the vocabulary; the interface just had to render it without damage.
FAQ
4Q1 //How do you localize a Next.js UI to Thai without breaking the layout?
Set the lang attribute so the browser applies the right line-breaking rules, then fix four things in your design tokens rather than in components: a taller line-height so stacked vowels and tone marks are not clipped, an explicit break policy for text with no word spaces, a font stack that actually has Thai coverage, and a rule that Thai never renders italic. Component-level patches will not hold, because every new component reintroduces the same four bugs.
Q2 //Why does Thai text break lines badly in a browser?
Thai is written without spaces between words, so a line-breaker with no dictionary sees one long token. Depending on the engine, you get either a line that overflows its container or a break placed in the middle of a word. Declaring the language and setting an explicit line-break and overflow-wrap policy gives the browser permission to break sensibly instead of guessing.
Q3 //Should Thai text ever be italic?
No. Thai has no italic tradition, and browsers respond to an italic request by synthesizing an oblique — a mechanical slant that Thai readers see as a rendering fault rather than as emphasis. Use weight, size or colour for emphasis instead, all of which read correctly in both scripts.
Q4 //Is it safe to replace a design system and localize an app in the same release?
Only when one person owns both changes and there is no live traffic to regress. The two touch the same surface, so batching them avoids restyling every component twice. The price is attribution: when something looks wrong afterwards you cannot tell from the history which change caused it, so you bisect by hand. With a reviewer or a QA pass in the loop, split them.