This is a submission for DEV's Summer Bug Smash: Smash Stories powered by Sentry.
⚙️I Thought I Was Solving Localization. I Ended Up Building an Industrial Vocabulary Engine.
When I picked up this task, I thought it was just another localization feature.
You know the drill—create language files, wrap labels with translation keys, fetch the dictionary, and call it a day.
I even estimated two days.
Then the first client requirement landed.
"This should say CNC Machine for Client A, Blow Moulding Unit for Client B, and Injection Press for Client C."
Same screen.
Same database row.
Same physical machine.
Three different names.
At that moment I realized I wasn't solving a localization problem anymore.
I was solving an industrial vocabulary problem.
In manufacturing, operators don't think in generic software terms. They think in the language they've used every day for years. If someone has always called a machine "Blow-2", your beautifully designed dashboard instantly loses credibility the moment it says "Machine 12."
That one requirement slowly expanded beyond machines.
Soon I had to support moulds, products, RFID stations, manual entry forms, production dashboards, alarms, and reports.
What started as "translate a few labels" became building a scalable engine capable of handling an entire industrial ecosystem.
🏭 The Stack
There wasn't anything fancy about the project.
- HTML
- JavaScript
- PHP
No build tools.
No third-party localization framework.
So the solution had to stay lightweight.
I built a runtime translation engine using data-i18n attributes and a MutationObserver so dynamically injected content could be translated automatically.
<div class="asset-card">
<span data-i18n="asset.machine.label">Machine</span>
<span data-i18n="asset.status.running">Running</span>
</div>
The first version looked perfectly reasonable.
function translate(root) {
root.querySelectorAll("[data-i18n]").forEach((element) => {
const key = element.dataset.i18n;
const translation = translations.find(t => t.key === key);
if (translation) {
element.textContent = translation.value;
}
});
}
It worked beautifully during development.
Which, as every engineer knows, is usually when the real problems are still politely waiting for production.
🐞 The Bug That Didn't Look Like a Bug
Nothing crashed.No red errors in the console.The dashboard simply became... slower.
Profiling eventually revealed the culprit.Every time I translated text, I modified the DOM.
Every DOM modification triggered the MutationObserver.
Which called the translator again.
Which modified the DOM again.
Thankfully it wasn't an infinite loop, but it was enthusiastically doing the same work hundreds of times.
To make things worse, every lookup searched an array linearly.
One translated element wasn't expensive.
Thousands certainly were.
🔧 Small Changes, Big Difference
The solution wasn't rewriting everything.
It was removing unnecessary work.
First, I converted the translation list into a Map.
const dictionary = new Map();
translations.forEach(item => {
dictionary.set(item.key, item.value);
});
Now every lookup became constant time instead of repeatedly scanning the entire array.
Next, I gave translated elements a memory.
Instead of translating every node repeatedly, I stored a small version stamp.
<span
data-i18n="asset.machine.label"
data-i18n-done="en:v3">
</span>
If an element had already been translated for the current dictionary version, it was skipped.
Finally, I temporarily disconnected the MutationObserver while applying updates.
That one change stopped the observer from reacting to its own work.
Sometimes debugging feels like arguing with yourself.
In this case, the DOM literally was.
📈 Scaling Beyond Machines
Performance was only half the problem.
The more interesting challenge was designing something that would continue working as the application grew.
Initially I considered maintaining separate translation files for every client.
That approach lasted about five minutes.
Every new feature meant updating multiple dictionaries, and eventually someone would forget one.
Instead, I designed a layered vocabulary system.
The server resolved the correct terminology for each client and sent one flattened dictionary to the browser.
The frontend didn't need to know whether a label came from a client override, an industry profile, or the default language.
It simply performed one lookup.
The exact same engine eventually handled:
| Domain | Example |
|---|---|
| Machines | CNC Machine, Blow Moulding Unit |
| Moulds | Client-specific mould names |
| Products | Factory terminology |
| RFID Stations | Scanner labels |
| Manual Entry Forms | Operator-friendly field names |
The code never changed.
Only the vocabulary did.
That turned out to be one of the biggest wins of the project.
📊 Before / After
| Before | After | |
|---|---|---|
| Lookup cost per key |
O(n) array scan |
O(1) Map read |
| Re-processing already-resolved nodes | Every pass | Never (guard stamp) |
| Observer reacting to its own writes | Yes | No (disconnect + rAF batch) |
| Writes per navigation (dense screen) | ~2,400 | ~0 for unchanged nodes |
| Visible label flicker | Yes | Gone |
| Language switch | Full manual re-render | Bump version, DOM self-heals |
💡 What This Project Taught Me
Every production bug teaches something.
This one taught me that solving the immediate problem isn't always enough—you have to understand the domain behind it.
A few lessons have stayed with me ever since:
Build for change, not just today's requirements. What started as machine labels quickly expanded to moulds, products, RFID stations, manual entry forms, and dashboards. A flexible design saved countless future changes.
Generic solutions outlast duplicated ones. One reusable vocabulary engine proved far more maintainable than creating separate implementations for every client.
Model your data carefully. Once the vocabulary became structured data instead of hardcoded strings, the UI became simpler, faster, and easier to extend.
Performance is often about eliminating unnecessary work. Converting lookups to a
Maphelped, but the biggest improvement came from preventing repeated translations in the first place.Technology should adapt to the business—not the other way around. Every factory had its own terminology, and the software needed to respect that rather than forcing a single vocabulary.
Looking back, the biggest improvement wasn't the optimization itself—it was designing an architecture that could evolve as the business grew.
🎯 Looking Back
When I accepted this task, I expected to build a simple localization feature.
Instead, I ended up designing a scalable vocabulary engine that now supports machines, moulds, products, RFID workflows, manual entry forms, and multiple industrial domains—all using the same underlying architecture.
The most valuable lesson wasn't learning how to optimize a MutationObserver or replace an array with a Map.
It was realizing that in industrial software, words are part of the domain model.
The labels on a screen aren't just UI text—they represent years of operational knowledge and the language people trust every day.
Once I started treating vocabulary as business data instead of presentation text, many architectural decisions became surprisingly clear.
I started by solving one machine.
I ended up building a platform capable of understanding an entire industrial ecosystem. 🚀
Top comments (2)
Did you ever ship a MutationObserver that ended up observing itself? I'd love to hear the war story - drop it in the comments. And if you've solved per-tenant vocabulary in a legacy stack differently, genuinely tell me, I'm still not sure I picked the best hill
Fantastic read! "In this case, the DOM literally was arguing with itself" had me laughing because we've all been trapped in observer loops at some point!