Yesterday a lead from Ukraine asked to see my work. My case study was in Russian. Sending a Russian-language portfolio to a Ukrainian client is a good way to lose them before hello.
The site is static HTML on Vercel. No framework, no build step, and I wanted to keep it that way. An i18n library for two pages felt like overkill, so here's what I did instead.
Russian stays in the DOM, other languages are dictionaries
The page source is the Russian version. On load I walk all text nodes once and remember their original value, whitespace and a normalized key:
function norm(s){ return s.replace(/[\s ]+/g,' '
).trim(); }
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
var nodes = [];
while (walker.nextNode()) {
var n = walker.currentNode;
var key = norm(n.nodeValue);
if (!key) continue;
n.__ru = n.nodeValue; n.__key = key;
n.__lead = n.nodeValue.match(/^[\s ]/)[0];
n.__trail = n.nodeValue.match(/[\s ]$/)[0];
nodes.push(n);
}
Enter fullscreen mode Exit fullscreen mode
EN and UK are plain objects keyed by the normalized Russian string. Switching languages is rewriting nodeValue, nothing else:
function setLang(lang, persist){
var dict = lang === 'en' ? EN : (lang === 'uk' ? UK : null);
nodes.forEach(function(node){
node.nodeValue = dict ? (node.__lead + (dict[node.__key] || node.__ru) + node.__trail) : node.__ru;
});
document.documentElement.lang = lang;
if (persist) localStorage.setItem('langManual', '1');
localStorage.setItem('lang', lang);
}
Enter fullscreen mode Exit fullscreen mode
If a string is missing from the dictionary it stays Russian, so a half-translated page still works.
## [](#detection-order-matters-more-than-detection)Detection order matters more than detection
1. manual choice from localStorage wins forever
2. otherwise country by IP (ipapi.co free tier, 1.5s timeout), UA gets Ukrainian
3. if the IP call is slow or blocked, navigator.language already picked a language, so nothing blinks
The first version waited for the IP response and the page sat in Russian for a second. Felt broken. Render from browser language immediately, treat geo as an upgrade.
Limitations, honestly: this only swaps text nodes. Attributes like placeholder or aria-label need separate handling, and if one sentence is split across several tags the keys get ugly. For a portfolio it's more than enough.
Live: [https://maegamidev.vercel.app](https://maegamidev.vercel.app/) (dropdown top right). The case study it was built for: [https://maegamidev.vercel.app/euphoria.html](https://maegamidev.vercel.app/euphoria.html)
Sent the links to the lead the same evening.
🎉 Mid-Year Sale — Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all 635+ tutorials.
Sign In / Register — Start Free Trial7-day free trial · Cancel anytime · 30-day money-back