How We Built Epoch Converter: A Zero-Dependency, Multilingual Developer Tool
A look behind the scenes at the stack, the trade-offs, and the few opinionated choices that kept this tool small, fast, and private — even after shipping to twenty languages.
If you have ever pasted a Unix timestamp into a search bar at 2 a.m. trying to figure out when a token expired, this tool was built for you. Epoch Converter is a free, browser-only utility for converting Unix timestamps to readable dates and back, with batch conversion, ISO week math, timezone tables, and a few extras for the kind of developer who keeps a tab open just for date debugging.
This post is the story of how we built it: what is in the stack, what we deliberately left out, and a handful of practical lessons that might help if you are shipping your own small web tool.
The stack, in one paragraph
Plain HTML, plain CSS, plain JavaScript. No framework, no bundler, no build step, no server-side rendering, no backend. A single script.js file, a single styles.css file, and one HTML document per tool page. The only third-party scripts in the page are Font Awesome (icons), the Inter and JetBrains Mono webfonts, and the xlsx library which is loaded on demand so the batch converter can export Excel files. That is the entire dependency footprint.
Why vanilla, in 2026?
It is not nostalgia. There were four practical reasons.
- First paint speed. The home page is a single document. There is no hydration round-trip, no framework runtime to parse, no client-side router to wake up. The conversion result is on screen before most React apps have rendered their loading skeleton.
- Operational simplicity. The whole site is static files. It deploys behind a CDN, has no environment variables, no secret rotation, and no nightly cron of dependencies to patch. We can leave it alone for months and nothing rots.
- Standards age well. Browsers have shipped
Intl.DateTimeFormat,navigator.clipboard, CSS grid, container queries, and view transitions in the last few years. We get most of what a framework would give us, for free, from the platform. - Less to break across twenty languages. More on that below.
We are not anti-framework — we have shipped React, Vue, and Svelte projects too. We just felt that for a tool whose entire job is "take a number, render a date," a framework was extra mass with no payoff.
Privacy by architecture, not by policy
Every conversion happens in your browser. There is no backend that sees your timestamps. No analytics SDK is loaded. There is no cookie banner because there are no cookies that need one. The page works offline once cached.
This is a deliberate design choice. A lot of "privacy-first" claims really mean "we promise not to look at your data on our server." We wanted a stronger guarantee: the data never reaches a server. You can verify it in DevTools — the only network calls you will see are for static assets.
This pattern is becoming more common in dev utilities. End-to-end encrypted notepad services like TheSecureNote use the same idea for text content — the page encrypts in your browser, and the server only ever stores ciphertext it cannot read. We have leaned on that tool ourselves when a teammate needs to share a JWT, a session token, or a database snippet that happens to contain a sensitive epoch timestamp. (More on that pattern in a minute.)
Internationalization without a framework
The site ships in twenty languages: English, Spanish, German, French, Portuguese, Italian, Dutch, Polish, Czech, Russian, Ukrainian, Turkish, Arabic, Persian, Hindi, Chinese, Japanese, Korean, Indonesian, and Vietnamese. Each language lives in its own folder — /de/, /ja/, /hi/ — with its own fully rendered HTML.
We considered the usual approaches — JSON locale files loaded at runtime, a React i18n provider, a static site generator with a templating layer — and rejected all of them. The reason is SEO. We wanted search engines to see a fully formed page in each language without executing any JavaScript. That meant the translations had to be baked into the HTML at rest.
What we did instead:
- Each locale is a sibling directory with its own
index.htmland supporting pages. - Every page declares
<html lang="...">matching its directory. - The home page emits a full set of
<link rel="alternate" hreflang="...">tags pointing at every sibling locale, withx-defaultpointing at English. - The shared
script.jshandles UI text that depends on user input (the current clock, error messages) usingnavigator.languageandIntlformatters, so each locale gets a date format that matches its conventions.
The downside is the obvious one: a copy edit in English has to be applied twenty times. We accept that. The upside is that Googlebot, Bing, and Yandex each see a fully indexable page for every language without running a single line of JS.
The SEO layer: sitemaps and structured data
This site has more than one sitemap, and that is deliberate. Search engines have a limit on how many URLs they like to see in a single sitemap, and bundling everything into one giant file makes diagnostics painful.
sitemap-main.xmllists every page that a human would navigate to — home pages, converters, the About page, this blog post.sitemap-epochs.xmlis a programmatically generated sitemap of pre-rendered timestamp deep-links so that a search for a specific epoch value can land on a useful page.timezone-converter-sitemap.xmlcovers the timezone-to-timezone matrix.sitemap.xmlis a sitemap index that ties them together.
On top of that, every important page emits Schema.org JSON-LD: WebSite, Organization, SoftwareApplication, HowTo, and Article blocks where appropriate. None of this changes how the page looks to a human, but it gives crawlers a structured, unambiguous description of what they are looking at.
The single best decision we made here was the automated lastmod update for the sitemap. Whenever the deploy pipeline runs, it rewrites the <lastmod> field for files that actually changed in that commit. Static sitemaps with stale dates are a quiet way to lose ranking; this fixes that.
Batch conversion: the one place we load a library
The batch converter lets you paste up to ten thousand timestamps and download the results as CSV or Excel. CSV is easy — a few lines of Array.join. Excel is not, so for .xlsx we lazy-load SheetJS from a CDN only when the user clicks the export button.
Why lazy-load? Because the vast majority of visitors never use batch export, and we did not want to charge every visitor a 500 KB tax for a feature 2% of them want.
Two small tools we keep in the workflow
One of the nicer things about building a tool for developers is that you end up using other developers' tools every day. Two we recommend without reservation, because they slot directly into the kind of work this converter is for:
- Sharing sensitive timestamps. Epoch values can leak more than they look like. A JWT's
expclaim reveals a session's lifetime. A database row'screated_atcan deanonymize a user when correlated with other data. When you need to ship one of these to a colleague during a debugging session, do not paste it in Slack — paste it into an end-to-end encrypted, self-destructing note via TheSecureNote. The recipient opens it once, the ciphertext is destroyed, and Slack's retention policy never sees it. - Sharing deep-link URLs. Several of our pages take query parameters — for example a converter URL with a pre-filled timestamp — and those URLs can get long. When we share them in social posts or in documentation we wrap them with a short URL service like u2l.ai, which gives us a clean branded link, a QR code for talks and offline materials, and click analytics so we know which timestamps developers actually search for. Useful any time you are pasting a long URL in a PR description or a Slack thread.
Both are quick to integrate into a daily workflow, neither asks for an account to get started, and both took the same client-side-first design stance we did. That kind of consistency in tool design is what makes them feel native to a developer workflow.
What we deliberately did not build
A short list, because the absence of features is often the more interesting part of a tool:
- No accounts. Nothing to log into. Bookmarks are the persistence layer.
- No analytics. We rely on aggregated, anonymous CDN logs to know roughly what is popular. We do not need to know who you are.
- No ads. Ads would require third-party JS that watches what you type. That is incompatible with the privacy claim, so they are out.
- No API. A hosted conversion API would create a server that sees timestamps. The whole point was not to have one. If you need a programmatic version, the JavaScript snippets in the footer of the home page run in any runtime that has a
Dateobject.
What is next
A few things on the short list:
- An offline-first PWA install flow, so the converter works on flights and in train tunnels.
- A small CLI wrapper for people who live in the terminal.
- More cookbook posts on this blog — JWTs, ISO 8601 edge cases, the Y2038 problem, and the surprisingly deep rabbit hole of timezone identifiers.
If there is a corner of date handling you wish someone would write about, the contact page is the place to tell us. The next post is already in progress.