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
/ena č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:
/enje defaultní verze prox-default; root/nesmí renderovat duplicitní obsah, má přesměrovat na/ennebo jinak jednoznačně kanonizovat. - Žádné SEO duplicity: každá lokalizovaná stránka má canonical sama na sebe a kompletní sadu
hreflangodkazů 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ód | Jazyk | URL prefix | Role |
|---|---|---|---|
en | English | /en | vý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
- validace
- 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_translationstopic_translationsprocessing_method_translationsregion_translationsspiritual_guide_translationsrecipe_translations
Volitelně později:
plant_part_translationshabitat_translationssafety_warning_translationsspiritual_use_translationsscientific_evidence_translationsjen 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:
| Entita | Překladová pole navíc |
|---|---|
| Byliny | identification_notes, confusion_risk_notes, drug_interactions_note, phototoxicity_note |
| Témata | name, description, případně intro |
| Zpracování | name, short_description, full_description, required_equipment, safety_notes, typical_prep_note, safety_summary |
| Regiony | name; typ regionu zůstává enum/slovník |
| Rituální návody | title, intent, category, intro, steps_json, materials_json, safety_notes, references_note |
| Recepty | title, 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_publishedper 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
*_translationsse 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_idna 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.
qhledá 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,partpouží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. hreflangse generuje jen pro reálně dostupné publikované varianty.
SEO metadata
Každá indexovatelná lokalizovaná HTML stránka musí mít:
html langpodle locale (en,cs)titleadescriptionv jazyce stránky- canonical na vlastní lokalizovanou URL
hreflang="en"na anglickou variantu, pokud existujehreflang="cs"na českou variantu, pokud existujehreflang="x-default"na defaultní anglickou variantu- OG/Twitter metadata v jazyce stránky
og:localeodpoví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 / CSnebo 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-currentnebo 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_USacs_CZpro OG locale. - Potvrdit
*_translationsmodel 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
hreflanglinky,x-defaulta 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
/ena/csjako 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/:slugna locale-aware read modely. - Ověřit detail s odlišným slugem per locale.
Fáze 4: SEO a sitemap pro první řez
- Doplnit
hreflangna indexové stránky. - Doplnit
hreflangna detailní stránky podle dostupných alternativ. - Upravit
/sitemap.xmlna 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:
/encanonical +x-default/cscanonical + 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. -
/ena/csfungují jako jazykové vstupy. - Hlavička i patička obsahují language switcher.
- Hlavní indexové routy mají správné canonical,
hreflangax-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
*_translationss 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
/ennesmí působit jako nedokončený web.
Související dokumenty
- SEO metadata: seo-metadata-social-a-nadpisy.md
- Sitemap: sitemap-xml-vylepseni.md
- JSON-LD: seo-strukturovana-data-json-ld.md
- Routy: ../23-api-and-routes.md
- Copy pravidla: ../30-content-guidelines.md