DEV Community

Cover image for I found 216 missing translations. There were 55.
Hammad Shams Uddin
Hammad Shams Uddin

Posted on

I found 216 missing translations. There were 55.

I run a site with about 6,000 pages across five languages. Last week I went looking for interface strings that had never been translated — the labels on form fields, the options in a dropdown, the text on a button.

I wrote a script. It walked the tool registry, collected every input_label, every option label, every placeholder, every select choice, and diffed that set against the Spanish translation catalogue.

216 missing.

I spent the evening writing translations for all of them, in four languages.

They were not missing.

Two dictionaries

The site has two translation catalogues, and I had forgotten there were two.

data/i18n/{lang}/tool-labels.php     read by the PHP view
data/i18n/{lang}/widget-labels.php   shipped to the browser as JSON
Enter fullscreen mode Exit fullscreen mode

The first one is for strings the server renders into the HTML — form labels, buttons, the options inside a <select>. The second is for strings the JavaScript builds at runtime, after the page has loaded: the word "Copy" on a copy button, a result label that only exists once you've clicked something.

They exist separately for a boring, correct reason. The second one gets serialised into every page as a <script> blob, so it holds only what the browser genuinely needs. Putting the server-side labels in there would ship a few hundred strings to every visitor for nothing.

I diffed against the second one. The view reads the first.

The real gap was 55 strings, and the mechanism that filled them had existed and worked for months.

Every step was defensible

This is the part I keep turning over.

Nothing I did was sloppy. I read the registry that defines the tools. I read the catalogue that holds the translations. I wrote a set difference. I sorted the output and eyeballed it, and the strings in it were plainly English — Cost factor, Background color, Algorithm — which is exactly what a missing translation looks like.

At no point did the evidence contradict itself. The numbers agreed with each other all the way down, because they were all answering the same wrong question: is this string in that file?

The question I actually had was: does a Spanish visitor see this in Spanish?

One HTTP request would have answered it:

curl -s https://example.com/es/documents/rotate-pdf | grep -o '<label[^>]*>[^<]*'
Enter fullscreen mode Exit fullscreen mode

Rotación. 90° en sentido horario. Already there, in the first minute, instead of the fourth hour.

It got worse before it got better

Having "found" the gap, I fixed it — by changing the view to look strings up in the JavaScript dictionary as well.

That looked like it worked. Pages rendered, labels appeared, nothing broke.

What it actually did was give the application two dictionaries answering the same question, and they did not agree. The English string Order was translated as 顺序 in one and 排序 in the other. Both are correct Chinese. Which one a visitor saw came down to which lookup ran first — an accident of the order of two lines in a template.

I only caught it because a page showed me 顺序 while the file I had just edited said 排序, and those two facts could not both be true.

That is the second lesson hiding inside the first: a fix built on a wrong diagnosis does not fail loudly. It produces a system that works and is wrong, which is considerably harder to find later.

The same week, twice more

Once you have a name for a mistake you start seeing it.

Two new pages, no internal links. I added two tools to the site and checked whether anything linked to them, by reading the related arrays in the data file. Zero. I was about to spend an hour adding links by hand when I loaded one of the existing pages instead. The links were already there — the registry's helper pads a short related list with other tools from the same category, and every page in that category already pointed at the new ones.

A backlink that does not exist. I was about to submit the site's API to a directory, and the question I set out to answer was whether their listing gives a dofollow or a nofollow link. I fetched a real listing page. 230 KB of HTML, and not one link to the provider's own site. The provider's domain does not appear anywhere on the page. Their profile URL 404s. robots.txt disallows the whole /provider/ path.

The question was not dofollow-or-nofollow. There is no link. I had spent my effort on a refinement of a premise I never checked.

What I actually learned

The tempting lesson is "always verify manually," and that is both true and useless — you cannot manually verify everything, and reading files is how most work gets done.

The useful version is narrower:

Every question has an artifact that is the ground truth for it, and it is often not the artifact you are already holding.

  • Is this string in the catalogue? → the catalogue is ground truth. Read the file.
  • Does the user see this translated? → the rendered page is ground truth. Read the page.
  • Does this page link to that one? → the rendered page, not the config that generates it.
  • Does this site give me a link? → their rendered page, not their marketing copy about their programme.

The failure in all three cases was substituting a source that was upstream of the truth for the truth itself. Upstream sources are seductive because they are structured, greppable, and fast. The rendered page is none of those things, which is exactly why it is the one that settles arguments.

A cheap habit that would have caught all three: before writing the script that measures the thing, load the thing once and look at it. Not as a test — as a sanity check on what you are about to spend the evening measuring.

Fifty-five strings needed translating. I found that out the second time.


I build Utilorax, a set of free browser-based tools. It is five languages deep, which is how you end up with two translation dictionaries and forget one of them exists.

Top comments (0)