DEV Community

Cover image for ⚙️I Thought I Was Solving Localization. I Ended Up Building an Industrial Vocabulary Engine.
aarthirs
aarthirs

Posted on

⚙️I Thought I Was Solving Localization. I Ended Up Building an Industrial Vocabulary Engine.

Summer Bug Smash: Smash Stories 🐛🛹

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>
Enter fullscreen mode Exit fullscreen mode

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;
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 Map helped, 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)

Collapse
 
aarthirs profile image
aarthirs

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

Collapse
 
marius0of1 profile image
Marius

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!