Reference · i18n · Contributor Guide
📅 July 2026 ⏱ ≈ 14 min 🎯 Contributor

i18n Guide for the Project

mysimulator.uk ships every article, tutorial, and reference page in two locales — English (en) and Ukrainian (uk) — as fully mirrored static HTML files rather than a runtime translation layer. This guide documents the directory convention, the URL and metadata contract every page must satisfy, and the checklist for adding a new localized page without breaking the language switcher.

1. Mirrored Directory Architecture

The site has no build step and no runtime i18n framework — no i18next, no server-side locale negotiation, no client-side string tables for page content. Instead, English is the source of truth at the site root, and Ukrainian is a parallel tree rooted at /uk/ that mirrors the exact same path structure:

mysimulator.uk/
├── content/
│   ├── articles/backpropagation.html        ← EN
│   ├── tutorials/build-physics-engine.html  ← EN
│   └── references/i18n-guide.html           ← EN (this page)
├── uk/
│   └── content/
│       ├── articles/backpropagation.html        ← UK mirror
│       ├── tutorials/build-physics-engine.html  ← UK mirror
│       └── references/i18n-guide.html           ← UK mirror
└── shared/
    ├── theme.css
    ├── components.css
    └── components.js   ← navbar/footer injected on every page

Every EN file under content/<type>/<slug>.html has a byte-for-byt structurally identical sibling at uk/content/<type>/<slug>.html. The slug never changes between locales — only the text, and the uk/ path prefix. This makes the mapping between locales purely mechanical: strip or add uk/ from the path.

Why full mirroring instead of query params or a locale cookie? Static files mean every localized URL is directly crawlable and indexable by search engines, cacheable at the CDN edge, and diffable in code review — a translator can see exactly what changed between the EN and UK version of a page in a normal git diff.

2. URL Structure & Canonical Rules

Because both locales are real, independently servable HTML files, each one needs its own <link rel="canonical"> pointing at itself, not at the EN version. This is different from parameter-based i18n, where only one canonical URL exists for all locales.

Locale URL Canonical target
en /content/references/i18n-guide.html itself
uk /uk/content/references/i18n-guide.html itself

Both canonical tags use the absolute https://www.mysimulator.uk/… form — relative canonicals are ambiguous once a page is mirrored under two different path depths, and several crawlers ignore relative canonical URLs entirely.

3. The data-base Depth Contract

The navbar and footer are not duplicated per page — they are injected at runtime by shared/components.js into two placeholder elements, #navbar-root and #footer-root. Because the placeholders load partials and assets with relative paths, each page must tell the script how many directory levels separate it from the site root via a data-base attribute:

depth(path) = number of "/" segments between site root and the file's directory data-base = "../".repeat(depth) content/references/i18n-guide.html → directory "content/references/" → depth 2 → data-base="../../" uk/content/references/i18n-guide.html → directory "uk/content/references/" → depth 3 → data-base="../../../"
// shared/components.js resolves every asset/link relative to data-base
function renderNavbar(rootEl) {
  const base = rootEl.getAttribute('data-base') || './';
  const locale = detectLocale();     // 'en' | 'uk', from the URL path
  const homeHref = locale === 'uk' ? base + 'uk/' : base;
  rootEl.innerHTML = buildNavHTML(base, homeHref, locale);
}

A wrong data-base value is the single most common i18n bug on this site: it silently loads the navbar CSS/JS from the wrong depth, which either 404s the stylesheet or — worse — renders a stale unstyled navbar that still looks plausible in a quick visual check.

Rule of thumb: count the folders in the file's own path (excluding the filename) below the segment that is the site root for that locale. content/references/ is 2 folders deep from /. uk/content/references/ is 3 folders deep, because uk/ itself counts as one level.

4. hreflang Wiring

Every page — in both locales — carries the same three <link rel="alternate" hreflang="…"> tags, pointing at the same two absolute URLs. This tells search engines the two files are translations of one logical page, not duplicate or unrelated content:

<link rel="alternate" hreflang="en"
      href="https://www.mysimulator.uk/content/references/i18n-guide.html" />
<link rel="alternate" hreflang="uk"
      href="https://www.mysimulator.uk/uk/content/references/i18n-guide.html" />
<link rel="alternate" hreflang="x-default"
      href="https://www.mysimulator.uk/content/references/i18n-guide.html" />

x-default always points at the EN URL, since English is the fallback for any locale the site doesn't yet support. The set of three tags is byte-identical between the EN file and its UK mirror — only the page's own canonical tag differs between the two.

5. Per-Locale Structured Data

The TechArticle/Article JSON-LD block in each page's <head> must set inLanguage to match the file's own locale, and the Open Graph og:locale meta tag follows the same rule using the long-form locale code:

Field EN file UK file
html lang en uk
og:locale en_US uk_UA
JSON-LD inLanguage en uk
JSON-LD headline/description English text Ukrainian text

datePublished stays identical between both files — it describes when the logical page was published, not when each translation was produced. Titles, meta descriptions, keywords, and the JSON-LD headline/description must be genuinely translated, not copy-pasted, since these are what appear directly in localized search results.

6. The Language Switcher & 404-Safety

The navbar's language switcher does not simply prepend or strip /uk/ from location.pathname — early versions of the site did this and it generated 404s whenever a page existed in only one locale. The current implementation checks a manifest of known localized slugs before rewriting the URL, and falls back to each locale's homepage otherwise:

function switchLocale(targetLocale) {
  const path = location.pathname;
  const isUk = path.startsWith('/uk/');
  const currentLocale = isUk ? 'uk' : 'en';
  if (currentLocale === targetLocale) return;

  const enPath = isUk ? path.replace(/^\/uk\//, '/') : path;
  const candidate = targetLocale === 'uk' ? '/uk' + enPath : enPath;

  // KNOWN_SLUGS is generated at content-collection time (articles.json etc.)
  if (KNOWN_SLUGS.has(candidate)) {
    location.href = candidate;
  } else {
    // safe fallback: locale homepage, never a raw guessed URL
    location.href = targetLocale === 'uk' ? '/uk/' : '/';
  }
}
Why a manifest, not a HEAD request: a live existence check (fetch(candidate, {method:'HEAD'})) would work but adds a network round-trip to every switcher click and can't run before first paint. A static manifest baked in at content-collection time is instant and matches how the rest of the site avoids client-side data fetching for navigation.

8. Adding a New Localized Page

  1. Write the EN page under content/<type>/<slug>.html, copying the head/structure of an existing page of the same type (article, tutorial, or reference) so the CSS classes, JSON-LD shape, and data-base depth are already correct.
  2. Set lang="en", the EN canonical, all three hreflang links (EN target, UK target, x-default → EN), og:locale=en_US, and inLanguage: "en" in the JSON-LD block.
  3. Copy the finished EN file to uk/content/<type>/<slug>.html — same slug, one extra directory level.
  4. In the UK copy: set lang="uk", switch the canonical to the UK URL, bump data-base by one extra ../, set og:locale=uk_UA and inLanguage: "uk", then translate every visible string — title, meta description, keywords, headline/lead, section prose, table cells, and code comments where relevant. Formulas, code syntax, and variable names stay unchanged.
  5. Leave datePublished identical between both files.
  6. Register the new slug in the site's content manifest (articles.json or equivalent) so the language switcher's KNOWN_SLUGS set and the sitemap both pick it up — this is a separate step from writing the HTML itself.

9. Common Pitfalls