DEV Community

takahiro hashito
takahiro hashito

Posted on

My English page shipped with Japanese error messages, because the logic owned the strings

Background

I ship small single-file browser tools. Every tool has a Japanese page and an English page, and the rule I set for myself is that the two pages share the exact same logic. Same functions, same parsing, same edge cases. Only the wording differs.

Today I added a Cache-Control analyzer and shipped it. Then I opened the English page and the result panel said this:

Browser cache
  Freshness lifetime  365d
  Remaining           365d
  State               新鮮
Enter fullscreen mode Exit fullscreen mode

新鮮 means "fresh". The labels were English, the values were Japanese.

The mistake was in what the shared function returned

The shared analyzer looked like this. analyze() takes a Cache-Control value and returns freshness plus a list of findings.

var DIRECTIVES = {
  "max-age": { arg: "delta", who: "both",
    desc: "応答が新鮮でいられる秒数。Age を差し引いた残りで判定されます。" },
  "no-store": { arg: "none", who: "both",
    desc: "どのキャッシュにも保存させない。最も強い指定です。" },
  // ...
};

function freshness(limit, age) {
  if (limit === null) return { lifetime: null, state: "指定なし(ヒューリスティックに委ねられる)" };
  var rem = limit - age;
  return { lifetime: limit, remaining: rem, state: rem > 0 ? "新鮮" : "古い(要再検証)" };
}
Enter fullscreen mode Exit fullscreen mode

Both pages loaded this same file, and each page defined its own label table:

// on the English page
var L = { browser: "Browser cache", lifetime: "Freshness lifetime", state: "State" };
Enter fullscreen mode Exit fullscreen mode

So the page owned the labels and the logic owned the values. That split looked fine while I was writing it, and it is exactly the bug. The English page had no way to say "fresh", because the only place that word existed was inside a Japanese string literal in the shared file.

Nothing failed. No console error, no missing key, no fallback. The page rendered perfectly in two languages at once.

What I changed

I pulled every human-readable string out of the logic and turned it into a message pack the page supplies. The logic keeps the branching; the page keeps the wording.

The message pack below is generated per language and concatenated in front of the shared logic, so MSG and DIRTEXT are already in scope when the parser runs.

// generated per language, concatenated before the shared logic
var DIRTEXT = {
  "max-age": "How many seconds the response stays fresh. Freshness is measured against Age.",
  "no-store": "No cache may store the response at all. This is the strongest directive."
};
var MSG = {
  stHeuristic: "Not set (left to heuristics)",
  stFresh: "Fresh",
  stStale: "Stale (needs revalidation)",
  noCacheMaxAge: function (n) {
    return "no-cache is present, so every reuse revalidates even though max-age=" + n + ".";
  }
};
Enter fullscreen mode Exit fullscreen mode

DIRTEXT is keyed by directive name and feeds the explanation shown under each directive; MSG holds everything else the parser needs to say. The shared logic then reads like the code below, where limit is the max-age in seconds and age comes from the Age response header.

// shared logic, now with no literals a reader can see
function freshness(limit, age) {
  if (limit === null) return { lifetime: null, state: MSG.stHeuristic };
  var rem = limit - age;
  return { lifetime: limit, remaining: rem, state: rem > 0 ? MSG.stFresh : MSG.stStale };
}
Enter fullscreen mode Exit fullscreen mode

The branching is untouched. The only difference is that the three outcomes now name a key instead of spelling out a word, so the English page renders "Fresh" and the Japanese page renders the same state in Japanese from the same call.

Messages that interpolate a number became functions rather than templates with placeholders. That is deliberate: word order differs between the two languages, and a function lets each language put the number wherever it belongs instead of forcing both into one sentence shape.

I checked the result with a grep over the shared file. Zero lines containing CJK characters, apart from one code comment. If a Japanese literal ever comes back, that check finds it.

The part I did not expect

The obvious win is the English page being English. The unexpected win was that writing the two message packs side by side made me reread every message.

Three of them were vague in Japanese and I only noticed while translating. One said the equivalent of "this directive does not work", which is not useful. Writing the English version forced me to say why: private stops shared caches from storing the response, so s-maxage has no audience. I then rewrote the Japanese one to match.

Translation turned out to be a review pass I had not budgeted for.

The rule I am keeping

A shared function that returns a display string has silently chosen a language for every caller. If some callers are in a different language, that choice is a bug, and it is a bug that renders fine.

So: return keys, or take the strings as a parameter. Either way, the page decides the words.

Both tools are here:

https://hashitosystem.com/en/tools/cachecontrol/

If you maintain a page in more than one language, grep your shared modules for literals in your own first language today. Anything that comes back is a string your other pages cannot translate, and it will ship looking fine.


This article is about my own side project. It was written with AI assistance.

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

This approach of decoupling logic from localization is a solid improvement, ensuring that shared functions remain language-agnostic while allowing for flexibility in message formats. By using functions for interpolated messages, you also elegantly handle the nuances of language structure, which can be a common pitfall in multi-language applications. If you’re looking to expand this further, consider implementing a more structured localization library that supports additional languages seamlessly. I’d be interested in contributing to any enhancements or additional features in this area, especially if you're considering more complex localization requirements. What other challenges have you encountered in managing multi-language support?