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.
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:
// 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.
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/' : '/';
}
}
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
-
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, anddata-basedepth are already correct. -
Set
lang="en", the ENcanonical, all threehreflanglinks (EN target, UK target, x-default → EN),og:locale=en_US, andinLanguage: "en"in the JSON-LD block. -
Copy the finished EN file to
uk/content/<type>/<slug>.html— same slug, one extra directory level. -
In the UK copy: set
lang="uk", switch thecanonicalto the UK URL, bumpdata-baseby one extra../, setog:locale=uk_UAandinLanguage: "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. -
Leave
datePublishedidentical between both files. -
Register the new slug in the site's content manifest
(
articles.jsonor equivalent) so the language switcher'sKNOWN_SLUGSset and the sitemap both pick it up — this is a separate step from writing the HTML itself.
9. Common Pitfalls
-
Wrong data-base depth: forgetting that
uk/adds one level breaks every relative asset and internal link on the page. -
Relative canonical URLs: always use the full
https://www.mysimulator.uk/…form; relative canonicals collapse the EN/UK distinction. -
Copy-pasted JSON-LD: leaving
inLanguage: "en"or English headline text in the UK file after translating the visible body copy. - Guessed switcher URLs: building the target locale's URL by string substitution without checking it actually exists — always route through the known-slugs manifest.
- Machine-translated prose left unedited: the UK mirror should read as natural Ukrainian, not a literal word-for-word pass — formulas and identifiers are the only things that should stay untouched.