-
The Smallest Possible i18n: One JavaScript File, No Framework, No Build Step
2 days ago • 0 commentsThe Smallest Possible i18n: One JavaScript File, No Framework, No Build Step
I have a static HTML site with no React, no Vue, no bundler, no build step. cp is my deploy tool. I wanted to translate the whole thing into Japanese, Chinese, and French. The finished site lives at saturacorp.com, and this post walks through the i18n approach I used to build it. The translation file and runtime are visible via view-source on any page, so you can follow along with real examples as you read.
Every i18n tutorial I could find assumed a framework.
react-intl,vue-i18n, Next.js'snext-i18next,formatjs, they all want either a component model or a build step, usually both. The pattern is always the same: annotate your markup with abstract keys, store translations in JSON files keyed by those names, and let the build or runtime swap the strings.That didn't fit saturacorp.com. The whole appeal of the site was that the HTML stayed human-readable. Annotating every heading and paragraph with
data-i18n="home.businesses.heading"would have polluted the markup with framework apparatus before any visitor saw the page.So I took a different path. I used CSS selectors as translation keys. This post is about what that looks like, why it worked for the site, and the specific kinds of bugs it produces that a key-based system would never have. The point isn't "here's a new way to do i18n," it's "here's an approach with a very specific shape of constraint where it fits, and a very specific failure mode that should keep you from using it almost anywhere else."
What the standard approach looks like
In a key-based i18n system, the homepage's "Group Companies" heading on saturacorp.com would look like this in the markup:
<h2 data-i18n="home.groupCompanies.heading">Group Companies & Affiliates</h2>And the translation file:
{ "en": { "home.groupCompanies.heading": "Group Companies & Affiliates" }, "ja": { "home.groupCompanies.heading": "グループ会社・関連法人" } }A runtime, or a build step, finds all elements with
data-i18nattributes, reads the key, looks it up in the active language's dictionary, and replaces the content. This is what every mainstream i18n library does, with framework-specific variations on the markup syntax.What I did instead
The dictionary on saturacorp.com is keyed by CSS selectors that target the rendered DOM, not by abstract names that the HTML knows about. The entries look like this, pulled directly from the live site's saturacorp-language.js:
{ "ja": { "#group-companies h2": "グループ会社・関連法人", ".ref-aff-chaos": "人間・AI協調システムおよび製品開発に向けたベンチャー・テクノロジー・スタジオ。", "#privacy > p:nth-of-type(2)": "技術的開示事項として一点..." } }A roughly 30-line runtime walks each entry, runs
document.querySelectorAll(selector), and rewrites the matched elements' content. The HTML itself is completely untouched. Nodata-i18nattributes anywhere, no template syntax, no key references. View-source on saturacorp.com from any page and you'll see ordinary semantic markup with nothing translation-aware in it. The HTML doesn't know it's being translated at all.That's the unusual part. I haven't seen a major published i18n library that uses this approach as its primary mechanism. The closest cousins are not mainstream i18n libraries but userscript translation tools, the Greasemonkey and Tampermonkey scripts that translate sites the user doesn't control. When you can't modify the HTML, you have to target what's already there with selectors. A few small experimental libraries have explored CSS-selector i18n, but none have become standard. The pattern is "known but not...
Read more » -
SaturaCorp: A Fake Conglomerate for Real Side Projects
2 days ago • 0 commentsSaturaCorp: A Fake Japanese Conglomerate That Catalogs My Real Side Projects
I have a problem common to anyone who tinkers across too many domains. I run a handful of websites for a variety of purposes, plus have a fistful of GitHub repos, project pages on Hackaday, Printables, and Adafruit Playground, and a long tail of one-off experiments. None of these have a unified "this is the whole picture" landing page that doesn't feel either narcissistic or undersold.
So I built a satirical website for a fake Japanese mega-conglomerate that catalogs them all as if I were running a global enterprise. It's live at saturacorp.com.
![]()
The premise
SaturaCorp, short for Satura Corporation, is presented as a Tokyo-listed (it's not) sōgō shōsha (it isn't) with operating divisions, group affiliates, investor-relations, a stock ticker, multi-language support across English / Japanese / Chinese / French, and a President's Message signed by yours truly in katakana (トレヴァー・ジョンソン). The aesthetic is the early-2000s Japanese investor-relations corporate site: tiny serif fonts, table-driven layouts, gradient-shaded navigation bars, dark navy with a single red accent, and a sub-line in katakana under nearly every link. If you ever clicked through to a Sony, Mitsui, or Marubeni global site around 2001, you remember the vibe.
The governing rule I gave myself was: no fabrication that doesn't anchor to something real. Every fictional Group entity links to an actual project. "Pen Plotter Reference Engineering Co." is the ESP32 pen plotter repo. "Oregon Trail Heritage Preservation Group" is the 1978 Apple BASIC port to Python. "SaturaCorp Open Hardware Reference Bureau" is my Hackaday.io profile. The framing is fictional; the substance is real.
![]()
The buildThe whole thing is written static HTML, CSS, and vanilla JavaScript. No framework, no bundler, no build step. The entire site weighs about 100 KB on disk. It deploys via
cpto an nginx webroot. A few pieces are worth calling out.A CSS-selector translation dictionary. Instead of pulling in an i18n framework, multi-language support is a single JS file containing four flat dictionaries keyed by CSS selector:
"#group-companies h2": "グループ会社・関連法人", ".ref-aff-chaos": "人間・AI協調システムおよび製品開発に向けた...", "#privacy > p:nth-of-type(2)": "技術的開示事項として一点..."A 30-line runtime walks each entry, runs
document.querySelectorAll(selector), and replacesinnerHTML. Language choice persists across page navigation viasessionStorage. About 250 entries per non-English language. Debuggable in the browser inspector. Zero build-time tooling. Adding a fifth language would mean appending one more block to the file. For a site that will never need React-level interactivity, this turned out to be the simplest i18n approach I've ever shipped.The stock ticker, or: how non-US market data quietly disappeared from the free web. The masthead has a stock ticker cycling between TSE 8031 (Mitsui & Co.) and TSE 6758 (Sony Group) with prices in yen. Sounds trivial — except every free market-data API I tried gates Tokyo Stock Exchange symbols behind paid tiers. Alpha Vantage, Finnhub, Twelve Data: all return some variant of "this symbol requires a Pro plan" when you query
8031.Tor6758.Ton a free key. Yahoo Finance's unofficial endpoint returns the data fine to a server-side client like Python'syfinance, but Yahoo sends no CORS headers, so a static page can't read it. Every free public CORS proxy I tested (corsproxy.io, allorigins.win, codetabs, cors.lol, half a dozen others) is now either dead, paywalled, or rate-limited into uselessness in 2026.The workaround: pull the underlying ADR price, SONY on NYSE, MITSY on OTC, from Finnhub's...
Read more »
JohnsonFarms.us

Jules Thuillier
VÃctor Mayoral Vilches
patrickpoirier51
Tim
ken.do
Robbie
Alex Muir
Aaron Dominic Richmond
Michael Frank Taylor
nick.r.brewer
Wesley Archer
Dennis Johansson
Bertrand Selva