Přeskočit na obsah

i18n: vícejazyčnost, jazykové URL a hreflang

Stav dokumentu: Návrh — aplikace je dnes prakticky jednojazyčná; cílový stav je veřejný web s jazykovým prefixem v URL, výchozí angličtinou na /en a českou verzí na /cs.

Cíl

Připravit Soul of Herbs jako vícejazyčný web bez SEO duplicit: každý veřejný obsah má jednoznačnou jazykovou URL, správné canonical, hreflang alternativy a přepínač jazyka v hlavičce i patičce. Výchozí jazyk webu je angličtina na /en; čeština je dostupná na /cs.

Principy

  • URL jako zdroj pravdy: jazyk je vždy první segment veřejné HTML stránky: /en/..., /cs/....
  • Výchozí jazyk: /en je defaultní verze pro x-default; root / nesmí renderovat duplicitní obsah, má přesměrovat na /en nebo jinak jednoznačně kanonizovat.
  • Žádné SEO duplicity: každá lokalizovaná stránka má canonical sama na sebe a kompletní sadu hreflang odkazů na dostupné jazykové varianty.
  • Jazykový přepínač je URL-first: přepnutí jazyka vede na ekvivalentní stránku v druhém jazyce, pokud existuje; jinak na jazykový overview nebo bezpečný fallback.
  • Postupná migrace: nejdřív architektura rout a meta helperů, potom lokalizace UI textů, potom obsahová data.

Rozsah jazyků

První fáze:

KódJazykURL prefixRole
enEnglish/envýchozí veřejná verze, x-default
csČeština/csčeská lokalizace

Backlog:

  • Přidat další jazyky jen přes centrální konfiguraci podporovaných locales.
  • Nepřidávat jazyk do UI, dokud nemá rozhodnuté URL, metadata a fallback chování.

URL a routy

Cílový tvar

  • /en
  • /en/herbs
  • /en/herbs/:slug
  • /en/season/:month
  • /en/topics
  • /en/topics/:slug
  • /en/regions
  • /en/regions/:slug
  • /en/processing
  • /en/processing/:slug
  • /en/rituals
  • /en/rituals/:slug
  • /en/recipes
  • /en/docs
  • /en/docs/:slug

České ekvivalenty:

  • /cs
  • /cs/byliny
  • /cs/byliny/:slug
  • /cs/sezona/:month
  • /cs/symptomy
  • /cs/symptomy/:slug
  • /cs/region
  • /cs/region/:slug
  • /cs/zpracovani
  • /cs/zpracovani/:slug
  • /cs/ritualy
  • /cs/ritualy/:slug
  • /cs/recepty
  • /cs/dokumentace
  • /cs/dokumentace/:slug

Přechod ze současných URL

  • Bez zpětné kompatibility: staré české URL bez prefixu (/byliny, /symptomy, …) není potřeba dlouhodobě udržovat.
  • Po zapnutí i18n staré neprefixované veřejné HTML routy nesmí renderovat indexovatelný obsah.
  • Preferované chování:
    • / → redirect na /en
    • staré české routy bez prefixu → 404; žádná garance redirectů ani zpětné kompatibility
    • sitemap a interní odkazy obsahují pouze /en/... a /cs/...
  • Alias routy (/co-sbirat/:month, /byliny-na-zpracovani/:slug) nepřenášet jako veřejné i18n canonical routy, pokud k nim nebude explicitní produktový důvod.

Routing a technická architektura

  • Zavést centrální locale konfiguraci, např. app/lib/i18n-locales.ts:
    • podporované locale kódy
    • default locale en
    • validace locale segmentu
    • labely pro přepínač jazyka
  • Zavést helpery pro lokalizované URL:
    • sestavení cesty pro konkrétní route key a locale
    • mapování aktuální route na alternativní locale URL
    • fallback, pokud ekvivalentní stránka v cílovém jazyce neexistuje
  • Route soubory držet tenké:
    • validace params.locale
    • loader předává locale do DB/lib vrstvy
    • meta helper dostane locale a alternativy
  • Nepřidávat i18n logiku ad hoc do jednotlivých komponent.

Obsah a data

UI texty

  • UI copy přes malý slovník po doménách, např. app/lib/i18n/messages/*.ts.
  • Nepřekládat přes runtime string concatenation tam, kde se mění slovosled; používat celé věty nebo malé formattery.
  • Pluralizaci řešit přes explicitní helper per locale, ne české fráze použité pro angličtinu.

DB obsah

Rozhodnutý směr: samostatné překladové tabulky per veřejná entita (*_translations). Současné české sloupce zůstávají zdroj pro první migraci /cs, ale cílové veřejné čtení má používat překladové řádky podle locale.

  • Krátkodobě: existující česká pole zůstávají zdroj pro /cs, anglická verze může začít jen pro stránky, kde jsou překlady dostupné.
  • Cílově: lokalizovat názvy, popisy a textové obsahové bloky per entity.

Překladové tabulky

Minimální sada:

  • herb_translations
  • topic_translations
  • processing_method_translations
  • region_translations
  • spiritual_guide_translations
  • recipe_translations

Volitelně později:

  • plant_part_translations
  • habitat_translations
  • safety_warning_translations
  • spiritual_use_translations
  • scientific_evidence_translations jen pro redakční shrnutí/omezení, ne pro citovaná metadata studie

Společný tvar překladové tabulky

Každá překladová tabulka má držet stejný základ:

CREATE TABLE herb_translations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  herb_id INTEGER NOT NULL,
  locale TEXT NOT NULL,
  slug TEXT NOT NULL,
  name TEXT NOT NULL,
  short_description TEXT,
  full_description TEXT,
  seo_title TEXT,
  seo_description TEXT,
  is_published INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (herb_id) REFERENCES herbs(id) ON DELETE CASCADE,
  UNIQUE (herb_id, locale),
  UNIQUE (locale, slug)
);

Konkrétní entity mohou mít navíc vlastní textová pole:

EntitaPřekladová pole navíc
Bylinyidentification_notes, confusion_risk_notes, drug_interactions_note, phototoxicity_note
Témataname, description, případně intro
Zpracováníname, short_description, full_description, required_equipment, safety_notes, typical_prep_note, safety_summary
Regionyname; typ regionu zůstává enum/slovník
Rituální návodytitle, intent, category, intro, steps_json, materials_json, safety_notes, references_note
Receptytitle, steps_json, notes, science_rationale_cs přejmenovat cílově na locale-neutral překladové pole

Konkrétní návrh tabulek

herb_translations

CREATE TABLE herb_translations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  herb_id INTEGER NOT NULL,
  locale TEXT NOT NULL,
  slug TEXT NOT NULL,
  name TEXT NOT NULL,
  short_description TEXT,
  full_description TEXT,
  identification_notes TEXT,
  confusion_risk_notes TEXT,
  drug_interactions_note TEXT,
  phototoxicity_note TEXT,
  seo_title TEXT,
  seo_description TEXT,
  is_published INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (herb_id) REFERENCES herbs(id) ON DELETE CASCADE,
  UNIQUE (herb_id, locale),
  UNIQUE (locale, slug)
);

Stávající herbs.local_name zůstává jen zdroj pro první českou migraci; veřejné read modely mají cílově číst herb_translations.name.

topic_translations

CREATE TABLE topic_translations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  topic_id INTEGER NOT NULL,
  locale TEXT NOT NULL,
  slug TEXT NOT NULL,
  name TEXT NOT NULL,
  description TEXT,
  seo_title TEXT,
  seo_description TEXT,
  is_published INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE,
  UNIQUE (topic_id, locale),
  UNIQUE (locale, slug)
);

processing_method_translations

CREATE TABLE processing_method_translations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  processing_method_id INTEGER NOT NULL,
  locale TEXT NOT NULL,
  slug TEXT NOT NULL,
  name TEXT NOT NULL,
  short_description TEXT,
  full_description TEXT,
  difficulty_label TEXT,
  required_equipment TEXT,
  safety_notes TEXT,
  typical_prep_note TEXT,
  safety_summary TEXT,
  seo_title TEXT,
  seo_description TEXT,
  is_published INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (processing_method_id) REFERENCES processing_methods(id) ON DELETE CASCADE,
  UNIQUE (processing_method_id, locale),
  UNIQUE (locale, slug)
);

region_translations

CREATE TABLE region_translations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  region_id INTEGER NOT NULL,
  locale TEXT NOT NULL,
  slug TEXT NOT NULL,
  name TEXT NOT NULL,
  seo_title TEXT,
  seo_description TEXT,
  is_published INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (region_id) REFERENCES regions(id) ON DELETE CASCADE,
  UNIQUE (region_id, locale),
  UNIQUE (locale, slug)
);

regions.type, country_code a hierarchie zůstávají sdílené; label typu regionu řeší locale slovník v kódu.

spiritual_guide_translations

CREATE TABLE spiritual_guide_translations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  spiritual_guide_id INTEGER NOT NULL,
  locale TEXT NOT NULL,
  slug TEXT NOT NULL,
  title TEXT NOT NULL,
  intent TEXT,
  category TEXT,
  intro TEXT NOT NULL,
  steps_json TEXT NOT NULL,
  materials_json TEXT,
  safety_notes TEXT,
  references_note TEXT,
  seo_title TEXT,
  seo_description TEXT,
  is_published INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (spiritual_guide_id) REFERENCES spiritual_guides(id) ON DELETE CASCADE,
  UNIQUE (spiritual_guide_id, locale),
  UNIQUE (locale, slug)
);

recipe_translations

CREATE TABLE recipe_translations (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  herb_method_recipe_id INTEGER NOT NULL,
  locale TEXT NOT NULL,
  slug TEXT,
  title TEXT NOT NULL,
  steps_json TEXT NOT NULL,
  notes TEXT,
  science_rationale TEXT,
  seo_title TEXT,
  seo_description TEXT,
  is_published INTEGER NOT NULL DEFAULT 0,
  created_at TEXT NOT NULL,
  updated_at TEXT NOT NULL,
  FOREIGN KEY (herb_method_recipe_id) REFERENCES herb_method_recipes(id) ON DELETE CASCADE,
  UNIQUE (herb_method_recipe_id, locale),
  UNIQUE (locale, slug)
);

slug u receptu je volitelný, dokud recept nemá vlastní detailní routu; pokud zůstane /recepty jen index, unique index pro slug musí povolit NULL.

Co se nepřekládá v DB tabulkách

Tyto věci zůstávají sdílené napříč locale:

  • stabilní interní id
  • vazby mezi entitami (herb_id, topic_id, region_id, processing_method_id)
  • latinský název byliny
  • číselná data: měsíce, roky, časy přípravy, pořadí
  • R2 klíče médií a technická metadata obrázků
  • DOI, URL zdrojů, autoři, časopis a další bibliografická metadata
  • enum slugy (safety_level, study_type, evidence_level, claim_nature, facet) — veřejné labely se překládají slovníkem v kódu
  • publish stav původní entity jako hrubý master switch; detailní viditelnost řídí ještě is_published per překlad

Publikování a workflow

  • Entity má master is_published; locale verze má vlastní is_published.
  • Veřejná stránka v locale existuje jen když jsou publikované obě vrstvy: master entita i překlad.
  • Překlad se nevytváří automaticky při změně master dat.
  • Seed/migrace pro první fázi vytvoří české překlady z existujících českých sloupců.
  • Anglické překlady se přidávají postupně a publikují per entita.

Migrace existujících textů

  • První D1 migrace vytvoří překladové tabulky a naplní locale = 'cs' z dnešních českých sloupců.
  • Po přepnutí veřejných read modelů na *_translations se původní textové sloupce v master tabulkách přestanou používat pro veřejný render.
  • Master tabulky si cílově mají nechat jen sdílená technická pole, vazby, enumy a publish master switch.
  • Staré textové sloupce se nemažou ve stejném kroku jako přechod read modelů; odstranění je samostatný cleanup po ověření produkce.
  • Nový obsah se po migraci zapisuje už jen do překladových tabulek.

Čtení dat v aplikaci

DB read modely mají mít locale-aware variantu:

getPublishedHerbDetailBySlug(db, locale, slug)
listPublishedHerbs(db, { locale, ...filters })
listPublishedHerbSlugs(db, locale)

Pravidla:

  • Detail hledá řádek přes translations.locale + translations.slug.
  • Po nalezení detailu se alternativní URL generují přes stejné entity ID a dostupné publikované překlady.
  • Katalogové listy vrací jen entity s publikovaným překladem pro dané locale.
  • Filtry nad sdílenými vztahy (region, topic, processing) musí přijímat lokalizovaný slug a převést ho na stabilní ID.
  • Výstup read modelu nemá míchat česká pole z master tabulky do anglické stránky bez explicitního fallback rozhodnutí.

Fallback politika

SEO-safe výchozí pravidlo:

  • Detailní stránka bez publikovaného překladu v daném locale vrací 404.
  • Taková varianta se neobjeví v hreflang.
  • Taková varianta se neobjeví v sitemapě.
  • Indexové stránky mohou zobrazit méně položek podle dostupných překladů.
  • Počty na indexových stránkách (shown, total) se počítají jen z entit s publikovaným překladem pro aktuální locale.
  • Filter options se zobrazují jen pro číselníky/entity, které mají publikovaný překlad pro aktuální locale.
  • Empty state v daném locale je platný výsledek, pokud zatím nejsou publikované překlady.

Dočasný fallback českého obsahu do /en je povolen jen pro interní kontrolu nebo noindex stránky, ne pro indexovatelný veřejný detail.

Dokumentace a Markdown obsah

Docs stránky potřebují samostatné rozhodnutí:

  • Varianta A: lokalizované Markdown soubory podle locale (docs/en/..., docs/cs/...).
  • Varianta B: registry mapující doc_id na locale soubory a slugy.

Doporučení: pro veřejnou dokumentaci použít registry s doc_id, locale, slug, title, path, is_published, aby šlo generovat hreflang stejně jako u DB entit.

Vyhledávání a indexace

  • Textové vyhledávání musí být locale-aware.
  • Fold/search helpery nesmí předpokládat češtinu pro angličtinu.
  • Budoucí FTS/embeddings indexovat odděleně per locale.
  • Query parametry filtrů používají lokalizované slugy v URL, ale DB dotazy pracují se stabilními ID.
  • q hledá jen v překladových polích aktuálního locale a v locale-neutral polích, kde to dává smysl (např. latinský název).
  • Parametry jako region, topic, pm, part používají lokalizovaný slug pro aktuální locale.
  • Loader převede lokalizovaný slug na stabilní interní ID a až potom skládá SQL filtr.
  • Kombinace filtrů nesmí míchat slugy z různých locale; neplatný slug pro locale se chová jako neexistující filtr nebo 404 podle typu routy.

Slugy a fallbacky

  • Slug může být odlišný per jazyk (/cs/byliny/kopriva-dvojdoma, /en/herbs/stinging-nettle).
  • Každá detailní stránka musí umět najít alternativní URL podle stabilního entity ID, ne podle stejného slug stringu.
  • Pokud překlad entity chybí, platí výchozí fallback politika výše: žádný indexovatelný detail, žádný hreflang, žádná sitemap URL.
  • hreflang se generuje jen pro reálně dostupné publikované varianty.

SEO metadata

Každá indexovatelná lokalizovaná HTML stránka musí mít:

  • html lang podle locale (en, cs)
  • title a description v jazyce stránky
  • canonical na vlastní lokalizovanou URL
  • hreflang="en" na anglickou variantu, pokud existuje
  • hreflang="cs" na českou variantu, pokud existuje
  • hreflang="x-default" na defaultní anglickou variantu
  • OG/Twitter metadata v jazyce stránky
  • og:locale odpovídající jazyku (en_US, cs_CZ)

Hreflang musí být reciproční: pokud /en/herbs/stinging-nettle odkazuje na /cs/byliny/kopriva-dvojdoma, česká stránka musí odkazovat zpět na anglickou.

Sitemap

  • Sitemap musí obsahovat lokalizované kanonické URL, ne staré neprefixované URL.
  • Pro každou URL s alternativami doplnit xhtml:link rel="alternate" hreflang="...".
  • Nezahrnovat jazykové varianty, které vrací 404, redirect nebo fallback bez indexace.
  • Sladit se zadáním sitemap-xml-vylepseni.md.

Příklad cílového XML tvaru:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
        xmlns:xhtml="http://www.w3.org/1999/xhtml">
  <url>
    <loc>https://soulofherbs.com/en/herbs/stinging-nettle</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://soulofherbs.com/en/herbs/stinging-nettle" />
    <xhtml:link rel="alternate" hreflang="cs" href="https://soulofherbs.com/cs/byliny/kopriva-dvojdoma" />
    <xhtml:link rel="alternate" hreflang="x-default" href="https://soulofherbs.com/en/herbs/stinging-nettle" />
  </url>
  <url>
    <loc>https://soulofherbs.com/cs/byliny/kopriva-dvojdoma</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://soulofherbs.com/en/herbs/stinging-nettle" />
    <xhtml:link rel="alternate" hreflang="cs" href="https://soulofherbs.com/cs/byliny/kopriva-dvojdoma" />
    <xhtml:link rel="alternate" hreflang="x-default" href="https://soulofherbs.com/en/herbs/stinging-nettle" />
  </url>
</urlset>

Přepínač jazyka

  • Viditelný v hlavní hlavičce i patičce.
  • Na desktopu jednoduchý přepínač EN / CS nebo menu, na mobilu stejně dostupný v navigaci.
  • Odkaz vede na ekvivalentní lokalizovanou URL aktuální stránky.
  • Aktivní jazyk je označený srozumitelně pro screen reader (aria-current nebo text).
  • Přepínač nesmí měnit jazyk přes klientský stav bez změny URL.

Fázovaný rollout

Fáze 0: Dokumentační rozhodnutí

  • Zapsat, že staré neprefixované URL nemají garantovanou zpětnou kompatibilitu.
  • Potvrdit en_US a cs_CZ pro OG locale.
  • Potvrdit *_translations model jako jediný veřejný obsahový zdroj po migraci.

Fáze 1: i18n kostra bez DB překladů

  • Přidat locale konfiguraci a helper pro validaci locale segmentu (en, cs).
  • Přidat route/mapping helper pro lokalizované URL.
  • Přidat meta helper pro hreflang linky, x-default a lokalizované og:locale.
  • Přidat testy pro canonical + hreflang sadu na reprezentativních routách.
  • Root / přesměrovat na /en.

Fáze 2: První veřejné routy a language switcher

  • Zavést /en a /cs jako jazykové homepage.
  • Přidat language switcher do hlavičky.
  • Přidat language switcher do patičky.
  • Upravit interní odkazy, aby používaly aktuální locale.
  • Upravit 404 texty a redirecty pro locale-aware chování.

Fáze 3: První obsahový řez přes byliny

  • Přidat herb_translations.
  • Naplnit české překlady z dnešních polí herbs.
  • Přidat malou sadu anglických překladů pro reprezentativní byliny.
  • Přepnout /en/herbs, /cs/byliny, /en/herbs/:slug, /cs/byliny/:slug na locale-aware read modely.
  • Ověřit detail s odlišným slugem per locale.

Fáze 4: SEO a sitemap pro první řez

  • Doplnit hreflang na indexové stránky.
  • Doplnit hreflang na detailní stránky podle dostupných alternativ.
  • Upravit /sitemap.xml na lokalizované URL a alternate linky.
  • Přidat smoke test pro vybrané dvojice /en/... + /cs/....
  • Ověřit reciproční hreflang na vzorku detailů.

Fáze 5: Rozšíření na další entity

  • Přidat topic_translations, processing_method_translations, region_translations, spiritual_guide_translations.
  • Přidat recipe_translations, pokud recepty dostanou detail nebo locale-specific obsah na indexu.
  • Přepnout přehledy a detaily témat, regionů, zpracování a rituálů na locale-aware read modely.
  • Implementovat fallback pravidla pro chybějící překlad podle tohoto dokumentu.

Fáze 6: Cleanup starých textových sloupců

  • Ověřit, že veřejný render nepoužívá staré české textové sloupce v master tabulkách.
  • Aktualizovat 22-data-model.md.
  • Teprve v samostatné migraci odstranit nebo archivovat nepoužívané textové sloupce, pokud už nejsou potřeba pro interní tooling.

Testování

  • Unit:
    • locale parser
    • URL buildery
    • route key → localized path mapping
    • hreflang builder
    • pluralizace / label helpery
  • Meta tests:
    • /en canonical + x-default
    • /cs canonical + alternates
    • detail entity s odlišnými slugy per locale
    • chybějící lokalizace se neobjeví v alternates
  • Smoke:
    • /en, /cs
    • jedna detailní bylina v obou jazycích
    • sitemap obsahuje lokalizované URL a alternate linky
    • staré neprefixované URL se nechová jako indexovatelná duplicita

Akceptace

  • Root / má jednoznačné chování bez duplicitní indexace, preferovaně redirect na /en.
  • /en a /cs fungují jako jazykové vstupy.
  • Hlavička i patička obsahují language switcher.
  • Hlavní indexové routy mají správné canonical, hreflang a x-default.
  • Minimálně jeden detailní typ stránky podporuje odlišný slug per locale a generuje reciproční hreflang.
  • Veřejné entity používají překladové tabulky *_translations s unikátními indexy (entity_id, locale) a (locale, slug).
  • Sitemap neobsahuje staré neprefixované duplicity a obsahuje lokalizované URL.
  • Testy pokrývají locale parsing, URL buildery, hreflang a sitemap alternates.

Rizika a rozhodnutí

  • Změna URL je SEO citlivá: před nasazením redirectů připravit matici stará URL → nová URL.
  • Překlad slugů vyžaduje stabilní entity ID: nelze spoléhat na stejný slug napříč jazyky.
  • Částečné překlady mohou vytvářet tenké stránky: chybějící obsah raději nevkládat do hreflang/sitemap.
  • Angličtina jako default mění produktovou prioritu: současný český obsah bude muset mít plán překladu, jinak /en nesmí působit jako nedokončený web.

Související dokumenty