Довідник · i18n · Для контриб'юторів
📅 Липень 2026 ⏱ ≈ 14 хв 🎯 Контриб'ютор

Посібник з i18n для проєкту

mysimulator.uk постачає кожну статтю, туторіал і довідник у двох локалях — англійській (en) та українській (uk) — у вигляді повністю дзеркальних статичних HTML-файлів, а не через рантайм-шар перекладу. Цей посібник описує конвенцію каталогів, контракт URL і метаданих, якому має відповідати кожна сторінка, та чек-лист для додавання нової локалізованої сторінки без поломки перемикача мов.

1. Дзеркальна архітектура каталогів

Сайт не має етапу білду і не використовує рантайм-фреймворк i18n — жодного i18next, жодного узгодження локалі на боці сервера, жодних клієнтських таблиць рядків для контенту сторінок. Натомість англійська версія є джерелом істини в корені сайту, а українська — паралельним деревом, коренем якого є /uk/, і яке точно повторює ту саму структуру шляхів:

mysimulator.uk/
├── content/
│   ├── articles/backpropagation.html        ← EN
│   ├── tutorials/build-physics-engine.html  ← EN
│   └── references/i18n-guide.html           ← EN (ця сторінка)
├── uk/
│   └── content/
│       ├── articles/backpropagation.html        ← дзеркало UK
│       ├── tutorials/build-physics-engine.html  ← дзеркало UK
│       └── references/i18n-guide.html           ← дзеркало UK
└── shared/
    ├── theme.css
    ├── components.css
    └── components.js   ← навбар/футер вставляються на кожній сторінці

Кожен EN-файл за шляхом content/<type>/<slug>.html має структурно ідентичного «близнюка» за адресою uk/content/<type>/<slug>.html. Slug ніколи не змінюється між локалями — лише текст і префікс шляху uk/. Це робить відповідність між локалями суто механічною: додати або прибрати uk/ зі шляху.

Чому повне дзеркалювання, а не query-параметри чи cookie локалі? Статичні файли означають, що кожен локалізований URL напряму індексується пошуковими системами, кешується на CDN-межі та легко порівнюється в код-рев'ю — перекладач бачить точно, що змінилося між EN- та UK-версією сторінки, у звичайному git diff.

2. Структура URL і правила canonical

Оскільки обидві локалі — це реальні, незалежно доступні HTML-файли, кожна з них потребує власного тега <link rel="canonical">, що вказує на саму себе, а не на EN-версію. Це відрізняється від i18n на основі параметрів, де для всіх локалей існує лише один канонічний URL.

Локаль URL Ціль canonical
en /content/references/i18n-guide.html сама на себе
uk /uk/content/references/i18n-guide.html сама на себе

Обидва теги canonical використовують абсолютну форму https://www.mysimulator.uk/… — відносні canonical неоднозначні, щойно сторінка дзеркалюється на двох різних рівнях глибини шляху, і деякі краулери взагалі ігнорують відносні canonical URL.

3. Контракт глибини data-base

Навбар і футер не дублюються на кожній сторінці — вони вставляються під час рантайму скриптом shared/components.js у два елементи-заповнювачі, #navbar-root і #footer-root. Оскільки заповнювачі завантажують часткові файли та ресурси за відносними шляхами, кожна сторінка повинна повідомити скрипту, скільки рівнів каталогів відділяють її від кореня сайту, через атрибут data-base:

depth(шлях) = кількість сегментів "/" між коренем сайту та каталогом файлу data-base = "../".repeat(depth) content/references/i18n-guide.html → каталог "content/references/" → глибина 2 → data-base="../../" uk/content/references/i18n-guide.html → каталог "uk/content/references/" → глибина 3 → data-base="../../../"
// shared/components.js вирішує кожен ресурс/посилання відносно data-base
function renderNavbar(rootEl) {
  const base = rootEl.getAttribute('data-base') || './';
  const locale = detectLocale();     // 'en' | 'uk', з URL-шляху
  const homeHref = locale === 'uk' ? base + 'uk/' : base;
  rootEl.innerHTML = buildNavHTML(base, homeHref, locale);
}

Неправильне значення data-base — найпоширеніша i18n-помилка на цьому сайті: вона мовчки завантажує CSS/JS навбару з неправильної глибини, що або спричиняє 404 для стилів, або — гірше — рендерить застарілий нестилізований навбар, який усе одно виглядає правдоподібно при швидкій візуальній перевірці.

Правило великого пальця: порахуйте кількість папок у власному шляху файлу (без імені файлу) нижче сегмента, що є коренем сайту для цієї локалі. content/references/ — це 2 папки від /. uk/content/references/ — це 3 папки, тому що сам uk/ рахується як один рівень.

4. Підключення hreflang

Кожна сторінка — в обох локалях — містить однакові три теги <link rel="alternate" hreflang="…">, що вказують на ті самі два абсолютні URL. Це повідомляє пошуковим системам, що два файли є перекладами однієї логічної сторінки, а не дубльованим чи непов'язаним контентом:

<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 завжди вказує на EN-URL, оскільки англійська — це резервний варіант для будь-якої локалі, яку сайт ще не підтримує. Набір із трьох тегів побайтово ідентичний між EN-файлом та його UK-дзеркалом — відрізняється лише власний тег canonical сторінки.

5. Структуровані дані для кожної локалі

Блок JSON-LD TechArticle/Article у <head> кожної сторінки повинен встановлювати inLanguage відповідно до локалі самого файлу, а Open Graph-тег og:locale підпорядковується тому ж правилу, використовуючи повний код локалі:

Поле EN-файл UK-файл
html lang en uk
og:locale en_US uk_UA
JSON-LD inLanguage en uk
JSON-LD headline/description Англійський текст Український текст

datePublished лишається однаковим в обох файлах — він описує, коли логічна сторінка була опублікована, а не коли був зроблений кожен переклад. Заголовки, meta-описи, ключові слова та headline/description у JSON-LD мають бути справді перекладені, а не скопійовані, оскільки саме вони з'являються безпосередньо в локалізованих результатах пошуку.

6. Перемикач мов і 404-безпека

Перемикач мов у навбарі не просто додає чи прибирає /uk/ з location.pathname — ранні версії сайту робили саме так, і це генерувало 404, коли сторінка існувала лише в одній локалі. Поточна реалізація перевіряє маніфест відомих локалізованих slug'ів перед переписуванням URL і в іншому випадку повертається на головну сторінку відповідної локалі:

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 генерується під час збору контенту (articles.json тощо)
  if (KNOWN_SLUGS.has(candidate)) {
    location.href = candidate;
  } else {
    // безпечний фолбек: головна сторінка локалі, ніколи не «вгаданий» URL
    location.href = targetLocale === 'uk' ? '/uk/' : '/';
  }
}
Чому маніфест, а не HEAD-запит: перевірка існування наживо (fetch(candidate, {method:'HEAD'})) спрацювала б, але додає мережевий раунд-тріп до кожного кліку перемикача і не може виконатися до першого рендеру. Статичний маніфест, зібраний під час збору контенту, спрацьовує миттєво і відповідає тому, як решта сайту уникає клієнтських запитів даних для навігації.

8. Додавання нової локалізованої сторінки

  1. Напишіть EN-сторінку за шляхом content/<type>/<slug>.html, скопіювавши head/структуру наявної сторінки того ж типу (стаття, туторіал чи довідник), щоб класи CSS, форма JSON-LD і глибина data-base вже були правильними.
  2. Встановіть lang="en", EN-canonical, усі три посилання hreflang (ціль EN, ціль UK, x-default → EN), og:locale=en_US і inLanguage: "en" у блоці JSON-LD.
  3. Скопіюйте готовий EN-файл до uk/content/<type>/<slug>.html — той самий slug, на один рівень каталогу більше.
  4. У UK-копії: встановіть lang="uk", замініть canonical на UK-URL, збільште data-base на один додатковий ../, встановіть og:locale=uk_UA та inLanguage: "uk", а потім перекладіть кожен видимий рядок — заголовок, meta-опис, ключові слова, headline/lead, текст секцій, комірки таблиць та коментарі в коді, де це доречно. Формули, синтаксис коду й назви змінних лишаються незмінними.
  5. Залиште datePublished ідентичним в обох файлах.
  6. Зареєструйте новий slug у маніфесті контенту сайту (articles.json чи еквіваленті), щоб набір KNOWN_SLUGS перемикача мов і sitemap підхопили його — це окремий крок від написання самого HTML.

9. Типові граблі