VORA serves users in both Korean and English, with full parity between the two language versions. Every page exists in both languages, every feature works in both, the blog has both language versions, even the Terms of Service comes in two versions. We built this without a localization framework, using a dual-file architecture and a runtime i18n object in JavaScript. This post explains the approach, the specific problems it created, and what we'd do differently with hindsight.
The Initial Architecture Decision: Dual HTML Files
The simplest possible i18n approach for a static site: maintain two separate HTML files for each page. index.html (English) and index_ko.html (Korean). Same CSS, same JavaScript, different HTML content. Language switching is a link to the counterpart file.
This is the approach we chose, and it has genuine advantages for a static site:
- No JavaScript i18n library needed.
- Search engines see completely separate pages for each language, which is correct for SEO with hreflang.
- Each language version can be fully optimized independently — different images, different marketing copy, different content emphasis.
- No language detection logic to maintain or debug.
The disadvantages become apparent as the site grows.
The Synchronization Problem That Never Goes Away
When you make a UI change to index.html, you must also make the equivalent change to index_ko.html. When you add a new feature to app.html, you must add it to app_ko.html. Every structural change to the HTML is doubled work. Every time you fix a CSS class in one file, you have to remember to fix it in the counterpart.
We have the git history to demonstrate how often this synchronization breaks down. The commit "Fix language consistency: correct Korean links in English pages" reveals that the Korean version of some pages had links pointing to the Korean version of other pages — correct — while the English version of those same pages was also linking to Korean pages instead of English equivalents. Users navigating from English FAQ to English blog would land on the Korean blog.
This type of error is the predictable failure mode of the dual-file architecture: you make a change in one file, forget to propagate it to the counterpart, and the inconsistency only surfaces when someone follows a specific navigation path.
The hreflang Implementation
For Google Search Console and international SEO, we needed to implement hreflang tags correctly. Every page needs three meta tags in its <head>:
<link rel="alternate" hreflang="en" href="https://...pages.dev/blog.html" />
<link rel="alternate" hreflang="ko" href="https://...pages.dev/blog_ko.html" />
<link rel="alternate" hreflang="x-default" href="https://...pages.dev/blog.html" />
The x-default tag signals to Google which version to serve when there's no better language match — we use English as the default. Every English page links to its Korean counterpart, and every Korean page links to its English counterpart. The implementation is tedious but important for international search visibility.
We audited the hreflang implementation after noticing that some Korean pages were not appearing in Korean language searches. The audit found: three Korean pages had the hreflang URL pointing to wrong English counterparts (copy-paste error from another page's code). Two pages were missing the x-default tag entirely. One page had the English and Korean hreflang URLs swapped. All of these were silent — no browser errors, no deployment failures — just wrong information being sent to Google's crawler.
The JavaScript i18n Object: A Practical Compromise
For the app page (app.html and app_ko.html), the dynamic UI elements needed to display language-appropriate strings. Rather than fully duplicating the JavaScript, we implemented a simple i18n object inside the app class:
this.i18n = {
ko: {
recording: '녹음 중',
waiting: '대기 중',
apiKeyNotSet: 'API 키 미설정',
startRecording: '녹음 시작',
// ... ~20 more strings
},
en: {
recording: 'Recording',
waiting: 'Waiting',
apiKeyNotSet: 'API Key Not Set',
startRecording: 'Start Recording',
// ...
}
};
// Detect language from the HTML lang attribute
this.currentLang = document.documentElement.lang === 'en' ? 'en' : 'ko';
The app itself is identical in both app.html and app_ko.html — the only difference is the lang attribute on the <html> tag (lang="ko" vs lang="en"). The JavaScript detects this attribute and selects the appropriate string dictionary. This means we maintain one JavaScript file (enhanced-app-v2.js) that serves both language versions — a meaningful reduction in maintenance burden for the most complex part of the codebase.
The Terms of Service Split: A Case Study in Localization Scope
An interesting decision point was the Terms of Service. Initially we had a single Korean terms page. When we decided to add English support, the options were:
- Translate the Korean terms into English directly.
- Write new English terms from scratch appropriate for an English-language user.
- Have one terms document that serves both languages (bilingual document).
We went with option 1 (separate English and Korean versions) for legal clarity: the Korean version is the binding version for Korean users under Korean law, and the English version is the binding version for international users. A bilingual document with ambiguity about which version controls in case of conflict is a legal problem we didn't need.
The commit "Separate Terms of Service into English and Korean versions" was a larger refactor than expected: renaming the original Korean terms to terms_ko.html, creating new terms.html for English, updating every page that linked to the terms (all of them), and updating the sitemap. A two-file change became a 12-file change when accounting for all the link updates.
The Language Consistency Audit
Late in development, we did a systematic audit of language consistency across the site. The findings were instructive about where the dual-file architecture breaks down:
- Navigation links: 3 English pages linking to Korean equivalents (fixed by grep and replace).
- Footer content: Some Korean pages had English footer text (from copying the English template without translating the footer).
- Meta descriptions: Two Korean pages had English meta descriptions (SEO implication: Google shows the description in search results, so Korean search users saw English previews).
- Image alt text: Several pages had English alt text on images regardless of page language. Low impact for sighted users but affects screen readers for Korean users.
- Dates: Some Korean pages were displaying dates in
Feb 2026format rather than2026년 2월format.
None of these were caught by any automated test — they required manual review. For a site of VORA's current scale (20+ pages in 2 languages), this is manageable. At larger scale, the dual-file architecture requires either automation (scripts that check link consistency, language tag presence, etc.) or a proper i18n system.
What We'd Build Instead with Modern Tooling
With the benefit of hindsight and if we were starting fresh with a build system, we'd use one of these approaches:
- Static site generator with i18n support: Eleventy, Hugo, or Astro all have first-class internationalization. You write templates once and provide translation files (YAML or JSON). Content variants get generated automatically. Link consistency is guaranteed by the template engine.
- JSON-based translation with a build script: Even without a full SSG, a Node.js build script that takes a template HTML file and generates language variants from JSON translation files would eliminate the synchronization problem. Changes to the template propagate automatically to all language variants.
The reason we didn't use these approaches originally: we wanted zero build tooling, zero npm dependencies, and zero complexity overhead. A static site you can edit in a text editor and commit directly is genuinely simpler to maintain than one with a build pipeline. The dual-file approach was the right call at the project's scale when we started. As VORA grows, we'll likely migrate to Eleventy or a similar SSG, but we'll do so when the maintenance pain of the dual-file approach exceeds the migration cost — and we're not there yet.
The bilingual architecture also reflects a product truth: Korean and English users of VORA have somewhat different contexts and needs. Korean users are in a market where meeting culture has specific conventions (the 개조식 minutes format we discussed, specific technical vocabulary patterns). English users might be in different meeting cultures with different conventions. The dual-file approach, despite its maintenance cost, gives us the flexibility to optimize each version independently. That flexibility has a real value that a purely translation-based i18n system would partially sacrifice.