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.
Chování při přepnutí jazyka
- Přepnutí jazyka z libovolné stránky vždy zachová kontext: uživatel se dostane na překlad té samé stránky v cílovém jazyce, ne na jazykovou homepage.
- Mapování ekvivalentní URL vychází ze stabilního interního ID entity (route key + entity ID), ne ze stejného slug stringu napříč jazyky.
- Pravidla mapování podle typu stránky:
- Statická routa (
/en↔/cs,/en/herbs↔/cs/byliny,/en/season↔/cs/sezona, atd.): přepnutí vede na ekvivalentní statickou routu v cílovém jazyce přes route key. - Detail entity (
/en/herbs/:slug↔/cs/byliny/:slug): přepnutí najde záznam*_translationspro cílovýlocalepřes stejnéentity_ida sestaví URL z jeho lokalizovaného slugu. - Indexy s filtry (
?region=...,?topic=...,?pm=...,?part=...,?q=...): lokalizované slugy ve query se přemapují přes stabilní ID; locale-neutral filtry (?month=...) zůstávají beze změny. Pokud některý filtr nelze přeložit (chybí překlad číselníku v cílovém locale), filtr se zahodí, ne celé URL.
- Statická routa (
- Fallback, když ekvivalent neexistuje:
- Detail bez publikovaného překladu pro cílový jazyk → přepínač vede na index dané sekce v cílovém jazyce (např.
/en/herbs), ne na homepage. - Pokud i ten chybí, fallback na jazykovou homepage (
/en,/cs). - Položka v menu pro nedostupný překlad může být vizuálně utlumená s aria poznámkou („překlad není k dispozici“), ale stále vede na výše popsaný fallback, ne na homepage.
- Detail bez publikovaného překladu pro cílový jazyk → přepínač vede na index dané sekce v cílovém jazyce (např.
- Přepínač nesmí ztratit hloubku navigace: query parametry s lokalizovanými slugy se přemapují (viz výše), kotvy (
#…) se zachovávají beze změny. - Volba jazyka může být persistována (cookie /
localStorage) jen jako preference pro budoucí návštěvy; nikdy nesmí přepsat URL při kliknutí ani způsobit redirect z explicitně vyžádané locale URL. - Pro výpočet cílové URL existuje helper, např.
getLocalizedHref(routeKey, locale, params), který používají loadery ihreflanglinky, aby přepínač a SEO metadata byly konzistentní.
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). →app/lib/i18n/locales.ts - Přidat route/mapping helper pro lokalizované URL. →
app/lib/i18n/route-keys.ts+app/lib/i18n/localized-href.ts - Přidat meta helper pro
hreflanglinky,x-defaulta lokalizovanéog:locale. →app/lib/i18n/hreflang-meta.ts+app/lib/i18n/website-meta.ts - Přidat testy pro canonical + hreflang sadu na reprezentativních routách.
- Root
/přesměrovat na/en. (routes/home.tsx→redirect("/en", 308))
Fáze 2: První veřejné routy a language switcher
- Zavést
/ena/csjako jazykové homepage. →routes/locale-home.tsx+route(":locale", …) - Přidat language switcher do hlavičky. →
LanguageSwitcher(hlavička) - Přidat language switcher do patičky. →
LanguageSwitcher placement="top" align="center"(patička) - Upravit interní odkazy, aby používaly aktuální locale. (rámcový UI: hlavička/patička přes
getMessages(locale); content routy zůstávají neprefixované do Fáze 3+) - Upravit 404 texty a redirecty pro locale-aware chování. (
ErrorBoundaryvroot.tsxčte locale z URL, zpětný odkaz vede na/<locale>)
Poznámky k výsledku Fáze 2 a aktuální stav:
/enuž není zrcadlo CS: po Fázi 3–5 a EN seedu z migrace0084má vlastní katalogové sekce a sezónu.routes/locale-home.tsxprotonoindexneposílá — homepage/enje indexovatelná a v sitemap.- Prefixované content routy (
/cs/byliny,/en/herbs,/cs/sezona,/en/season, …) jsou hotové; legacy neprefixované české URL vrací 308 na/cs/...(viz § Fáze 3 / 5). LanguageSwitcherna detailech používá mapy*TranslationSlugsz React Contextu (HerbCatalog…,TopicCatalog…, atd.). Pokud cílový překlad neexistuje, padáme na index dané sekce (isFallback: true) — UI to ukáže přestitle.PLANNED_LOCALES(pl, de, fr, es, it, pt) jsou v menu jako „(brzy)“ saria-disabled— bez<Link>a mimohreflangsadu.
Fáze 3: První obsahový řez přes byliny
- Přidat
herb_translations. →migrations/0082_herb_translations.sql - Naplnit české překlady z dnešních polí
herbs. (INSERT … SELECT FROM herbsv migraci 0082) - Přidat malou sadu anglických překladů pro reprezentativní byliny. (
kopriva-dvojdoma→stinging-nettle,pampeliska-leciva→dandelion; další byliny EN řádek zatím nemají → nezobrazí se v/en/herbs.) - Přepnout
/en/herbs,/cs/byliny,/en/herbs/:slug,/cs/byliny/:slugna locale-aware read modely. →app/routes/locale-herb-catalog._index.tsx+locale-herb-catalog.$slug.tsx,listPublishedHerbs(..., { locale }),getPublishedHerbDetailBundleForLocale(db, locale, slug),listPublishedHerbSlugs(db, locale). - Ověřit detail s odlišným slugem per locale. (Karta byliny si přes
HerbCatalogSwitchContextpředává slugy doLanguageSwitcher;computeSwitcherTargetmapuje/cs/byliny/kopriva-dvojdoma↔/en/herbs/stinging-nettle. Testy:app/lib/i18n/switch-target.test.ts.)
Poznámky k výsledku Fáze 3:
- Legacy
/bylinya/byliny/:slugvrací 308 redirect na/cs/byliny[/:slug](zachovává query a hash) — místo doc-policy „staré české routy → 404“ jsme zvolili 308, aby externí odkazy a interní legacy linky nepadaly. Sitemap už staré neprefixované URL neobsahuje (viz Fáze 4). HerbDetailExtendedmá nověid(kvůlilistPublishedSpiritualGuideSummariesForHerbIdagetPublishedHerbTranslationSlugsByHerbId).localizedWebsiteMetamá nové parametryhreflangAlternates,ogTypeaogImageHref— využívá jeherbDetailRouteMetapro reciproční hreflang u karet s odlišnými slugy a proog:imagez první karty.
Fáze 4: SEO a sitemap pro první řez
- Doplnit
hreflangna indexové stránky. (localizedWebsiteMetavroutes/locale-herb-catalog._index.tsxskládá hreflang sadu zROUTE_PATTERNS["herbs.index"]přesbuildHreflangMeta. Stejnou cestou jsou doplněné i indexy témat, zpracování, regionů a rituálů ve Fázi 5.) - Doplnit
hreflangna detailní stránky podle dostupných alternativ. (localizedHerbDetailRouteMeta+ analogickélocalizedTopicDetailRouteMeta,localizedProcessingDetailRouteMeta,localizedRegionDetailRouteMeta,localizedRitualDetailRouteMeta. Každá generujehreflangjen pro publikované překlady z*_translations.) - Upravit
/sitemap.xmlna lokalizované URL a alternate linky. (app/routes/sitemap[.]xml.tsxvrací jen prefixované/cs/...a/en/..., EN segmenty se přidají jen když existují EN slugy. Staré neprefixované katalogové URL už v mapě nejsou; aliasy/byliny-na-zpracovani/:slugzůstávají jen u processing kvůli historickým odkazům.) - Přidat smoke test pro vybrané dvojice
/en/...+/cs/.... (app/lib/i18n/switch-target.test.tskryje herb / topic / processing / region / ritual párů přescomputeSwitcherTarget;app/lib/i18n/herb-catalog-path.test.ts,topic-catalog-path.test.ts,processing-catalog-path.test.ts,region-catalog-path.test.ts,rituals-catalog-path.test.tsověřují parser pro každou entitu.) - Ověřit reciproční hreflang na vzorku detailů. (
hreflangAlternatesvlocalized*DetailRouteMetase počítá zgetPublished*TranslationSlugsBy*Id— chybějící překlad se prostě neobjeví; CS i EN strana používají stejnou mapu, takže reciprocita drží z jednoho zdroje.)
Fáze 5: Rozšíření na další entity
- Přidat
topic_translations,processing_method_translations,region_translations,spiritual_guide_translations. (Migracemigrations/0083_*_translations.sql— sdílený tvar překladové tabulky,UNIQUE (entity_id, locale)aUNIQUE (locale, slug). Migrace0082přidalaherb_translationsve Fázi 3.) - Recepty:
/cs/recepty↔/en/recipes(parserapp/lib/i18n/recipes-catalog-path.ts, routaroutes/locale-recipes._index.tsx); legacy/recepty→ 308 na/cs/recepty.listPublishedRecipesCatalog(db, locale)joinujeherb_translationsaprocessing_method_translations(INNER) +LEFT JOIN recipe_translations rt ON locale = ? AND is_published = 1sCOALESCE(rt.title, hmr.title)/COALESCE(rt.science_rationale, hmr.science_rationale_cs). Stejné překlady tahá karta byliny (herb-detail.server.ts) pro title/notes/steps_json/science_rationale aprocessing-methods.server.tspro odkazy z detailu zpracování. Migrace0090_recipe_translations.sqlzaložila tabulku (UNIQUE (herb_method_recipe_id, locale), sloupcetitle,notes,steps_json,science_rationale,is_published) a naseedovala CS řádky z masteru; EN řádky se přidávají ručně, do té doby fallbackuje COALESCE na master. - Přepnout přehledy a detaily témat, regionů, zpracování a rituálů na locale-aware read modely.
- Témata:
app/db/topics.server.tsčte ztopic_translations; routyapp/routes/locale-topics._index.tsxaapp/routes/locale-topics.$slug.tsxslouží/cs/symptomy[/:slug]a/en/topics[/:slug]. Legacyroutes/symptomy.*→ 308 na/cs/symptomy[/:slug]. - Zpracování:
app/db/processing-methods.server.tsčte zprocessing_method_translations; routyroutes/locale-processing._index.tsxaroutes/locale-processing.$slug.tsx. Legacyroutes/zpracovani.*→ 308 na/cs/zpracovani[/:slug]; alias/byliny-na-zpracovani/:slugpřesměrovává na/cs/zpracovani/:slug. - Regiony:
app/db/regions.server.tsčteregion_translations.name/sluga joinregion_translationspro lokalizovanýcontinentName/subregionName(fallback na mastername). Routyroutes/locale-regions._index.tsxaroutes/locale-regions.$slug.tsx; legacyroutes/region.*→ 308 na/cs/region[/:slug]. - Rituály:
app/db/spiritual-guides.server.tsčtespiritual_guide_translations;SpiritualGuideDetailmá nověid. Routyroutes/locale-rituals._index.tsxaroutes/locale-rituals.$slug.tsx; legacyroutes/ritualy.*→ 308 na/cs/ritualy[/:slug]. - Parsery URL:
app/lib/i18n/topic-catalog-path.ts,processing-catalog-path.ts,region-catalog-path.ts,rituals-catalog-path.ts— všechny pokryté unit testy.computeSwitcherTargetrozezná každý typ stránky a používá příslušné*TranslationSlugsz React Contextu (TopicCatalogSwitchProvider,ProcessingCatalogSwitchProvider,RegionCatalogSwitchProvider,SpiritualGuideSwitchProvider) napojené vroot.tsx. - Hlavní navigace (
app/root.tsx) odkazuje na lokalizované cíle přesgetLocalizedHref(routeKey, locale)(bylo by zbytečné chodit přes legacy 308).
- Témata:
- Implementovat fallback pravidla pro chybějící překlad podle tohoto dokumentu.
- Detail bez publikovaného překladu → 404 přes
notFoundResponse(...)v locale rutě (/en/topics/jen-cs-slugse nezobrazí). - Indexové stránky listují jen entity s
is_published = 1v*_translations.locale. hreflangse generuje jen pro reálně publikované překlady (mapagetPublished*TranslationSlugsBy*Id).LanguageSwitcherpro detail bez ekvivalentního slugu fallbackuje na index dané sekce v cílovém jazyce ({ href: getLocalizedHref("rituals.index", target), isFallback: true }) — UI to indikuje přestitleatribut s nápovědou „překlad zatím není k dispozici“.- Sitemap vynechává jazykové varianty bez slugu (např. EN segment
/en/ritualsse přidá jen kdyžlistPublishedSpiritualGuideSlugs(db, "en")vrátí aspoň jeden záznam).
- Detail bez publikovaného překladu → 404 přes
Poznámky k výsledku Fáze 5:
- Master tabulky (
topics,processing_methods,regions,spiritual_guides) si dál drží stabilníid, vazby, enumy a masteris_published. Veřejné slugy a názvy řídí výhradně*_translations. RegionDetail/RegionListItemmácontinentNameasubregionNamelokalizované (LEFT JOINregion_translationsjakocrt/srts fallbackem nacontinents.name/subregions.name), ale slugy kontinentu a subregionu zůstávají locale-neutral (anchor#continent-…musí být stejný pro CS i EN).LanguageSwitcherpřijímá pět volitelných map slugů (herbTranslationSlugs,topicTranslationSlugs,processingMethodTranslationSlugs,regionTranslationSlugs,spiritualGuideTranslationSlugs). Kontext si na detailu nastaví loader přesuseLayoutEffecta smaže ho na unmount, takže navigace mezi typy detailů nemíchá staré slugy do nového targetu.- Sezóna měsíční (
/cs/sezona[/:month]↔/en/season[/:month]):season.monthparametr je locale-neutral (1–12); URL prefix se ale liší (sezonavsseason), takžeparseSeasonCatalogPathnamevcomputeSwitcherTargetho rozezná a přepne na cílový segment přesgetLocalizedHref("season.month", target, { params: { month } }). Query (?region=…) a hash se zachovávají.- DB read modely v
app/db/sezona.server.ts(loadSezonaMonthHerbContextBySlug,listDistinctPlantPartsForMonth,countPublishedHerbsWithHarvestInMonth) přijímajílocalea join naherb_translationszajišťuje, že klíče vctxBySlugjsou ve stejném locale jakoHerbCatalogItem.slugzlistPublishedHerbs(..., { locale }).harvest_periodsje locale-neutral fakt, aleherb_in_regionpracuje sregion_translationsper locale. plant_parts.slugzůstává locale-neutral (URL filter?part=list); od migrace0085se alenamepřekládá přesplant_part_translations(LEFT JOIN sCOALESCE(ppt.name, pp.name)v sezóně, katalogovém filter optionsu i detailu byliny). EN seed obsahujeLeaf / Root / Flowerprolist / koren / kvet.- UI texty (breadcrumbs, intro, tabulka, prázdný stav, filtr regionů, krátké názvy měsíců) jsou lokalizované přes
UiMessages.seasonvapp/lib/i18n/messages/{cs,en}.ts.MONTH_LABELS_ENaMONTH_LABELS_SHORT_ENpřibyly vapp/lib/month-labels-en.ts. localizedSezonaMonthRouteMetavapp/lib/sezona-month-meta.tsskládá title + description +hreflangpřeslocalizedWebsiteMeta(route keyseason.month); regionální „vybraný region" / „selected region" varianta se přepíná podle locale.- Legacy
routes/sezona.*→ 308 na/cs/sezona[/:month](zachovává query/hash). Sitemap nově obsahuje/cs/sezona,/en/seasona 12 měsíců v každém locale; staré neprefixovanésezonaMonthPathsse nahradily. - Alias
/co-sbirat/:monthzůstává jen jako redirect (routes/co-sbirat.$month.tsx) — locale verze pro něj nevzniká, dokud nebude explicitní produktový důvod.
Fáze 5.5: Seed prvního EN řezu + recepty
- Dokumentace (
/cs/dokumentace[/:slug]↔/en/docs[/:slug]) — parserapp/lib/i18n/docs-catalog-path.ts, routyroutes/locale-docs._index.tsxaroutes/locale-docs.$slug.tsx, legacyroutes/dokumentace.*→ 308.rewriteDocsHref(href, locale)přepisuje relativní.mdodkazy na lokalizovaný cíl,DocsMarkdownpřijímálocale. Slug.mdsouboru je locale-neutral (sdílí hodocs/v repu);localizedDokumentaceDocRouteMetaskládácanonical+hreflangpřeslocalizedWebsiteMeta.UiMessages.docspokrývá breadcrumb, intro a count fráze; sitemap nově emituje CS i EN docs URL; switch-target zná oba prefixy. (Veřejná routa/roadmapbyla zrušena —11-roadmap.mda24-engineering-roadmap.mdzůstávají dostupné jen přes/cs/dokumentace/:slug↔/en/docs/:slug.) - Recepty (
/cs/recepty↔/en/recipes) — parser, locale routa, legacy 308, locale-awarelistPublishedRecipesCatalog,UiMessages.recipes, lokalizované odkazy z karet receptu na byliny a způsoby. Switch-target a sitemap aktualizovány. - Migrace
0088_en_translations_fifty_percent.sql(generujescripts/gen_migration_0088.py): doplní publikovaným bylinám a tématům bez EN řádku seed pro(id % 2) = 1(lichá ID; překryv s 0084 se přeskočí). Zpracování a regiony se neopakují. - Migrace
0084_en_translations_twenty_percent.sql(generujescripts/gen_migration_0084.py):herb_translations: pro publikované byliny bez EN řádku přidá EN pro(herb_id % 5) = 1(~20 %), slugherb-<id>, jméno zlatin_name(fallback CS), dlouhé texty zatím přebírá z CS varianty (označeno vshort_description).topic_translations: EN pro(topic_id % 5) = 1, slug<cs-slug>-en, anglické názvy z mapy ve skriptu, popis podletopics.type.processing_method_translations: EN pro celý katalog (<cs-slug>-en, anglické názvy;difficulty_labelmapovaný naEasy/Medium/Hard).region_translations: EN pro kontinenty +cr(Czechia), stejný slug jako CS — kontinenty jsou kotvy#continent-….
- Migrace
0085_plant_part_translations.sql—plant_part_translationsse sdíleným slugem a překlademname; LEFT JOIN vsezona.server.ts,herb-detail.server.tsagetCatalogFilterOptions. -
/enpřestalo být zrcadlo —noindexvyhozen zlocale-home.tsx, sitemap/enznovu obsahuje. -
countPublishedHerbsWithHarvestInMonthpřijímálocale(počítá jen byliny, které mají publikovanýherb_translationsřádek).
Fáze 6: Cleanup starých textových sloupců
- Audit veřejného renderu — katalog, detaily témat/zpracování/regionů/rituálů, sezóna a karta byliny používají
*_translationstam, kde tabulka existuje. Opraveno v tomto PR: na kartě byliny (getPublishedHerbDetailBundleForLocale) se pro výskyt, sběr (region u řádku) a blok zpracování bereregion_translations/processing_method_translations(sCOALESCEna master při chybějícím řádku); vazby téma ↔ bylina na kartě jdou přestopic_translationsv danémlocale(téma bez publikovaného překladu v jazyce se v seznamu neukáže). - Migrace
0086_storage_method_translations.sql—storage_method_translationsse sdíleným slugem master tabulky a překlademname+short_description; LEFT JOIN sCOALESCE(smt.name, sm.name)ve skladovacím dotazu vherb-detail.server.ts. EN seed pro 5 stávajících slugů (suseni,chlad,med,alkohol,hermeticky). - Migrace
0092_harvest_period_translations.sql— překladharvest_notesu řádků sběru:- Tabulka
harvest_period_translations(UNIQUE (harvest_period_id, locale), sloupceharvest_notes,is_published, časové stopy) + index(locale, is_published). - CS seed = kopie master
harvest_periods.harvest_notes;is_published = 1(masterharvest_periodsnemáis_published, viditelnost se řídí přesherbs.is_published). - Read model v
herb-detail.server.ts(sekce „Sběr a sklizeň") přepnut naLEFT JOIN harvest_period_translations hpt … COALESCE(hpt.harvest_notes, hp.harvest_notes).sezona.server.tsani jiné dotazyharvest_notesna veřejné stránce nečtou. - Master sloupce
growth_phase,best_time_of_day,weather_noteszůstávají locale-neutral a zatím nejsou v public renderu; pokud se začnou zobrazovat, doplní se buď enumy s UI labely, nebo doharvest_period_translationsv navazující migraci.
- Tabulka
- Migrace
0091_content_block_translations.sql— tři lokalizační tabulky pro redakční bloky karty byliny:spiritual_use_translations(title, description, source_note). Enumy (category,use_form,claim_strength,claim_nature,facets) acultural_contextzůstávají v masteruspiritual_uses.scientific_evidence_translations(claim_summary, limitations, preparation_form, dosage_notes). V masteruscientific_evidencezůstávajíactive_compound, enumy (evidence_level,study_type) a citační metadata (source_title,source_url,doi,authors,journal,publication_year).safety_warning_translations(title, description). Enumy (warning_type,severity) asource_urlzůstávají v masterusafety_warnings.- CS seed = kopie master polí;
is_publishedse zrcadlí (usafety_warnings, který nemáis_published, je CS řádek vždy publikovaný). herb-detail.server.tspřepnut naLEFT JOIN *_translations AS … COALESCE(…)pro všechny tři bloky i pro recept-evidence join (/byliny/.../#receptypřebírá lokalizovanouclaim_summary/limitations/preparation_form/dosage_notes).
- Aktualizovat 22-data-model.md — nová sekce „i18n a veřejné čtení (Fáze 6)“ + pravidlo, že DROP master textů je samostatná migrace.
- Samostatná migrace: odstranit nebo archivovat nepoužívané textové sloupce v
herbs,topics,spiritual_uses,scientific_evidence,safety_warnings,herb_method_recipes,harvest_periodsaž po dalším auditu a přepnutí admin nástrojů.
Zbývající dluh (veřejný text z masteru / bez locale): žádný okruh veřejně renderovaného obsahu karty byliny už nečte volnotextový sloupec přímo z masteru. Karta byliny i recepty mají dedikované překladové tabulky pro všechny renderované bloky — recipe_translations (0090), spiritual_use_translations / scientific_evidence_translations / safety_warning_translations (0091), harvest_period_translations (0092). CS seed je všude nasazen, EN řádky se přidávají postupně — do té doby vrací COALESCE master. Viz sekce v 22-data-model.md.
Přehled zbývající práce (orientační)
| Oblast | Stav | Poznámka |
|---|---|---|
Routy + switcher + sitemap + UiMessages | Hotovo | Jádro i18n v provozu. |
| EN seed bylin/témat (0084 + 0088) | Hotovo | ~20 % (id % 5 = 1) + lichá id bez EN; sudá id mimo 0084 stále často bez EN — další vlna seedu nebo ruční překlady. |
recipe_translations + read model receptů | Hotovo (struktura) | Tabulka + CS seed v 0090, read modely COALESCE. Migrace 0093_recipe_translations_en_catalog_seed.sql: pro recepty s publikovaným herb_translations + processing_method_translations v en přidá EN řádek s anglickým titulkem (jméno byliny + metoda + část rostliny z EN překladů); kroky a science_rationale zatím z CS řádku / masteru — ruční EN text postupně. |
| Spirituální / vědecké / varování na kartě | Hotovo (struktura) | Tabulky spiritual_use_translations, scientific_evidence_translations, safety_warning_translations v 0091 + read modely COALESCE (včetně recept-evidence joinu). EN obsah se dohrává ručně (zatím fallback na CS). |
harvest_periods.harvest_notes | Hotovo (struktura) | Tabulka harvest_period_translations v 0092 + read model herb-detail.server.ts COALESCE. EN poznámky se dohrávají ručně. |
/cs/roadmap ↔ /en/roadmap | Vyřazeno | Veřejná routa /roadmap smazána; produktová a engineering roadmapa žijí jen jako docs/11-roadmap.md a docs/24-engineering-roadmap.md (přístupné přes /cs/dokumentace/:slug ↔ /en/docs/:slug). |
| DROP master textových sloupců | Zbývá | Samostatná migrace po dokončení přepnutí a admin nástrojů. |
| Plánované jazyky (pl, de, …) | Backlog | Jen UI „(brzy)“ bez rout. |
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. (routes/home.tsx→ 308 na/en.) -
/ena/csfungují jako jazykové vstupy. (routes/locale-home.tsx+ parserparseSupportedLocale.) - Hlavička i patička obsahují language switcher. (
LanguageSwitchervapp/root.tsx.) - Hlavní indexové routy mají správné canonical,
hreflangax-default. (localizedWebsiteMeta+buildHreflangMetanalocale-herb-catalog._index.tsx,locale-topics._index.tsx,locale-processing._index.tsx,locale-regions._index.tsx,locale-rituals._index.tsx,locale-season._index.tsx.) - Minimálně jeden detailní typ stránky podporuje odlišný slug per locale a generuje reciproční hreflang. (Karta byliny:
/cs/byliny/kopriva-dvojdoma↔/en/herbs/stinging-nettlepřesgetPublishedHerbTranslationSlugsByHerbId.) - Veřejné entity používají překladové tabulky
*_translationss unikátními indexy(entity_id, locale)a(locale, slug). (Migrace 0082–0085;plant_part_translationsmá jenUNIQUE (plant_part_id, locale)— slug zůstává master.) - Sitemap neobsahuje staré neprefixované duplicity a obsahuje lokalizované URL. (
routes/sitemap[.]xml.tsxskládá jen/cs/...a/en/....) - Testy pokrývají locale parsing, URL buildery, hreflang a sitemap alternates. (
app/lib/i18n/*.test.ts,app/lib/i18n/switch-target.test.ts,*-catalog-path.test.ts.)
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